2、angr核心概念:Project、State、SolverEngine、SimulationManager

好,咱们直接进入正题。angr 这东西,说白了就是一个用 Python 写的二进制分析框架。你把它想象成一个「机器人」,能替你去逆向、去破解、去分析那些你不想手撸的代码。

但机器人也得有零件对吧?今天我就带你拆开 angr 的四个核心零件:ProjectStateSolverEngineSimulationManager。这四个东西你搞明白了,angr 就算入门了。

核心观点:angr 的整个工作流,就是「加载程序 → 设置状态 → 符号执行 → 管理路径」。每一步对应一个核心概念。

Project 加载二进制文件 State 程序执行快照 SolverEngine 约束求解引擎 SimulationManager 路径管理调度 加载 符号化 求解 & 管理

2.1 Project —— 一切从这里开始

Project 是 angr 的入口。你给它一个二进制文件,它帮你把程序「吃进去」并解析好。我个人习惯把 Project 理解为「程序的抽象表示」——它包含了二进制文件的架构、入口点、段信息、符号表等等。

创建 Project 很简单,就一行代码:

import angr

proj = angr.Project('./crackme', auto_load_libs=False)

这里有个坑,我一开始踩过。 auto_load_libs=False 这个参数,建议你默认就加上。为什么?因为 angr 默认会去加载所有依赖的动态库,比如 libc.so。这会导致符号执行时路径爆炸,你想想看,一个 printf 函数内部有多少条路径?

我的经验:做 CTF 题或者破解简单的授权验证时,关掉自动加载库能省掉 80% 的烦恼。除非你确实需要分析库函数内部逻辑,否则别开。

Project 对象里藏着不少好东西。比如 proj.arch 告诉你架构信息,proj.entry 是程序入口点,proj.loader 是加载器对象。我经常用 proj.loader.find_symbol('main') 来定位 main 函数的地址。

2.2 State —— 程序的「快照」

State 是什么?说白了,就是程序在某一时刻的执行状态。包括寄存器值、内存内容、文件描述符、符号约束等等。你可以把它理解成一个「虚拟机快照」。

从 Project 创建 State 非常直接:

state = proj.factory.entry_state()

这行代码创建了一个从程序入口点开始的状态。但实际工作中,我们往往需要更灵活的控制。比如我想从 main 函数开始分析:

main_addr = proj.loader.find_symbol('main').rebased_addr
state = proj.factory.blank_state(addr=main_addr)

这里我用了 blank_state,它创建一个「空白」状态,然后指定从 main 地址开始执行。这样做的好处是跳过了那些烦人的初始化代码。

State 里最常用的几个属性:

  • state.regs —— 访问寄存器,比如 state.regs.rax
  • state.mem —— 访问内存,比如 state.mem[addr].int.resolved
  • state.solver —— 这就是我们马上要讲的 SolverEngine 的接口
  • state.globals —— 一个字典,你可以存一些自定义数据

注意:State 是「不可变」的。每次执行一步,angr 会生成一个新的 State。别想着修改一个 State 来推进执行,那不管用。

2.3 SolverEngine —— 约束求解的核心

SolverEngine 是 angr 的大脑。它负责处理符号变量和约束条件,然后给出具体的解。angr 底层用的是 Z3 求解器,但 SolverEngine 把它封装得更易用。

我举个例子你就明白了。假设我们有一个符号变量 x,并且我们知道 x > 10x < 20

# 创建一个符号变量
x = state.solver.BVS('x', 32)  # 32位符号位向量

# 添加约束
state.solver.add(x > 10)
state.solver.add(x < 20)

# 求解
if state.solver.satisfiable():
    solution = state.solver.eval(x)
    print(f'x = {solution}')  # 输出可能是 11 到 19 之间的某个值

这里 BVS 是 BitVectorSymbol 的缩写,表示符号位向量。32 表示 32 位宽度。你还可以用 BVV 创建具体的常量值。

我个人在破解授权验证时,最常用的套路是:把用户输入变成符号变量,然后让程序去执行,最后检查程序是否到达了「成功」的路径。如果到达了,就用 SolverEngine 反推出满足条件的输入值。

关键点:state.solver.satisfiable() 检查当前约束是否可满足。state.solver.eval() 返回一个具体的解。state.solver.eval_upto() 可以返回多个解。

2.4 SimulationManager —— 路径的「交通警察」

SimulationManager,简称 SM。它的职责是管理所有正在执行的 State。你可以把它想象成一个交通警察,指挥着成千上万条执行路径。

创建 SM 很简单:

simgr = proj.factory.simulation_manager(state)

然后你就可以指挥它执行了:

simgr.explore(find=0x400000, avoid=0x400010)

这行代码的意思是:让 SM 去探索所有路径,找到能到达地址 0x400000 的路径,同时避开地址 0x400010。执行完后,simgr.found 列表里就是所有成功到达目标的状态。

SM 内部维护了几个状态列表:

列表名 含义
active 当前正在执行的路径
deadended 执行到终点的路径
errored 执行出错的路径
found 通过 explore 找到的目标路径
avoid 进入避开区域的路径

我曾经遇到过一个情况:程序里有反调试逻辑,导致符号执行时总是走到错误路径。后来我用 simgr.move('active', 'avoid', filter_func=lambda s: s.addr == 0xbad_addr) 手动把那些路径移走,才顺利找到正确路径。

小技巧:如果路径爆炸了,试试 simgr.use_technique(angr.exploration_techniques.LoopSeer()),它能帮你限制循环次数,减少路径数量。

2.5 四个概念如何协同工作?

咱们捋一下整个流程:

  1. Project 加载二进制文件,提供程序的基本信息。
  2. 从 Project 创建 State,设置初始执行环境。
  3. 在 State 中使用 SolverEngine 创建符号变量、添加约束。
  4. 将 State 交给 SimulationManager,让它去探索路径、求解目标。

你看,这四个概念环环相扣。Project 是地基,State 是砖块,SolverEngine 是水泥,SimulationManager 是施工队。缺了哪个都盖不起楼。

嗯,说到这我想起一个真实的案例。有一次我分析一个软件授权验证,它把用户名和序列号做了复杂的运算,然后比对结果。我用 angr 的这四个组件,先加载程序,然后把用户名设成符号变量,让 SM 去探索,最后用 SolverEngine 解出了合法的序列号。整个过程不到 50 行代码。

这就是 angr 的魅力。你不需要理解每一条汇编指令,你只需要告诉它「我要去哪里」,它就能帮你找到路。


专注资料整理