1. 随机控制流

1. 原理分析

随机控制流,是虚假控制流的一种变体,随机控制流通过克隆基本块,以及添加随机跳转来混淆控制流。与虚假控制流不同,随机控制流中不存在不可达基本块,因此用于去虚假控制流的手段(消除不透明谓词、符号执行获得不可达基本块后去除)基本失效。随机控制流的控制流图与虚假控制流类似,都呈长条形。

而且随机控制流会引入rdrand指令,可以干扰某些符号执行引擎(如angr)的分析。

随机控制流同样是以基本块为单位进行混淆的,每个基本块经过分裂、克隆、构造随机跳转和构造虚假随机跳转四个操作。

2. 代码实现

主要步骤如下:

  • 基本块拆分

  • 基本块克隆

  • 插入生成随机数指令和随机跳转

  • 插入虚假指令跳转

2.1 基本块拆分

将基本块拆成头部、中部、尾部三个基本块,同虚假控制流。

image-Nqvo.png

  • 通过getFirstNonPHI函数获取第一个不是PHINode的指令,以该指令为界限进行分割,得到entryBB和BodyBB

  • 以bodyBB的终结指令为界限进行分割,最后得到头部、中部和尾部三个基本块,也就是entryBB,bodyBB和endBB。

2.2 基本块克隆

  • 将中间的基本块bodyBB进行克隆,这里可以悬着对基本块进行变异,但不改变基本块的功能。

  • 与虚假控制不同,随机控制流在克隆前还需要修复逃逸变量。原因是克隆块均可被执行,每个克隆块都是潜在的有效执行路径,所有克隆块均需要保持变量作用域一致性。而虚假控制流中,克隆块(不可达块)受不透明谓词保护,实际运行时永远不会被执行。

image-aUun.png

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);