98、自动化逆向(三):符号执行(Angr)、污点分析

说实话,前两讲我们聊了自动化逆向的基础框架和脚本化操作。今天要聊的这两个东西——符号执行和污点分析,才是真正让逆向工程从「手工活」变成「自动化流水线」的关键。

我个人习惯把符号执行比作「暴力穷举的优雅版本」。你想想看,传统逆向里我们遇到分支判断,得手动跟踪每个条件。符号执行呢?它把所有输入都当成符号变量,然后自动探索所有可能的路径。说白了,就是让计算机替你把所有可能性都跑一遍。

符号执行:让计算机替你思考

符号执行的核心思想其实很简单:不关心具体数值,只关心符号关系。举个例子,你看到一个 if (x > 10) 的分支,传统调试你得设个断点,看看 x 到底是多少。符号执行直接说:「行,我记下 x > 10 和 x <= 10 两条路,都走一遍。」

我在项目中遇到过最典型的场景:分析一个恶意软件的注册算法。那家伙有 20 多层嵌套判断,手动跟踪简直要命。用符号执行,几分钟就找到了所有能通过验证的输入组合。

Angr 实战:从零开始

Angr 是目前最成熟的符号执行框架之一。嗯,这里要注意,它学习曲线有点陡,但一旦上手,威力巨大。

先看一个最简单的例子:

import angr

# 加载二进制文件
proj = angr.Project('./target_binary', auto_load_libs=False)

# 设置起始地址(通常是 main 函数)
state = proj.factory.entry_state()

# 创建模拟管理器
simgr = proj.factory.simulation_manager(state)

# 探索到目标地址
simgr.explore(find=0x400A1E)  # 成功路径的地址

if simgr.found:
    found_state = simgr.found[0]
    # 获取触发成功路径的输入
    solution = found_state.posix.dumps(0)
    print(f"找到有效输入: {solution}")
else:
    print("未找到可达路径")

这段代码干了什么?它告诉 Angr:「从程序入口开始跑,帮我找到能到达 0x400A1E 这个地址的输入。」Angr 会自动处理所有分支、循环、系统调用。

核心概念速查表

概念说明我的经验
Project加载二进制文件的对象记得关掉 auto_load_libs,否则库函数会拖慢速度
State程序在某时刻的状态(寄存器、内存等)可以手动设置初始状态,比如跳过某些初始化代码
Simulation Manager管理所有探索路径的容器路径爆炸时可以用 `simgr.stashes` 查看各状态
Explore自动探索到目标地址可以同时指定 find 和 avoid,加速搜索

路径爆炸:符号执行的阿喀琉斯之踵

为什么会这样?因为每个分支都会产生两条路径。如果程序有 30 个连续分支,理论上就有 2^30 条路径。这就是著名的「路径爆炸」问题。

我曾经遇到过一个加壳程序,它用了一个循环展开的混淆器,生成了上千个基本块。Angr 跑了两个小时还没收敛。后来怎么解决的?我用了 符号执行 + 具体执行混合模式

# 混合执行:对复杂路径用具体值,简单路径用符号
state = proj.factory.entry_state(
    add_options=angr.options.unicorn,
    remove_options=angr.options.LAZY_SOLVES
)

加上 unicorn 选项后,Angr 会在遇到复杂循环时自动切换到具体执行,速度提升了 10 倍以上。

避坑指南:我曾经在分析一个带反调试的程序时,Angr 一直卡在某个系统调用上。后来发现是程序用了 ptrace 检测调试器。解决方案是在创建 state 时手动跳过 ptrace 调用:

state = proj.factory.entry_state()
# 跳过 ptrace 系统调用
state.libc.skipped_syscalls.add('ptrace')

污点分析:追踪数据的「犯罪现场」

污点分析,说白了就是给数据打标签。你标记某些输入为「污点」,然后跟踪这些污点数据在程序中的传播路径。最终看它们流向了哪里——是敏感函数?还是内存写入?

我经常把污点分析比作「数据侦探」。你标记一个可疑的输入,然后看它经过了哪些处理、被哪些函数使用、最终去了哪里。这在分析漏洞利用和恶意软件行为时特别有用。

Angr 中的污点分析

Angr 内置了污点分析引擎,但说实话,它的原生接口有点晦涩。我习惯用 angr-management 配合自定义脚本:

import angr
import claripy

# 创建带污点标记的符号变量
proj = angr.Project('./vulnerable_bin')
state = proj.factory.entry_state()

# 标记 stdin 的前 10 个字节为污点
taint_bytes = [claripy.BVS(f'taint_{i}', 8) for i in range(10)]
for i, byte in enumerate(taint_bytes):
    state.posix.files[0].content[i] = byte

# 模拟执行
simgr = proj.factory.simulation_manager(state)
simgr.run()

# 检查污点传播路径
for dep in state.history.depths:
    if dep.taint:
        print(f"污点传播: {dep.ins_addr} -> {dep.reg_name}")

重要提醒:污点分析不是万能的。如果程序使用了隐式流(比如通过控制流传递数据),污点分析可能会漏报。我遇到过最坑的情况:程序用 if (tainted_data) { x = 1; } else { x = 2; } 这种隐式流,污点引擎完全没检测到。

符号执行 vs 污点分析:什么时候用哪个?

很多新手会问:「这两个东西到底有什么区别?」我一般这么解释:

  • 符号执行:适合找「路径」——比如破解验证、寻找特定输入、分析算法逻辑
  • 污点分析:适合找「流向」——比如追踪用户输入是否到达危险函数、分析数据泄露路径

在实际项目中,我经常把它们组合使用。先用污点分析缩小范围,找到可疑的数据流路径,再用符号执行精确求解触发条件。

知识体系总览

下面这张图是我自己总结的自动化逆向知识体系,重点标注了符号执行和污点分析的位置:

自动化逆向知识体系 自动化逆向工程 静态分析 动态分析 混合分析 反汇编 / 控制流图 / 数据流 调试器 / Hook / Fuzzing 符号执行 + 污点分析 ★ 本章重点:符号执行(Angr) 污点分析(数据追踪) 路径探索 数据流向追踪

实战技巧:组合拳打法

我个人最常用的套路是这样的:

  1. 先用污点分析做粗筛:标记所有用户输入为污点,运行程序,看哪些代码路径被污染了
  2. 锁定关键路径:找到污点流向的目标地址(比如 strcpy、system 等危险函数)
  3. 符号执行精确求解:对锁定的路径用 Angr 进行符号执行,找到触发该路径的具体输入

举个例子,分析一个 CTF 题目时,我用了这个组合拳:

# 第一步:污点分析
# 标记输入为污点,运行到目标函数
state = proj.factory.entry_state()
# ... 设置污点标记 ...

# 第二步:符号执行求解
simgr = proj.factory.simulation_manager(state)
simgr.explore(find=0x401234)  # 目标函数地址

if simgr.found:
    # 第三步:提取输入
    input_data = simgr.found[0].posix.dumps(0)
    print(f"Flag: {input_data}")

我的小技巧:如果 Angr 跑得太慢,试试用 simgr.use_technique(angr.exploration_techniques.DFS) 切换到深度优先搜索。默认是 BFS,在路径很多时 DFS 往往能更快找到目标。

避坑指南

做了这么多年逆向,我踩过的坑能写一本书。这里挑几个和符号执行、污点分析相关的:

  • 符号执行别碰大循环:遇到循环展开或复杂循环,直接跳过或用具体执行代替。我曾经让 Angr 分析一个 1000 次循环的程序,它跑了 6 个小时还没出来。
  • 污点分析注意隐式流:如果程序用条件判断来传递数据(比如 if (a) b = 1; else b = 2;),污点引擎可能检测不到。这时候需要手动标记。
  • 环境依赖问题:Angr 模拟的系统调用并不完整。我遇到过程序调用 ioctl 导致崩溃的情况,解决方案是用 state.posix.ignore_syscalls = True 跳过不支持的调用。

好了,这一讲的内容就到这里。符号执行和污点分析是自动化逆向的两把利器,但记住——工具只是工具,真正值钱的是你对程序逻辑的理解。多练、多踩坑,慢慢就上手了。


公众号:蓝海资料掘金营,微信deep3321