Polaris-Obfuscator中IndirectCall简要分析+反混淆

admin 2026-04-21 02:32:55 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文分析了Polaris-Obfuscator中的IndirectCall混淆技术,该技术通过将直接函数调用改为间接调用并添加随机掩码来隐藏真实函数地址。作者详细解析了LLVM层面的实现代码,指出单独使用该技术防护效果有限,但结合MBA混淆会显著增加分析难度。文章提出了基于约束求解和符号执行的反混淆方案,包括收集变量约束、使用Z3或angr求解、以及通过指令替换进行二进制修补,并提供了可在MBA混淆样本上工作的原型工具。 综合评分: 85 文章分类: 二进制安全,逆向分析,恶意软件,安全工具,漏洞分析


cover_image

Polaris-Obfuscator中IndirectCall简要分析+反混淆

Taardisaa Taardisaa

看雪学苑

2026年4月17日 18:09 上海

在小说阅读器读本章

去阅读

IndirectCall 简要介绍

这个obfuscation pass简单来说就是把直接调用改成间接调用。比如:

call func_1

改成:

; perform some operations to compute the address of func_1
; and load the address into a register, say rax
call rax

在我所测试用的例子里面,代码如下:

MOV        RAX ,qword ptr [DAT_00104040 ]                    = 00000000328B36C6h
SUB        RAX ,0x327b23c6
CALL       RAX => platform_main_begin                        void platform_main_begin(void)

反编译出来就是:

(*(code *)(DAT_00104040 + -0x327b23c6))();

#

具体实现

在该Obfuscator的具体实现中(见src/llvm/lib/Transforms/Obfuscation/IndirectCall.cpp):

void IndirectCall::process(Function &F) {
  // F.getParent() 返回 llvm::Module*,
  // DataLayout 包含 该 Module 的各种数据布局信息。详见https://llvm.org/doxygen/classllvm_1_1DataLayout.html
  DataLayout Data = F.getParent()->getDataLayout();
  // F.getContext() 返回 llvm::LLVMContext&
  // ->getPointerTo() 返回一个指向该函数的指针类型
  // Data.getTypeAllocSize(ptr) 返回该指针类型的大小(以字节为单位)。
  int PtrSize =
      Data.getTypeAllocSize(Type::getInt8Ty(F.getContext())->getPointerTo());
  // getIntNTy 返回一个具有指定位数的整数类型。假设前面函数指针的大小(PtrSize)是 8 字节(64 位系统),则 PtrValueType 将是一个 64 位的整数类型。
  Type *PtrValueType = Type::getIntNTy(F.getContext(), PtrSize * 8);

  // 遍历函数 F 中的所有基本块和指令,寻找调用指令(CallInst)。对于每个调用指令,检查它是否调用了一个具有确切定义的函数(即不是间接调用)。如果是,则将该调用指令添加到 CIs 中,以便稍后处理。
&nbsp; std::vector<CallInst *> CIs;
&nbsp;&nbsp;for&nbsp;(BasicBlock &BB : F) {
&nbsp; &nbsp;&nbsp;for&nbsp;(Instruction &I : BB) {
&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(isa<CallInst>(I)) {
&nbsp; &nbsp; &nbsp; &nbsp; CallInst *CI = (CallInst *)&I;
&nbsp; &nbsp; &nbsp; &nbsp; Function *Func = CI->getCalledFunction();
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(Func && Func->hasExactDefinition()) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; CIs.push_back(CI);
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }
&nbsp; }

&nbsp;&nbsp;for&nbsp;(CallInst *CI : CIs) {
&nbsp; &nbsp;&nbsp;// 函数指针类型
&nbsp; &nbsp; Type *Ty = CI->getFunctionType()->getPointerTo();

&nbsp; &nbsp; Constant *Func = (Constant *)CI->getCalledFunction();
&nbsp; &nbsp;&nbsp;// First of all, 将函数cast成一个整数值(即函数的地址)
&nbsp; &nbsp; Constant *CValue = ConstantExpr::getPtrToInt(
&nbsp; &nbsp; &nbsp; &nbsp; ConstantExpr::getBitCast(Func, Ty,&nbsp;false), PtrValueType,&nbsp;false);
&nbsp; &nbsp;&nbsp;// 然后随机生成一个掩码(Mask),32bit
&nbsp; &nbsp; unsigned Mask =&nbsp;getRandomNumber();
&nbsp; &nbsp;&nbsp;// 将掩码添加到函数地址上,得到一个新的整数值
&nbsp; &nbsp; CValue = ConstantExpr::getAdd(CValue, ConstantInt::get(PtrValueType, Mask));
&nbsp; &nbsp;&nbsp;// 最后将这个整数值转换回一个指针类型。
&nbsp; &nbsp; CValue = ConstantExpr::getIntToPtr(
&nbsp; &nbsp; &nbsp; &nbsp; CValue, Type::getInt8Ty(F.getContext())->getPointerTo());
&nbsp; &nbsp;&nbsp;// 总结以上操作便是:CValue = Func + getRandomNumber()

&nbsp; &nbsp;&nbsp;// 创建一个全局变量GV(好糟糕的名字)
&nbsp; &nbsp;&nbsp;// 后面这个全局变量被设置成了CValue,即Func + Mask
&nbsp; &nbsp; GlobalVariable *GV = new&nbsp;GlobalVariable(
&nbsp; &nbsp; &nbsp; &nbsp; *(F.getParent()), Type::getInt8Ty(F.getContext())->getPointerTo(),
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;false, GlobalValue::PrivateLinkage, NULL);

&nbsp; &nbsp;&nbsp;// 使用 IRBuilder 来构建新的指令。
&nbsp; &nbsp;&nbsp;/* 这里创建的IRs就是:(伪代码)

&nbsp; &nbsp; &nbsp; MaskValue = (uint64_t) Mask &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// zero-extend Mask to pointer width
&nbsp; &nbsp; &nbsp; loaded &nbsp; &nbsp;= *GV &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// load the masked pointer (Func + Mask)
&nbsp; &nbsp; &nbsp; int_val &nbsp; = (uint64_t) loaded &nbsp; &nbsp; &nbsp; &nbsp;// reinterpret as integer
&nbsp; &nbsp; &nbsp; real_addr = int_val - MaskValue &nbsp; &nbsp; &nbsp;// subtract Mask → recovers Func
&nbsp; &nbsp; &nbsp; CallPtr &nbsp; = (FuncType*) real_addr &nbsp; &nbsp;// cast back to function pointer type
&nbsp; &nbsp; */
&nbsp; &nbsp; IRBuilder<>&nbsp;IRB((Instruction *)CI);
&nbsp; &nbsp; Value *MaskValue = IRB.getInt32(Mask);
&nbsp; &nbsp; MaskValue = IRB.CreateZExt(MaskValue, PtrValueType);
&nbsp; &nbsp; Value *CallPtr = IRB.CreateIntToPtr(
&nbsp; &nbsp; &nbsp; &nbsp; IRB.CreateSub(IRB.CreatePtrToInt(IRB.CreateLoad(IRB.getInt8PtrTy(), GV),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;PtrValueType),
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; MaskValue),
&nbsp; &nbsp; &nbsp; &nbsp; Ty);
&nbsp; &nbsp; CI->setCalledFunction(CI->getFunctionType(), CallPtr);
&nbsp; &nbsp; GV->setInitializer(CValue);
&nbsp; }
}

总结一下就是:

  • 创建一个全局变量GV,设置成func_addr + random_value。个人认为创建全局变量的主要目的是为了防止被编译器优化掉。
  • 然后创建IR来构造以下公式:GV – random_value. 这个值也就是func_addr。不过由于这个地址是动态创建的,因此编译器会使用间接调用(e.g., call rax)来调用函数。

个人认为单独使用这个obfuscation pass并不能提供太强的保护。从上面Ghidra提供的例子都可以发现,在间接调用上都已经直接表明了会跳转到的真实函数。但是一但结合其他的obfuscation pass,尤其是MBA obfuscation,分析地址的过程就会变得复杂很多。

Deobfuscation

考虑到该obfuscation pass通常还会与更复杂的数据流混淆pass结合使用,比如MBA obfuscation,本人并不打算直接针对该pass的构造流程来进行deobfuscation。

我的想法是,先提出一个更通用的去混淆方法(一个框架/workflow),进而实现通用性的deobfuscation方法。

我将这个问题建模为一个约束求解问题:在一个间接调用点I处,我们已知的信息是:call var。这里的var可以是一个寄存器(如call rax),也可以是一个内存地址(如call [0x114514]);但无论哪种情况,它都不是一个直接调用(即不是一个常量)。我们的目标是找出var的所有可能取值。

如果这个间接调用是由类似上述obfuscation pass构造的,那么var的可能取值应该只有一个。我们可以将其形式化为一个更通用的约束收集与求解问题:

其中 VV 是所有参与构造 var 的变量集合,CC是所有参与构造的常量集合。函数 FF 是对构造 var 的数据流的一般性表示。以上面的例子为例,FF 可以表示为GV-MaskValue.

所以说我们的目标其实很简单,就三个步骤:

collect:做backward slice,收集所有与var相关的变量和常量,并从其数据流关系中构造出一系列的symbolic constraints。

solve: 使用constraint solver来求解这些constraints,得到var的值.比如Z3,或者也可以使用其他求解工具,比如MBA-Blast(https://www.usenix.org/conference/usenixsecurity21/presentation/liu-binbin),这是专门用于求解MBA的。或者更暴力的方法:用angr做符号执行,这样collect+solve就可以合成在一起了。不过具体而言还是要有些trick的,主要核心点就是剪枝:只对backward slice影响到的basic blocks做符号执行,其他全部丢掉。

patch(optional): 将求解得到的var的值直接patch回二进制中。直接改成直接调用是很困难的,因为indirect call的size一般比direct call要小不少,所以根本没那么多空间塞多余的字节。但是有一个简单的这种方法:因为obfuscation本身会添加很多的多余的计算指令;那么我们只需要在其中找到任意一个足够大的instruction,把它patch成mov rax, func_addr,然后将剩余的计算指令nop掉就行了。这样,反编译器会把它正常恢复成一个直接调用了。

Deobfuscation Prototype

这是我针对上述概念简单编写的一个working prototype,目前在IndirectCall+MBA Obfuscation的一个样本上通过了测试。

import&nbsp;angr
from&nbsp;angr&nbsp;import&nbsp;sim_options&nbsp;as&nbsp;o
from&nbsp;angr.analyses.cdg&nbsp;import&nbsp;CDG, TemporaryNode
from&nbsp;collections&nbsp;import&nbsp;deque

TARGET_BINARY =&nbsp;"examples/sample_001_indcall_mba"
OUTPUT_BINARY =&nbsp;"examples/sample_001_mba_patched"
TARGET_FUNC_NAME =&nbsp;"main"

# Patch CDG: _entry defaults to project.entry which may not be in a starts=[main]-only CFG
@staticmethod
def&nbsp;_patched_pd_graph_successors(graph, node):
&nbsp; &nbsp;&nbsp;if&nbsp;node&nbsp;is&nbsp;None&nbsp;or&nbsp;type(node)&nbsp;is&nbsp;TemporaryNode:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;iter([])
&nbsp; &nbsp;&nbsp;return&nbsp;(s&nbsp;for&nbsp;s&nbsp;in&nbsp;graph.model.get_successors(node)&nbsp;if&nbsp;s&nbsp;is&nbsp;not&nbsp;None)
CDG._pd_graph_successors = _patched_pd_graph_successors

proj = angr.Project(TARGET_BINARY, auto_load_libs=False)
main_addr = proj.loader.find_symbol(TARGET_FUNC_NAME).rebased_addr &nbsp;# type: ignore

cfg = proj.analyses.CFGEmulated(
&nbsp; &nbsp; keep_state=True,
&nbsp; &nbsp; normalize=True,
&nbsp; &nbsp; starts=[main_addr],
&nbsp; &nbsp; state_add_options={o.TRACK_REGISTER_ACTIONS, o.TRACK_MEMORY_ACTIONS, o.TRACK_TMP_ACTIONS},
)
ddg = proj.analyses.DDG(cfg, start=main_addr)

def&nbsp;find_indirect_calls(proj, func):
&nbsp; &nbsp;&nbsp;"""Return addresses of all indirect call instructions in a function."""
&nbsp; &nbsp;&nbsp;import&nbsp;pyvex
&nbsp; &nbsp; result = []
&nbsp; &nbsp;&nbsp;for&nbsp;block_addr&nbsp;in&nbsp;func.block_addrs:
&nbsp; &nbsp; &nbsp; &nbsp; block = proj.factory.block(block_addr)
&nbsp; &nbsp; &nbsp; &nbsp; irsb = block.vex
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# Indirect call: exit jumpkind is Call and target is not a constant
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;irsb.jumpkind ==&nbsp;'Ijk_Call'&nbsp;and&nbsp;not&nbsp;isinstance(irsb.next, pyvex.expr.Const):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;# The call instruction is the last one in the block
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; call_insn = block.capstone.insns[-1]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; result.append(call_insn.address)
&nbsp; &nbsp;&nbsp;return&nbsp;result

def&nbsp;slice_to_symbolic(proj, slice_cls, target_reg='rax'):
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; Symbolically execute the blocks in a backward slice and return
&nbsp; &nbsp; the symbolic expression for target_reg at the end of the slice.
&nbsp; &nbsp; """
&nbsp; &nbsp; block_addrs =&nbsp;sorted(set(
&nbsp; &nbsp; &nbsp; &nbsp; cl.block_addr&nbsp;for&nbsp;cl&nbsp;in&nbsp;slice_cls&nbsp;if&nbsp;cl.block_addr&nbsp;is&nbsp;not&nbsp;None
&nbsp; &nbsp; ))
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;block_addrs:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None

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

&nbsp; &nbsp;&nbsp;# Step through each block, keeping only states headed to the next slice block
&nbsp; &nbsp;&nbsp;for&nbsp;next_addr&nbsp;in&nbsp;block_addrs[1:]:
&nbsp; &nbsp; &nbsp; &nbsp; simgr.step()
&nbsp; &nbsp; &nbsp; &nbsp; simgr.move('active',&nbsp;'deadended',&nbsp;lambda&nbsp;s, na=next_addr: s.addr != na)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;simgr.active:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

&nbsp; &nbsp;&nbsp;# Step the final block
&nbsp; &nbsp;&nbsp;if&nbsp;simgr.active:
&nbsp; &nbsp; &nbsp; &nbsp; simgr.step()

&nbsp; &nbsp; all_states = simgr.active + simgr.deadended + simgr.unsat
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;all_states:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;None

&nbsp; &nbsp;&nbsp;return&nbsp;all_states[0].regs.get(target_reg)

def&nbsp;build_slice_patch(proj, slice_cls, target_addr):
&nbsp; &nbsp;&nbsp;"""
&nbsp; &nbsp; Compute the patch bytes for one slice: returns a dict {file_offset: bytes}.
&nbsp; &nbsp; Finds the first contiguous slice region >= 5 bytes, places 'call target' there,
&nbsp; &nbsp; and NOPs out everything else.
&nbsp; &nbsp; """
&nbsp; &nbsp;&nbsp;import&nbsp;keystone
&nbsp; &nbsp; ks = keystone.Ks(keystone.KS_ARCH_X86, keystone.KS_MODE_64)
&nbsp; &nbsp; CALL_SIZE =&nbsp;5

&nbsp; &nbsp; seen = {}
&nbsp; &nbsp;&nbsp;for&nbsp;cl&nbsp;in&nbsp;slice_cls:
&nbsp; &nbsp; &nbsp; &nbsp; addr = cl.ins_addr
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;addr&nbsp;is&nbsp;None&nbsp;or&nbsp;addr&nbsp;in&nbsp;seen:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;insn&nbsp;in&nbsp;proj.factory.block(addr).capstone.insns:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;insn.address == addr:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; seen[addr] = insn.size
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; insns =&nbsp;sorted(seen.items())

&nbsp; &nbsp; patch_start = patch_total =&nbsp;None
&nbsp; &nbsp;&nbsp;for&nbsp;i, (addr, size)&nbsp;in&nbsp;enumerate(insns):
&nbsp; &nbsp; &nbsp; &nbsp; run_size = size
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;j&nbsp;in&nbsp;range(i +&nbsp;1,&nbsp;len(insns)):
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;insns[j-1][0] + insns[j-1][1] != insns[j][0]:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; run_size += insns[j][1]
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;run_size >= CALL_SIZE:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;run_size >= CALL_SIZE:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; patch_start, patch_total = addr, run_size
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

&nbsp; &nbsp;&nbsp;if&nbsp;patch_start&nbsp;is&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"No contiguous slice region >=&nbsp;{CALL_SIZE}&nbsp;bytes for call 0x{target_addr:x}")

&nbsp; &nbsp; call_bytes, _ = ks.asm(f"call 0x{target_addr:x}", addr=patch_start)
&nbsp; &nbsp;&nbsp;assert&nbsp;call_bytes&nbsp;is&nbsp;not&nbsp;None
&nbsp; &nbsp; file_base = proj.loader.main_object.min_addr
&nbsp; &nbsp; patches = {}
&nbsp; &nbsp; patches[patch_start - file_base] =&nbsp;bytes(call_bytes) +&nbsp;b'\x90'&nbsp;* (patch_total -&nbsp;len(call_bytes))
&nbsp; &nbsp;&nbsp;for&nbsp;addr, size&nbsp;in&nbsp;insns:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;patch_start <= addr < patch_start + patch_total:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;continue
&nbsp; &nbsp; &nbsp; &nbsp; patches[addr - file_base] =&nbsp;b'\x90'&nbsp;* size

&nbsp; &nbsp;&nbsp;print(f" &nbsp;-> call 0x{target_addr:x}&nbsp;at 0x{patch_start:x}&nbsp;(+{patch_total - CALL_SIZE}&nbsp;nops)")
&nbsp; &nbsp;&nbsp;return&nbsp;patches

def&nbsp;apply_patches(patches_list, input_file, output_file):
&nbsp; &nbsp;&nbsp;"""Write all accumulated patches to output_file in one pass."""
&nbsp; &nbsp;&nbsp;import&nbsp;shutil
&nbsp; &nbsp; shutil.copy(input_file, output_file)
&nbsp; &nbsp;&nbsp;with&nbsp;open(output_file,&nbsp;"r+b")&nbsp;as&nbsp;f:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;patches&nbsp;in&nbsp;patches_list:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;offset, data&nbsp;in&nbsp;patches.items():
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; f.seek(offset)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; f.write(data)

def&nbsp;backward_slice_from(proj, cfg, ddg, target_insn_addr):
&nbsp; &nbsp;&nbsp;"""Return all DDG nodes in the backward slice of the instruction at target_insn_addr."""
&nbsp; &nbsp;&nbsp;# Find the containing block
&nbsp; &nbsp; block_node = cfg.model.get_any_node(target_insn_addr, anyaddr=True)
&nbsp; &nbsp;&nbsp;if&nbsp;block_node&nbsp;is&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"No CFG node found containing 0x{target_insn_addr:x}")

&nbsp; &nbsp;&nbsp;# Use only the block exit node (stmt_idx == -2), which represents
&nbsp; &nbsp;&nbsp;# the indirect jump/call target — avoids pulling in call mechanics (RSP chain)
&nbsp; &nbsp; seed_nodes = [
&nbsp; &nbsp; &nbsp; &nbsp; n&nbsp;for&nbsp;n&nbsp;in&nbsp;ddg.graph.nodes()
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;getattr(n,&nbsp;'block_addr',&nbsp;None) == block_node.addr
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;and&nbsp;getattr(n,&nbsp;'stmt_idx',&nbsp;None) == -2
&nbsp; &nbsp; ]
&nbsp; &nbsp;&nbsp;if&nbsp;not&nbsp;seed_nodes:
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;raise&nbsp;RuntimeError(f"No DDG nodes found for ins_addr=0x{target_insn_addr:x}")

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

main_func = cfg.kb.functions[main_addr]
indirect_calls = find_indirect_calls(proj, main_func)
print(f"Indirect calls in main:&nbsp;{[hex(a)&nbsp;for&nbsp;a&nbsp;in&nbsp;indirect_calls]}\n")

all_patches = []
for&nbsp;call_addr&nbsp;in&nbsp;indirect_calls:
&nbsp; &nbsp;&nbsp;# call_rax_addr = 0x004011c7
&nbsp; &nbsp; slice_cls = backward_slice_from(proj, cfg, ddg, call_addr)

&nbsp; &nbsp;&nbsp;print(f"\nBackward slice of 0x{call_addr:x}&nbsp;({len(slice_cls)}&nbsp;nodes):")
&nbsp; &nbsp;&nbsp;for&nbsp;cl&nbsp;in&nbsp;sorted(slice_cls, key=lambda&nbsp;x: (x.block_addr&nbsp;or&nbsp;0, x.stmt_idx&nbsp;or&nbsp;0)):
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;cl.ins_addr&nbsp;is&nbsp;not&nbsp;None:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; block = proj.factory.block(cl.ins_addr)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;for&nbsp;insn&nbsp;in&nbsp;block.capstone.insns:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;insn.address == cl.ins_addr:
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;print(f" &nbsp;[{cl.stmt_idx:>3}] 0x{insn.address:x}: &nbsp;{insn.mnemonic}{insn.op_str}")
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break

&nbsp; &nbsp;&nbsp;#&nbsp;TODO:&nbsp;the register here is hardcoded as `rax`. We should change to a more generic approach that detects which register is used in the indirect jump/call and tracks that instead.
&nbsp; &nbsp; sym = slice_to_symbolic(proj, slice_cls, target_reg='rax')
&nbsp; &nbsp;&nbsp;print(f" &nbsp;symbolic rax:&nbsp;{sym}")

&nbsp; &nbsp;&nbsp;if&nbsp;sym&nbsp;is&nbsp;not&nbsp;None&nbsp;and&nbsp;sym.concrete:
&nbsp; &nbsp; &nbsp; &nbsp; all_patches.append(build_slice_patch(proj, slice_cls, sym.concrete_value))

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

这是IndirectCall+MBA混淆后的:

patch后:

优化后:

可见已经恢复了所有的call。

Limitations

现在只是个prototype,所以还有些缺陷。

  • 代码里有一些硬编码的玩意,比如默认indirect call的寄存器总是rax;
  • 该工具会遍历所有的indirect call并尝试patch,或许会影响到一些本来就需要使用indirect call的地方(比如遇到一个jump table);
  • 并未对复杂程序进行大规模测试,性能未知;符号执行收集约束的方法可能会影响到实用性。

#

看雪ID:Taardisaa

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

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

往期推荐

安卓逆向基础知识之frida Hook

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

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

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

某安全so库深度解析

球分享

球点赞

球在看

点击阅读原文查看更多


免责声明:

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

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

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

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

评论:0   参与:  0