利用ollvm进行代码混淆

OLLVM简介

OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学于2010年6月份发起的一个项目,该项目旨在提供一套开源的针对LLVM的代码混淆工具,以增加对逆向工程的难度。

OLLVM是基于LLVM实现的,LLVM是一个编译器框架,它也采用经典的三段式设计。前端可以使用不同的编译工具对代码文件做词法分析以形成抽象语法树AST,然后将分析好的代码转换成LLVM的中间表示IR(intermediate representation);中间部分的优化器只对中间表示IR操作,通过一系列的Pass对IR做优化;后端负责将优化好的IR解释成对应平台的机器码。LLVM的优点在于,不同的前端语言最终都转换成同一种的IR。

OLLVM的混淆操作就是在中间表示IR层,通过编写Pass来混淆IR,然后后端依据IR来生成的目标代码也就被混淆了。得益于LLVM的设计,OLLVM适用LLVM支持的所有语言(C, C++, Objective-C, Ada 和 Fortran)和目标平台(x86, x86-64, PowerPC, PowerPC-64, ARM, Thumb, SPARC, Alpha, CellSPU, MIPS, MSP430, SystemZ, 和 XCore)。

OLLVM的几种混淆方式

OLLVM默认支持 -fla -sub -bcf 三个混淆参数,这三个参数可以单独使用,也可以配合着使用。-fla 参数表示使用控制流平展(Control Flow Flattening)模式,-sub参数表示使用指令替换(Instructions Substitution)模式,-bcf参数表示使用控制流伪造(Bogus Control Flow)模式

  • -sub instruction substitution(指令替换)
  • -fla control flow flattening(控制流平坦化)
  • -bcf bogus control flow(控制流伪造)

此外,OLLVM支持对单个函数进行混淆,即Functions annotations模式

以下分别介绍这几种方式并举例说明:

instruction substitution(指令替换)

指令替换是一种比较简单的混淆方式,OLLVM将一些简单的运算复杂化,但这种方式容易被代码优化给去除,目前OLLVM只实现对整数运算的混淆。

  • 加法混淆

例如 a = b + c 可以被混淆为:

1
2
3
4
1.  a = b - (-c)
2. a = -(-b + (-c))
3. r = rand (); a = b + r; a = a + c; a = a - r
4. r = rand (); a = b - r; a = a + b; a = a + r
  • 减法混淆
    例如 a = b-c 可以被混淆为:
1
2
3
1. a = b + (-c)
2. r = rand (); a = b + r; a = a - c; a = a - r
3. r = rand (); a = b - r; a = a - c; a = a + r
  • AND运算混淆

a = b & c => a = (b ^ ~c) & b

  • OR运算混淆

a = b | c => a = (b & c) | (b ^ c)

  • XOR运算混淆

a = a ^ b => a = (~a & b) | (a & ~b)

如果一种运算对应多种混淆方式,OLLVM将会随机选择一种,以下为使用OLLVM进行指令替换后的前后对比:

源码:
1
2
3
4
5
int test(int a,int b)
{
int c = a + b;
return c+2;
}
sub前:

upload successful

sub后

upload successful

分析汇编指令得出,a+b+2 运算变成了 -(-b-(a+2))

control flow flattening(控制流平坦化)

control flow flattening(控制流平坦化)通过多个case-swich结构将程序的控制流变成扁平形状,打破原有的逻辑结构,增加逆向的难度。
例如对于以下代码:

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
if(a == 0)
return 1;
else
return 10;
return 0;
}

OLLVM将把它变为如下结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
int b = 0;
while(1) {
switch(b) {
case 0:
if(a == 0)
b = 1;
else
b = 2;
break;
case 1:
return 1;
case 2:
return 10;
default:
break;
}
}
return 0;
}

从代码中可以看到,OLLVM将源代码分割为几个基本块,并放在一个while循环结构中无线循环,程序的流程由变量b控制。

以下是使用OLLVM进行控制混淆后的前后对比:

源码
1
2
3
4
5
6
7
8
9
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
if(a == 0)
return 1;
else
return 10;
return 0;
}
fla之前:

upload successful

fla之后:

upload successful

F5之后的代码:  

upload successful

由此可见,fla之后的控制流比较复杂,通过分析fla之后的代码,我们发现代码中的result仅仅受v7的影响,但分析起来着实费力。

bogus control flow(控制流伪造)

bogus control flow通过在源程序的控制流中添加一些基本块,这些基本块仅仅起了连接作用,并不影响实际的执行逻辑。

以下为使用OLLVM进行控制流伪造后的对比:
源码

1
2
3
4
5
6
7
8
9
#include <stdlib.h>
int main(int argc, char** argv) {
int a = atoi(argv[1]);
if(a == 0)
return 1;
else
return 10;
return 0;
}

bcf之前:
![upload successful](/images/pasted-128.png)
bcf之后:   
![upload successful](/images/pasted-131.png)
F5之后的代码:
![upload successful](/images/pasted-132.png)

由此看出,bcf的方式在程序中增加了一个代码块,这个代码块对函数返回值没有任何影响,但对于逆向人员来说,增加了其逆向分析的无用功。

Functions annotations

有时候为了提高效率,开发者仅需要对指定的函数进行混淆,OLLVM的Functions annotations模式支持对单个函数进行混淆。比如,想对函数func()使用bcf混淆,只需要给函数func()增加bcf属性即可。

1
int func() __attribute((__annotate__(("bcf"))));

OLLVM的fla,sub和bcf三个属性可以搭配使用,只需要添加对应的编译选项即可。

利用OLLVM进行Android native代码混淆

1.下载并编译ollvm

1
2
3
4
5
$ git clone -b llvm-4.0 https://github.com/obfuscator-llvm/obfuscator.git
$ mkdir build
$ cd build
$ cmake -DCMAKE_BUILD_TYPE=Release ../obfuscator/
$ make -j7

2.配置NDK以支持ollvm

  • 新建编译链
    在 android-ndk-r14b/toolchains 下新建目录 ollvm-4.0/prebuilt/darwin-x86_64(我的环境是mac),把前一步编译生成的结果拷贝到此目录下(主要是bin和lib)
  • 配置编译链
    在 android-ndk-r14b/build/core/toolchains 目录下,新建目录 arm-linux-androideabi-clang-ollvm4.0,拷贝目录 arm-linux-androideabi-clang 下的文件 config.mk 与 setup.mk 到 arm-linux-androideabi-clang-ollvm4.0 中,修改setup.mk文件。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    ############################ 原始配置 ############################
    #LLVM_TOOLCHAIN_PREBUILT_ROOT := $(call get-toolchain-root,llvm)
    #LLVM_TOOLCHAIN_PREFIX := $(LLVM_TOOLCHAIN_PREBUILT_ROOT)/bin/
    #################################################################
    ############################ 修改后 #############################
    OLLVM_NAME := ollvm-4.0
    LLVM_TOOLCHAIN_PREBUILT_ROOT := $(call get-toolchain-root,$(OLLVM_NAME))
    LLVM_TOOLCHAIN_PREFIX := $(LLVM_TOOLCHAIN_PREBUILT_ROOT)/bin/
    #其他配置不做修改

3.使用ollvm进行编译

使用 ollvm 进行 ndk 的编译需要对 Application.mk 和 Android.mk 文件做相应的修改。
Android.mk 中添加混淆编译参数:

1
LOCAL_CFLAGS += -mllvm -sub -mllvm -bcf -mllvm -fla

Application.mk 中配置 NDK_TOOLCHAIN_VERSION

1
2
APP_ABI := x86 armeabi-v7a x86_64 arm64-v8a armeabi mips64
NDK_TOOLCHAIN_VERSION := clang-ollvm-4.0

参考资料:

  1. OLLVM wiki https://github.com/obfuscator-llvm/obfuscator/wiki
  2. 利用OLLVM混淆Android Native代码 https://geneblue.github.io/2016/10/09/%E5%88%A9%E7%94%A8OLLVM%E6%B7%B7%E6%B7%86Android%20Native%E4%BB%A3%E7%A0%81%E7%AF%87%E4%B8%80/
  3. OLLVM + NDK 混淆编译环境搭建http://gnaixx.cc/2017/07/25/20170725-ollvm/