因为要做代码保护,所以抽时间研究了下OLLVM中的三种保护方案:BCF(Bogus Control Flow,中文名虚假控制流)、FLA(Control Flow Flattening,中文名控制流平坦化)、SUB(Instructions Substitution,中文名指令替换),本文是FLA介绍篇。
1,查看头文件 llvm/Transforms/Obfuscation/Flattening.h 可知,FLA暴露给外界的函数如下:
Pass *createFlattening();
Pass *createFlattening(bool flag);
这两个函数用于创建FLA对应的PASS,这两个函数主要区别表现在toObfuscate函数上,用于判断当前函数是否需要进行FLA保护,具体分析见第4步。
2,在OLLVM中,FLA的PASS通过PassManager进行管理,FLA对应的PASS添加和调用参见://need add
3,FLA调用入口是runOnFunction函数,如下所示:首先调用toObfuscate函数判断当前函数是否需要采用FLA保护,如果采用,则调用flatten函数执行真正的FLA保护。接下来,我们首先分析toObfuscate函数的判断逻辑,再分析FLA的保护逻辑。
bool Flattening::runOnFunction(Function &F) {
Function *tmp = &F;
// Do we obfuscate
if (toObfuscate(flag, tmp, "fla")) {
if (flatten(tmp)) {
++Flattened;
}
}
return false;
}
4,Utils(/lib/Transforms/Obfuscation/Utils.cpp)-->toObfuscate
其函数原型如下:
bool toObfuscate(bool flag, Function *f, std::string attribute)
它可以接收三个参数:第一个参数flag就是我们在创建FLA的PASS时传入的那个flag值(见第1步),如果在创建PASS时没有传递flag值,则默认为false;第二个参数是我们当前正在分析的函数对应的指针,注意这里的函数指的是处于IR状态下的函数;第三个参数是标注字符串(可以在编写代码时通过attribute关键字添加)。这个函数总体来说分为两个步骤,标注分析与flag分析:
4.1,标注分析
std::string attr = attribute;
std::string attrNo = "no" + attr;
// Check if declaration
if (f->isDeclaration()) {
return false;
}
// Check external linkage
if(f->hasAvailableExternallyLinkage() != 0) {
return false;
}
// We have to check the nofla flag first
// Because .find("fla") is true for a string like "fla" or
// "nofla"
if (readAnnotate(f).find(attrNo) != std::string::npos) {
return false;
}
// If fla annotations
if (readAnnotate(f).find(attr) != std::string::npos) {
return true;
}
首先检查当前函数是不是仅仅是一个函数声明,如果是的话则返回false,即不进行fla保护;
接着检查这个函数是不是extern函数,如果是的话返回false;
再接着读取这个函数上的标注值,如果找到了'nofla',则返回false;
读取函数标注值时如果找到了'fla',则返回true;
4.2,flag分析
// If fla flag is set
if (flag == true) {
return true;
}
在上面的检测都完成后如果还没有返回,则再检查一下flag(能到这一步说明函数上不属于外部函数,也不是纯声明函数,而且没有对应的标注),如果是true,则返回true,否则返回false。
5,Flattening(/lib/Transforms/Obfuscation/Flattening.cpp)-->flatten
在toObfuscate函数调用后,如果判定需要混淆,那么开始调用flatten函数进行FLA保护,主要原理就是创建一个大的switch循环,第一个块直接跳入这个循环中,其余块在将该函数。具体细节我们分段来分析(省略部分无关代码):
5.1,生成随机数key
// SCRAMBLER
char scrambling_key[16];
llvm::cryptoutils->get_bytes(scrambling_key, 16);
// END OF SCRAMBLER
这一段很简单,就是相当于生成了一个16位的key,这个key主要是用来生成随机数的
5.2,转换所有的switch指令
// Lower switch
FunctionPass *lower = createLowerSwitchPass();
lower->runOnFunction(*f);
这一段是通过一个LowerSwitch PASS来把该函数中的所有switch指令转为if指令
5.3,保存函数中需要平坦化的块
// 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(bb->getTerminator())) {
return false;
}
}
// Nothing to flatten
if (origBB.size() <= 1) {
return false;
}
// Remove first BB
origBB.erase(origBB.begin());
遍历函数中的所有块,并把它们添加到origBB这个集合中。如果在遍历过程中发现某个块末尾是一条Invoke指令(也就是调用其它函数指令,则返回,该函数不进行保护。遍历完成后,如果发现origBB块的数目不大于1,则返回(块太少了)。最后的最后,把origBB中保存的函数的第一个块去掉(第一块是函数的入口块,顺序不能乱啊)
5.4,第一个基本块处理
// 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(insert->getTerminator())) {
br = cast(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);
}
令insert指针指向函数的第一个基本块,如果第一个基本块末尾是一条BranchInst指令,也就是分支指令(注意:可能是有条件分支,也就是通常的if else形式的,也有可能是无条件分支,相当于goto了),则令br指向这条分支指令;
如果br是一条有条件跳转分支指令(后继块这时有两个),则对这个基本块进行切割,会将最后一条分支跳转指令连同它前面的一条指令一起分割到一个新的基本块中,并为其取名first块,举个例子:
define i32 @fib(i32 %AnArg) {
EntryBlock:
%cond = icmp sle i32 %AnArg, 2
br i1 %cond, label %return, label %recurse
return: ; preds = %EntryBlock
ret i32 1
recurse: ; preds = %EntryBlock
%arg = sub i32 %AnArg, 1
%fibx1 = tail call i32 @fib(i32 %arg)
%arg1 = sub i32 %AnArg, 2
%fibx2 = tail call i32 @fib(i32 %arg1)
%addresult = add i32 %fibx1, %fibx2
ret i32 %addresult
}
这时一个斐波那契数列的函数对应的IR指令,可以看到EntryBlock块末尾就是一个有条件跳转,有两个后继块,分别是return块和recurse块,这个时候对这个块分割后就变成下面这个样子:
define i32 @fib(i32 %AnArg) {
EntryBlock:
first:
%cond = icmp sle i32 %AnArg, 2
br i1 %cond, label %return, label %recurse
return: ; preds = %EntryBlock
ret i32 1
recurse: ; preds = %EntryBlock
%arg = sub i32 %AnArg, 1
%fibx1 = tail call i32 @fib(i32 %arg)
%arg1 = sub i32 %AnArg, 2
%fibx2 = tail call i32 @fib(i32 %arg1)
%addresult = add i32 %fibx1, %fibx2
ret i32 %addresult
}
由于EntryBlock只有两条指令,所以分割后暂时就是一个空块了
5.5,第一个基本块中创建switch循环的判定变量
// Remove jump
insert->getTerminator()->eraseFromParent();
// Create switch variable and set as it
switchVar =
new AllocaInst(Type::getInt32Ty(f->getContext()), 0, "switchVar", insert);
new StoreInst(
ConstantInt::get(Type::getInt32Ty(f->getContext()),
llvm::cryptoutils->scramble32(0, scrambling_key)),
switchVar, insert);
上面提到,insert块指向函数中的第一个基本块。
首先移除insert块的terminator指令(其实上面应该就算是移除了);
接着通过AllocaInst指令为变量switchVar申请一块空间,switchVar变量的类型是32位(LLVM中的类型用多少多少位表示);
最后通过StoreInst指令为switchVar变量赋一个伪随机值,注意这个值的生成(通过scramble32函数生成)受两个因素影响,如果这两个值不变,生成的随机值也不变。
在创建AllocaInst指令和StoreInst指令时指定了insert指针作为最后一个变量,所以这两条指令被添加到insert执行的块,也就是第一块中。
5.6,设置循环块
// Create main loop
loopEntry = BasicBlock::Create(f->getContext(), "loopEntry", f, insert);
loopEnd = BasicBlock::Create(f->getContext(), "loopEnd", f, insert);
load = new LoadInst(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);
// Remove branch jump from 1st BB and make a jump to the while
f->begin()->getTerminator()->eraseFromParent();
BranchInst::Create(loopEntry, &*f->begin());
首先创建两个基本块loopEntry、loopEnd;
然后在loopEntry块中创建一条读取switchVar值的指令,然后以这个switchVar为判定变量创建一个switch循环;
调整一下insert块(也就是函数中第一个基本块)与loopEntry的顺序,insert块在前,loopEntry块在后;
再创建一个switchDefault块,switchDefault块添加无条件跳转到loopEnd块指令;
在第一个基本块尾部添加无条件跳转到loopEntry块指令。
还是以上面介绍的斐波那契数列函数为例,这个时候就成这个样子了
define i32 @fib(i32 %AnArg) {
EntryBlock:
%switchVar = alloca i32
store i32 -1287043439, i32* %switchVar
br label %loopEntry
loopEntry: ; preds = %EntryBlock, %loopEnd
%switchVar2 = load i32, i32* %switchVar
switch i32 %switchVar2, label %switchDefault [
]
switchDefault: ; preds = %loopEntry
br label %loopEnd
loopEnd: ; preds = %first, %switchDefault
br label %loopEntry
}
5.7,为原有基本块设置跳入的label case值
// Put all BB in the switch
for (vector::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::get(
switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(switchI->getNumCases(), scrambling_key)));
switchI->addCase(numCase, i);
}
逻辑很简单,对于在origBB中的每个块,都为其生成一个case值,并把这个case值添加到swich指令中,然后把当前这个块添加到loopEnd前。
这里有个地方需要注意,对于origBB中的第一个块来说,在计算case值时由于给llvm::cryptoutils->scramble32函数传入的第一个参数为0,所以这个地方计算出的随机数(也就是当前块对应的switch指令case值)与5.5中给switchVar变量赋的初值相同,这就保证了从第一个基本块进行loopEntry做switch跳转时可以顺利跳转后它的后继块(也就是origBB中的第一个块)
5.8,调整原有基本块与后继块的跳转
// Recalculate switchVar
for (vector::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::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::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}
if (numCaseFalse == NULL) {
numCaseFalse = cast(
ConstantInt::get(switchI->getCondition()->getType(),
llvm::cryptoutils->scramble32(
switchI->getNumCases() - 1, scrambling_key)));
}
// Create a SelectInst
BranchInst *br = cast(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;
}
}
上面这一堆代码主要做了一件事,那就是循环遍历origBB中的所有块,找到当前块的后继块对应的switch循环中的case值,然后把它赋值给switchVar变量,最后再无条件跳转到loopEnd块。由于loopEnd会无条件跳转到loopEntry,所以也就实际上跳入了loopEntry循环。具体来说又分为三种情况:
1)如果当前块没有后继块,什么都不做了,这个块也算是switch循环的跳出点了
2)当前块有一个后继块,那就首先找到这个后继块succ,然后在switch指令中找到succ对应的case值,接着更新switchVar变量的值,保证下一次循环可以跳到succ块,最后令当前块无条件跳入loopEnd块
3)当前块有两个后继块,那就分别找到这两个后继块对应的case值,暂时标记为numCaseTrue和numCaseFalse,然后创建一条SelectInst指令,以便选择出当前块后继块对应的case值,用这个选出来的case值更新switchVar变量,最后令当前块无条件跳入loopEnd块
好了,以上就是OLLVM中FLA算法的主要实现逻辑,最后添加一个做完变换后的IR
define i32 @fib(i32 %AnArg) {
EntryBlock:
%switchVar = alloca i32
store i32 -1287043439, i32* %switchVar
br label %loopEntry
loopEntry: ; preds = %EntryBlock, %loopEnd
%switchVar2 = load i32, i32* %switchVar
switch i32 %switchVar2, label %switchDefault [
i32 -1287043439, label %first
i32 1604828523, label %return
i32 -296943929, label %recurse
]
switchDefault: ; preds = %loopEntry
br label %loopEnd
first: ; preds = %loopEntry
%cond = icmp sle i32 %AnArg, 2
%0 = select i1 %cond, i32 1604828523, i32 -296943929
store i32 %0, i32* %switchVar
br label %loopEnd
return: ; preds = %loopEntry
ret i32 1
recurse: ; preds = %loopEntry
%arg = sub i32 %AnArg, 1
%fibx1 = tail call i32 @fib(i32 %arg)
%arg1 = sub i32 %AnArg, 2
%fibx2 = tail call i32 @fib(i32 %arg1)
%addresult = add i32 %fibx1, %fibx2
ret i32 %addresult
loopEnd: ; preds = %first, %switchDefault
br label %loopEntry
}