这里有这题的解答,这位大佬通过分析IDA Trace分析出算法逻辑,然后写出逆向算法,求得答案。实在强大。不过这解法太考验耐性了,我的想法还是尽可能的过掉混淆,还原出代码真实面目,然后分析算法。所以,我的解法就是先去除混淆,然后通过动态分析,定位到关键代码后,再还原算法。
接下来的内容分两部分,反混淆,包括机器代码和ollvm的反混淆和算法分析。
后端混淆包含一个后端实现的控制流平坦混淆(CFF)和几组隐藏函数调用,无条件跳转,全局变量,常量引用的花指令。
不了解控制流程平坦混淆反混淆的朋友可以先自行研究下ollvm的反混淆。后端实现的CFF反混淆做法和ir实现的是通用的,
我们都需要先获取块编号到真实块地址的映射关系,然后将跳转到分发器的边修改为真实代码的块。
先看下外层CFF混淆。
任意混淆后的函数都有类似下面的入口:
最后一行跳转,最终走到下面的代码片段
为了弄清这个代码片段的作用,我使用miasm进行符号执行
可以看到,执行该代码片段之后,会恢复R0, R1, LR,平衡栈,并跳转到
这其实就是把 LR_init + 4作为跳转表基址, R0为索引的switch-case实现。把
代入,可计算得跳转目标0xF738 + @32[0x10BE4] = 0xF738 + 0x97DC = 0x18F14
在这块代码最后,设置R0后又跳转回函数入口。计算0x5AA的目标块地址为0x00019B68
经过上面简单分析,我们知道这个crackme的最外层有如下结构
这是个典型的CFF混淆。我们只需要遍历跳转表,将各case最后的跳转指令定向到真实的块,就完成了外层的CFF反混淆。要遍历跳转表,我们还得知道跳转表的大小。
我是通过第一个case的地址计算的
即第一个((case的偏移地址 - 4) / 4)就是跳转表大小。
处理完之后,我们尝试在0x0000F72C创建新函数(快捷键p),发现部分代码已经恢复,但是反编译出来的代码还不完整。
发现有如下代码干扰的IDA的分析:
使用miasm辅助分析00001F80
现在可以知道000199A2-000199A8的作用是加载一个常量到R0,然后跳转到LR_init + 4继续执行。
直接使用movw, movt拼接这个常量到r0就可以去除该混淆。清理后的代码:
crackme里面还有几组类似的混淆,通过前面的CFF反混淆,我们已经知道各case块的起始地址,进而可以计算出case块的大小,遍历case块中的所有指令,使用简单的模式匹配就可以去除所有的后端混淆。
感兴趣的朋友可以自行分析。附件包含我清除后端混淆后的二进制和完整的ida脚本。
后端混淆清理完之后,就可以IDA反编译所有函数了。
反混淆前的代码:
反混淆后的效果:
在IDA中随意反编译一个函数,发现这crackme还有ollvm的混淆等着我们,控制流平坦,指令替换,假分支一个都不少。
网上关于ollvm反混淆的研究已经很多了,相信大家都有自己反混淆方案,或Unicorn,或符号执行。下面简单介绍我使用的方法。
我之前的方案是使用Binary Ninja进行控制流平坦反混淆,但只支持arm64,而这里的crackme是32位,用不上。
使用IDA的微码进行反流程平坦混淆应该是一种通用的,跟体系无关的方案。我也尝试过使用IDA微码进行反混淆,在经历无数次修改微码IDA崩溃后,我崩溃了,彻底放弃修改微码这条路。
考虑到以后分析虚拟机保护的代码,我觉得还有必要维护自己的一个反编译器,看比较高级控制流结构的伪码总比看汇编效率高,所以决定自己实现反编译器。
这次的控制流平坦的反混淆就是用的我自己的反编译器进行的。反混淆的流程如下
之前不好处理的case,现在自己的IR上就很好办了。
如
我的做法是使用指令和代码块复制,把他们转换成下面样子:
经过处理之后,就可以统一的把跳转到BB_0012的指令修改为r0_10对应的代码块了。
当然还是有无法处理的case
这里使用变量对state进行更新。
指令替换在我自己的IR上进行,IDA已经帮我做好表达式传播,我只需进行简单的模式匹配就可以对表达式进行化简。如
我当前实现的反编译器还只具备非常有限的优化能力,为了使用IDA的优化能力,假分支的反混淆是用Binary ninja进行分析和patch,然后把结果导入IDA。
假分支的识别在Binary Ninja MLIL的SSA形式上进行。
如对于下面指令序列,x为全局变量
判断c & 1是否是不透明的谓词的步骤
附件包含反混淆前后的两个关键,反混淆的效果我还是很满意的,当然还有巨大的改进空间。
之前一些关于这个crackme的分析都是基于IDA trace, 而我则是试水我的内存trace工具,测试他是否能处理现实中程序。
逆向的时候有两个非常常见的需求,找到数据在哪被使用和相关数据的来源。对于这个crackme就是找到我们输入的座位编号在哪被处理,处理过程中用到的数据来源。
为了解决两问题,我实现了自己的内存读写trace工具,这个工具可以记录每条指令对内存读写的地址和内容。
打开附件libcrackme-clean-memory-trace.txt,输入座位号"zzzzzzzzszzzzzzz",马上定位到关键函数sub_F72C。
搜索地址0xb4b39400,它有如下引用
打开附件sub_F72C_check-deobfuscated.c,0x0001877d位于一个循环内,循环256次
每次循环读取的地址和内容,都可以在trace找到:
一次迭代对输入的处理过程:
人肉化简anonymous2
最后在0x00014a28写入结果,首次写入的地址和内容
这个循环对应Python伪码
同样,在trace文件过滤出0xbea4aca0就可以找到对他的处理过程。
依次分析对这个地址的写入内容,就可以弄清整个算法处理过程。
这个crackme使用的算法是应该是rc4,sbox使用0xf6f8开始代码构建,使用sbox解密0x2d01c,256字节大小的密文即可得到座位号,与标准rc4差异是,
在跟sbox异或前后多了对数据进行循环位移或者加密的操作。
可以看到,通过内存trace,我们可以很快就定位到关键代码,再结合反混淆后的伪码,这道压轴题已经变成送分题了。
对算法细节有兴趣的朋友可以结合附件trace,伪码里面关键点注释和solve.py,自行分析。
附件内容:
以上内容均可从我的github仓库获取:msc2-crackme3
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)