发布于 

Ida Trace 分析 非标准算法 和 OLLVM混淆的非标准算法

IDA Trace 功能介绍

IDA Pro 的 Trace 功能是分析复杂算法和混淆代码的强大工具。与简单的断点调试不同,Trace 能够记录一段时间内指令的完整执行序列,包括每条指令的地址、寄存器值、内存读写等信息,为分析提供全局视角。

Trace 的类型

IDA Pro 提供多种 Trace 模式:

Trace 类型 记录内容 适用场景
指令 Trace 每条执行过的指令地址 控制流分析、混淆还原
寄存器 Trace 每步的寄存器快照 算法参数追踪
内存读 Trace 所有内存读操作 数据流分析
内存写 Trace 所有内存写操作 缓冲区追踪
函数调用 Trace 函数的调用和返回 调用链分析

启用 Trace

在 IDA 中启用 Trace 的方法:

Debugger → Tracing → Trace instructions / registers / memory reads / memory writes

Trace 数据会记录到 IDA 的 Trace 窗口中,可以导出为文本文件进行后续分析。

IDA 调试器断点和条件断点

基本断点

在 IDA 调试模式下,按 F2 在当前地址设置断点。程序运行到断点时会暂停,可以查看当前寄存器和内存状态。

条件断点

条件断点是 IDA 的高级功能,允许在满足特定条件时才触发暂停:

# IDA Python 条件断点示例
# 在特定地址设置条件断点:当 x0 寄存器等于特定值时暂停
import idaapi
import idc

addr = 0x1A4C  # 目标地址

# 设置条件断点
idc.add_bpt(addr)
# 条件:ARM64 下 x0 寄存器等于 0x48656C6C
idc.set_bpt_cond(addr, "x0 == 0x48656C6C")

断点脚本化

对于需要大量断点的场景,可以编写 IDA Python 脚本批量设置:

# IDA Python - 批量设置断点
import idc

# 在函数的每个基本块入口设置断点
func_start = 0x1A2B
func_end = 0x1A90
addr = func_start

while addr < func_end:
    # 跳过数据区(检查是否是指令)
    if idc.print_insn_mnem(addr) != "":
        idc.add_bpt(addr)
        print(f"断点: 0x{addr:X}")
    addr = idc.next_head(addr, func_end)

使用 IDA Trace 分析非标准算法

基本分析流程

1. 附加调试器到目标进程
2. 定位算法函数入口
3. 启用指令 Trace + 寄存器 Trace
4. 触发算法执行(输入测试数据)
5. 停止 Trace,分析执行记录
6. 还原算法逻辑

实际操作步骤

# IDA Python - 分析非标准算法的 Trace 脚本
import idaapi
import idc
import idautils

def trace_algorithm():
    """Trace 非标准加密算法的执行流程"""

    # 定义算法函数的范围
    func_start = 0x4A20
    func_end = 0x4B80

    # 获取函数内所有指令
    instructions = []
    addr = func_start
    while addr < func_end:
        mnem = idc.print_insn_mnem(addr)
        disasm = idc.generate_disasm_line(addr, 0)
        if mnem:
            instructions.append({
                'addr': addr,
                'mnemonic': mnem,
                'disasm': disasm
            })
        addr = idc.next_head(addr, func_end)

    print(f"函数包含 {len(instructions)} 条指令")

    # 分析 Trace 数据中的关键模式
    # 1. 统计各指令的执行频率
    exec_count = {}
    for inst in instructions:
        exec_count[inst['addr']] = 0

    # 2. 识别循环结构(重复执行的指令块)
    # 3. 识别查表操作(LDR/STR 指令的基地址)
    load_addresses = set()
    for inst in instructions:
        if inst['mnemonic'] in ['LDR', 'LDRB', 'LDRH']:
            # 提取加载的目标地址
            op1 = idc.print_operand(inst['addr'], 1)
            print(f"加载操作 @ 0x{inst['addr']:X}: {inst['disasm']}")

trace_algorithm()

Trace 数据后处理

# 后处理 Trace 数据,提取算法的关键步骤
def analyze_trace_data(trace_file):
    """分析导出的 Trace 数据"""

    with open(trace_file, 'r') as f:
        lines = f.readlines()

    # 提取执行序列
    exec_sequence = []
    for line in lines:
        if line.strip().startswith('0x'):
            addr = int(line.split()[0], 16)
            exec_sequence.append(addr)

    # 识别基本块边界(B/BL 指令)
    branches = []
    for addr in exec_sequence:
        mnem = idc.print_insn_mnem(addr)
        if mnem in ['B', 'B.EQ', 'B.NE', 'B.LT', 'B.GT', 'BL']:
            branches.append(addr)

    print(f"总执行指令: {len(exec_sequence)}")
    print(f"分支指令: {len(branches)}")
    print(f"基本块估计: ~{len(branches)} 个")

    # 识别循环(重复出现的地址序列)
    seen_sequences = {}
    for i in range(len(exec_sequence) - 5):
        seq = tuple(exec_sequence[i:i+5])
        if seq in seen_sequences:
            seen_sequences[seq] += 1
        else:
            seen_sequences[seq] = 1

    # 找出重复次数最多的序列
    repeated = sorted(seen_sequences.items(),
                      key=lambda x: x[1], reverse=True)
    print("\n重复最多的指令序列(可能是循环体):")
    for seq, count in repeated[:5]:
        if count > 1:
            addrs = [f"0x{a:X}" for a in seq]
            print(f"  重复 {count} 次: {' → '.join(addrs)}")

OLLVM 混淆下的 IDA Trace 分析技巧

绕过虚假分支

OLLVM 的虚假控制流程(BCF)会生成永远不会执行的基本块。通过 IDA Trace,我们可以准确识别这些虚假块:

# IDA Python - 识别虚假基本块
def identify_bogus_blocks(trace_file, all_blocks):
    """
    trace_file: Trace 数据文件路径
    all_blocks: 函数内所有基本块的地址列表
    """

    # 从 Trace 中提取实际执行的地址
    executed_addrs = set()
    with open(trace_file, 'r') as f:
        for line in f:
            line = line.strip()
            if line.startswith('0x'):
                try:
                    addr = int(line.split()[0], 16)
                    executed_addrs.add(addr)
                except ValueError:
                    pass

    # 对比所有块,找出从未执行的
    bogus_blocks = []
    for block in all_blocks:
        if block not in executed_addrs:
            bogus_blocks.append(block)

    print(f"总基本块: {len(all_blocks)}")
    print(f"执行过的: {len(executed_addrs)}")
    print(f"虚假块: {len(bogus_blocks)}")

    # 在 IDA 中标记虚假块
    for addr in bogus_blocks:
        # 添加注释
        idc.set_cmt(addr, "[BOGUS] 虚假块 - 从不执行", 0)
        # 修改块颜色为灰色
        idaapi.set_item_color(addr, 0x808080)

    return bogus_blocks

分析控制流平坦化的 Trace

# IDA Python - 从 Trace 还原 FLA 的真实执行路径
def restore_fla_path(trace_file):
    """从 Trace 数据还原控制流平坦化的真实路径"""

    # 读取 Trace
    executed = []
    with open(trace_file, 'r') as f:
        for line in f:
            line = line.strip()
            if line.startswith('0x'):
                addr = int(line.split()[0], 16)
                executed.append(addr)

    # 去除连续重复(同一条指令被多次 Trace 记录)
    unique_path = []
    prev = None
    for addr in executed:
        if addr != prev:
            unique_path.append(addr)
            prev = addr

    # 按基本块分组
    blocks = []
    current_block = [unique_path[0]]
    for i in range(1, len(unique_path)):
        addr = unique_path[i]
        # 检测基本块边界(分支指令或标签)
        mnem = idc.print_insn_mnem(unique_path[i-1])
        if mnem and mnem.startswith('B'):
            blocks.append(current_block)
            current_block = [addr]
        else:
            current_block.append(addr)
    if current_block:
        blocks.append(current_block)

    # 输出还原后的执行路径
    print(f"\n=== 还原的执行路径 ({len(blocks)} 个基本块) ===")
    for i, block in enumerate(blocks):
        start = block[0]
        end = block[-1]
        print(f"Block {i}: 0x{start:X} - 0x{end:X} ({len(block)} 条指令)")

        # 查找对应的 case 标签
        mnem = idc.print_insn_mnem(start)
        disasm = idc.generate_disasm_line(start, 0)
        if 'case' in disasm.lower() or 'switch' in disasm.lower():
            print(f"  ← {disasm}")

    return blocks

结合 Frida 和 IDA Trace 的分析流程

Frida 和 IDA Trace 各有优势,结合使用可以大幅提升分析效率。

优势对比

特性 Frida IDA Trace
动态性 实时运行时分析 需要调试器附加
灵活性 脚本热加载、即改即用 需要重启 Trace
细粒度 Hook 特定函数 指令级完整记录
性能影响 轻量 Trace 全量记录较重
自动化 JavaScript 脚本 Python 脚本
上下文 实时调用栈 完整执行历史

组合分析策略

Phase 1: Frida 快速定位
  → Hook 关键函数,确认算法输入输出
  → 缩小分析范围

Phase 2: IDA Trace 精细分析
  → 对定位到的函数进行指令级 Trace
  → 获取完整的执行序列

Phase 3: 交叉验证
  → Frida 的实时数据与 IDA Trace 的历史记录互相对照
  → 确保分析的准确性

联合分析脚本模板

# IDA Python 端 - 配合 Frida 的分析脚本
import idaapi
import idc

class FridaIDAAnalyzer:
    """Frida + IDA 联合分析框架"""

    def __init__(self, func_addr, func_size):
        self.func_addr = func_addr
        self.func_size = func_size
        self.trace_data = []
        self.key_instructions = []

    def set_breakpoints_at_offsets(self, offsets):
        """根据 Frida 确定的偏移设置断点"""
        for off in offsets:
            bp_addr = self.func_addr + off
            idc.add_bpt(bp_addr)
            print(f"[IDA] 断点: 0x{bp_addr:X} (offset +0x{off:X})")

    def trace_with_conditions(self, conditions):
        """
        设置条件断点进行选择性 Trace
        conditions: dict, {offset: condition_string}
        """
        for offset, cond in conditions.items():
            addr = self.func_addr + offset
            idc.add_bpt(addr)
            idc.set_bpt_cond(addr, cond)
            print(f"[IDA] 条件断点: 0x{addr:X}, 条件: {cond}")

    def analyze_trace_output(self, trace_log):
        """分析 Trace 输出"""
        print("\n[Trace 分析结果]")
        # 解析 Trace 日志,提取关键信息
        # ...(具体分析逻辑)

IDA 脚本编写自动化分析

自动化分析脚本框架

# IDA Python - 自动化分析非标准算法
import idaapi
import idc
import idautils
import ida_funcs

def auto_analyze_encryption(func_addr):
    """
    自动分析加密函数
    1. 识别 S-Box / 查找表
    2. 识别循环结构
    3. 识别关键运算指令
    4. 生成分析报告
    """
    func = ida_funcs.get_func(func_addr)
    if not func:
        print("未找到函数")
        return

    print(f"\n===== 自动分析: 0x{func_addr:X} =====")
    print(f"函数范围: 0x{func.start_ea:X} - 0x{func.end_ea:X}")
    print(f"函数大小: {func.end_ea - func.start_ea} 字节")

    # 1. 统计指令类型
    inst_stats = {}
    addr = func.start_ea
    while addr < func.end_ea:
        mnem = idc.print_insn_mnem(addr)
        if mnem:
            inst_stats[mnem] = inst_stats.get(mnem, 0) + 1
        addr = idc.next_head(addr, func.end_ea)

    print(f"\n[指令统计]")
    for mnem, count in sorted(inst_stats.items(),
                              key=lambda x: x[1], reverse=True):
        marker = ""
        if mnem in ['LDRB', 'STRB']:
            marker = " ← 可能查表"
        elif mnem in ['EOR', 'XOR']:
            marker = " ← 可能加密运算"
        elif mnem in ['LSL', 'LSR', 'ASR', 'ROR']:
            marker = " ← 位运算"
        print(f"  {mnem}: {count}{marker}")

    # 2. 识别数据引用(S-Box、常量表)
    print(f"\n[数据引用]")
    addr = func.start_ea
    while addr < func.end_ea:
        for xref in idautils.DataRefsFrom(addr):
            seg_name = idc.get_segm_name(xref)
            if seg_name == '.rodata' or seg_name == '.data':
                # 可能是 S-Box 或常量表
                data_size = idc.get_item_size(xref)
                print(f"  0x{addr:X} → 0x{xref:X} ({seg_name}, {data_size} 字节)")
        addr = idc.next_head(addr, func.end_ea)

    # 3. 识别循环
    print(f"\n[循环检测]")
    # 查找向后跳转的 B 指令
    addr = func.start_ea
    while addr < func.end_ea:
        mnem = idc.print_insn_mnem(addr)
        if mnem == 'B':
            target = idc.get_operand_value(addr, 0)
            if target < addr and target >= func.start_ea:
                loop_size = addr - target
                print(f"  循环: 0x{target:X} → 0x{addr:X} ({loop_size} 字节)")
        addr = idc.next_head(addr, func.end_ea)

    print("\n[分析完成]")

# 使用
auto_analyze_encryption(0x4A20)

实际案例演示

案例:分析 OLLVM 混淆的自定义加密

场景:某金融 APP 的签名算法使用自定义加密,且 SO 文件经过 OLLVM 混淆。

Step 1 - Frida 初步探测

// Frida 确认算法函数的输入输出
var mod = Process.findModuleByName("libsign.so");
var signFunc = mod.base.add(0x2A10);

Interceptor.attach(signFunc, {
    onEnter: function (args) {
        console.log("[签名函数] 输入:");
        console.log(hexdump(args[0], { length: args[2].toInt32() }));
    },
    onLeave: function (retval) {
        console.log("[签名函数] 输出:");
        console.log(hexdump(retval, { length: 32 }));
    }
});

Step 2 - IDA Trace 精细分析

在 IDA 中对该函数执行指令 Trace,导出执行记录,运行上面的 identify_bogus_blocksrestore_fla_path 脚本。

Step 3 - 还原算法

根据 Trace 数据和 Frida 收集的信息,逐步还原算法实现,最终用 Python 完整重写。

总结

本文介绍了 IDA Trace 功能的详细用法,以及如何将 IDA 的静态分析能力与 Frida 的动态分析能力相结合。IDA Trace 提供指令级的完整执行记录,特别适合分析 OLLVM 混淆代码——可以准确识别虚假分支、还原平坦化的控制流。配合 Frida 的实时 Hook 能力,两者互补,能够高效地还原复杂的非标准加密算法。