3、angr项目结构:Project、State、SimulationManager、SolverEngine

好,咱们直接切入正题。angr 这个框架,说白了就是一套积木。你只要搞懂四块核心积木怎么拼,就能搭出你自己的漏洞挖掘工具。这四块积木分别是:ProjectStateSimulationManagerSolverEngine

我个人习惯把这四个组件比作一个「实验室」:

  • 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 符号约束求解 创建 传入执行 约束求解 路径约束 angr 核心组件关系图 Project 创建 State → State 交给 SimulationManager 执行 → SolverEngine 求解约束

从图中可以看出,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 求解。搞懂这个结构,你就能开始写自己的漏洞挖掘脚本了。


专注资料整理