OLLVM简单理解

OLLVM是什么?

OLLVM 是一个基于 LLVM 编译器框架的开源代码混淆工具,旨在通过多种混淆技术增强程序的安全性,使逆向工程(反编译、反汇编)变得更加困难。

OLLVM 的核心功能

OLLVM 通过修改 LLVM 的中间表示(IR),在编译阶段对代码进行混淆,主要支持以下技术:

  • 控制流平坦化(Control Flow Flattening)

    • 将程序的控制流图(CFG)转换为 状态机 结构,使分支和循环逻辑变得难以分析。
  • 指令替换(Instruction Substitution)

    • 将简单的算术运算(如 +, -, *****, /)替换为更复杂的等效表达式。
  • 虚假控制流(Bogus Control Flow)

    • 插入 永远不会执行 的代码块(基于恒真/假条件),干扰逆向分析工具。
  • 字符串加密(String Encryption)

    • 对程序中的字符串进行加密,运行时动态解密,防止静态分析工具直接提取敏感信息。

控制流平坦化/控制流扁平化

控制流平坦化(control flow flattening)是作用于控制流图的代码混淆技术,其基本思想是重新组织函数的控制流图中的基本块关系,通过插入一个 “主分发器” 来控制基本块的执行流程。

控制流扁平化的核心机制:

所有原本的控制流(条件/循环/跳转)都被统一塞进一个dispatcher(调度器)循环中。

每个原本的逻辑块变成一个 case,通过一个“状态变量”控制跳转,流程就像是跑状态机一样。

流程图对比

下图是正常的执行流程:

0

经过控制流平坦化处理之后便变成了这个样子,由一个 “主分发器” 负责控制程序执行流:

0

代码对比

原始控制流(以if-else为例)

1
2
3
4
5
6
7
8
// 原始代码
int a = 10;
if (a > 5) {
printf("a > 5"); // 块1
} else {
printf("a <= 5"); // 块2
}
printf("end"); // 块3

平坦化后的控制流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 平坦化代码
int state = 0; // 全局状态变量
while (1) {
switch (state) {
case 0: // 初始状态
int a = 10;
if (a > 5) {
state = 1; // 下一步执行块1
} else {
state = 2; // 下一步执行块2
}
break;
case 1: // 块1
printf("a > 5");
state = 3; // 下一步执行块3
break;
case 2: // 块2
printf("a <= 5");
state = 3; // 下一步执行块3
break;
case 3: // 块3
printf("end");
state = -1; // 退出循环
break;
default:
return; // 退出程序
}
}

此时控制流被 “拉平”:所有逻辑块通过state变量和switch-case跳转,原始的if-else分支被隐藏在状态变量的更新中,逆向分析者无法直接从代码结构中看出 “块 1 和块 2 是分支关系”。

简单来说控制流平坦化就是打乱程序原有的控制流结构,将分支逻辑(如if-else**、switch-case、循环)转化为一个单一的 “主循环” 加 “跳转表” 的形式(如switch-case结构)**

控制流扁平化结构组成:

部分 描述
dispatcher 调度循环(通常是 while(1) + switch-case)
state 状态变量 控制当前执行哪个 case
case 代码块 原程序的每个基本块变成一个 case
state = x 替代了原本的 goto、if、while等流程控制

控制流平坦化的实现步骤如下

  1. 分解基本块(Basic Blocks)
    • 将函数拆分为多个基本块(BB),每个基本块是连续执行的指令序列(无分支)。
    • 记录原始控制流的跳转关系(如 if-else、while 等)。
  1. 创建调度循环(Dispatcher Loop)
    • 引入一个状态变量(如 state)作为 switch 的选择器。
    • 构建一个无限循环,内部包含一个 switch(state) 语句,每个 case 对应一个基本块。
  1. 重定向控制流
    • 移除所有原始跳转指令(如 jmp、br 等)。
    • 在每个基本块末尾插入状态更新指令,指定下一个要执行的基本块状态值。
    • 用 break 退出当前 case,返回调度循环。
  1. 初始化状态变量
    • 在函数入口设置初始状态(如 state = BB_start),指向第一个基本块。
  1. 处理函数返回
    • 设置一个特殊状态(如 state = BB_exit)表示退出。
    • 在调度循环中检测退出状态,跳出循环并返回。、

简单来讲就是

(1)把控制流图中的各个基本块全部放到switch语句中;

(2)把switch语句封装到死循环中。

算法在每个基本块中添加next变量以在switch结构中维护正确的控制流结构。这样控制流仍然会正确的执行但是控制流图的结构已经被彻底改变了。各个基本块中已经失去了明确记载控制流流向的基本信息,在逆向分析的过程中也只能一步步记录哪些基本块被执行过。

控制流平坦化特征

控制流平坦化的特征还是很明显的,可以结合上面的代码进行理解

统一的调度循环(dispatcher loop)

  • 几乎所有扁平化都需要一个无限循环:
1
while (1) { ... }
  • 里面有一个 switch-case 或者 “跳表” (jump table)。

状态变量(state variable)

  • 控制下一个执行的分支,代替了所有 if、goto。
  • 每个逻辑块会通过
1
2
state = next_id;
break;

来跳转。

每个原始基本块变成了一个case

  • 原本有10个逻辑块,就会生成10个case。

去平坦化方法

ollvm去混淆

一,D810

D810插件可以直接去github上面下载,这里给安装教程的文章:

ida插件-d810安装和使用 |

D810去平坦化脚本使用方法

Edit->plugins-D-810或快捷键CTRL+shift+D启动D810

1:选中你需要的反混淆规则,我是反ollvm所以选ollvm的

2:start点击后右边会变成绿色loaded

3:回到需要反混淆的函数,F5大法好

此处Decompiling需要等一会,等待出现D-810 ready to deobfuscate…后f5反编译时会自动进行去混淆。

二,使用基于 angr 的脚本 deflat.py 去除控制流平坦化

这个方法我没使用过,这里不做讲解,感兴趣的师傅可以去搜一下

指令替换

指令替换,Instruction-Substitution(sub),是一种比较简单的混淆方式,会将代码中一些简单的数学运算复杂化,但这种方式容易被代码优化给去除,且目前 OLLVM 只实现对整数运算的混淆。如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
原式子:
a=b+c
混淆后:
a = b - (-c)
a = -(-b + (-c))
r = rand (); a = b + r; a = a + c; a = a - r
r = rand (); a = b - r; a = a + b; a = a + r

原式子:
a = b-c
混淆后:
a = b + (-c)
r = rand (); a = b + r; a = a - c; a = a - r
r = rand (); a = b - r; a = a - c; a = a + r

原指令:
a = b & c
混淆后:
a = (b ^ ~c) & b
a = ~(~a | ~b) & (r | ~r)

原指令:
a = b | c
混淆后:
a = (b & c) | (b ^ c)
a = (((~a & r) | (a & ~r)) ^ ((~b & r) | (b & ~r))) | (~(~a | ~b) & (r | ~r))

原指令:
a = a ^ b
混淆后:
a = (~a & b) | (a & ~b)
a = ((~a & r) | (a & ~r)) ^ ((~b & r) | (b & ~r))

虚假控制流

虚假控制流混淆(Bogus Control Flow Obfuscation,简称 BCF),在代码混淆领域是非常核心的一种手段,尤其是与控制流平坦化一起使用,能够极大提高逆向分析的难度。

1.什么是虚假控制流?

虚假控制流混淆(BCF),顾名思义:

在程序中注入一些看似正常、实际上永远不会被执行的伪造分支路径,让控制流图(CFG)变得复杂,从而迷惑逆向分析人员和自动化工具。

2.原理

虚假控制流混淆通常有两个关键要素:

伪条件(永远为假或永远为真)

  • 混淆插入if/else分支
  • 条件看起来和程序变量有关
  • 实际上是恒定的

伪造分支逻辑

  • 条件成立时跳转到一个垃圾块(不会被执行)
  • 条件不成立时走正常逻辑

这样在汇编或IR里看:

  • 有多条分支路径
  • 每个路径都有正常代码
  • 实际执行时只走一条

举个例子

原始代码

1
2
3
4
5
if (x > 0) {
yyy();
} else {
111();
}

加入虚假控制流

1
2
3
4
5
6
7
if ((x > 0) || (rand() == -1)) { // rand()==-1永远不成立
yyy();
} else if (x < -99999999) { // x不可能小于-99999999
nonono(); // 虚假逻辑
} else {
111();
}

从静态来看:

  • 有三个分支
  • 实际只会走两个

再进一步强化:

  • 111()里写垃圾逻辑,比如无意义计算、伪造赋值

3.BCF如何生成伪条件

通常有几种做法:

  1. 常量恒真/恒假
1
if ((x > 0) || (1 == 0)) // 永远假
  1. 不可能成立的范围
1
if (x < -9999999)
  1. 加密条件
1
if (((x ^ key) & 0xFF) == 0x7A)
    • 看似复杂
    • 实际输入域无法满足
  1. 伪随机条件
1
if (rand() == -1)
    • 静态等价恒假

字符串加密

参考文章:关于ollvm字符串加密那些事

字符串加密在混淆领域指:

在编译阶段把程序里的**明文字符串(Literals)**加密,防止逆向工程直接在二进制中搜索到关键字(如密码、敏感信息、函数标识)。

这个字符串加密可以参考上面的文章 我不会