一、angr 的核心四件套
说实话,刚接触 angr 的时候,我也被它那一堆概念搞得头晕。Project、State、SolverEngine、SimulationManager…… 这四个东西到底是什么关系?
我个人的理解是:Project 是你要分析的二进制文件,State 是程序在某个时刻的快照,SolverEngine 是帮你解约束条件的引擎,SimulationManager 则是管理所有执行路径的管家。
嗯,咱们一个一个来拆解。
1.1 Project —— 一切从这里开始
Project 就是你要分析的二进制程序。它负责加载文件、解析架构、处理符号表等等。说白了,它就是 angr 世界里的大门。
核心要点:一个 Project 对应一个二进制文件。你只需要告诉它文件路径,它就能帮你把整个程序“读”进内存。
import angr
# 加载一个二进制文件
proj = angr.Project('./vuln', auto_load_libs=False)
这里有个小坑:auto_load_libs=False 是我个人习惯加的。为什么?因为 angr 默认会尝试加载所有依赖的动态库,这会让分析变得非常慢。我刚开始用的时候没注意,一个简单的程序跑了十分钟还没出结果…… 后来才发现是 libc 被完整加载了。
Project 里藏着很多有用的信息:
proj.arch—— 架构信息(x86、ARM、MIPS 等)proj.entry—— 程序入口地址proj.filename—— 文件名proj.loader—— 加载器,负责解析 ELF/PE 等格式
1.2 State —— 程序的“快照”
State 是什么?你可以把它想象成程序在某个时刻的“内存快照”。它记录了寄存器值、栈内容、堆数据、文件描述符状态等等。
你想想看,符号执行的核心就是“探索所有可能的状态”。每一条分支指令(比如 if-else)都会产生两个新的 State。
# 从入口点创建一个初始状态
state = proj.factory.entry_state()
# 查看某个寄存器的值
print(state.regs.rax)
# 查看栈顶的值
print(state.regs.rsp)
State 里最常用的几个属性:
| 属性 | 说明 |
|---|---|
state.regs |
寄存器访问接口 |
state.mem |
内存访问接口 |
state.stack |
栈操作接口 |
state.solver |
符号求解器接口(后面会讲) |
state.globals |
用户自定义的全局变量存储 |
小技巧:如果你只想分析某个函数,可以用 proj.factory.call_state(addr) 创建一个调用状态,这样能省去前面一大段无关代码的执行时间。
1.3 SolverEngine —— 解约束的“大脑”
符号执行和普通执行最大的区别是什么?普通执行处理的是具体值,符号执行处理的是“符号变量”。
比如,程序读入一个用户输入 a,然后判断 a > 10。在符号执行中,a 是一个符号变量,a > 10 就是一个约束条件。SolverEngine 就是用来解这些约束的。
# 创建一个符号变量
sym_var = state.solver.BVS('input', 32) # 32位符号位向量
# 添加约束
state.solver.add(sym_var > 10)
state.solver.add(sym_var < 20)
# 求解
if state.solver.satisfiable():
solution = state.solver.eval(sym_var)
print(f"找到一个可行解: {solution}")
else:
print("约束不可满足")
这里要注意:BVS 是 BitVectorSymbol 的缩写,表示一个符号位向量。我刚开始总记不住,后来就记住了——BVS = 符号变量。
避坑指南:我曾经在一个 CTF 题目里卡了整整一天,就是因为忘了检查 satisfiable()。如果约束条件矛盾(比如同时要求 a > 10 和 a < 5),求解器会返回不可满足。不检查的话,直接调用 eval() 会抛异常。
SolverEngine 支持的操作远不止加减乘除:
state.solver.BVS(name, size)—— 创建符号变量state.solver.BVV(value, size)—— 创建具体值state.solver.eval(expr)—— 求解一个表达式state.solver.eval_upto(expr, n)—— 求解最多 n 个解state.solver.min(expr)/state.solver.max(expr)—— 求最小/最大值
1.4 SimulationManager —— 路径的“管家”
好了,现在我们有 Project、有 State、有 SolverEngine。但程序执行会产生很多分支,每个分支都是一个 State。谁来管理这些 State?
SimulationManager 就是干这个的。它维护一个“活跃状态列表”,帮你自动探索所有路径。
# 创建 SimulationManager
simgr = proj.factory.simulation_manager(state)
# 执行,直到遇到第一个分支
simgr.step()
print(simgr.active) # 当前活跃的状态列表
# 或者直接跑到底
simgr.run()
print(simgr.deadended) # 执行结束的状态
SimulationManager 里最常用的几个状态列表:
| 列表名 | 说明 |
|---|---|
active |
正在执行的状态 |
deadended |
执行结束的状态(比如程序退出) |
errored |
执行出错的状态 |
unconstrained |
指令指针被符号化的状态(危险!) |
重点:unconstrained 列表里的状态意味着程序的控制流被符号变量劫持了。这在分析漏洞时特别有用——比如栈溢出导致返回地址被覆盖成符号值,angr 就会自动把这种状态放到 unconstrained 里。
二、四者之间的关系
说了这么多,这四个东西到底怎么配合?我画了一张图,你看完就明白了。
你看,整个流程其实很清晰:
- Project 加载二进制文件,提供架构信息
- 从 Project 创建初始 State,记录程序状态
- State 内部包含 SolverEngine,负责处理符号变量和约束
- 将 State 交给 SimulationManager,它会自动探索所有路径分支
- 每个分支产生新的 State,循环往复
三、实战小例子:把四件套串起来
光说不练假把式。咱们用一个最简单的例子,把这四个概念串起来。
import angr
# 1. 创建 Project
proj = angr.Project('./test', auto_load_libs=False)
# 2. 创建初始 State
state = proj.factory.entry_state()
# 3. 创建一个符号变量,模拟用户输入
sym_input = state.solver.BVS('user_input', 32)
state.memory.store(0x804a000, sym_input) # 假设输入存放在这个地址
# 4. 创建 SimulationManager
simgr = proj.factory.simulation_manager(state)
# 5. 探索,直到找到某个目标地址
target_addr = 0x8049000 # 假设这是成功分支的地址
simgr.explore(find=target_addr)
if simgr.found:
found_state = simgr.found[0]
# 6. 使用 SolverEngine 求解输入
solution = found_state.solver.eval(sym_input)
print(f"找到触发目标路径的输入: {hex(solution)}")
else:
print("未找到目标路径")
这个例子虽然简单,但涵盖了 angr 符号执行的完整流程。我在实际分析 CTF 题目和漏洞利用时,基本都是这个套路——只是目标地址和约束条件会更复杂一些。
个人经验:刚开始学的时候,别急着跑复杂的程序。先用一个只有几个分支的小程序练手,把 Project → State → SolverEngine → SimulationManager 这个流程跑通。我当初就是太心急,直接上了一个 10MB 的二进制文件,结果等了半小时没出结果……
四、总结一下
这四个核心概念,说白了就是:
- Project —— 你要分析的那个程序
- State —— 程序在某个时刻的“照片”
- SolverEngine —— 解数学题的“大脑”
- SimulationManager —— 帮你管理所有“照片”的管家
记住这个比喻,后面学起来会轻松很多。嗯,下一节咱们会深入 State 的内部结构,看看它到底能记录哪些信息——到时候你会发现,State 远比你想的要强大。
公众号:蓝海资料掘金营,微信deep3321