0x01 控制流平坦化基本介绍

控制流平坦化是指将正常程序控制流中基本块之间的跳转关练删除,用一个集中的主分发块来调度基本块的执行顺序。相当于把原有程序正常的逻辑改为一个循环嵌套一个switch的逻辑。

正常情况:

正常情况

控制流平坦化之后:

混淆后

控制流平坦化的基本结构如下:

image-20220708115813228

  • 入口块:进入函数第一个执行的基本块
  • 分发块:负责跳转到下一个要执行的原基本块
  • 原基本块:混淆之前的基本块,实际完成程序工作的基本块
  • 返回块:返回到主分发块

修改了程序的控制流,导致逆向分析人员不容易直接的理清程序执行流程,增加分析难度。

0x02 实现方式

本节以https://github.com/bluesadi/Pluto-Obfuscator/tree/kanxue 项目为基准进行分析。主要的代码实现分为五大块如下图所示:

image-20220708122724034

1. 保存原基本块

将除入口块以外的以外的基本块保存到 vector 容器中,方便后续处理。如果入口块的终结指令是条件分支指令,则将该指令单独分离出来作为一个基本块,加入到 vector 容器的最前面。

    // 将除入口块(第一个基本块)以外的基本块保存到一个 vector 容器中,便于后续处理
    // 首先保存所有基本块
    vector<BasicBlock*> origBB;
    for(BasicBlock &BB: F){
        origBB.push_back(&BB);
    }
    // 从vector中去除第一个基本块
    origBB.erase(origBB.begin());
    BasicBlock &entryBB = F.getEntryBlock();
    // 如果第一个基本块的末尾是条件跳转,单独分离
    if(BranchInst *br = dyn_cast<BranchInst>(entryBB.getTerminator())){
        if(br->isConditional()){
            BasicBlock *newBB = entryBB.splitBasicBlock(br, "newBB");
            origBB.insert(origBB.begin(), newBB);
        }
    }

2. 创建分发块和返回块

除了原基本块之外,我们还要续创建一个分发块来调度基本块的执行顺序。并建立入口块到分发块的绝对跳转。再创建一个返回块,原基本块执行完后都需要跳转到这个返回块,返回块会直接跳转到分发块进行下一次的基本块跳转。

// 创建分发块和返回块
    BasicBlock *dispatchBB = BasicBlock::Create(*CONTEXT, "dispatchBB", &F, &entryBB);
    BasicBlock *returnBB = BasicBlock::Create(*CONTEXT, "returnBB", &F, &entryBB);
    BranchInst::Create(dispatchBB, returnBB);
    entryBB.moveBefore(dispatchBB);
    // 去除第一个基本块末尾的跳转
    entryBB.getTerminator()->eraseFromParent();
    // 使第一个基本块跳转到dispatchBB
    BranchInst *brDispatchBB = BranchInst::Create(dispatchBB, &entryBB);

3. 实现分发块调度

在入口块中创建并初始化 switch 要使用的变量,在调度块中插入switch-case 指令实现分发功能。将原基本块移动到返回块之前,并给每一个原基本块分配随机的 case 值,并将其添加到 switch 指令的对应case分支中。

// 在入口块插入alloca和store指令创建并初始化switch变量,初始值为随机值
    int randNumCase = rand();
    AllocaInst *swVarPtr = new AllocaInst(TYPE_I32, 0, "swVar.ptr", brDispatchBB);
    new StoreInst(CONST_I32(randNumCase), swVarPtr, brDispatchBB);
    // 在分发块插入load指令读取switch变量
    LoadInst *swVar = new LoadInst(TYPE_I32, swVarPtr, "swVar", false, dispatchBB);
    // 在分发块插入switch指令实现基本块的调度
    BasicBlock *swDefault = BasicBlock::Create(*CONTEXT, "swDefault", &F, returnBB);
    BranchInst::Create(returnBB, swDefault);
    SwitchInst *swInst = SwitchInst::Create(swVar, swDefault, 0, dispatchBB);
    // 将原基本块插入到返回块之前,并分配case值
    for(BasicBlock *BB : origBB){
        BB->moveBefore(returnBB);
        swInst->addCase(CONST_I32(randNumCase), BB);
        randNumCase = rand();
    }

4. 实现调度变量自动调整

在每个原基本块最后添加修改 switch 要使用的变量值的指令,以便返回分发块之后,能够正确执行到下一个基本块。删除原基本块末尾的跳转,使其结束执行后跳转到返回块,这一步需要注意判断原基本块末尾跳转的语句。(类似于VMP3每一个handler的末尾指定下一个要跳转的handler)

     // 在每个基本块最后添加修改switch变量的指令和跳转到返回块的指令
    for(BasicBlock *BB : origBB){
        // retn BB
        if(BB->getTerminator()->getNumSuccessors() == 0){
            continue;
        }
        // 非条件跳转
        else if(BB->getTerminator()->getNumSuccessors() == 1){
            BasicBlock *sucBB = BB->getTerminator()->getSuccessor(0);
            BB->getTerminator()->eraseFromParent();
            ConstantInt *numCase = swInst->findCaseDest(sucBB);
            new StoreInst(numCase, swVarPtr, BB);
            BranchInst::Create(returnBB, BB);
        }
        // 条件跳转
        else if(BB->getTerminator()->getNumSuccessors() == 2){
            ConstantInt *numCaseTrue = swInst->findCaseDest(BB->getTerminator()->getSuccessor(0));
            ConstantInt *numCaseFalse = swInst->findCaseDest(BB->getTerminator()->getSuccessor(1));
            BranchInst *br = cast<BranchInst>(BB->getTerminator());
            SelectInst *sel = SelectInst::Create(br->getCondition(), numCaseTrue, numCaseFalse, "", BB->getTerminator());
            BB->getTerminator()->eraseFromParent();
            new StoreInst(sel, swVarPtr, BB);
            BranchInst::Create(returnBB, BB);
        }
    }

当原基本块出现switch-case等大于2个分支的情况时,我们可以在优化过程中使用lowerswitch将其变成只有2个及以下分支的状态,使用命令如下:

opt -lowerswitch -S TestProgram_orig.ll -o TestProgram_lowerswitch.ll

关于lowerswitch的使用也可以直接在代码中实现,该项目是在LLVM12.0.1中编译的,所以不能直接使用createLowerSwitchPass函数否则会导致崩溃,应该在Flattening中添加如下函数

void getAnalysisUsage(AnalysisUsage &AU) const override{
    errs() << "Require LowerSwitchPass\r\n";
    AU.addRequiredID(LowerSwitchID);
    FunctionPass::getAnalysisUsage(AU);
}

image-20220708144027160

5. 修复PHI指令和逃逸变量

PHI 指令的值由前驱块决定,平坦化后所有原基本块的前驱块都变成了分发块,因此 PHI 指令发生了损坏。

逃逸变量指在一个基本块中定义,并且在另一个基本块被引用的变量。在原程序中某些基本块可能引用之前某个基本块中的变量,平坦化后原基本块之间不存在确定的前后关系了(由分发块决定),因此某些变量的引用可能会损坏。

修复的方法是,将 PHI 指令和逃逸变量都转化为内存存取指令。

void fixStack(Function &F) {
    vector<PHINode*> origPHI;
    vector<Instruction*> origReg;
    BasicBlock &entryBB = F.getEntryBlock();
    // 搜索PHI指令和逃逸变量添加到对应vector容器
    for(BasicBlock &BB : F){
        for(Instruction &I : BB){
            if(PHINode *PN = dyn_cast<PHINode>(&I)){
                origPHI.push_back(PN);
            }else if(!(isa<AllocaInst>(&I) && I.getParent() == &entryBB) 
                && I.isUsedOutsideOfBlock(&BB)){
                origReg.push_back(&I);
            }
        }
    }
    for(PHINode *PN : origPHI){
        DemotePHIToStack(PN, entryBB.getTerminator());
    }
    for(Instruction *I : origReg){
        DemoteRegToStack(*I, entryBB.getTerminator());
    }
}

0x03 参考链接

https://security.tencent.com/index.php/blog/msg/112

https://www.kanxue.com/book-88-2111.htm

https://github.com/bluesadi/Pluto-Obfuscator/blob/kanxue/Transforms/src/Flattening.cpp

https://github.com/obfuscator-llvm/obfuscator/blob/llvm-4.0/lib/Transforms/Obfuscation/Flattening.cpp

https://www.52pojie.cn/thread-1369130-1-1.html