阅读笔记 —— 《Obfuscator-LLVM — Software Protection》
LLVM
LLVM独立于语言和平台。它由以下三部分组成:
前端
前端负责解析源码,验证源码的正确性,然后生成IR代码,交付给优化器。LLVM支持两种前端:clang和基于GNU编译器集合解析器的前端
优化器(亦称为中端)
优化器负责删除IR代码中的冗余代码和死代码、内联函数、展开循环、删除死循环、简化控制流图等。之后将结果传送给后端。
后端
根据目标架构生成高效的汇编代码
OLLVM
OLLVM中的大多数混淆和防护机制都是在中端进行的。它具有以下功能:
指令替换(instruction substitutions)
虚假控制流插入(bogus control-flow insertion)
基本块分割(basic block splitting)
控制流平坦化(control-flow flattening)
程序合并(procedures merging)
防篡改代码插入(insertion of code tamper-proofing)
为了使生成的代码能够多样化,使用PRNG生成随机数来决定混淆代码的生成。
指令替换
不支持浮点数运算的指令替换,因为会带来数值不准确性,即认为生成的代码不具有功能等效性。
由于指令替换很直接,并没有增加逆向的阻力,因为这种混淆很容易通过重新优化生成代码来规避。这也就是为什么指令替换pass运行在所有LLVM优化passes之后。
虚假控制流插入
包括在函数控制流图中插入条件跳转结构,该结构要么指向原始基本块,要么指向循环回条件跳转块的假基本块。
不透明谓词:
干扰逆向人员但值不变的表达式,用于确保运行时只执行原始基本块,同样也使优化器不能够通过识别死代码来简化生成调用图。
控制流平坦化和基本块分割
控制流平坦化
通过移除所有易辨别的条件和循环结构来破坏函数控制流图。
基本上是使用一个大型switch结构来实现。通过路由变量(switch中用到的条件变量)控制代码流进入正确的基本块。在每个基本块的末尾,设置路由变量的正确值,使得代码流进入下一个正确的基本块。
路由变量的更新使用动态更新,这样能够迫使逆向工程师对可执行文件进行动态分析。
代码防篡改
代码防篡改机制用于确保运行时代码不被修改。它通过两种机制实现:
- check():负责检查代码是否被修改。
- respond():检测到代码被篡改后采取的措施。
这些check()和respond()分布在整个程序中,并且它们之间相互依赖。
check()
对一段代码进行32位CRC校验和的计算,其中段的开始和结束时随机选择的。
check()的结果可用于更新路由变量。路由变量不仅是动态更新的,而且依赖于编译阶段生成的静态值$s$:
$$
s=\rho⊕d_1⊕d_2⊕…⊕d_n
$$
其中$\rho$的值指向下一个基本块,$d_i$是基本块中第$i$个check()的结果,其值在编译后才能知道。
程序合并
将所有函数合并到一个唯一的新函数中,然后使用新函数merged()替换所有原始函数的调用。
机制的实现大致如下:
每个原始函数的代码被提取、放入merged()中,并作为switch-case中个一个分支。然后每个原始函数都被一个简单的包装函数替换,该函数带有标识原始函数的参数,在调用merged()时能够找到正确的原始函数。