提到代码混淆时,我首先想到的是著名的代码混淆工具OLLVM 。OLLVM(Obfuscator-LLVM)是瑞士西北应用科技大学安全实验室于2010年6月份发起的一个项目,该项目旨在提供一套开源的基于LLVM的代码混淆工具,以增加逆向工程的难度。
OLLVM的核心功能,也就是代码混淆,基于LLVM的一个重要框架——LLVM Pass 。简单来说,LLVM Pass可以对代码编译的结果产生影响,以达到优化、混淆等目的。本文的主要内容即是讲解基于LLVM Pass框架的代码混淆方法,以及动手实现一个简易的控制流平坦化混淆。
在学习LLVM Pass之前,我们有必要对LLVM有一些简单的了解。简单来说,我们可以把LLVM看成一个先进的编译器。
传统编译器(比如我们熟悉的GCC)的工作原理基本上都是三段式的,可以分为前端(Frontend)、优化器(Optimizer)、后端(Backend)。前端负责解析源代码,将其翻译为抽象的语法树(Abstract Syntax Tree);优化器对这一中间代码进行优化;后端则负责将优化器优化后的中间代码转换为目标机器的代码。
LLVM本质上还是三段式,但LLVM框架不同语言的前端,会产生语言无关 的的中间代码LLVM Intermediate Representation (LLVM IR )。优化器对LLVM IR进行处理,产生新的LLVM IR,最后后端将LLVM IR转化为目标平台的机器码。其过程如下图所示:
这样设计的好处是当我们需要新支持一种语言时,由于统一的中间代码LLVM IR的存在,我们只需要实现该语言的前端即可,可拓展性极强。并且我们可以通过LLVM提供的一系列丰富的函数库操控LLVM IR的生成过程,我们用来操控LLVM IR生成过程的框架被叫做LLVM Pass ,官方文档称其为“where most of the interesting parts of the compiler exist”,可见其功能之强大。
还有一个容易混淆的点是Clang与LLVM的关系,可以用一张图来解释: 更详细的内容可以看:深入浅出让你理解什么是LLVM
我们的第一个目标是让我们写的LLVM Pass能够顺利运行,之后的工作无非是往我们的Pass里不断添加内容罢了。
首先我们需要从官网下载LLVM Project中LLVM和Clang部分的源码,将其放在同一个目录,编译。这个过程可以参考知乎上的一个教程——LLVM Pass入门导引 以及官方文档,这里不再赘述了。我的编译环境是Ubuntu 18.04 、gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0 。
官方的教程Writing an LLVM Pass 中提供了一个示例,它的作用是打印所有函数的名称:test.sh
test.cpp
然而示例代码还是有点复杂,可以把它的代码简化一下,方便我们理解:Obfu.cpp
写好之后重新编译LLVM,之前编译过的话重新编译的速度会很快,然后运行一下我们写的Pass: OK!这样后续的混淆功能只需要在这个框架上添加行了。
控制流平坦化(Control Flow Flattening)的基本思想主要是通过一个主分发器来控制程序基本块的执行流程,例如下图是正常的执行流程: 经过控制流平坦化后的执行流程就如下图: 可以看到除了基本块1外,其他的基本块都集中到了同一个层次上。不同基本块的调度顺序由主分发器决定(在程序里可以看做一个switch,不同的基本块就对应不同的case)。这样可以模糊基本块之间的前后关系,增加程序分析的难度。
控制流平坦化的过程相当于把原有程序正常的逻辑改为一个循环嵌套一个switch 的逻辑。
以上图代表的程序为例,执行完基本块1后程序进入主分发器,然后执行基本块2对应的case。在原逻辑中基本块2的末尾是一个条件跳转 ,可以跳转到基本块3或者基本块4,在平坦化中基本块2的末尾会根据原有跳转的条件修改switch变量的值,使其接下来能执行到基本块3或者基本块4对应的case,然后返回主分发器(即进入下一个循环)。
如果是非条件跳转的话,比如基本块5到基本块6,在基本块5的末尾修改switch变量的值,使下一个循环中switch能到达基本块6对应的case即可。
用伪代码表示,未混淆的逻辑是这样:
控制流平坦化的逻辑是这样:
LLVM Pass的所有操作都是基于LLVM IR的,因此你需要对LLVM IR 有所了解:LLVM IR Tutorial LLVM Pass的一些重要API也很有必要看一看:LLVM Programmer’s Manual
控制流平坦化的实现代码我参考的是:OLLVM控制流平坦化源代码
首先把函数的定义移到外面去,让重点更突出一点,现在我们只需要关注flatten函数的实现就可以了:
首先遍历函数中所有基本块,将其存到一个vector中:
根据平坦化的基本思想,第一个基本块是要单独拿出来处理的:
如果第一个基本块的末尾是一个条件分支,则把条件跳转的两个IR指令 (类似于汇编里的cmp和jmp)单独分离出来作为一个基本块,方便与非条件跳转统一处理:
这里出现了一个函数splitBasicBlock ,如果你想知道这个函数到底做了什么操作,可以直接阅读源码内的注释,其他函数也是一样。简而言之,splitBasicBlock 函数在给定位置将一个基本块分为两个,并且在第一个基本块的末尾加上一个非条件跳转: 有关isa 和cast 两个泛型函数的用法,参考上面提到的重要API文档: 接下来创建循环的循环头和循环尾,注意到新创建的基本块是被插入到firstBB 前面的,所以还需要把firstBB 移回顶部:
对第一个基本块做一些处理,主要包括去除第一个基本块原来的跳转,插入初始化switch on变量的指令,插入新的跳转使其进入循环:
在loopEntry中插入load指令,load指令类似于C语言里的指针取值:
创建循环内的switch。这里swVar是LoadInst 类型,它被当做switch on的变量传入了SwitchInst的构造函数,在LLVM Pass中,常数(Constant)、参数(Argument)、指令(Instruction)和函数(Function)都有一个共同的父类Value 。Value class是LLVM Pass很重要的基类(参见:The Value class ):
创建完switch之后,插入原基本块到switch中,注意这里仅是位置意义上 的插入,而不是逻辑意义上的:
接下来要从逻辑意义上往switch中插入基本块了,即添加新的case。 所有基本块按后继基本块的数量分成了三类:
实现代码如下:
至此整个平坦化的过程就已经完成了。
编译,运行test.sh测试:
IDA打开,查看CFG: 是不是有内味了: F5查看伪代码,可以看到IDA并没有像我们预想的那样识别出switch。经过我的测试如果switch on的变量很规律(比如1,2,3,4,5,6...),IDA就能准确识别出switch,如果是随机数则不行,所以随机数的混淆效果比单纯的递增要好:
obfu.cpp
Rimao大佬的文章:基于LLVM的控制流平坦化的魔改和混淆Pass实战
.
/
build
/
bin
/
clang
-
c
-
emit
-
llvm test.cpp
-
o test.bc
.
/
build
/
bin
/
opt
-
load .
/
build
/
lib
/
LLVMHello.so
-
hello test.bc
-
o
/
dev
/
null
.
/
build
/
bin
/
clang
-
c
-
emit
-
llvm test.cpp
-
o test.bc
.
/
build
/
bin
/
opt
-
load .
/
build
/
lib
/
LLVMHello.so
-
hello test.bc
-
o
/
dev
/
null
void func1(){}
void func2(){}
int
main(){
puts(
"Hello!"
);
}
void func1(){}
void func2(){}
int
main(){
puts(
"Hello!"
);
}
using namespace llvm;
namespace{
struct Obfu : public FunctionPass{
static char
ID
;
Obfu() : FunctionPass(
ID
){}
bool
runOnFunction(Function &F) override{
outs() <<
"Function: "
<< F.getName() <<
"\n"
;
return
false;
}
};
}
char Obfu::
ID
=
0
;
static RegisterPass<Obfu> X(
"obfu"
,
"My obfuscating pass"
);
using namespace llvm;
namespace{
struct Obfu : public FunctionPass{
static char
ID
;
Obfu() : FunctionPass(
ID
){}
bool
runOnFunction(Function &F) override{
outs() <<
"Function: "
<< F.getName() <<
"\n"
;
return
false;
}
};
}
char Obfu::
ID
=
0
;
static RegisterPass<Obfu> X(
"obfu"
,
"My obfuscating pass"
);
基本块
1
基本块
2
if
(condition){
基本块
3
}
else
{
基本块
4
}
基本块
5
基本块
6
基本块
1
基本块
2
if
(condition){
基本块
3
}
else
{
基本块
4
}
基本块
5
基本块
6
基本块
1
switchVar
=
2
;
while
(true){
switch(switchVar){
case
2
:
基本块
2
switchVar
=
condition ?
3
:
4
;
case
3
:
基本块
3
switchVar
=
5
case
4
:
基本块
4
switchVar
=
5
case
5
:
基本块
5
switchVar
=
6
case
6
:
基本块
6
goto end;
}
}
end:
基本块
1
switchVar
=
2
;
while
(true){
switch(switchVar){
case
2
:
基本块
2
switchVar
=
condition ?
3
:
4
;
case
3
:
基本块
3
switchVar
=
5
case
4
:
基本块
4
switchVar
=
5
case
5
:
基本块
5
switchVar
=
6
case
6
:
基本块
6
goto end;
}
}
end:
using namespace llvm;
namespace{
struct Obfu : public FunctionPass{
static char
ID
;
Obfu() : FunctionPass(
ID
){}
bool
flatten(Function
*
f);
bool
runOnFunction(Function &F);
};
}
bool
Obfu::runOnFunction(Function &F){
return
flatten(&F);
}
bool
Obfu::flatten(Function
*
f){
}
char Obfu::
ID
=
0
;
static RegisterPass<Obfu> X(
"obfu"
,
"My obfuscating pass"
);
using namespace llvm;
namespace{
struct Obfu : public FunctionPass{
static char
ID
;
Obfu() : FunctionPass(
ID
){}
bool
flatten(Function
*
f);
bool
runOnFunction(Function &F);
};
}
bool
Obfu::runOnFunction(Function &F){
return
flatten(&F);
}
bool
Obfu::flatten(Function
*
f){
}
char Obfu::
ID
=
0
;
static RegisterPass<Obfu> X(
"obfu"
,
"My obfuscating pass"
);
/
/
遍历函数所有基本块,将其存到vector中
vector<BasicBlock
*
> origBB;
for
(BasicBlock &BB:
*
f){
origBB.push_back(&BB);
}
/
/
基本块数量不超过
1
则无需平坦化
if
(origBB.size() <
=
1
){
return
false;
}
/
/
遍历函数所有基本块,将其存到vector中
vector<BasicBlock
*
> origBB;
for
(BasicBlock &BB:
*
f){
origBB.push_back(&BB);
}
/
/
基本块数量不超过
1
则无需平坦化
if
(origBB.size() <
=
1
){
return
false;
}
/
/
从vector中去除第一个基本块
origBB.erase(origBB.begin());
BasicBlock
*
firstBB
=
&f
-
>front();
/
/
从vector中去除第一个基本块
origBB.erase(origBB.begin());
BasicBlock
*
firstBB
=
&f
-
>front();
/
/
如果第一个基本块的末尾是条件跳转
if
(isa<BranchInst>(firstBB
-
>getTerminator())){
BranchInst
*
br
=
cast<BranchInst>(firstBB
-
>getTerminator());
if
(br
-
>isConditional()){
CmpInst
*
cmpInst
=
cast<CmpInst>(firstBB
-
>getTerminator()
-
>getPrevNode());
BasicBlock
*
newBB
=
firstBB
-
>splitBasicBlock(cmpInst,
"newBB"
);
origBB.insert(origBB.begin(), newBB);
}
}
/
/
如果第一个基本块的末尾是条件跳转
if
(isa<BranchInst>(firstBB
-
>getTerminator())){
BranchInst
*
br
=
cast<BranchInst>(firstBB
-
>getTerminator());
if
(br
-
>isConditional()){
CmpInst
*
cmpInst
=
cast<CmpInst>(firstBB
-
>getTerminator()
-
>getPrevNode());
BasicBlock
*
newBB
=
firstBB
-
>splitBasicBlock(cmpInst,
"newBB"
);
origBB.insert(origBB.begin(), newBB);
}
}
/
/
创建循环
BasicBlock
*
loopEntry
=
BasicBlock::Create(f
-
>getContext(),
"loopEntry"
, f, firstBB);
BasicBlock
*
loopEnd
=
BasicBlock::Create(f
-
>getContext(),
"loopEnd"
, f, firstBB);
firstBB
-
>moveBefore(loopEntry);
/
/
创建循环
BasicBlock
*
loopEntry
=
BasicBlock::Create(f
-
>getContext(),
"loopEntry"
, f, firstBB);
BasicBlock
*
loopEnd
=
BasicBlock::Create(f
-
>getContext(),
"loopEnd"
, f, firstBB);
firstBB
-
>moveBefore(loopEntry);
/
/
去除第一个基本块末尾的跳转
firstBB
-
>getTerminator()
-
>eraseFromParent();
/
/
用随机数初始化switch on变量
srand(time(
0
));
int
randNumCase
=
rand();
AllocaInst
*
swVarPtr
=
new AllocaInst(int32Type,
0
,
"swVar.ptr"
, firstBB);
new StoreInst(ConstantInt::get(int32Type, randNumCase), swVarPtr, firstBB);
/
/
使第一个基本块跳转到loopEntry
BranchInst::Create(loopEntry, firstBB);
/
/
去除第一个基本块末尾的跳转
firstBB
-
>getTerminator()
-
>eraseFromParent();
/
/
用随机数初始化switch on变量
srand(time(
0
));
int
randNumCase
=
rand();
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2021-2-20 09:26
被34r7hm4n编辑
,原因: