一、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 加载二进制文件 创建 State 程序快照 包含 SolverEngine 约束求解 交给 SimulationManager 路径探索 active | deadended | errored 分支1 (if-true) 分支2 (if-false) 工作流程: Project 加载二进制 → 创建初始 State → State 包含 SolverEngine 处理符号变量 → State 交给 SimulationManager 管理路径探索

你看,整个流程其实很清晰:

  1. Project 加载二进制文件,提供架构信息
  2. 从 Project 创建初始 State,记录程序状态
  3. State 内部包含 SolverEngine,负责处理符号变量和约束
  4. 将 State 交给 SimulationManager,它会自动探索所有路径分支
  5. 每个分支产生新的 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