流程平坦化混淆是ollvm里面最难还原,最影响分析效率的一个混淆pass,最近花了点时间,终于把它搞掉了.
刚开始是打算使用IDA的微码进行反混淆,搜到了Rolf Rolles大佬的HexRaysDeob,
还有它的Python版本PyHexRaysDeob.
研究了下发现微码API确实是太难用了,而且很多分析好像都得自己做,所以我暂时放弃该方案.
随后转到了binja, 发现这篇文章Dissecting LLVM Obfuscator Part 1,貌似简单可行,修改了下,用无名侠大佬基于Unicorn 的ARM64 OLLVM反混淆帖子的样本跑,失败了.
看来还是得自己干.
下文中反混淆用的样本是无名侠的libvdog.so(vdog)和自己编译的另外一个小程序(cff).
反混淆主要分两步:分析和修复.
这里思路源于llvm-defobfuscator,熟悉它的话可以直接跳到后面修复内容.
流程平坦混淆代码结构:
反混淆需要的信息主要有:
这些信息通过binja还是很容易分析出来的.
vdog中的代码:
我们可以看到,最后一条指令是状态变量与0x76579ace进行比较,相等的情况下走0x70688(假分支),这个地址就是case 0x76579aced对应代码区域的起始地址.
对于这个测试, 0xdcf244e1对应的case 入口是0x71080(真分支)
用这个方式来获取case代码区域的起始地址是比较可靠的.
因为llvm在给switch生成代码时,据我观察,在无法使用跳转表的情况下,会转到使用二分查找实现,查找到最后一般都会有这么一条测试case编号的指令.
找到case出口也比较简单:包含对状态变量进行更新基本块的出口就是case出口.
实际上,现实代码中会存在有多个出口的情况,这个问题在修复的时候再讨论.
如果是条件跳转,我们还需要知道跳转目标与测试条件的对应关系.
case 0x91c5439 对应的条件可以通过查看入口边的类型得到
到这反混淆所需信息就完全获取到了.
以上我主要说明了思路, 省略了很多细节.
例如cff中下面这种case
这个基本块既是case入口,又是出口,还是分发器代码的一部分.
vdog JNI_OnLoad里面的一个case:
313处, 在分发器里面,状态变量(x9.w9)跟另外一个变量比较(x8_22.w8), 这时候x8_22.w8的值是多少?
上面两个问题我在代码里面都有处理,有兴趣可以参考代码.
即使没有处理也没有关系,我反混淆的修复是保守的,对于无法处理的情况,仍然会走原来逻辑,只是最终反混淆效果会差一点.
下面开始修复部分内容,修复难度在我看来要比分析难. 修复cff这个看起来简单的样本要比vdog难.
代码修复主要有两个难点:
修复时,在代码中寻找用来patch代码空洞还是比较困难的.
之前考虑使用IDA微码进行反混淆,这是最主要的原因.
llvm-deobfuscator是把混淆框架代码作为空洞使用.
而我反混淆目标是为了在IDA中, 使用他的反编译器进行静态分析, 不需要创建一个新的可执行的二进制, 所有我是用IDA创建一个新的代码段来存放patch代码.
这种处理方法能让我保留原来的分发器,有意的保留分发器, 可以在未识别到真实后继基本块或者case出入口的情况下,走原来分发器流程.
如果所有状态的后继都被我处理了, 分发器就变成不可达代码, IDA的反编译器会自动把它优化掉.
目前看到的反混淆文章好像都没有提到如何处理这个问题的.
如下函数:
有可能会被编译器优化成:
我解决的办法是通过代码拷贝,把foo2转换成foo1.具体做法是
问题2, 属于分析阶段的问题, 我现在还没有处理.
当前反混淆方案会在进入case 1之后, 由于state 2对应case 2的入口没有识别到, 控制流会重新进入分发器, 从分发器进入case 2.
现实代码中,还会出现下面的情况
case有多个出口情况,伪码:
为了处理这种代码, 我们在修复的时候, 需要找到case的所有出口,在每个出口处都进行patch.
综上,我们需要进行如下修复操作:
修复后的代码有如下两种布局.
下面是两个具体的修复实例.
cff样本的分发器位于0x00000768
分发器入口代码
这个块内的W0是函数返回值,不可以删除这条指令.
从函数入口到真实后继的修复代码:
可以看到,修复代码复制了分发器到case入口路径上的所有指令.
再来看下vdog中起始地址为0x000704dc的基本块
从函数入口到真实后继的修复代码:
0x000704dc基本块另外一个前驱
修复后:
当前实现拷贝了大量的垃圾指令,有时候函数过大会导致IDA无法反编译;同时也会增加IDA分析用时.修复vdog JNI_OnLoad大概用了21K字节代码,还原用时15分.
拷贝pc相关指令,如函数调用,全局变量引用,需要进行重定位修复.
vdog的JNI_OnLoad里面有6个分发器,不知道是改了ollvm还是inline进来的.
附件包含所有的代码和样本.
所有代码和样本也可从github下载: ollvm-breaker
另外, 反混淆脚本是在binja GUI-less模式下运行, 需要Binary Ninja商业版.
最后,放效果.
在IDA中打开cff或vdog后,运行对应的fix-xxx.py脚本反混淆.
我修复了vdog五个函数,JNI_OnLoad,crazy::GetPackageName,prevent_attach_one,attach_thread_scn,crazy::CheckDex.在IDA中运行修复脚本,重新分析分析程序后,可以查看这些函数的反混淆效果.
先看cff的原始代码
还原后
vdog的JNI_OnLoad还原后
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)