Polaris-Obfuscator中BogusControlFlow简要分析反混淆

admin 2026-03-27 14:19:09 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析了Polaris-Obfuscator中的BogusControlFlow混淆技术,详细解析了克隆基本块、拆分基本块和通过模运算构造不透明谓词的实现原理,指出了无符号整数运算导致的实现bug,对比了BogusControlFlow与BogusControlFlow2的差异,并提出基于DDG反向切片和符号执行的反混淆原型,具有较强的理论深度和实践价值。 综合评分: 88 文章分类: 逆向分析,代码审计,二进制安全,安全工具,实战经验


cover_image

Polaris-Obfuscator中BogusControlFlow简要分析 反混淆

Taardisaa Taardisaa

看雪学苑

2026年3月25日 18:00 上海

这个混淆也算是从OLLVM继承下来的老玩意了;简单来说就是对于一个basic block,在它之前创建一个相同的basic block,这个新的basic block包含一个opaque predicate(一个永远为真/永远为假的条件,即new block永远也不会被真正执行到);然后在这个新的block里面还可以相应的加入一些junk instructions,使得反编译器无法正确解析这个block。

#

具体可以看此篇:https://github.com/obfuscator-llvm/obfuscator/wiki/Bogus-control-flow

Implementation Details

https://github.com/za233/Polaris-Obfuscator/blob/main/src/llvm/lib/Transforms/Obfuscation/BogusControlFlow.cpp

我们逐个函数的来分析:

1. 克隆basic block(cloneAlterBasicBlock

BasicBlock *BogusControlFlow::cloneAlterBasicBlock(BasicBlock *BB) {
  ValueToValueMapTy VMap;
  BasicBlock *CBB = CloneBasicBlock(BB, VMap, "cloneBB", BB->getParent());
  BasicBlock::iterator Iter = BB->begin();
for (Instruction &I : *CBB) {
for&nbsp;(unsigned&nbsp;i =&nbsp;0; i < I.getNumOperands(); i++) {
&nbsp; &nbsp; &nbsp; Value *V =&nbsp;MapValue(I.getOperand(i), VMap);
if&nbsp;(V) {
&nbsp; &nbsp; &nbsp; &nbsp; I.setOperand(i, V);
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; SmallVector<std::pair<unsigned, MDNode *>,&nbsp;4> MDs;
&nbsp; &nbsp; I.getAllMetadata(MDs);
for&nbsp;(std::pair<unsigned, MDNode *> pair : MDs) {
&nbsp; &nbsp; &nbsp; MDNode *MD =&nbsp;MapMetadata(pair.second, VMap);
if&nbsp;(MD) {
&nbsp; &nbsp; &nbsp; &nbsp; I.setMetadata(pair.first, MD);
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; &nbsp; I.setDebugLoc(Iter->getDebugLoc());
&nbsp; &nbsp; Iter++;
&nbsp; }
return&nbsp;CBB;
}

这个函数的功能就是克隆一个basic block。当然也进行了相应的变量重命名,以及metadata的克隆。但是并没有对其进行任何的修改,没有添加junk code。

2. 拆分basic block

/* 将函数中的每个基本块按每 Size 条指令拆分成多个较小的块 */
voidBogusControlFlow::splitBasicBlock(Function &F,&nbsp;unsigned&nbsp;Size)&nbsp;{
&nbsp; std::vector<Instruction *> SplitPoints;
for&nbsp;(BasicBlock &BB : F) {
unsigned&nbsp;Idx =&nbsp;0;
for&nbsp;(auto&nbsp;Iter = BB.getFirstInsertionPt(); Iter != BB.end(); Iter++) {
&nbsp; &nbsp; &nbsp; Instruction &I = *Iter;
if&nbsp;(I.isTerminator()) {
continue;
&nbsp; &nbsp; &nbsp; }
if&nbsp;(Idx % Size == Size -&nbsp;1) {
&nbsp; &nbsp; &nbsp; &nbsp; SplitPoints.push_back(&I);
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; Idx = (Idx +&nbsp;1) % Size;
&nbsp; &nbsp; }
&nbsp; }
for&nbsp;(Instruction *I : SplitPoints) {
&nbsp; &nbsp; BasicBlock *BB = I->getParent();
&nbsp; &nbsp; BB->splitBasicBlock(I);
&nbsp; }
}

这个函数的功能就是将函数中的每个基本块按每Size条指令拆分成多个较小的块。这样做的目的是为了增加基本块的数量,后面便可以插入更多的opaque predicates。

3.process函数

首先在函数头创建两个变量,VarVar0

IRBuilder<>&nbsp;IRB(&*F.getEntryBlock().getFirstInsertionPt());
Value *Var = IRB.CreateAlloca(IRB.getInt64Ty());
Value *Var0 = IRB.CreateAlloca(IRB.getInt64Ty());

紧接着,得到两个变量ModX

Mod=0x100000000−rand32()Mod = 0x100000000 – rand32()Mod=0x100000000−rand32()

X=randPrime()modModX = randPrime() \mod ModX=randPrime()modMod

uint64_t&nbsp;Mod =&nbsp;0x100000000&nbsp;-&nbsp;getRand32();
// X is just a random prime % Mod
uint64_t&nbsp;X =
&nbsp; &nbsp; primes[getRandomNumber() % (sizeof(primes) /&nbsp;sizeof(primes[0]))] % Mod;

然后VarVar0都初始化成了X

// Both Var and Var0 are set to X at the beginning of the function.
IRB.CreateStore(IRB.getInt64(X), Var);
IRB.CreateStore(IRB.getInt64(X), Var0);

接下来的就是比较tricky的部分了。我先把代码贴上来,然后再进行分析:

&nbsp; std::vector<BasicBlock *> BBs;
for&nbsp;(BasicBlock &BB : F) {
// At the end of each basic block
&nbsp; &nbsp; IRB.SetInsertPoint(BB.getTerminator());
// a*x - b = x (mod m)
// (a - 1) * x = b (mod m)
uint64_t&nbsp;B =&nbsp;getRand32() % Mod;
uint64_t&nbsp;Inv =&nbsp;getInverse(X, Mod);
uint64_t&nbsp;A = ((B * Inv) % Mod +&nbsp;1) % Mod;

&nbsp; &nbsp; Value *V = IRB.CreateLoad(IRB.getInt64Ty(), Var0);
&nbsp; &nbsp; IRB.CreateStore(
&nbsp; &nbsp; &nbsp; &nbsp; IRB.CreateURem(
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; IRB.CreateSub(IRB.CreateURem(IRB.CreateMul(IRB.getInt64(A), V),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;IRB.getInt64(Mod)),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; IRB.getInt64(B)),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; IRB.getInt64(Mod)),
&nbsp; &nbsp; &nbsp; &nbsp; Var0);
&nbsp; &nbsp; BBs.push_back(&BB);
&nbsp; }

首先来说说下面的这个IR到底是啥玩意:

%v = load i64, ptr %Var0
%mul &nbsp;= mul i64 A, %v
%rem1 = urem i64 %mul, Mod
%sub &nbsp;=&nbsp;sub&nbsp;i64&nbsp;%rem1,&nbsp;B
%rem2&nbsp;=&nbsp;urem&nbsp;i64&nbsp;%sub,&nbsp;Mod
store&nbsp;i64&nbsp;%rem2,&nbsp;ptr&nbsp;%Var

转化成high level code的话就是:

Var0&nbsp;= ((A * Var0) % Mod - B) % Mod

那么这个公式到底算了啥玩意?从上面的计算公式可知:

B=rand32()modModB = rand32() \bmod ModB=rand32()modMod

A=((B∗Inv(X,Mod))modMod+1)modModA = ((B * Inv(X, Mod)) \bmod Mod + 1) \bmod ModA=((B∗Inv(X,Mod))modMod+1)modMod

这两个刻意构造的变量AB满足如下的关系:

(A−1)∗X=BmodMod(A – 1) * X = B \bmod Mod(A−1)∗X=BmodMod

其中,X可以是任何一个小于Mod的数。这个公式只需要将上面的AB代入进去就可以轻松验证。

下面,只需要稍微变形一下上面的公式就可以得到:

A∗X=(X+B)modModA * X = (X + B) \bmod ModA∗X=(X+B)modMod

将这个玩意带入到上面用于变换Var0的公式中就可以得到:

所以实际上Var0的值是永远不会改变的,一直和Var的值相等的。上面花里胡哨的计算其实只是为了让编译器和反编译器无法进行优化。

注意:这里的实现实际上有一个bug。上面的推导在数学模运算下是正确的,但IR中使用的是sub i64(无符号64位减法)+urem i64。当(A * X) % Mod < B时,sub会发生unsigned underflow,wrap到2642^{64}264附近的大数,后续的urem就不再等价于数学模运算了。例如取Mod=11, X=3, B=8,数学上(−8)mod11=3(-8) \bmod 11 = 3(−8)mod11=3,但uint64下0 - 8 = 2^{64} - 8,(264−8)mod11=8≠3(2^{64} – 8) \bmod 11 = 8 \neq 3(264−8)mod11=8=3。这会导致opaque predicate不再恒真,进而使程序陷入死循环。

接下来就是注入blocks和opaque predicates了:

for&nbsp;(BasicBlock *BB : BBs) {
if&nbsp;(isa<InvokeInst>(BB->getTerminator()) || BB->isEHPad() ||
&nbsp; &nbsp; &nbsp; &nbsp; BB->isEntryBlock()) {
continue;
&nbsp; &nbsp; }
&nbsp; &nbsp; BasicBlock *BodyBB =
&nbsp; &nbsp; &nbsp; &nbsp; BB->splitBasicBlock(BB->getFirstNonPHIOrDbgOrLifetime(),&nbsp;"bodyBB");
&nbsp; &nbsp; BasicBlock *TailBB =
&nbsp; &nbsp; &nbsp; &nbsp; BodyBB->splitBasicBlock(BodyBB->getTerminator(),&nbsp;"endBB");
&nbsp; &nbsp; BasicBlock *CloneBB =&nbsp;cloneAlterBasicBlock(BodyBB);
&nbsp; &nbsp; BB->getTerminator()->eraseFromParent();
&nbsp; &nbsp; BodyBB->getTerminator()->eraseFromParent();
&nbsp; &nbsp; CloneBB->getTerminator()->eraseFromParent();
&nbsp; &nbsp; IRB.SetInsertPoint(BB);
if&nbsp;(getRandomNumber() %&nbsp;2) {
&nbsp; &nbsp; &nbsp; Value *Cond = IRB.CreateICmpEQ(IRB.CreateLoad(IRB.getInt64Ty(), Var),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;IRB.CreateLoad(IRB.getInt64Ty(), Var0));
&nbsp; &nbsp; &nbsp; IRB.CreateCondBr(Cond, BodyBB, CloneBB);
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; Value *Cond = IRB.CreateICmpNE(IRB.CreateLoad(IRB.getInt64Ty(), Var),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;IRB.CreateLoad(IRB.getInt64Ty(), Var0));
&nbsp; &nbsp; &nbsp; IRB.CreateCondBr(Cond, CloneBB, BodyBB);
&nbsp; &nbsp; }

&nbsp; &nbsp; IRB.SetInsertPoint(BodyBB);
if&nbsp;(getRandomNumber() %&nbsp;2) {
&nbsp; &nbsp; &nbsp; Value *Cond = IRB.CreateICmpEQ(IRB.CreateLoad(IRB.getInt64Ty(), Var),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;IRB.CreateLoad(IRB.getInt64Ty(), Var0));
&nbsp; &nbsp; &nbsp; IRB.CreateCondBr(Cond, TailBB, CloneBB);
&nbsp; &nbsp; }&nbsp;else&nbsp;{
&nbsp; &nbsp; &nbsp; Value *Cond = IRB.CreateICmpNE(IRB.CreateLoad(IRB.getInt64Ty(), Var),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;IRB.CreateLoad(IRB.getInt64Ty(), Var0));
&nbsp; &nbsp; &nbsp; IRB.CreateCondBr(Cond, CloneBB, TailBB);
&nbsp; &nbsp; }

&nbsp; &nbsp; IRB.SetInsertPoint(CloneBB);
&nbsp; &nbsp; IRB.CreateBr(BodyBB);
&nbsp; }

具体的插入稍微有点复杂,涉及在graph上的变换。不过整体变化后的图像可以直接参考:

来源:https://www.apriorit.com/dev-blog/obfuscating-code-to-secure-android-apps

不过核心点其实还是在于opaque predicate的构造上。只要能够把opaque predicate给优化掉,那么具体的插入方式实际上并不重要;反编译器会自动把dead block给剪枝掉。

BogusControlFlow2分析

刚刚快速看了一下源码,发现里面有两个都是BogusControlFlow的pass,分别是BogusControlFlowBogusControlFlow2。前者就是我们上面分析的那个,而后者则是一个更为简单的版本?我没记错的话,以前OLLVM用的就是这个:

Value *createBogusCmp(BasicBlock *insertAfter) {
// if((y < 10 || x * (x + 1) % 2 == 0))
&nbsp; Module *M = insertAfter->getModule();
&nbsp; LLVMContext &context = M->getContext();
&nbsp; GlobalVariable *xptr =&nbsp;new&nbsp;GlobalVariable(
&nbsp; &nbsp; &nbsp; *M,&nbsp;Type::getInt32Ty(context),&nbsp;false,&nbsp;GlobalValue::CommonLinkage,
ConstantInt::get(Type::getInt32Ty(context),&nbsp;0),&nbsp;"x");
&nbsp; GlobalVariable *yptr =&nbsp;new&nbsp;GlobalVariable(
&nbsp; &nbsp; &nbsp; *M,&nbsp;Type::getInt32Ty(context),&nbsp;false,&nbsp;GlobalValue::CommonLinkage,
ConstantInt::get(Type::getInt32Ty(context),&nbsp;0),&nbsp;"y");

&nbsp; IRBuilder<>&nbsp;builder(context);
&nbsp; builder.SetInsertPoint(insertAfter);
&nbsp; LoadInst *x = builder.CreateLoad(Type::getInt32Ty(context), xptr);
&nbsp; LoadInst *y = builder.CreateLoad(Type::getInt32Ty(context), yptr);
&nbsp; Value *cond1 =
&nbsp; &nbsp; &nbsp; builder.CreateICmpSLT(y,&nbsp;ConstantInt::get(Type::getInt32Ty(context),&nbsp;10));
&nbsp; Value *op1 =
&nbsp; &nbsp; &nbsp; builder.CreateAdd(x,&nbsp;ConstantInt::get(Type::getInt32Ty(context),&nbsp;1));
&nbsp; Value *op2 = builder.CreateMul(op1, x);
&nbsp; Value *op3 =
&nbsp; &nbsp; &nbsp; builder.CreateURem(op2,&nbsp;ConstantInt::get(Type::getInt32Ty(context),&nbsp;2));
&nbsp; Value *cond2 =
&nbsp; &nbsp; &nbsp; builder.CreateICmpEQ(op3,&nbsp;ConstantInt::get(Type::getInt32Ty(context),&nbsp;0));
return&nbsp;BinaryOperator::CreateOr(cond1, cond2,&nbsp;"", insertAfter);
}

这里面生成的opaque predicate明显要简单很多,并没有涉及mod+invserse的计算。

在Pipeline.cpp里面,BogusControlFlow2才是那个被注册成pass的,而那个看上去更复杂的BogusControlFlow反而完全没有被使用上。

elseif&nbsp;(pass ==&nbsp;"bcf") {
FunctionPassManagerFPM;
FPM.addPass(BogusControlFlow2());
MPM.addPass(createModuleToFunctionPassAdaptor(std::move(FPM)));
&nbsp; &nbsp; }

Example

对一个csmith生成的测试样本进行bcf混淆后,得到的混淆片段如下:

if&nbsp;(iVar4 ==&nbsp;0) {
if&nbsp;(y.12&nbsp;<&nbsp;10&nbsp;|| ((x.11&nbsp;+&nbsp;1) * x.11&nbsp;&&nbsp;1U) ==&nbsp;0)&nbsp;goto&nbsp;LAB_001012af;
do&nbsp;{
&nbsp; &nbsp; &nbsp; &nbsp; local_28->f0 =&nbsp;1;
LAB_001012af:
&nbsp; &nbsp; &nbsp; &nbsp; local_28->f0 =&nbsp;1;
&nbsp; &nbsp; &nbsp; }&nbsp;while&nbsp;(9&nbsp;< y.14&nbsp;&& ((x.13&nbsp;+&nbsp;1) * x.13&nbsp;&&nbsp;1U) !=&nbsp;0);
&nbsp; &nbsp; }
&nbsp; }
if&nbsp;(y.16&nbsp;<&nbsp;10&nbsp;|| ((x.15&nbsp;+&nbsp;1) * x.15&nbsp;&&nbsp;1U) ==&nbsp;0)&nbsp;goto&nbsp;LAB_0010132a;

注意,这里由于还是使用的旧的BogusControlFlow2, 所以生成的opaque predicate比较简单。

Deobfuscation Prototype

核心点在于对opaque predicate的判断。我认为可以分成如下流程:

  • collect. 使用基于DDG的backward slicing收集影响branch guard的所有指令。
  • merge(optional). 有的时候,一个opaque predicate可能会在编译后被拆分成独立的多个条件进行判断;因此需要对这些条件进行合并,得到一个完整的predicate。在测试的样本里面并没有遇到过这种情况,暂时跳过这层的考虑。
  • solve. 使用符号执行对收集到的predicate进行求解,得到一个优化后的判断条件。在这个BogusControlFlow的例子中,得到的条件应该就是要么True,要么False。
  • patch. 对binary进行patch,消除opaque predicates对反编译器的影响。

不过在涉及到细节的时候,对这三个步骤的实现会复杂不少。具体而言:

  • collect. 我们需要确定到底是哪种code construct需要进行collect。对于IndirectCall,我们只需要遍历,对所有的indirect call site进行collect即可;但是对于BogusControlFlow这种就比较tricky;理论上我们需要对所有的branch predicates进行收集。这显然增加了不少工作量。
  • solve. 使用符号执行沿slice路径执行到branch处,然后检查guard是否只能取一个值。由于opaque predicate永远为真或者永远为假,因此我们可以从这个入手。
  • patch. 只需要patch成直接跳转即可,难度不大。

下面简单post一下实现的代码:

Step 1: 找到所有branch

首先,我们需要找到目标函数中所有的conditional branch指令。在VEX IR中,一个conditional branch对应的block会有Ijk_Boring类型的jumpkind,同时block内部会有一个Exit语句(也是Ijk_Boring类型)。

def&nbsp;find_branches(proj, func):
&nbsp; &nbsp; result = []
&nbsp; &nbsp; for block_addr in func.block_addrs:
&nbsp; &nbsp; &nbsp; &nbsp; block = proj.factory.block(block_addr)
&nbsp; &nbsp; &nbsp; &nbsp; irsb = block.vex
&nbsp; &nbsp; &nbsp; &nbsp; if irsb.jumpkind ==&nbsp;'Ijk_Boring'&nbsp;and&nbsp;any(
isinstance(s, pyvex.stmt.Exit) and s.jumpkind ==&nbsp;'Ijk_Boring'
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; for s in irsb.statements
&nbsp; &nbsp; &nbsp; &nbsp; ):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; branch_insn = block.capstone.insns[-1]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result.append(branch_insn.address)
&nbsp; &nbsp; return result

这里的逻辑很简单:遍历函数中的所有basic block,找到那些包含conditional branch的block(block的default exit是Ijk_Boring,且内部有一个Exit语句也是Ijk_Boring),然后把branch指令的地址记录下来。

Step 2: Backward Slicing

对于每个branch,我们需要进行backward slicing来收集所有影响这个branch guard的指令。这里使用了angr的DDG(Data Dependency Graph)来进行backward slicing。

def&nbsp;backward_slice_from(proj, cfg, ddg, target_insn_addr):
&nbsp; &nbsp; block_node = cfg.model.get_any_node(target_insn_addr, anyaddr=True)
if&nbsp;block_node&nbsp;is&nbsp;None:
raise&nbsp;RuntimeError(f"No CFG node found containing 0x{target_insn_addr:x}")

&nbsp; &nbsp; irsb = proj.factory.block(block_node.addr).vex
&nbsp; &nbsp; exit_indices = {
&nbsp; &nbsp; &nbsp; &nbsp; i&nbsp;for&nbsp;i, s&nbsp;in&nbsp;enumerate(irsb.statements)
if&nbsp;isinstance(s, pyvex.stmt.Exit)
&nbsp; &nbsp; }&nbsp;or&nbsp;{-2}

&nbsp; &nbsp; seed_nodes = [
&nbsp; &nbsp; &nbsp; &nbsp; n&nbsp;for&nbsp;n&nbsp;in&nbsp;ddg.graph.nodes()
if&nbsp;getattr(n,&nbsp;'block_addr',&nbsp;None) == block_node.addr
and&nbsp;getattr(n,&nbsp;'stmt_idx',&nbsp;None)&nbsp;in&nbsp;exit_indices
&nbsp; &nbsp; ]
if&nbsp;not&nbsp;seed_nodes:
raise&nbsp;RuntimeError(f"No DDG nodes found for ins_addr=0x{target_insn_addr:x}")

# BFS backward through the DDG
&nbsp; &nbsp; visited =&nbsp;set()
&nbsp; &nbsp; queue = deque(seed_nodes)
&nbsp; &nbsp; slice_cls =&nbsp;set()
while&nbsp;queue:
&nbsp; &nbsp; &nbsp; &nbsp; cl = queue.popleft()
if&nbsp;cl&nbsp;in&nbsp;visited:
continue
&nbsp; &nbsp; &nbsp; &nbsp; visited.add(cl)
&nbsp; &nbsp; &nbsp; &nbsp; slice_cls.add(cl)
for&nbsp;pred&nbsp;in&nbsp;ddg.graph.predecessors(cl):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; queue.append(pred)
return&nbsp;slice_cls

具体流程如下:

  • 首先通过CFG找到包含目标指令的block node。
  • 然后在VEX IR中找到所有的Exit语句——这些就是conditional branch的guard所在的位置。
  • 在DDG中找到对应的节点作为seed。
  • 从seed节点开始,沿DDG的反向边进行BFS,收集所有能够到达seed的节点——这就是backward slice。

最终得到的slice_cls就是所有影响这个branch guard值的指令集合。

Step 3: 符号执行求解

拿到backward slice之后,接下来就是通过符号执行来判断这个branch guard是不是一个opaque predicate。

def&nbsp;analyze_branch_guard(proj, slice_cls, branch_block_addr):
&nbsp; &nbsp; block_addrs =&nbsp;sorted(set(
&nbsp; &nbsp; &nbsp; &nbsp; cl.block_addr for cl in slice_cls if cl.block_addr is not None
&nbsp; &nbsp; ))
&nbsp; &nbsp; if branch_block_addr not in block_addrs:
&nbsp; &nbsp; &nbsp; &nbsp; block_addrs.append(branch_block_addr)

&nbsp; &nbsp; state = proj.factory.blank_state(addr=block_addrs[0])
&nbsp; &nbsp; simgr = proj.factory.simgr(state)

&nbsp; &nbsp; for next_addr in block_addrs[1:]:
&nbsp; &nbsp; &nbsp; &nbsp; simgr.step()
&nbsp; &nbsp; &nbsp; &nbsp; simgr.move('active',&nbsp;'deadended', lambda s, na=next_addr: s.addr != na)
&nbsp; &nbsp; &nbsp; &nbsp; if not simgr.active:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; break

首先,我们收集backward slice中涉及到的所有block地址并排序。然后从第一个block开始进行符号执行,逐步step到下一个block。在每次step之后,我们把那些跑到了”错误”地址的state移到deadendedstash里——这样就保证我们只沿着slice中的路径执行。

# 从VEX IR中获取conditional exit语句
&nbsp; &nbsp; irsb = proj.factory.block(branch_block_addr).vex
&nbsp; &nbsp; cond_exit =&nbsp;next(
&nbsp; &nbsp; &nbsp; &nbsp; (s&nbsp;for&nbsp;s&nbsp;in&nbsp;irsb.statements&nbsp;if&nbsp;isinstance(s, pyvex.stmt.Exit)),
None,
&nbsp; &nbsp; )
if&nbsp;cond_exit&nbsp;is&nbsp;None:
return&nbsp;'unconditional',&nbsp;None

# 在branch block再step一次,观察产生的successor数量
&nbsp; &nbsp; succs = simgr.active[0].step()

if&nbsp;len(succs.successors) ==&nbsp;1:
&nbsp; &nbsp; &nbsp; &nbsp; taken_addr = cond_exit.dst.value
&nbsp; &nbsp; &nbsp; &nbsp; resolved_addr = succs.successors[0].addr
if&nbsp;resolved_addr == taken_addr:
return&nbsp;'always_true',&nbsp;None
else:
return&nbsp;'always_false',&nbsp;None

# 有两个successor,用solver检查guard的可满足性
&nbsp; &nbsp; guard = succs.successors[0].history.jump_guard
&nbsp; &nbsp; solver = simgr.active[0].solver

&nbsp; &nbsp; can_be_true &nbsp;= solver.satisfiable(extra_constraints=[guard])
&nbsp; &nbsp; can_be_false = solver.satisfiable(extra_constraints=[claripy.Not(guard)])

if&nbsp;can_be_true&nbsp;and&nbsp;not&nbsp;can_be_false:
return&nbsp;'always_true', guard
elif&nbsp;can_be_false&nbsp;and&nbsp;not&nbsp;can_be_true:
return&nbsp;'always_false', guard
else:
return&nbsp;'symbolic', guard

到达branch block之后,我们再step一次,检查产生了多少个successor:

  • 只有1个successor:说明angr已经具体化了guard的值,直接判定为opaque predicate。通过比较resolved地址和Exit语句的目标地址来判断是always_true还是always_false

  • 有2个successor:说明guard是符号化的。这时候我们用solver分别检查guard能否为true和能否为false:

  • 只能为true →always_true(opaque predicate)

  • 只能为false →always_false(opaque predicate)

  • 两者皆可 →symbolic(真正的branch,不是opaque predicate)

Step 4: Patch

一旦确定了一个branch是opaque predicate,接下来就需要对binary进行patch。

def&nbsp;build_slice_patch(proj, slice_cls, target_addr, insn="jmp"):
# 收集slice中所有指令的地址和大小
&nbsp; &nbsp; seen = {}
for&nbsp;cl&nbsp;in&nbsp;slice_cls:
&nbsp; &nbsp; &nbsp; &nbsp; addr = cl.ins_addr
if&nbsp;addr&nbsp;is&nbsp;None&nbsp;or&nbsp;addr&nbsp;in&nbsp;seen:
continue
for&nbsp;i&nbsp;in&nbsp;proj.factory.block(addr).capstone.insns:
if&nbsp;i.address == addr:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; seen[addr] = i.size
break
&nbsp; &nbsp; insns =&nbsp;sorted(seen.items())

# 找到第一个连续区域 >= 5 bytes(jmp指令需要5字节)
&nbsp; &nbsp; patch_start = patch_total =&nbsp;None
for&nbsp;i, (addr, size)&nbsp;in&nbsp;enumerate(insns):
&nbsp; &nbsp; &nbsp; &nbsp; run_size = size
for&nbsp;j&nbsp;in&nbsp;range(i +&nbsp;1,&nbsp;len(insns)):
if&nbsp;insns[j-1][0] + insns[j-1][1] != insns[j][0]:
break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; run_size += insns[j][1]
if&nbsp;run_size >= INSN_SIZE:
break
if&nbsp;run_size >= INSN_SIZE:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; patch_start, patch_total = addr, run_size
break

# 在找到的区域放置jmp指令,其余全部NOP
&nbsp; &nbsp; asm_bytes, _ = ks.asm(f"{insn}&nbsp;0x{target_addr:x}", addr=patch_start)
&nbsp; &nbsp; patches = {}
&nbsp; &nbsp; patches[patch_start - file_base] =&nbsp;bytes(asm_bytes) +&nbsp;b'\x90'&nbsp;* (patch_total -&nbsp;len(asm_bytes))
for&nbsp;addr, size&nbsp;in&nbsp;insns:
if&nbsp;patch_start <= addr < patch_start + patch_total:
continue
&nbsp; &nbsp; &nbsp; &nbsp; patches[addr - file_base] =&nbsp;b'\x90'&nbsp;* size
return&nbsp;patches

Patch的策略:

  • 收集backward slice中所有指令的地址和大小。
  • 找到一段连续的、至少5字节的区域——因为x86-64的jmp rel32指令正好需要5字节。
  • 在这个区域放置一条无条件跳转指令jmp target,跳转目标是opaque predicate恒真/恒假所对应的那个分支。
  • 把slice中其余所有指令全部NOP掉——因为这些指令都只是用来计算opaque predicate的,去掉它们不会影响程序的正确性。

主流程

最后,把上面的步骤串起来:

proj, main_func, cfg, ddg = load_everything(
&nbsp; &nbsp; TARGET_BINARY, target_func_name=TARGET_FUNC_NAME,
&nbsp; &nbsp; cfg_type="Emulated", auto_load_libs=False
)

branches = find_branches(proj, main_func)
print(f"Found&nbsp;{len(branches)}&nbsp;branches in&nbsp;{TARGET_FUNC_NAME}.")

all_patches = []
for&nbsp;branch_addr&nbsp;in&nbsp;branches:
&nbsp; &nbsp; block_node = cfg.model.get_any_node(branch_addr, anyaddr=True)
&nbsp; &nbsp; block_addr = block_node.addr

&nbsp; &nbsp; slice_cls = backward_slice_from(proj, cfg, ddg, branch_addr)

&nbsp; &nbsp; kind, guard = analyze_branch_guard(proj, slice_cls, block_addr)
print(f" &nbsp;Guard:&nbsp;{kind}")

if&nbsp;kind&nbsp;in&nbsp;('always_true',&nbsp;'always_false'):
&nbsp; &nbsp; &nbsp; &nbsp; irsb = proj.factory.block(block_addr).vex
&nbsp; &nbsp; &nbsp; &nbsp; cond_exit =&nbsp;next(s&nbsp;for&nbsp;s&nbsp;in&nbsp;irsb.statements&nbsp;if&nbsp;isinstance(s, pyvex.stmt.Exit))
&nbsp; &nbsp; &nbsp; &nbsp; taken_addr = cond_exit.dst.value
&nbsp; &nbsp; &nbsp; &nbsp; fall_addr = irsb.next.con.value
&nbsp; &nbsp; &nbsp; &nbsp; patch_target = taken_addr&nbsp;if&nbsp;kind ==&nbsp;'always_true'&nbsp;else&nbsp;fall_addr
&nbsp; &nbsp; &nbsp; &nbsp; all_patches.append(build_slice_patch(proj, slice_cls, patch_target, insn='jmp'))

if&nbsp;all_patches:
&nbsp; &nbsp; apply_patches(all_patches, TARGET_BINARY, OUTPUT_BINARY)
print(f"\nWrote&nbsp;{len(all_patches)}&nbsp;patches ->&nbsp;{OUTPUT_BINARY}")

对于每个branch:

  • 做backward slicing
  • 符号执行判断是否为opaque predicate
  • 如果是,确定正确的跳转目标(always_true取taken分支,always_false取fallthrough分支),生成patch
  • 最后一次性把所有patch写入到输出文件

评测效果

这是原来混淆的:

去混淆后:

Limitations

当前的实现存在一些限制:

目前的实现对于x*(x+1) % 2 == 0这类简单的opaque predicate效果很好;但对于更复杂的构造(如前面分析的基于modular inverse的方案),可能需要更强的约束求解能力。

Backward slice的连续性假设

Patch阶段假设slice中存在一段至少5字节的连续指令区域来放置jmp指令。虽然在实际中这个假设基本成立,但理论上可能存在slice中指令极度碎片化的极端情况。

代码地址

https://github.com/Taardisaa/DePolaris

#

#

看雪ID:Taardisaa

https://bbs.kanxue.com/user-home-1070326.htm

*本文为看雪论坛优秀文章,由 Taardisaa 原创,转载请注明来自看雪社区

往期推荐

安卓逆向基础知识之frida Hook

2025 强网杯和强网拟态部分题解

在逆向分析方面-unidbg真的适合 MCP 吗?

AI静态分析,内核模块隐藏 Frida 特征,绕过linker私有结构遍历崩溃链

某安全so库深度解析

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:看雪学苑 Taardisaa Taardisaa《Polaris-Obfuscator中BogusControlFlow简要分析 反混淆》

被美国悬赏后的影响有多大 网络安全文章

被美国悬赏后的影响有多大

文章总结: 本文作者讲述了被美国悬赏后的亲身经历,指出悬赏虽非制裁但对个人生活造成毁灭性打击。主要影响包括健康受损、职业生涯中断及人际关系破裂,导致生活陷入贫困
评论:0   参与:  0