1. 随机控制流
1. 原理分析
随机控制流,是虚假控制流的一种变体,随机控制流通过克隆基本块,以及添加随机跳转来混淆控制流。与虚假控制流不同,随机控制流中不存在不可达基本块,因此用于去虚假控制流的手段(消除不透明谓词、符号执行获得不可达基本块后去除)基本失效。随机控制流的控制流图与虚假控制流类似,都呈长条形。
而且随机控制流会引入rdrand指令,可以干扰某些符号执行引擎(如angr)的分析。
随机控制流同样是以基本块为单位进行混淆的,每个基本块经过分裂、克隆、构造随机跳转和构造虚假随机跳转四个操作。
2. 代码实现
主要步骤如下:
基本块拆分
基本块克隆
插入生成随机数指令和随机跳转
插入虚假指令跳转
2.1 基本块拆分
将基本块拆成头部、中部、尾部三个基本块,同虚假控制流。
通过getFirstNonPHI函数获取第一个不是PHINode的指令,以该指令为界限进行分割,得到entryBB和BodyBB
以bodyBB的终结指令为界限进行分割,最后得到头部、中部和尾部三个基本块,也就是entryBB,bodyBB和endBB。
2.2 基本块克隆
将中间的基本块bodyBB进行克隆,这里可以悬着对基本块进行变异,但不改变基本块的功能。
与虚假控制不同,随机控制流在克隆前还需要修复逃逸变量。原因是克隆块均可被执行,每个克隆块都是潜在的有效执行路径,所有克隆块均需要保持变量作用域一致性。而虚假控制流中,克隆块(不可达块)受不透明谓词保护,实际运行时永远不会被执行。
BasicBlock* llvm::createCloneBasicBlock(BasicBlock *BB){
// 克隆之前先修复所有逃逸变量
vector<Instruction*> origReg;
BasicBlock &entryBB = BB->getParent()->getEntryBlock();
for(Instruction &I : *BB){
if(!(isa<AllocaInst>(&I) && I.getParrent() == &entryBB)
&& I.isUsedOutSideOfBlock(BB)){
origReg.push_back(&I);
}
}
for(Instruction &I : origReg){
DemoteRegToStrack(*I, entryBB.getTerminator());
}
// 下面步骤与虚假控制流一致
ValueToValueMapTy VMap;
BasicBlock *cloneBB = CloneBasicBlock(BB, VMap, BB->getName() + "cloneBB", BB->getParent());
// 对克隆基本块的引用进行修复
for(Instruction &I : *cloneBB){
for(int i = 0; i < I.getNumOperands(); i++){
Value *V = MapValue(I,getOperand(i), VMap);
if(V){
I.setOperand(i,V);
}
}
}
return cloneBB;
}
2.3 插入生成随机指令和随机跳转
向entryBB中插入生成随机数的指令和随机跳转,使其能够跳转到bodyBB或者bodyBB克隆块
其中随机指令可以通过LLVM内置函数
rdrand
// 在 entryBB 后插入随机跳转,使其能够随机跳转到 bodyBB 或者其他克隆块 cloneBB
entryBB->getTerminator()->eraseFromParent();
Functon *rdrand = Intrinsic::getDeclaration(entryBB->getModule(), Intrinsic::x86_rdrand_32);
CallInst *randVarStruct = CallInst::Create(rdrand->getFunctionType(), rdrand, "", entryBB);
// 通过 rdrand 内置函数获取随机数
Value *randVar = ExtractValueInst::Create(randVarStruct, 0, "", entryBB);
// 插入随机跳转
void RandomControlFlow::insertRandomBranch(Value *randVar, BasicBlock, ifTrue, BasickBlock *ifFalse, BasicBlock *insertAfter){
// 对随机数进行等价变换
Value *alterdRandVar = alterVal(randVar, insertAfter);
Value *randMod2 = BinaryOperator::Create(Instruction::And, alteredRandVar, CONST(1), "", insertAfter);
ICmpInst *condition = new ICmpInst(*insertAfter, ICmptINst::ICMP_EQ, randMod2, CONST(1));
BranchInst::Create(ifTrue, ifFalse, condition, insertAfter);
}
//调用
insertRandomBranch(randVar, bodyBB, cloneBB, entryBB);
// 对随机变量的恒等变换
Value* RandomControlFlow::alterVal(Value *startVar, BasicBlock *insertAfter){
uint32_t code = rand() % 3;
Value *result;
if(code == 0){
// x = x * (x+1) - X^2
BinaryOperator *op1 = BinaryOperator::Create(Instruction::Add, startVar, CONST(1), "", insertAfter);
BinaryOperator *op2 = BinaryOperator::Create(Instruction::Mul, startVar, op1, "", insertAfter);
BinaryOperator *op3 = BinaryOperator::Create(Instruction::Mul, startVal, startVal, "", insertAfter);
BinaryOperator *op4 = BinaryOperator::Create(Instruction::Sub, op2, op3, "", insertAfter);
result = op4;
}else if(code == 1){
// x = 3 * X * (x-2) -3 * X ^ 2 + 7 * X
...
}
}
2.4 插入虚假指令跳转
在bodyBB和cloneBB后插入虚假跳转指令(实际仍会直接跳转到endBB)
// 添加 bodyBB 到 bodyBB.clone 的虚假随机跳转
bodyBB->getTerminator()->eraseFromParent();
insertRandomBranch(randVar, endBB, cloneBB, bodyBB);
// 添加 bodyBB.clone 到 bodyBB 的虚假随机跳转
cloneBB->getTerminator()->eraseFromParent();
insertRandomBranch(randVar, bodyBB, endBB, cloneBB);
2. 常量替代
1. 原理分析
常量替代指将二元运算指令(如加法、减法、异或等)中使用的常数,替换为等效而更为复杂的表达式,以达到混淆某些计算过程或某些特殊常量的目的。
常量替代可进一步拓展为常量数组的替代和字符串替代。常量数组替代可以抹去AES、DES等加密算法中特征数组,字符串替代可以防止攻击者通过字符串定位关键代码。
实现思路是:扫描所有指令,对目标指令进行替换。
for(Instruction *I : origInst){
// 只对二元运算中的常量进行替换
if(BinaryOperator *BI = dyn_cast<BinaryOperator>(I)){
// 仅对整数进行替换
if(BI->getType()->isINtegerTy(32)){
substitute(BI);
}
}
}
替换方案:
线性替换:
val -> ax + by + c
其中val为原常量,a、b为随机常量,x、y为随机全局变量,
c= val - (ax + by)
按位运算替换:
val -> (x << 5 | y >> 3) ^ c
其中val为原常量,x、y为随机全局变量,
c=val ^ (x << 5 | y >> 3) ^ c
3. light weight vm
参考论文:https://dspace.cvut.cz/bitstream/handle/10467/82300/F8-DP-2019-Turcan-Lukas-thesis.pdf
参考项目:https://github.com/bigBestWay/CodeObfs
1. 原理分析
论文中介绍的实现原理是:将基本块中每一条指令,替换为一个新的基本块,单独分配一个命令码(opcode)。将命令码按照一定存储起来,执行时通过命令码执行对应的BasicBlock。有点像控制流平坦化。
VMInterpreter:虚拟机解释器初始化,例如创建并初始化状态计数器,加载操作码表,跳转到主调度循环。
VMInterpreterBody:虚拟机主循环调度,更新状态计数器,指令读取,译码(分发),循环控制
Handler:通过随机操作码标识,执行后自动返回主循环调度器。
2. 代码实现
主要流程如下:
+------------------+ +------------------+ +------------------+
| VMInterpreter | ----> | VMInterpreterBody| ----> | HandlerX |
| (初始化计数器) | | (调度中心) | | (指令执行单元) |
+------------------+ +------------------+ +------------------+
↑ ↑ |
| | ↓
+------------------+ +------------------+ +------------------+
| Entry | | 操作码表 | | HandlerY |
| (操作码表加载) | | (全局数组) | | (指令执行单元) |
+------------------+ +------------------+ +------------------+
2.1 指令拆分与操作码映射
循环读取原基本块,创建一个新的基本块,命名为"VMInterpreterHandler",将原本基本块第一条指令移动到新创建的阿基本块中。
生成一个随机操作码,并将其转换为LLVM的整型常量。
存储操作码与新基本块。
// 拆分原始基本块
while(!originBB.empty()) {
BasicBlock * new_bb = BasicBlock::Create(..., "VMInterpreterHandler");
new_bb->getInstList().splice(new_bb->end(), originBB.getInstList(), first_insn);
// 生成随机操作码
int code = rand();
switch_elems.push_back(ConstantInt::get(Int32Ty, code));
handlerbb_list.push_back(new_bb);
}
2.2 虚拟机核心构建
根据随机操作码,跳转到特定的handler去执行,可以类比为CPU的取指译码阶段
操作码表初始化:
解释器初始化,程序计数器(PC)初始化为0,跳转到主调度循环基本块
实现主调度循环:
加载当前PC值,并自增1
取指:从操作码表中取得当前指令
译码器:使用SwitchInst创建多路分支分发器
译码:为每个操作码添加对应的处理基本块
// 全局操作码表
GlobalVariable* opcodes = new GlobalVariable(
ArrayType::get(Int32Ty, handler_count),
true, GlobalValue::PrivateLinkage,
ConstantArray::get(array_type, opcode_array),
"opcodes");
// 解释器初始化
builder.SetInsertPoint(VMInterpreter_bb);
builder.CreateStore(ConstantInt::get(Int32Ty, 0), counter); // PC=0
builder.CreateBr(VMInterpreterbody_bb);
// 主调度循环
builder.SetInsertPoint(VMInterpreterbody_bb);
Value * pc = builder.CreateLoad(counter);
Value * new_pc = builder.CreateAdd(pc, ConstantInt::get(Int32Ty, 1)); // PC++
Value * opcode = load_opcode(opcodes, pc); // 取指
// 创建switch分发器
SwitchInst * switch_inst = builder.CreateSwitch(opcode, VMInterpreterbody_bb);
for(int i=0; i<handler_count; i++) {
switch_inst->addCase(opcode_array[i], handlerbb_list[i]);
}
2.3 修复PHI指令与逃逸变量
PHI指令的值由前驱块决定,平坦化所有原基本快的前驱块都变成分发块了,因此PHI指令发生了损坏。
逃逸变量指的是在一个基本块中定义,在另一个基本块中被引用的变量。在源程序中某些基本块可能引用之前你某个基本块中的变量,平坦化后原基本块之间不存在确定的前后关系了,改为由分发块决定,因此某些变量的引用可能回损坏。
修复方法:将PHI指令和逃逸变量都转化为内存存取指令。
// 入口块分配存储
builder.SetInsertPoint(entry_block);
AllocaInst *tmpPtr = builder.CreateAlloca(value_type);
// 定义块存储值
builder.SetInsertPoint(handlerA, handlerA->end());
builder.CreateStore(valueX, tmpPtr);
// 使用块加载值
builder.SetInsertPoint(handlerB, handlerB->begin());
Value *loadedX = builder.CreateLoad(tmpPtr);