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 污点分析:什么时候用哪个?
很多新手会问:「这两个东西到底有什么区别?」我一般这么解释:
- 符号执行:适合找「路径」——比如破解验证、寻找特定输入、分析算法逻辑
- 污点分析:适合找「流向」——比如追踪用户输入是否到达危险函数、分析数据泄露路径
在实际项目中,我经常把它们组合使用。先用污点分析缩小范围,找到可疑的数据流路径,再用符号执行精确求解触发条件。
知识体系总览
下面这张图是我自己总结的自动化逆向知识体系,重点标注了符号执行和污点分析的位置:
实战技巧:组合拳打法
我个人最常用的套路是这样的:
- 先用污点分析做粗筛:标记所有用户输入为污点,运行程序,看哪些代码路径被污染了
- 锁定关键路径:找到污点流向的目标地址(比如 strcpy、system 等危险函数)
- 符号执行精确求解:对锁定的路径用 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