OLLVM(全称 Obfuscator-LLVM)是基于 LLVM 源码二次开发的开源混淆编译器框架,它通过向 LLVM 编译流程中注入自定义混淆 Pass,实现控制流平坦化、虚假控制流等多种代码混淆功能,属于 LLVM 的第三方衍生版本,要了解 OLLVM,先要了解 LLVM。

clang编译器

Clang 是基于 LLVM 编译器基础设施 实现的主流前端编译器。说到编译器,大家最熟悉的就是 GCC,那 Clang(编译器) 和 GCC 有哪些不一样呢?支持多种语言:Swift/Rust/Objective-C,性能优化更好,模块化设计(前、中、后端解耦)。最重要的是 LLVM 的中端优化基于 SSA 形式的 LLVM IR,这一点与 GCC 的 GIMPLE SSA 在思想上是相通的。但 LLVM IR 作为稳定、对外暴露的中间表示,使得定制 Pass(如 OLLVM)在工程实现上更为友好,这会使得该编译器在常量传播、死代码消除更精准,OLLVM 作为二次开发的产物,也继承这一特点。如果你有过静态分析控制流平坦化函数中某个变量的经历,很容易就能观察到,某个变量从定义到后续使用的过程中,会有大量的其余变量来接收、转移,给人一种狡兔三窟的感觉,但这并不是混淆本身,只是编译器的优化策略使然。

SSA

静态单赋值,顾名思义,就是该变量从定义开始,只能被赋值一次。举个例子:
非 SSA 形式:

int a = 1;    // 第一次赋值
a = a + 2;    // 第二次赋值(修改原有变量)
if (cond) {
    a = a * 3; // 第三次赋值
} else {
    a = a - 1; // 第四次赋值
}
printf(a);

SSA 形式

int a1 = 1;                  // 初始赋值(版本1)
int a2 = a1 + 2;             // 新变量a2承载修改后的值(版本2)
if (cond) {
    int a3 = a2 * 3;         // 分支1:新变量a3(版本3)
} else {
    int a4 = a2 - 1;         // 分支2:新变量a4(版本4)
}
int a5 = φ(a3, a4);          // φ函数:根据执行路径选择a3或a4,赋值给a5(版本5)
printf(a5);

这样操作有个好处,就是通过静态分析就能观察到变量的变化和赋值情况,观察到 a5 变量就意味着前面已经被赋值了 4 次。

AST(抽象语法树)

LLVM 编译器中 clang 会将源码解析出树状结构
eg:
代码:

while b ≠ 0:
    if a > b:
        a := a - b
    else:
        b := b - a
return a

树形:

1-nrFb.png
```plaintext
语句序列
├─ while循环
│  ├─ 循环条件
│  │  ├─ 比较运算符:!=
│  │  ├─ 变量名:b
│  │  └─ 常量值:0
│  └─ 循环体
│     └─ 分支(if-else)
│        ├─ 分支条件
│        │  ├─ 比较运算符:>
│        │  ├─ 变量名:a
│        │  └─ 变量名:b
│        ├─ if分支体
│        │  └─ 赋值语句
│        │     ├─ 变量名:a
│        │     └─ 二元运算符:-
│        │        ├─ 变量名:a
│        │        └─ 变量名:b
│        └─ else分支体
│           └─ 赋值语句
│              ├─ 变量名:b
│              └─ 二元运算符:-
│                 ├─ 变量名:b
│                 └─ 变量名:a
└─ 返回语句
   └─ 变量名:a

Basic Block(基本块)

基本块是编译器根据 AST 转化出 IR 的同时,根据一些规则,同时划分出基本块:** 唯一入口、遇到跳转或者返回(ret)会形成新的块,ida 中 cfg 的代码块的划分也是基于这个规则。

IR(中间表达)

IR 是编译过程中沟通源码与汇编的一种中间抽象语言表达,不仅仅是语法表示还有基本块这个骨架。有以下作用:

  • 解耦 “前端(源码)” 和 “后端(机器码)”,实现跨语言、跨架构编译。

  • 作为 “编译优化的统一载体”,实现高效、通用的代码优化。eg:a1=1 → a2=a1+2 会被优化为 a2=3

  • 降低编译器扩展 / 定制的复杂度。ollvm 就是在这个阶段加入的混淆
    GCC、LLVM 都具有中间表达这种概念,但是 LLVM 是暴露开放的,有单独的中间文件,GCC 相反。
    IR 中间表达继承了前端源码的语义,以类汇编的语法进行表达,才沟通起了整个编译过程的始末。

微码(mc),ida 反编译过程中的一种中间表达,后面 D810 反混淆原理要进一步了解。