首页
社区
课程
招聘
[原创]基于LLVM Pass实现控制流平坦化
发表于: 2021-2-20 09:20 22140

[原创]基于LLVM Pass实现控制流平坦化

2021-2-20 09:20
22140

提到代码混淆时,我首先想到的是著名的代码混淆工具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.04gcc (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函数在给定位置将一个基本块分为两个,并且在第一个基本块的末尾加上一个非条件跳转:
图片描述
有关isacast两个泛型函数的用法,参考上面提到的重要API文档:
图片描述
接下来创建循环的循环头和循环尾,注意到新创建的基本块是被插入到firstBB前面的,所以还需要把firstBB移回顶部:

对第一个基本块做一些处理,主要包括去除第一个基本块原来的跳转,插入初始化switch on变量的指令,插入新的跳转使其进入循环:

在loopEntry中插入load指令,load指令类似于C语言里的指针取值:

创建循环内的switch。这里swVar是LoadInst类型,它被当做switch on的变量传入了SwitchInst的构造函数,在LLVM Pass中,常数(Constant)、参数(Argument)、指令(Instruction)和函数(Function)都有一个共同的父类ValueValue 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
#include <cstdio>
 
void func1(){}
 
void func2(){}
 
int main(){
    puts("Hello!");
}
#include <cstdio>
 
void func1(){}
 
void func2(){}
 
int main(){
    puts("Hello!");
}
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
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");
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
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:
 
 
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/Instructions.h"
#include <vector>
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");
#include "llvm/IR/Function.h"
#include "llvm/Pass.h"
#include "llvm/Support/raw_ostream.h"
#include "llvm/IR/Instructions.h"
#include <vector>
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();

[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2021-2-20 09:26 被34r7hm4n编辑 ,原因:
收藏
免费 31
支持
分享
打赏 + 3.00雪花
打赏次数 2 雪花 + 3.00
 
赞赏  demoscene   +1.00 2021/02/25
赞赏  kanxue   +2.00 2021/02/20 感谢分享~
最新回复 (18)
雪    币: 6
活跃值: (1141)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
学习了,可以做一个系列,加油
2021-2-20 09:26
0
雪    币: 401
活跃值: (2034)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
3
学习了,谢谢楼主
2021-2-20 11:32
0
雪    币: 66
活跃值: (2746)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
非常感谢!!!
2021-2-20 14:01
0
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
5
想问一下。LLVM如何开辟一块栈内存把Value*写入进去。万分感谢
2021-2-23 05:18
0
雪    币: 4168
活跃值: (15932)
能力值: ( LV9,RANK:710 )
在线值:
发帖
回帖
粉丝
6
太强了
2021-2-23 23:02
0
雪    币: 14303
活跃值: (10776)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
7
KomiMoe_ 想问一下。LLVM如何开辟一块栈内存把Value*写入进去。万分感谢
可以用AllocaInst和StoreInst实现,这两个指令最后转换成二进制文件就相当于定义了一个局部变量
2021-2-23 23:33
0
雪    币: 2
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
34r7hm4n 可以用AllocaInst和StoreInst实现,这两个指令最后转换成二进制文件就相当于定义了一个局部变量
谢谢。我是这么写的。编译也通过了。但是编译的时候报错AllocaInst Ptr must be a pointer to Val type!
2021-2-26 02:11
0
雪    币: 545
活跃值: (370)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
9
感谢分享,内容扎实,可实践性很强
2021-3-1 10:00
0
雪    币: 6911
活跃值: (9069)
能力值: ( LV17,RANK:797 )
在线值:
发帖
回帖
粉丝
10
FunctionPass *lower = createLowerSwitchPass();
lower->runOnFunction(*f);

这个是好东西,可以让你循环膨胀
2021-3-16 17:13
0
雪    币: 14303
活跃值: (10776)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
11
无名侠 FunctionPass *lower = createLowerSwitchPass(); lower->runOnFunction(*f); 这个是好东西,可以让你循环膨胀
学到,突然想起OLLVM源码也有这个玩意,当时没深究
2021-3-17 09:54
0
雪    币: 986
活跃值: (6167)
能力值: ( LV7,RANK:115 )
在线值:
发帖
回帖
粉丝
12
师傅是不是缺了fixStack(f)
2021-5-30 20:49
0
雪    币: 14303
活跃值: (10776)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
13

误发

最后于 2021-7-27 17:59 被34r7hm4n编辑 ,原因:
2021-7-27 17:57
0
雪    币: 14303
活跃值: (10776)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
14
无名侠 FunctionPass *lower = createLowerSwitchPass(); lower->runOnFunction(*f); 这个是好东西,可以让你循环膨胀
师傅最近有用过这个函数吗?
我在自己的Pass里调用LowerSwitchPass会崩溃(LLVM12.0):
代码:
bool Flattening::runOnFunction(Function &F){
    FunctionPass *pass = createLowerSwitchPass();
    return pass->runOnFunction(F);
}
报错:
[100%] Built target LLVMFlattening
PLEASE submit a bug report to https://bugs.llvm.org/ and include the crash backtrace.
Stack dump:
0.      Program arguments: opt -load ../Transforms/build/Flattening/LLVMFlattening.so -fla -S TestProgram.ll -o TestProgram_fla.ll
1.      Running pass 'Function Pass Manager' on module 'TestProgram.ll'.
2.      Running pass 'My obfuscating pass' on function '@_Z7encryptPhPc'
 #0 0x00007f56f5c38efc llvm::sys::PrintStackTrace(llvm::raw_ostream&, int) (/home/ctf/Desktop/llvm-project/build/bin/../lib/libLLVMSupport.so.12+0x198efc)
 #1 0x00007f56f5c36ca4 llvm::sys::RunSignalHandlers() (/home/ctf/Desktop/llvm-project/build/bin/../lib/libLLVMSupport.so.12+0x196ca4)
 #2 0x00007f56f5c36e13 SignalHandler(int) (/home/ctf/Desktop/llvm-project/build/bin/../lib/libLLVMSupport.so.12+0x196e13)
 #3 0x00007f56f5365040 (/lib/x86_64-linux-gnu/libc.so.6+0x3f040)
 #4 0x00007f56f6bdde94 (anonymous namespace)::LowerSwitchLegacyPass::runOnFunction(llvm::Function&) (/home/ctf/Desktop/llvm-project/build/bin/../lib/libLLVMTransformUtils.so.12+0x17ee94)
 #5 0x00007f56f140e773 (anonymous namespace)::Flattening::runOnFunction(llvm::Function&) (../Transforms/build/Flattening/LLVMFlattening.so+0xf773)
 #6 0x00007f56f6071587 llvm::FPPassManager::runOnFunction(llvm::Function&) (/home/ctf/Desktop/llvm-project/build/bin/../lib/libLLVMCore.so.12+0x1ea587)
 #7 0x00007f56f6071ca1 llvm::FPPassManager::runOnModule(llvm::Module&) (/home/ctf/Desktop/llvm-project/build/bin/../lib/libLLVMCore.so.12+0x1eaca1)
 #8 0x00007f56f60708ff llvm::legacy::PassManagerImpl::run(llvm::Module&) (/home/ctf/Desktop/llvm-project/build/bin/../lib/libLLVMCore.so.12+0x1e98ff)
 #9 0x00005637785eb262 main (/home/ctf/Desktop/llvm-project/build/bin/opt+0x1b262)
#10 0x00007f56f5347bf7 __libc_start_main /build/glibc-S9d2JN/glibc-2.27/csu/../csu/libc-start.c:344:0
#11 0x00005637785ebb5a _start (/home/ctf/Desktop/llvm-project/build/bin/opt+0x1bb5a)
./test.sh: line 7: 98152 Segmentation fault      (core dumped) opt -load ../Transforms/build/Flattening/LLVMFlattening.so -fla -S TestProgram.ll -o TestProgram_fla.ll
clang-12: error: no such file or directory: 'TestProgram_fla.ll'
clang-12: error: no input files
2021-7-27 17:59
0
雪    币: 6911
活跃值: (9069)
能力值: ( LV17,RANK:797 )
在线值:
发帖
回帖
粉丝
15
34r7hm4n 师傅最近有用过这个函数吗? 我在自己的Pass里调用LowerSwitchPass会崩溃(LLVM12.0): 代码: bool Flattening::runOnFunction(Functi ...
呜呜呜 ,没有
2021-7-28 00:42
0
雪    币: 330
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
16
大佬太强了,来学习
2021-8-4 21:10
0
雪    币: 14303
活跃值: (10776)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
17
Ssssone 师傅是不是缺了fixStack(f)[em_87]
对滴,我另开了一篇文章解释:
https://bbs.pediy.com/thread-268789.htm
2021-8-6 11:10
0
雪    币: 817
活跃值: (1611)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
18
34r7hm4n 师傅最近有用过这个函数吗? 我在自己的Pass里调用LowerSwitchPass会崩溃(LLVM12.0): 代码: bool Flattening::runOnFunction(Functi ...
师傅解决了吗
2022-11-10 17:27
0
雪    币: 10
活跃值: (1168)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
19
师傅我问下,你的这个代码我在windows上使用编译成exe并没有查看CFG并没有平坦化,不知道是我编译的时候的问题还是?
2024-4-5 13:44
0
游客
登录 | 注册 方可回帖
返回
//