1. 背景
1.1 控制流平坦化
经过控制流平坦化后的执行流程就如下图
这样可以模糊基本块之间的前后关系,增加程序分析的难度,同时这个流程也很像VM的执行流程。更多控制流平坦化的细节可以看Obfuscating C++ programs via control flow flattening,本文以Obfuscator-LLVM的控制流平坦化为例。
1.2 符号执行
符号执行的发展是从静态符号执行到动态符号执行到选择性符号执行,动态符号执行会以具体数值作为输入来模拟执行程序,是混合执行(concolic execution)的典型代表,有很高的精确度,目前较新的符号执行工具有Triton和angr,本文是以angr为例。
2. 分析
编译
用IDA查看未经过控制流平坦化的控制流程图(CFG)
添加控制流平坦化
可以看到控制流平坦化后的CFG非常漂亮
通过分析可以发现原始的执行逻辑只在真实块(自己想的名称...)以及序言和retn块中,其中会产生分支的真实块中主要是通过CMOV指令来控制跳转到哪一个分支,因此只要确定这些块的前后关系就可以恢复出原始的CFG,这个思路主要是参考Deobfuscation: recovering an OLLVM-protected program。
3. 实现
3.1 获取真实块、序言、retn块和无用块
1. 函数的开始地址为序言的地址
2. 序言的后继为主分发器
3. 后继为主分发器的块为预处理器
4. 后继为预处理器的块为真实块
5. 无后继的块为retn块
6. 剩下的为无用块
2. 序言的后继为主分发器
3. 后继为主分发器的块为预处理器
4. 后继为预处理器的块为真实块
5. 无后继的块为retn块
6. 剩下的为无用块
主要代码:
这个步骤主要是使用符号执行,为了方便,这里把真实块、序言和retn块统称为真实块,符号执行从每个真实块的起始地址开始,直到执行到下一个真实块。如果遇到分支,就改变判断值执行两次来获取分支的地址,这里用angr的inspect在遇到类型为ITE的IR表达式时,改变临时变量的值来实现,例如下面这个块
修改临时变量28为false或true再执行就可以得到分支的地址
3.2 确定真实块、序言和retn块的前后关系
使用statement before类型的inspect
如果遇到call指令,使用hook的方式直接返回
主要代码:
3.3 Patch二进制程序
首先把无用块都改成nop指令
然后针对没有产生分支的真实块把最后一条指令改成jmp指令跳转到下一真实块
针对产生分支的真实块把CMOV指令改成相应的条件跳转指令跳向符合条件的分支,例如CMOVZ 改成JZ ,再在这条之后添加JMP 指令跳向另一分支
上述就是去除控制流平坦化的总体实现思路。
4. 演示
用IDA查看恢复后的CFG
可以看到CFG跟原来的大致一样,然后反编译恢复出原始代码
5. 总结
由于小弟刚学习符号执行,可能有理解错误的地方,欢迎研究符号执行或者认为有更好思路的师傅们批评指正。最后,感谢angr主要开发者Fish Wang在这期间的耐心帮助。
6. 参考
2. https://github.com/obfuscator-llvm/obfuscator/tree/llvm-3.6.1
3. Symbolic Execution and Program Testing
4. Selective Symbolic Execution
5. CUTE: A Concolic Unit Testing Engine for C
6. https://github.com/JonathanSalwan/Triton
7. https://github.com/angr/angr
8. http://blog.quarkslab.com/deobfuscation-recovering-an-ollvm-protected-program.html
9. https://github.com/programa-stic/barf-project
10. https://github.com/angr/angr-management