3、angr项目结构:Project、State、SimulationManager、SolverEngine
好,咱们直接切入正题。angr 这个框架,说白了就是一套积木。你只要搞懂四块核心积木怎么拼,就能搭出你自己的漏洞挖掘工具。这四块积木分别是:Project、State、SimulationManager 和 SolverEngine。
我个人习惯把这四个组件比作一个「实验室」:
- Project 是实验室本身,装着你要分析的二进制文件。
- State 是实验台上的一个「瞬间快照」,记录了 CPU 寄存器、内存、栈等所有状态。
- SimulationManager 是实验员,负责让 State 动起来,模拟程序执行。
- SolverEngine 是数学计算器,专门解决「这条路能不能走通」这类问题。
下面我一个一个拆开讲。
3.1 Project:你的二进制文件分析入口
Project 是 angr 的起点。你拿到一个 ELF 或 PE 文件,第一件事就是创建一个 Project 对象。它负责加载二进制文件、解析符号表、识别架构信息。
核心要点:Project 是全局唯一的,一个二进制文件对应一个 Project。不要重复创建。
import angr
# 加载一个二进制文件
proj = angr.Project('./vuln_binary', auto_load_libs=False)
这里有个坑,我刚开始用的时候踩过。参数 auto_load_libs=False 建议加上。为什么呢?因为 angr 默认会尝试加载所有依赖的动态库,比如 libc.so.6。这会导致符号爆炸,分析速度慢得离谱。我在项目中遇到过,一个简单的栈溢出分析,因为加载了全部 libc,跑了半小时还没出结果。后来关掉这个选项,几秒钟就搞定了。
Project 对象暴露了几个常用属性:
proj.arch:架构信息,比如 x86、ARM、MIPS。proj.entry:程序入口点地址。proj.filename:二进制文件名。proj.loader:加载器对象,可以查看加载的段、符号等。
小技巧:如果你只想分析某个函数,可以用 proj.kb.functions 获取函数列表。我个人习惯先看符号表,确认目标函数地址再下手。
3.2 State:程序的「瞬间快照」
State 是 angr 里最灵活的概念。它代表程序在某一时刻的完整状态,包括寄存器值、内存内容、栈指针、标志位等。你可以把它理解成一个「虚拟机快照」。
创建 State 的方式很简单:
# 从入口点创建状态
state = proj.factory.entry_state()
# 从指定地址创建状态
state = proj.factory.blank_state(addr=0x400000)
# 从函数调用约定创建状态
state = proj.factory.call_state(0x400100, arg1, arg2)
嗯,这里要注意。三种创建方式各有用途:
entry_state():模拟程序从头开始执行,适合分析 main 函数之前的初始化逻辑。blank_state():从任意地址开始,适合分析某个特定函数或代码片段。call_state():模拟函数调用,自动设置参数和返回地址,适合分析函数内部的漏洞。
State 对象最常用的操作是读写寄存器和内存:
# 读取寄存器
eax_val = state.regs.eax
# 写入寄存器
state.regs.eax = 0xdeadbeef
# 读取内存(地址 0x1000,长度 4 字节)
mem_val = state.memory.load(0x1000, 4)
# 写入内存
state.memory.store(0x1000, b'\x41\x42\x43\x44')
避坑指南:我曾经犯过一个错误——直接修改 state.regs.eax 为一个具体数值,比如 0xdeadbeef。但 angr 的 State 默认使用符号变量,直接赋值会覆盖符号约束。正确做法是用 state.solver.BVS() 创建符号变量再赋值。后面 SolverEngine 会细讲。
3.3 SimulationManager:让程序「动」起来
有了 State 这个快照,怎么让它执行呢?SimulationManager 就是干这个的。它管理一组 State,模拟程序执行路径,处理分支、循环、系统调用等。
# 创建 SimulationManager
simgr = proj.factory.simulation_manager(state)
# 执行一步
simgr.step()
# 执行直到某个地址
simgr.explore(find=0x400200)
# 查看活跃状态
active_states = simgr.active
# 查看已到达目标的状态
found_states = simgr.found
SimulationManager 的核心是状态分类。它把 State 分成几类:
- active:正在执行的状态。
- deadended:执行结束的状态(比如程序退出)。
- errored:执行出错的状态(比如非法指令)。
- found:通过
explore()找到的目标状态。
我个人最常用的是 explore() 方法。比如分析一个缓冲区溢出漏洞,我想知道程序能否执行到 0x400200 这个地址(可能是 shellcode 入口),直接写:
simgr.explore(find=0x400200)
if simgr.found:
print("漏洞路径存在!")
found_state = simgr.found[0]
# 从 found_state 中提取触发漏洞的输入
你想想看,这比手动调试 GDB 快了多少倍?
核心要点:SimulationManager 的 explore() 是漏洞挖掘的利器。你可以指定 find(目标地址)和 avoid(避开地址),让 angr 自动寻找可行路径。
3.4 SolverEngine:解决「能不能」的问题
最后一块积木是 SolverEngine。它本质上是 Z3 求解器的封装,用来解决符号约束问题。比如:
- 「输入什么值能让程序执行到地址 A?」
- 「输入什么值能让变量 X 等于 0xdeadbeef?」
- 「输入什么值能绕过检查条件?」
SolverEngine 通过 state.solver 访问:
# 创建符号变量
sym_input = state.solver.BVS('input', 32) # 32 位符号变量
# 添加约束
state.solver.add(sym_input > 0x100)
state.solver.add(sym_input < 0x200)
# 求解
if state.solver.satisfiable():
concrete_val = state.solver.eval(sym_input)
print(f"满足条件的值: {concrete_val:#x}")
else:
print("无解")
这里有个关键点:符号变量。BVS 代表 BitVector Symbol,即符号位向量。你可以把它理解成一个「未知数」,SolverEngine 负责找出它的具体值。
我在项目中遇到过这样一个场景:分析一个格式化字符串漏洞,需要找到输入中哪个位置对应栈上的某个地址。用 SolverEngine 创建符号变量,添加约束让程序输出特定内容,然后求解输入值。整个过程自动化,省去了手动逆向的繁琐。
小技巧:如果求解结果不唯一,可以用 state.solver.eval_upto(sym_input, 5) 获取最多 5 个解。这在分析多路径漏洞时特别有用。
3.5 四者关系:一张图看懂
说了这么多,咱们用一张 SVG 图来总结这四块积木的关系:
从图中可以看出,Project 是入口,State 是核心数据,SimulationManager 负责驱动,SolverEngine 负责决策。四者配合,就能实现自动化漏洞挖掘。
3.6 实战小例子:组合使用
最后,咱们用一个完整的例子串起来。假设我们要分析一个简单的栈溢出漏洞,目标是找到输入,让程序执行到 0x400200 的 shellcode。
import angr
# 1. 创建 Project
proj = angr.Project('./vuln_binary', auto_load_libs=False)
# 2. 创建 State(从 main 函数开始)
state = proj.factory.entry_state()
# 3. 创建 SimulationManager
simgr = proj.factory.simulation_manager(state)
# 4. 探索路径,找到目标地址
simgr.explore(find=0x400200)
if simgr.found:
found_state = simgr.found[0]
# 5. 使用 SolverEngine 提取触发漏洞的输入
# 假设输入存储在 stdin 缓冲区
input_data = found_state.posix.dumps(0)
print(f"触发漏洞的输入: {input_data}")
else:
print("未找到可行路径")
你看,四行核心代码就完成了漏洞路径的自动探索。这就是 angr 的魅力。
避坑指南:我曾经在实战中遇到一个问题——explore() 跑了几分钟没结果。后来发现是路径爆炸了。解决方案是设置 simgr.use_technique(angr.exploration_techniques.LoopSeer()) 来限制循环次数。路径爆炸是 angr 的常见问题,后面章节会专门讲优化技巧。
好了,这四块积木的用法就讲到这里。记住它们的职责:Project 加载、State 快照、SimulationManager 执行、SolverEngine 求解。搞懂这个结构,你就能开始写自己的漏洞挖掘脚本了。