3、angr核心概念:Project、State、Solver、SimulationManager、CFG

说实话,刚接触angr的时候,我也被它那一堆概念搞得有点懵。什么Project、State、Solver……听着就头大。但用久了你会发现,其实核心就五个东西。搞懂它们,angr的大门就算被你踹开了。

我个人习惯把这五个概念比作一个「工厂流水线」:

  • Project —— 就是你要分析的二进制文件本身,相当于原材料
  • State —— 程序在某个时刻的「快照」,相当于流水线上的半成品
  • Solver —— 负责解约束条件的引擎,相当于质检员
  • SimulationManager —— 管理多个State的探索过程,相当于流水线调度员
  • CFG —— 程序的静态控制流图,相当于工厂的布局图

嗯,这么一讲是不是清晰多了?下面我们一个一个来拆。

angr 核心概念 Project State Solver SimulationManager CFG 加载二进制文件 程序快照 约束求解 路径探索管理 控制流分析

3.1 Project —— 一切从这里开始

Project 说白了就是 angr 对二进制文件的「封装」。你给它一个 ELF 或 PE 文件,它帮你解析出架构、入口点、段信息等等。

我在项目中遇到过一个问题:有些加了壳的二进制文件,直接加载会报错。后来发现是 angr 的自动加载器识别不了壳的入口。解决办法是手动指定 main_opts 参数。

import angr

# 最基本的用法
proj = angr.Project('./target_binary', auto_load_libs=False)

# 查看基本信息
print(f"架构: {proj.arch}")
print(f"入口点: {hex(proj.entry)}")
print(f"文件名: {proj.filename}")
💡 我的小技巧: 记得加上 auto_load_libs=False。不然 angr 会尝试加载所有依赖库,速度慢得你想哭。除非你确实需要分析库函数,否则关掉它。

3.2 State —— 程序的「快照」

State 代表程序在某个执行点的状态。包括寄存器值、内存内容、文件描述符状态等等。你可以把它理解成「如果程序执行到这里,世界会是什么样子」。

angr 提供了几种常见的 State 类型:

State 类型 说明 适用场景
entry_state() 程序入口点的状态 从 main 开始分析
blank_state() 空白状态,所有值未初始化 自定义初始条件
call_state() 调用某个函数时的状态 分析特定函数
registers 从指定地址恢复状态 反调试绕过后的恢复
# 创建不同状态
entry = proj.factory.entry_state()
blank = proj.factory.blank_state(addr=0x400000)
call = proj.factory.call_state(0x401234, argc=2)

# 操作寄存器
entry.regs.rax = 0x10
entry.regs.rdi = 0x7fffffff

# 操作内存
entry.memory.store(0x601000, b'hello')
data = entry.memory.load(0x601000, 5)
⚠️ 注意: 我曾经在分析一个反调试程序时,直接用 entry_state() 结果卡了半天。后来发现程序在入口点之前就调用了 ptrace 检测。解决办法是用 blank_state() 手动设置 PC 跳过检测代码。

3.3 Solver —— 约束求解引擎

Solver 是 angr 最核心也最强大的部分。它基于 Z3 求解器,能帮你回答「在什么条件下,程序会走到这里?」这类问题。

你想想看,逆向分析中最头疼的是什么?是那些复杂的条件判断。比如一个 if 语句里有十几个条件,人工分析得累死。但用 Solver,你只需要把条件告诉它,它就能算出满足条件的输入。

# 创建符号变量
import claripy

# 创建一个 32 位的符号变量
sym_var = claripy.BVS('input', 32)

# 添加约束
state = proj.factory.entry_state()
state.solver.add(sym_var > 0x10)
state.solver.add(sym_var < 0x100)

# 求解
if state.solver.satisfiable():
    solution = state.solver.eval(sym_var)
    print(f"满足条件的值: {hex(solution)}")
else:
    print("无解")

🔑 核心要点: Solver 的 satisfiable() 方法会告诉你当前约束是否有解。如果返回 False,说明你的约束条件矛盾了。我在调试一个反调试程序时,就遇到过这种情况——因为同时添加了「ptrace 返回 0」和「ptrace 返回 -1」两个矛盾约束。

3.4 SimulationManager —— 路径探索管家

SimulationManager(简称 simgr)负责管理多个 State 的探索过程。你可以把它想象成一个「状态池」,里面装着所有待探索的程序状态。

我个人习惯用 explore() 方法,它是最常用的探索方式。你告诉它「我要找哪个地址」,它就会自动帮你探索所有可能的路径。

# 创建 SimulationManager
simgr = proj.factory.simulation_manager(entry)

# 探索到目标地址
target_addr = 0x401234
simgr.explore(find=target_addr)

# 查看结果
if simgr.found:
    found_state = simgr.found[0]
    print(f"找到目标状态!")
    # 从状态中提取输入
    stdin = found_state.posix.dumps(0)
    print(f"标准输入: {stdin}")
else:
    print("未找到目标路径")
💡 避坑指南: 我曾经用 explore() 分析一个带循环的程序,结果跑了半小时没停。后来发现是循环次数太多导致路径爆炸。解决办法是设置 num_find 参数限制探索深度,或者用 avoid 参数避开某些地址。

3.5 CFG —— 控制流图

CFG(Control Flow Graph)是程序的静态分析结果。它把程序的所有基本块和它们之间的跳转关系画成一张图。对于理解程序结构、识别反调试代码非常有用。

angr 的 CFG 分析比 IDA 的还要细致一些。它能识别出间接跳转、虚函数调用这些 IDA 经常搞不定的东西。

# 生成 CFG
cfg = proj.analyses.CFGFast()

# 查看函数信息
func = cfg.functions['main']
print(f"main 函数地址: {hex(func.addr)}")
print(f"基本块数量: {len(func.block_addrs)}")

# 遍历基本块
for block_addr in func.block_addrs:
    block = cfg.model.get_any_node(block_addr)
    if block:
        print(f"基本块: {hex(block_addr)}")
        print(f"  后继: {[hex(s.addr) for s in block.successors]}")

🔑 实战经验: 我在分析一个反调试程序时,发现它用了大量间接跳转来混淆控制流。IDA 直接懵了,但 angr 的 CFG 分析成功还原了所有跳转目标。具体做法是先用 CFGFast() 生成图,然后遍历所有基本块,找出那些「可疑的间接跳转」。

3.6 五个概念如何协同工作?

说了这么多,你可能想问:这五个东西到底怎么配合?我举个例子你就明白了。

假设你要分析一个程序,它读取用户输入,然后做一系列检查。你的目标是找到能通过所有检查的输入。

  1. Project 加载二进制文件
  2. CFG 分析出所有基本块和跳转关系
  3. State 在入口点创建初始状态,把输入设为符号变量
  4. SimulationManager 沿着 CFG 的路径探索,遇到条件分支时自动分叉
  5. Solver 在每个分支点求解约束,判断哪些路径可达

说白了,这就是一个「静态分析 + 动态符号执行」的组合拳。CFG 提供地图,State 记录状态,Solver 解决约束,SimulationManager 负责调度。而 Project 就是这一切的起点。

⚠️ 最后提醒: 别想着一次就把所有概念吃透。我刚开始学的时候,光是 State 的 regsmemory 操作就折腾了两天。建议你先跑通一个最简单的例子,比如用 explore() 找一个地址,然后再慢慢深入。

公众号:蓝海资料掘金营,微信deep3321