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_blocks 和 restore_fla_path 脚本。
Step 3 - 还原算法:
根据 Trace 数据和 Frida 收集的信息,逐步还原算法实现,最终用 Python 完整重写。
总结
本文介绍了 IDA Trace 功能的详细用法,以及如何将 IDA 的静态分析能力与 Frida 的动态分析能力相结合。IDA Trace 提供指令级的完整执行记录,特别适合分析 OLLVM 混淆代码——可以准确识别虚假分支、还原平坦化的控制流。配合 Frida 的实时 Hook 能力,两者互补,能够高效地还原复杂的非标准加密算法。