OLLVM混淆学习(1)——控制流平坦化(FLA)
0x01 控制流平坦化基本介绍
控制流平坦化是指将正常程序控制流中基本块之间的跳转关练删除,用一个集中的主分发块来调度基本块的执行顺序。相当于把原有程序正常的逻辑改为一个循环嵌套一个switch的逻辑。
正常情况:
控制流平坦化之后:
控制流平坦化的基本结构如下:
- 入口块:进入函数第一个执行的基本块
- 分发块:负责跳转到下一个要执行的原基本块
- 原基本块:混淆之前的基本块,实际完成程序工作的基本块
- 返回块:返回到主分发块
修改了程序的控制流,导致逆向分析人员不容易直接的理清程序执行流程,增加分析难度。
0x02 实现方式
本节以https://github.com/bluesadi/Pluto-Obfuscator/tree/kanxue 项目为基准进行分析。主要的代码实现分为五大块如下图所示:
1. 保存原基本块
将除入口块以外的以外的基本块保存到 vector 容器中,方便后续处理。如果入口块的终结指令是条件分支指令,则将该指令单独分离出来作为一个基本块,加入到 vector 容器的最前面。
// 将除入口块(第一个基本块)以外的基本块保存到一个 vector 容器中,便于后续处理
// 首先保存所有基本块
vector<BasicBlock*> origBB;
for(BasicBlock &BB: F){
origBB.push_back(&BB);
}
// 从vector中去除第一个基本块
origBB.erase(origBB.begin());
BasicBlock &entryBB = F.getEntryBlock();
// 如果第一个基本块的末尾是条件跳转,单独分离
if(BranchInst *br = dyn_cast<BranchInst>(entryBB.getTerminator())){
if(br->isConditional()){
BasicBlock *newBB = entryBB.splitBasicBlock(br, "newBB");
origBB.insert(origBB.begin(), newBB);
}
}
2. 创建分发块和返回块
除了原基本块之外,我们还要续创建一个分发块来调度基本块的执行顺序。并建立入口块到分发块的绝对跳转。再创建一个返回块,原基本块执行完后都需要跳转到这个返回块,返回块会直接跳转到分发块进行下一次的基本块跳转。
// 创建分发块和返回块
BasicBlock *dispatchBB = BasicBlock::Create(*CONTEXT, "dispatchBB", &F, &entryBB);
BasicBlock *returnBB = BasicBlock::Create(*CONTEXT, "returnBB", &F, &entryBB);
BranchInst::Create(dispatchBB, returnBB);
entryBB.moveBefore(dispatchBB);
// 去除第一个基本块末尾的跳转
entryBB.getTerminator()->eraseFromParent();
// 使第一个基本块跳转到dispatchBB
BranchInst *brDispatchBB = BranchInst::Create(dispatchBB, &entryBB);
3. 实现分发块调度
在入口块中创建并初始化 switch 要使用的变量,在调度块中插入switch-case 指令实现分发功能。将原基本块移动到返回块之前,并给每一个原基本块分配随机的 case 值,并将其添加到 switch 指令的对应case分支中。
// 在入口块插入alloca和store指令创建并初始化switch变量,初始值为随机值
int randNumCase = rand();
AllocaInst *swVarPtr = new AllocaInst(TYPE_I32, 0, "swVar.ptr", brDispatchBB);
new StoreInst(CONST_I32(randNumCase), swVarPtr, brDispatchBB);
// 在分发块插入load指令读取switch变量
LoadInst *swVar = new LoadInst(TYPE_I32, swVarPtr, "swVar", false, dispatchBB);
// 在分发块插入switch指令实现基本块的调度
BasicBlock *swDefault = BasicBlock::Create(*CONTEXT, "swDefault", &F, returnBB);
BranchInst::Create(returnBB, swDefault);
SwitchInst *swInst = SwitchInst::Create(swVar, swDefault, 0, dispatchBB);
// 将原基本块插入到返回块之前,并分配case值
for(BasicBlock *BB : origBB){
BB->moveBefore(returnBB);
swInst->addCase(CONST_I32(randNumCase), BB);
randNumCase = rand();
}
4. 实现调度变量自动调整
在每个原基本块最后添加修改 switch 要使用的变量值的指令,以便返回分发块之后,能够正确执行到下一个基本块。删除原基本块末尾的跳转,使其结束执行后跳转到返回块,这一步需要注意判断原基本块末尾跳转的语句。(类似于VMP3每一个handler的末尾指定下一个要跳转的handler)
// 在每个基本块最后添加修改switch变量的指令和跳转到返回块的指令
for(BasicBlock *BB : origBB){
// retn BB
if(BB->getTerminator()->getNumSuccessors() == 0){
continue;
}
// 非条件跳转
else if(BB->getTerminator()->getNumSuccessors() == 1){
BasicBlock *sucBB = BB->getTerminator()->getSuccessor(0);
BB->getTerminator()->eraseFromParent();
ConstantInt *numCase = swInst->findCaseDest(sucBB);
new StoreInst(numCase, swVarPtr, BB);
BranchInst::Create(returnBB, BB);
}
// 条件跳转
else if(BB->getTerminator()->getNumSuccessors() == 2){
ConstantInt *numCaseTrue = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0));
ConstantInt *numCaseFalse = swInst->findCaseDest(BB->getTerminator()->getSuccessor(1));
BranchInst *br = cast<BranchInst>(BB->getTerminator());
SelectInst *sel = SelectInst::Create(br->getCondition(), numCaseTrue, numCaseFalse, "", BB->getTerminator());
BB->getTerminator()->eraseFromParent();
new StoreInst(sel, swVarPtr, BB);
BranchInst::Create(returnBB, BB);
}
}
当原基本块出现switch-case等大于2个分支的情况时,我们可以在优化过程中使用lowerswitch将其变成只有2个及以下分支的状态,使用命令如下:
opt -lowerswitch -S TestProgram_orig.ll -o TestProgram_lowerswitch.ll
关于lowerswitch的使用也可以直接在代码中实现,该项目是在LLVM12.0.1中编译的,所以不能直接使用createLowerSwitchPass函数否则会导致崩溃,应该在Flattening中添加如下函数
void getAnalysisUsage(AnalysisUsage &AU) const override{
errs() << "Require LowerSwitchPass\r\n";
AU.addRequiredID(LowerSwitchID);
FunctionPass::getAnalysisUsage(AU);
}
5. 修复PHI指令和逃逸变量
PHI 指令的值由前驱块决定,平坦化后所有原基本块的前驱块都变成了分发块,因此 PHI 指令发生了损坏。
逃逸变量指在一个基本块中定义,并且在另一个基本块被引用的变量。在原程序中某些基本块可能引用之前某个基本块中的变量,平坦化后原基本块之间不存在确定的前后关系了(由分发块决定),因此某些变量的引用可能会损坏。
修复的方法是,将 PHI 指令和逃逸变量都转化为内存存取指令。
void fixStack(Function &F) {
vector<PHINode*> origPHI;
vector<Instruction*> origReg;
BasicBlock &entryBB = F.getEntryBlock();
// 搜索PHI指令和逃逸变量添加到对应vector容器
for(BasicBlock &BB : F){
for(Instruction &I : BB){
if(PHINode *PN = dyn_cast<PHINode>(&I)){
origPHI.push_back(PN);
}else if(!(isa<AllocaInst>(&I) && I.getParent() == &entryBB)
&& I.isUsedOutsideOfBlock(&BB)){
origReg.push_back(&I);
}
}
}
for(PHINode *PN : origPHI){
DemotePHIToStack(PN, entryBB.getTerminator());
}
for(Instruction *I : origReg){
DemoteRegToStack(*I, entryBB.getTerminator());
}
}
0x03 参考链接
https://security.tencent.com/index.php/blog/msg/112
https://www.kanxue.com/book-88-2111.htm
https://github.com/bluesadi/Pluto-Obfuscator/blob/kanxue/Transforms/src/Flattening.cpp