2、angr核心概念:Project、State、SolverEngine、SimulationManager
好,咱们直接进入正题。angr 这东西,说白了就是一个用 Python 写的二进制分析框架。你把它想象成一个「机器人」,能替你去逆向、去破解、去分析那些你不想手撸的代码。
但机器人也得有零件对吧?今天我就带你拆开 angr 的四个核心零件:Project、State、SolverEngine、SimulationManager。这四个东西你搞明白了,angr 就算入门了。
核心观点:angr 的整个工作流,就是「加载程序 → 设置状态 → 符号执行 → 管理路径」。每一步对应一个核心概念。
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.raxstate.mem—— 访问内存,比如state.mem[addr].int.resolvedstate.solver—— 这就是我们马上要讲的 SolverEngine 的接口state.globals—— 一个字典,你可以存一些自定义数据
注意:State 是「不可变」的。每次执行一步,angr 会生成一个新的 State。别想着修改一个 State 来推进执行,那不管用。
2.3 SolverEngine —— 约束求解的核心
SolverEngine 是 angr 的大脑。它负责处理符号变量和约束条件,然后给出具体的解。angr 底层用的是 Z3 求解器,但 SolverEngine 把它封装得更易用。
我举个例子你就明白了。假设我们有一个符号变量 x,并且我们知道 x > 10 且 x < 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 四个概念如何协同工作?
咱们捋一下整个流程:
- Project 加载二进制文件,提供程序的基本信息。
- 从 Project 创建 State,设置初始执行环境。
- 在 State 中使用 SolverEngine 创建符号变量、添加约束。
- 将 State 交给 SimulationManager,让它去探索路径、求解目标。
你看,这四个概念环环相扣。Project 是地基,State 是砖块,SolverEngine 是水泥,SimulationManager 是施工队。缺了哪个都盖不起楼。
嗯,说到这我想起一个真实的案例。有一次我分析一个软件授权验证,它把用户名和序列号做了复杂的运算,然后比对结果。我用 angr 的这四个组件,先加载程序,然后把用户名设成符号变量,让 SM 去探索,最后用 SolverEngine 解出了合法的序列号。整个过程不到 50 行代码。
这就是 angr 的魅力。你不需要理解每一条汇编指令,你只需要告诉它「我要去哪里」,它就能帮你找到路。