从CFG图中分析ollvm控制流平坦化混淆源码

从CFG图中分析ollvm控制流平坦化混淆源码

本文中的代码是将ollvm的flatten以插件形式移植到了llvm18 new pass中, 代码会略有不同, 但逻辑是一样的。

1. 概览

控制流平坦化(control flow flattening)是作用于控制流图的代码混淆技术,其基本思想是重新组织函数的控制流图中的基本块关系,通过插入一个 “主分发器” 来控制基本块的执行流程:

从CFG图中分析ollvm控制流平坦化混淆源码

简单来说就是把每个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图:

从CFG图中分析ollvm控制流平坦化混淆源码

原逻辑是很清晰的, 满足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:

从CFG图中分析ollvm控制流平坦化混淆源码

  • 首先调用了LowerSwitchPass这个Pass将一个Function中全部的switch语句转为if-else语句:
// Lower switch
LowerSwitchPass lower;
lower.run(F, FAM);

看下转换后的CFG图, switch结构被改变了:

从CFG图中分析ollvm控制流平坦化混淆源码

  • 然后将所有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块中:

从CFG图中分析ollvm控制流平坦化混淆源码

  • 之后删除entry块的br无条件跳转:
// Remove jump
insert->getTerminator()->eraseFromParent();

看下CFG图, entry末尾的br被删掉了, 成为了独立的块:

从CFG图中分析ollvm控制流平坦化混淆源码

  • 之后用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图:

从CFG图中分析ollvm控制流平坦化混淆源码

  • 然后遍历之前保存的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上:

从CFG图中分析ollvm控制流平坦化混淆源码

  • 然后修正各个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图:

从CFG图中分析ollvm控制流平坦化混淆源码

以上逻辑的意思是, 比如说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控制流平坦化混淆源码

版权声明:admin 发表于 2024年8月22日 下午6:35。
转载请注明:从CFG图中分析ollvm控制流平坦化混淆源码 | CTF导航

相关文章