从CFG图中分析ollvm控制流平坦化混淆源码
本文中的代码是将ollvm的flatten以插件形式移植到了llvm18 new pass中, 代码会略有不同, 但逻辑是一样的。
1. 概览
控制流平坦化(control flow flattening)是作用于控制流图的代码混淆技术,其基本思想是重新组织函数的控制流图中的基本块关系,通过插入一个 “主分发器” 来控制基本块的执行流程:
简单来说就是把每个basicblock都转为了switch中的case, 抹去了原分支逻辑关系。
比如以下代码:
#include <stdio.h>
int main(int argc, const char* argv[])
{
if (argc > 2)
puts("argc > 2");
else
puts("argc <= 2");
return 0;
}
看下其IR和汇编层面的CFG图:
原逻辑是很清晰的, 满足if就走右分支, 不满足就走左分支, 即使在汇编层面的CFG图中也具有清楚的逻辑。
但是被flatten了之后就看不出原分支逻辑了, 从IR的CFG图中可以看出原逻辑被转为了一个switch, 比如以上代码逻辑会被转为类似于以下代码的逻辑:
DWORD switch_var = 0xd35df05b;
while(TRUE)
{
switch(switch_var)
{
case 0xd35df05b
//...
switch_var = 0x123;
break;
case 0x345:
exit(0);
break;
case 0x123:
//...
switch_var = 0x345;
break;
default:
break;
}
}
2. 分析源代码
下面我们将把关键阶段的LLVM IR转为CFG图进行更直观的分析(以下都以bb代指baiscblock)。
我们以下面的CFG图为例, 这张图表示是一个if衔接了一个switch:
-
首先调用了LowerSwitchPass这个Pass将一个Function中全部的switch语句转为if-else语句:
// Lower switch
LowerSwitchPass lower;
lower.run(F, FAM);
看下转换后的CFG图, switch结构被改变了:
-
然后将所有bb保存在origBB这个vector中, 并去除第一个bb(entry基本块):
// Save all original BB
for (Function::iterator i = f->begin(); i != f->end(); ++i) {
BasicBlock *tmp = &*i;
origBB.push_back(tmp);
BasicBlock *bb = &*i;
if (isa<InvokeInst>(bb->getTerminator())) {
return false;
}
}
// Remove first BB
origBB.erase(origBB.begin());
并且判断结尾是否是InvokeInst指令, 是就返回false了. Invoke指令是异常处理相关的, 也就是说标准ollvm是不支持异常处理的。
-
如果entry块是以if结尾的, 则把if分割出来(cmp和br指令), 因为flatten的逻辑是entry块后面直接接LoopEntry块, 进入分发逻辑. 所以如果entry块的最后一个指令是if的话, 就有两个后继节点了, 必须转为一个后继节点:
// Get a pointer on the first BB
Function::iterator tmp = f->begin(); //++tmp;
BasicBlock *insert = &*tmp;
// If main begin with an if
BranchInst *br = NULL;
if (isa<BranchInst>(insert->getTerminator())) {
br = cast<BranchInst>(insert->getTerminator());
}
if ((br != NULL && br->isConditional()) ||
insert->getTerminator()->getNumSuccessors() > 1) {
BasicBlock::iterator i = insert->end();
--i;
if (insert->size() > 1)
--i;
BasicBlock *tmpBB = insert->splitBasicBlock(i, "first");
origBB.insert(origBB.begin(), tmpBB);
}
其中i = insert->end();–i;把插入点提到了icmp指令处, 看下转换后的CFG图, entry块的icmp和br被分割了出来放到了first块中:
-
之后删除entry块的br无条件跳转:
// Remove jump
insert->getTerminator()->eraseFromParent();
看下CFG图, entry末尾的br被删掉了, 成为了独立的块:
-
之后用llvm::cryptoutils->scramble32(0, scrambling_key)生成了一个随机数当作switch变量的初始值, cryptoutils是ollvm自己写的一个工具类. 然后创建了一个loopEntry的bb, 一个loopEnd的bb, 并设置entry块跳转到loopEntry, 设置loopEnd跳转到loopEntry. 然后创建了一个switchDefault块, 并设置该块跳转到loopEnd. 然后创建了一个switch指令并插入到loopEntry块中。
// Create switch variable and set as it
Type *I32Ty = Type::getInt32Ty(F.getContext());
switchVar = new AllocaInst(I32Ty, 0, "switchVar", insert);
new StoreInst(
ConstantInt::get(I32Ty, llvm::cryptoutils->scramble32(0, scrambling_key)),
switchVar, insert);
// Create main loop
loopEntry = BasicBlock::Create(f->getContext(), "loopEntry", f, insert);
loopEnd = BasicBlock::Create(f->getContext(), "loopEnd", f, insert);
load = new LoadInst(I32Ty, switchVar, "switchVar", loopEntry);
// Move first BB on top
insert->moveBefore(loopEntry);
BranchInst::Create(loopEntry, insert);
// loopEnd jump to loopEntry
BranchInst::Create(loopEntry, loopEnd);
BasicBlock *swDefault =
BasicBlock::Create(f->getContext(), "switchDefault", f, loopEnd);
BranchInst::Create(loopEnd, swDefault);
// Create switch instruction itself and set condition
switchI = SwitchInst::Create(&*f->begin(), swDefault, 0, loopEntry);
switchI->setCondition(load);
这时的逻辑如下CFG图:
-
然后遍历之前保存的origBB(就是上图右边蓝色的所有基本块), 并生成switch随机的case值, 并把这些基本块放入到switch中:
// Put all BB in the switch
for (std::vector<BasicBlock *>::iterator b = origBB.begin(); b != origBB.end(); ++b) {
BasicBlock *i = *b;
ConstantInt *numCase = NULL;
// Move the BB inside the switch (only visual, no code logic)
i->moveBefore(loopEnd);
// Add case to switch
numCase = cast<ConstantInt>(ConstantInt::get(
switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(switchI->getNumCases(), scrambling_key)));
switchI->addCase(numCase, i);
}
此时的CFG图, 其实就是比上一步的CFG图让loopEntry块连接到了各个bb上:
-
然后修正各个bb(case块)的连接关系, ①如果是ret块(就是没有后继的块比如exit(0))则不管 ②如果是无条件跳转块, 则在此块的末尾添加一个Store指令把switch变量的值设置成其后继块的switch变量值, 然后跳转到loopEntry块 ③如果是条件跳转块, 则插入一个br指令设置不同的switch变量值(对应的两个后继块的值), 然后再跳转到loopEntry块
// Recalculate switchVar
for (std::vector<BasicBlock *>::iterator b = origBB.begin(); b != origBB.end(); ++b) {
BasicBlock *i = *b;
ConstantInt *numCase = NULL;
// Ret BB
if (i->getTerminator()->getNumSuccessors() == 0)
{
continue;
}
// If it's a non-conditional jump
if (i->getTerminator()->getNumSuccessors() == 1)
{
// Get successor and delete terminator
BasicBlock *succ = i->getTerminator()->getSuccessor(0);
i->getTerminator()->eraseFromParent();
// Get next case
numCase = switchI->findCaseDest(succ);
// If next case == default case (switchDefault)
if (numCase == NULL)
{
numCase = cast<ConstantInt>(
ConstantInt::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}
// Update switchVar and jump to the end of loop
new StoreInst(numCase, load->getPointerOperand(), i);
BranchInst::Create(loopEnd, i);
continue;
}
// If it's a conditional jump
if (i->getTerminator()->getNumSuccessors() == 2)
{
// Get next cases
ConstantInt *numCaseTrue =
switchI->findCaseDest(i->getTerminator()->getSuccessor(0));
ConstantInt *numCaseFalse =
switchI->findCaseDest(i->getTerminator()->getSuccessor(1));
// Check if next case == default case (switchDefault)
if (numCaseTrue == NULL)
{
numCaseTrue = cast<ConstantInt>(
ConstantInt::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}
if (numCaseFalse == NULL)
{
numCaseFalse = cast<ConstantInt>(
ConstantInt::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}
// Create a SelectInst
BranchInst *br = cast<BranchInst>(i->getTerminator());
SelectInst *sel =
SelectInst::Create(br->getCondition(), numCaseTrue, numCaseFalse, "",
i->getTerminator());
// Erase terminator
i->getTerminator()->eraseFromParent();
// Update switchVar and jump to the end of loop
new StoreInst(sel, load->getPointerOperand(), i);
BranchInst::Create(loopEnd, i);
continue;
}
}
这时平坦化的逻辑已经结束了, 此时的CFG图:
以上逻辑的意思是, 比如说bb1的后继块是bb2, 当switch变量var是0x1234时跳转到bb2, 那么则在bb1的末尾添加指令var = 0x1234;然后修改bb1的跳转(之前是跳转到它的后继块bb2)到loopEntry. 这样当进入bb1块时, 执行完了bb1的逻辑后会进入switch分发指令, swtich判断var是0x1234则跳转到bb2块, 抹去了块之间的直接逻辑关系。
-
最后进行栈修复, 就是把phi节点和非entry块的变量提到了entry块中去分配, 具体细节请查看ollvm源代码:
fixStack(f);
3. 结语
至此标准的控制流平坦化逻辑全部分析完毕, 但目前对于程序保护基本不再使用标准的控制流平坦化, 而是使用魔改的flatten变种, 并且这种变种已经非常多了, 例如:
-
把switch分配改为二叉树的分配 -
分发逻辑放到loopEnd、 loopEnd拆分成多个 -
通过支配树动态改变switch变量的值等等
因此没有一个通用的反flatten的方法, 需要具体情况具体分析。
原文始发于微信公众号(山石网科安全技术研究院):从CFG图中分析ollvm控制流平坦化混淆源码