LLVM与代码混淆技术

LLVM基础篇

LLVM是构架编译器(compiler)的框架系统,以C++编写而成,用于优化以任意程序语言编写的程序的编译时间(compile-time)、链接时间(link-time)、运行时间(run-time)以及空闲时间(idle-time),对开发者保持开放,并兼容已有脚本。

LLVM 编译流程是三段式:

举一个具体的例子

.bc文件是二进制格式的LLVM IR文件,这两个bc 经过优化器得到优化之后的中间代码文件,然后link成一个中间代码文件link.bc,然后编译

那llvm与gcc的优势在哪里呢?

  • 优势一:模块化

    • LLVM:LLVM 是高度模块化设计的,每一个模块都可以从 LLVM 项目中抽离出来单独使用。
    • GCC:而 GCC 虽然也是三段式编译,但各个模块之间是难以抽离出来单独使用的。
  • 优势二:可拓展

    • LLVM:LLVM 为开发者提供了丰富的 API ,例如开发者可以通过 LLVM Pass 框架干预中间代码优化过程,并且配备了完善的文档。
    • GCC:虽然 GCC 是开源的,但要在 GCC 的基础上进行拓展门槛很高、难度很大。

LLVM 环境搭建与基本用法

Ubuntu/LLVM/CMake 版本

  • Ubuntu 18.04

  • LLVM 12.0.1

  • CMake 3.21.1

这个CMake 网上有教程,cmake –version 看看版本就行

第一步:下载 LLVM-Core 和 Clang 源代码

在 llvm-project 仓库的 Releases 界面
https://github.com/llvm/llvm-project/releases/tag/llvmorg-12.0.1
下载 LLVM-Core 和 Clang 源代码:

1
2
llvm-12.0.1.src.tar.xz
clang-12.0.1.src.tar.xz

创建一个文件夹命名为LLVM , 将两个压缩包解压到这个文件夹之后改名为 llvm 和 clang ,方便后续使用。
在同一文件夹内创建名为 build 的文件夹,存放编译后的LLVM。

第二步:编译 LLVM 项目

还是在同一文件夹内创建 build.sh 文件,内容如下:

在LLVMProject路径下终端输入vi+bulid.sh 然后输入

1
2
3
4
5
6
cd build
cmake -G "Unix Makefiles" -DLLVM_ENABLE_PROJECTS="clang" \
-DCMAKE_BUILD_TYPE=Release -DLLVM_TARGETS_TO_BUILD="X86" \
-DBUILD_SHARED_LIBS=On ../llvm
make
make install

编译的时候只需要执行这个shell文件,就会自动编译

1
2
3
4
5
6
7
8
9
10
11
 cmake 参数解释:
-G “Unix Makefiles”:生成Unix下的Makefile
-DLLVM_ENABLE_PROJECTS=“clang”:除了 LLVM Core 外,还需要编译的子项目。
-DLLVM_BUILD_TYPE=Release:在cmake里,有四种编译模式:Debug, Release,
RelWithDebInfo, 和MinSizeRel。使用 Release 模式编译会节省很多空间。
-DLLVM_TARGETS_TO_BUILD=“X86”:默认是ALL,选择X86可节约很多编译时间。
-DBUILD_SHARED_LIBS=On:指定动态链接 LLVM 的库,可以节省空间。
make install 指令是将编译好的二进制文件和头文件等安装到本机的 /usr/local/bin
和 /usr/local/include 目录,方便后续使用。
执行 build.sh 文件自动安装和编译,编译时长从十多分钟到数小时,具体时间由机器性能决定。
输入 clang 确认编译和安装是否完成

然后执行sudo ./bulid.sh

第一步:将源代码转化成 LLVM IR

LLVM IR 有两种表现形式,一种是人类可阅读的文本形式,对应文件后缀为 .ll ;另一种是方便机器处
理的二进制格式,对应文件后缀为 .bc 。使用以下命令将源代码转化为 LLVM IR:

1
2
3
4
5
clang -S -emit-llvm hello.cpp -o hello.ll
-s是人类可以阅读的文本形式 -emit是否把源代码转化为LLVM IR

clang -c -emit-llvm hello.cpp -o hello.bc
-c 是二进制格式

我们先用一个源代码进行演示

1
2
3
4
5
6
7
8
9
10
11
12
13
#include<cstdio>

void hello()
{
printf("hello");

}

int main()
{
hello();
return 0;
}

然后就输出了一个.ll文件

第二步:优化 LLVM IR

使用 opt 指令对 LLVM IR 进行优化

1
opt -load LLVMObfuscator.so -hlw -S hello.ll -o hello_opt.ll

-load 加载特定的 LLVM Pass (集合)进行优化(通常为.so文件)
-hlw 是 LLVM Pass 中自定义的参数,用来指定使用哪个 Pass 进行优化

第三步:编译 LLVM IR 为可执行文件

这一步我们通过 Clang 完成,从 LLVM IR 到可执行文件中间还有一系列复杂的流程,Clang 帮助我们整
合了这个过程:

1
clang hello_opt.ll -o hello 

编写第一个 LLVM Pass

LLVM Pass 的基本概念

LLVM Pass 框架是整个 LLVM 提供给用户用来干预代码优化过程的框架,也是我们编写代码混淆工具的基础编译后的 LLVM Pass 通过优化器 opt 进行加载,可以对 LLVM IR 中间代码进行分析和修改,生成新的中间代码

LLVM Pass 框架为开发者提供了丰富的 API。开发者可以通过调用 API 方便地实现中间代码的分析和修改。

LLVM 源代码目录结构

llvm/include/llvm

文件夹存放了 LLVM 提供的一些公共头文件,即我们在开发过程中可以使用的头文件

llvm/lib

文件夹存放了 LLVM 大部分源代码(.cpp 文件)和一些不公开的头文件

llvm/lib/Transforms

文件夹存放所有 LLVM Pass 的源代码。文件夹也存放了一些 LLVM 自带的 Pass。同时你也把你自己写的LVMPass放入

LLVM Pass 的编写、编译以及加载

目标:编写一个 LLVM Pass,遍历程序中的所有函数,并输出 “Hello, ”+ 函数名

LLVM Pass 支持三种编译方式:第一种是与整个 LLVM 一起重新编译,Pass 代码需要存放在 llvm/lib/Transforms 文件夹中。

第二种方法是通过 CMake 对 Pass 进行单独编译。

第三种方法是使用命令行对 Pass 进行单独编译。

在设计一个新的 LLVM Pass 时,你最先要决定的就是选择 Pass 的类型,LLVM 有多种类型的 Pass 可供选择,包括:ModulePass、FuncitonPass、CallGraphPass、LoopPass等等本课程重点学习 FunctionPass。

FunctionPass 以函数为单位进行处理。 FunctionPass 的子类必须实现 runOnFunction(Function &F) 函数。在 FunctionPass 运行时,会对程序中的每个函数执行runOnFunction 函数

LLVM Pass 的编写:步骤

创建一个类(class),继承 FunctionPass 父类

在创建的类中实现 runOnFunction(Function &F) 函数。向 LLVM 注册我们的 Pass 类。

如果选择基于 CMake 的编译方法,直接使用 CMake 进行编译即可。在 Build 文件夹内可以找到编译好的 so 文件。

使用优化器 opt 将处理中间代码,生成新的中间代码

1
opt -load ./LLVMObfuscator.so -hlw -S hello.ll -o hello_opt.ll

-load 加载编译好的 LLVM Pass(.so文件)进行优化

开始编写

CMake 项目创建

整个项目目录结构如下:

  • Build
  • Test
    • TestProgram.cpp
  • Transforms
    • include
    • src
      • HelloWorld.cpp
    • CMakeLists.txt
  • test.sh

各目录功能介绍

Build 文件夹:存放编译后 LLVM Pass
Test 文件夹:存放测试程序 TestProgram.cpp
Test/TestProgram.cpp:一个简单的 CTF 逆向题

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
 \#include <cstdio>
\#include <cstring>
char input[100] = {0};
char enc[100] = "\x86\x8a\x7d\x87\x93\x8b\x4d\x81\x80\x8a\
\x43\x7f\x49\x49\x86\x71\x7f\x62\x53\x69\x28\x9d";
void encrypt(unsigned char *dest, char *src){
int len = strlen(src);
for(int i = 0;i < len;i ++){
dest[i] = (src[i] + (32 - i)) ^ i;
}
}
//flag{s1mpl3_11vm_d3m0}
int main(){
printf("Please input your flag: ");
scanf("%s", input);
unsigned char dest[100] = {0};
encrypt(dest, input);
bool result = strlen(input) == 22 && !memcmp(dest, enc, 22);
if(result){
printf("Congratulations~\n");
}else{
printf("Sorry try again.\n");
}
}

Transforms/include 文件夹:存放整个 LLVM Pass 项目的头文件,暂时还没有用到
Transforms/src 文件夹:存放整个 LLVM Pass 项目的源代码
Transforms/src/HelloWorld.cpp:HelloWorld Pass 的源代码,一般来说一个 Pass 使用一个 cpp 文件
实现即可。
Transforms/CMakeLists.txt:整个 CMake 项目的配置文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
 \# 参考官方文档:https://llvm.org/docs/CMake.html#developing-llvm-passes-out-of-
source
project(OLLVM++)
cmake_minimum_required(VERSION 3.13.4) //这个项目编译cmake的最低版本
find_package(LLVM REQUIRED CONFIG)
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(AddLLVM)
include_directories("./include") # 包含 ./include 文件夹中的头文件
separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})
add_definitions(${LLVM_DEFINITIONS_LIST})
include_directories(${LLVM_INCLUDE_DIRS})
add_llvm_library( LLVMObfuscator MODULE
src/HelloWorld.cpp
)

test.sh:编译 LLVM Pass 并对 Test 文件夹中的代码进行测试,内容如下

1
2
3
4
5
6
7
8
9
 cd ./Build
cmake ../Transforms
make
cd ../Test
clang -S -emit-llvm TestProgram.cpp -o TestProgram.ll
opt -load ../Build/LLVMObfuscator.so -hlw -S TestProgram.ll -o
TestProgram_hlw.ll
clang TestProgram_hlw.ll -o TestProgram_hlw
./TestProgram_hlw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
LLVM Pass 
d.Bidcae./rnfrsmk
d./etcag- ei-lmTsPormcp- etrga.lot-od./ul/LMbuctrs hw- etrga.l-
etrga_l.lcagTsPormhwl oTsPormhw.TsPormhw#nld lv/ash
icue"lmI/ucinh
icue"lmSpotrwotemh
sn aepc lm
aepc
casDm ulcFntoPs{ ulc
sai hrI; eo):FntoPs(D } olrnnucinFnto F; }


/rnnucin函实
olDm:rnnucinFnto F{/ osmtig}ca eo:D=0
/注该Dm as



ttcRgsePs<eo (xx,"as描.)

LLVM Pass 源代码模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include "llvm/Pass.h"
#include "llvm/IR/Function.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
namespace {
class Demo : public FunctionPass{
public:
static char ID;
Demo() : FunctionPass(ID) {}
bool runOnFunction(Function &F);
};

}
// runOnFunction 函数实现
bool Demo::runOnFunction(Function &F){
// do something
}
char Demo::ID = 0;
// 注册该 DemoPassstatic RegisterPass<Demo> X("xxx", "Pass 描述.")

LLVM IR 指令

终结指令Terminator Instructions

ret指令 函数的返回指令,对应 C/C++ 中的 return

ret ;返回特定类型返回值的return指令
ret void;无返回值的return指令

ret i32 5;返回整数5
ret void ;无返回值
ret { i32,i8 } { i32 4, i8 2 };返回一个结构体

br 指令

br 是”分支”的英文 branch 的缩写,分为非条件分支和条件分支,对应 C/C++ 的 if 语句。无条件分支类似于x86汇编中的 jmp 指令,条件分支类似于x86汇编中的 jnz, je 等条件跳转指令。

br i1, label ,label

br label ;无条件分支

实例:

1
2
3
4
Test:
%cond = icmp eq i32 %a,%b
br i1 %cond,label %IfEqual, label %IfUnequalIfEqual:
ret i32 1IfUnequal:ret i32 0

补充:比较指令 在x86汇编中,条件跳转指令(jnz, je 等)通常与比较指令 cmp, test 等一起出现。

icmp 指令

整数或指针的比较指令。条件 cond 可以是 eq(相等), ne(不相等), ugt(无符号大于)等等。

1
<result> = icmp <cond> <ty> <opl>,<op2> ;比较整数op1和 op2是否满足条件 cond

二元运算Binary Operations

###按位二元运算Bitwise Binary Operations
###内存访问和寻址操作Memory Access and AddressingOperations
###类型转换操作Conversion Operations

###其他操作 Other Operations

代码混淆基本原理

什么是代码混淆

代码混淆是将计算机程序的代码,转换成─种功能上等价,但是难以阅读和理解的形式的行为。

函数

函数是代码混淆的基本单位,一个函数由若干个基本块组成,有且仅有一个入口块,可能有多个出口块。一个函数可以用一个控制流图(Control Flow Graph, CFG)表示。

基本块

基本块由一组线性指令组成,每一个基本块都有一个入口点(第一条执行的指令)和一个出口点(最后一条执行的指令,亦即终结指令)。终结指令要么跳转到另一个基本块(br, switch),要么从函数返回(ret)。

控制流

控制流代表了一个程序在执行过程中可能遍历到的所有路径。通常情况下,程序的控制流很清晰地反映了程序的逻辑,但经过混淆的控制流会使得人们难以分辨正常逻辑。

不透明谓词

不透明谓词指的是其值为混淆者明确知晓,而反混淆者却难以推断的变量。例如混淆者在程序中使用一个恒为0的全局变量,反混淆者难以推断这个变量恒为0。

常见混淆思路

符号混淆

将函数的符号,如函数名、全局变量名去除或者混淆。对于 ELF 文件可以通过 strip 指令去除符号表完成。

未去除符号的程序

去除了符号的程序

控制流混淆

控制流混淆指的是混淆程序正常的控制流,使其在功能保持不变的情况下不能清晰地反映原程序的正常逻辑。本课程中要学习的控制流混淆有:控制流平坦化、虚假控制流、随机控制流。

未经控制流混淆的控制流图

经过控制流混淆的控制流图

计算混淆

计算混淆指的是混淆程序的计算流程,或计算流程中使用的数据,使分析者难以分辨某一段代码所执行的具体计算。本课程中要学习的计算混淆有:指令替代、常量替代。

虚拟机混淆

虚拟机混淆的思想是将一组指令集合(如一组 x86 指令),转化为一组攻击者未知的自定义指令集。并用与程序绑定的解释器解释执行。虚拟机混淆代表:VMProtect。 虚拟机混淆是目前最强力的混淆,但也有许多缺点:如性能损耗大、容易被杀毒软件报毒等。

经典代码混淆工具OLLVM初体验

OLLVM 介绍

Obfuscator-LLVM(简称OLLVM)是2010年6月由瑞士西部应用科学大学(HEIG-VD)的信息安全小组发
起的一个项目。 这个项目的目的是提供一个 LLVM 编译套件的开源分支,能够通过代码混淆和防篡改提
供更高的软件安全性。
OLLVM 提供了三种经典的代码混淆:

  • 控制流平坦化 Control Flow Flattening
  • 虚假控制流 Bogus Control Flow
  • 指令替代 Instruction Subsititution

OLLVM 在国内移动安全的使用非常广泛,虽然 OLLVM 已于2017年停止更新,并且到目前为止,三种
代码混淆方式均已有成熟的反混淆手段 。但是 OLLVM 的各种变体仍然在被开发和使用(如王者荣耀的
某个so文件),OLLVM 至今仍有很大的学习价值。


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 1204342476@qq.com

💰

×

Help us with donation