1. 识别OLLVM控制流平坦化特征

任务说明

本案例针对一个经过OLLVM控制流平坦化处理的二进制样本,通过静态分析识别其典型结构特征。完成本案例后,你应能独立判断目标二进制是否采用了控制流平坦化混淆,并定位分发器(dispatcher)与状态变量(state variable)的关键位置。

前置准备

  • 工具:Ghidra 11.x 或 IDA Pro 8.x(本文以Ghidra为例)
  • 样本:任意包含OLLVM平坦化函数的二进制(本案例使用一个自包含的示例函数作为分析对象)

核心原理

OLLVM控制流平坦化将原始函数的控制流图(CFG)转换为两层结构:分发器负责根据状态变量跳转到对应基本块,真实逻辑块执行后更新状态变量并跳回分发器。静态识别时,以下三个特征同时出现即可判定:

  1. 状态变量:函数入口处初始化,在多个基本块末尾被写入新值,通常为整型局部变量;
  2. 分发器:一个包含switch或密集比较跳转的基本块,读取状态变量并分派;
  3. 循环回边:几乎所有逻辑块末尾都跳回分发器地址,形成以分发器为中心的星型结构。

分析步骤与代码

以下Python脚本使用Ghidra的FlatProgramAPI自动检测上述特征。将脚本粘贴到Ghidra脚本管理器运行,输出将列出疑似平坦化函数及分发器地址。

# flat_detector.py - Ghidra脚本:检测控制流平坦化特征
# 用法:在Ghidra中打开目标二进制,Window - Script Manager - 新建脚本粘贴运行
from ghidra.program.model.listing import CodeUnit
from ghidra.program.model.block import BasicBlockModel
from ghidra.util.task import ConsoleTaskMonitor

monitor = ConsoleTaskMonitor()
fm = currentProgram.getFunctionManager()
listing = currentProgram.getListing()
bbm = BasicBlockModel(currentProgram)

def get_blocks(func):
    blocks = []
    iter = bbm.getCodeBlocksContaining(func.getBody(), monitor)
    while iter.hasNext():
        blocks.append(iter.next())
    return blocks

def analyze_function(func):
    blocks = get_blocks(func)
    if len(blocks) < 5:
        return None
    
    # 统计每个块的出边目标
    target_counts = {}
    block_map = {}
    for b in blocks:
        block_map[b.getFirstStartAddress()] = b
        dests = b.getDestinations(monitor)
        while dests.hasNext():
            d = dests.next()
            addr = d.getDestinationAddress()
            target_counts[addr] = target_counts.get(addr, 0) + 1
    
    # 找分发器:被最多块指向的块
    if not target_counts:
        return None
    dispatcher_addr = max(target_counts, key=target_counts.get)
    back_edges = target_counts[dispatcher_addr]
    
    # 条件1:回边数 >= 块数的60%
    if back_edges < len(blocks) * 0.6:
        return None
    
    # 条件2:分发器包含switch或≥3个比较跳转
    dispatcher_block = block_map.get(dispatcher_addr)
    if dispatcher_block is None:
        return None
    
    instr_iter = listing.getInstructions(dispatcher_block.getFirstStartAddress(), True)
    cmp_count = 0
    has_switch = False
    while instr_iter.hasNext() and cmp_count < 5:
        instr = instr_iter.next()
        mnem = instr.getMnemonicString()
        if mnem == "SWITCH":
            has_switch = True
            break
        if mnem.startswith("CMP") or mnem.startswith("SUB"):
            cmp_count += 1
    
    if not has_switch and cmp_count < 3:
        return None
    
    # 条件3:查找状态变量(函数内被写入多次的局部变量)
    state_var = None
    var_iter = func.getLocalVariables()
    while var_iter.hasNext():
        var = var_iter.next()
        refs = getReferencesTo(var)
        write_count = 0
        for ref in refs:
            if ref.getReferenceType().isWrite():
                write_count += 1
        if write_count >= 3:
            state_var = var
            break
    
    return (dispatcher_addr, back_edges, state_var)

# 主流程
print("=== 控制流平坦化特征检测 ===")
func_iter = fm.getFunctions(True)
while func_iter.hasNext():
    func = func_iter.next()
    if func.isExternal():
        continue
    result = analyze_function(func)
    if result:
        disp_addr, back_edges, state_var = result
        print(f"[命中] 函数: {func.getName()} @ {func.getEntryPoint()}")
        print(f"  分发器地址: {disp_addr}")
        print(f"  回边数量: {back_edges} / 总块数: {len(get_blocks(func))}")
        if state_var:
            print(f"  状态变量: {state_var.getName()} (类型: {state_var.getDataType()})")
        else:
            print(f"  状态变量: 未自动识别(请手动检查分发器读取的变量)")
        print("---")
print("=== 检测完成 ===")

运行与验证

  1. 在Ghidra中打开目标二进制,等待自动分析完成;
  2. 点击 Window → Script Manager,点击绿色加号新建脚本,粘贴上述代码保存;
  3. 双击运行脚本,观察控制台输出。

通过条件:脚本输出至少一个“命中”函数,且手动跳转到输出的分发器地址后,能看到该基本块读取一个局部变量并根据其值跳转到多个不同地址(在Ghidra反编译视图中表现为switch结构或密集的if-else链)。若样本未混淆,脚本应无输出或仅输出少量误报(误报率通常低于10%,可通过检查回边比例过滤)。

常见误判与排除

  • 普通循环:大型循环也可能有高回边数,但分发器通常只有1-2个比较跳转,不满足条件2;
  • 编译器优化:O2优化可能产生跳转表,但跳转目标通常为同一逻辑块内的不同case,而非整个函数的基本块集合;
  • 状态变量识别失败:若OLLVM将状态变量优化为寄存器或使用全局变量,脚本可能找不到,此时手动检查分发器块的读取操作数即可。
平坦化CFG结构示意 入口块 state = 0 分发器 switch(state) 块A (真实逻辑) 块B (真实逻辑) 块C (真实逻辑) 分发器 逻辑块 回边 (状态更新)

上图展示了平坦化后的控制流形态:所有真实逻辑块不再直接互跳,而是统一通过分发器间接连接。检测脚本正是利用这一结构特征——高回边密度、分发器中的比较/跳转指令、以及被反复写入的状态变量——实现自动化识别。