第3章:Angr框架入门——架构、Project与State的概念、基础API
好,咱们今天正式进入Angr的世界。说实话,我第一次接触Angr的时候,也被它那一堆概念搞得有点懵。什么Project、State、SimulationManager……听着就头大。但别急,我带你捋一遍,你会发现这东西其实挺直观的。
3.1 Angr的架构:它到底是怎么工作的?
Angr本质上是一个二进制分析框架。它把二进制程序加载进来,然后模拟执行。你想想看,它就像是一个“虚拟CPU”,能跑你的目标程序,但又不完全跑——它是在符号层面跑。
它的核心架构可以概括为三层:
- 底层: 负责加载二进制文件,解析指令。这部分用的是CLE(CLE Loads Everything)和PyVEX。CLE负责把ELF、PE这些格式的文件拆开,PyVEX则把机器码翻译成中间表示(VEX IR)。
- 中层: 符号执行引擎。这是Angr的大脑,负责维护符号状态、处理路径分支、求解约束条件。说白了,就是“如果走这条路,变量会变成什么值?”
- 上层: 各种分析模块。比如路径探索、漏洞检测、符号化参数注入等等。我们平时用的最多的就是这些。
我个人习惯把Angr想象成一个“程序模拟器+数学求解器”的组合体。它模拟程序执行,但遇到条件判断时,它不直接选true或false,而是把两个分支都记下来,然后用约束求解器(Z3)去算:走哪条路能满足条件?
核心思想: Angr不是“运行”程序,而是“探索”程序的所有可能路径。
下面这张图是我自己画的,能帮你快速理解Angr的架构:
3.2 Project:你的分析起点
在Angr里,Project代表你要分析的那个二进制程序。你可以把它理解成一个“程序对象”,它包含了程序的所有信息——指令、符号表、重定位信息等等。
创建Project很简单:
import angr
# 加载一个二进制文件
proj = angr.Project('./crackme', auto_load_libs=False)
这里有个小坑,我刚开始用的时候踩过。那个auto_load_libs=False参数,建议你加上。为什么呢?因为Angr默认会尝试加载所有依赖的动态库,比如libc.so.6。这会导致符号执行变得极其缓慢,甚至直接卡死。你想想看,连libc都符号化执行,那路径数量会爆炸的。
避坑指南: 我曾经在一个CTF题目里,忘记加 auto_load_libs=False,结果Angr跑了半小时还没出结果。后来加上这个参数,几秒钟就搞定了。所以,除非你明确需要分析库函数内部逻辑,否则一律关掉自动加载库。
Project对象提供了很多有用的属性:
| 属性 | 说明 | 示例 |
|---|---|---|
proj.arch |
获取目标架构信息(如x86、ARM) | proj.arch.bits → 32或64 |
proj.entry |
程序的入口点地址 | 0x400000 |
proj.filename |
二进制文件的路径 | './crackme' |
proj.loader |
CLE加载器对象,可以查看加载的段、符号等 | proj.loader.main_object.symbols |
3.3 State:程序的“快照”
有了Project,下一步就是创建State。State代表程序在某个时刻的执行状态。它包括:
- 寄存器值:比如eax、ebx、rip等
- 内存内容:栈、堆、全局变量
- 文件描述符:stdin、stdout等
- 约束条件:当前路径上所有满足的条件
创建State最常用的方式是entry_state():
# 创建一个从程序入口点开始的状态
state = proj.factory.entry_state()
这个API会创建一个“干净”的状态——程序刚启动,还没执行任何指令。寄存器被初始化为默认值,栈指针指向栈顶,等等。
除了entry_state(),还有几个常用的:
blank_state(addr=...):创建一个“空白”状态,你可以手动设置寄存器和内存。适合从中间某个地址开始分析。call_state(addr, ...):模拟调用某个函数时的状态。参数可以传符号值。full_init_state():类似entry_state,但会执行完所有初始化代码(比如全局构造器)。
我的经验: 如果你要分析的是main函数之后的逻辑,用entry_state()就够了。但如果你要分析某个特定函数,比如check_password,用call_state()会更高效,省去了前面一大段初始化代码的执行时间。
3.4 SimulationManager:路径探索的“指挥官”
有了State,我们还需要一个东西来管理它——SimulationManager。你可以把它理解成一个“路径管理器”,它负责跟踪所有活跃的执行路径,并决定下一步探索哪个路径。
创建SimulationManager很简单:
# 创建一个SimulationManager,从当前state开始探索
simgr = proj.factory.simulation_manager(state)
SimulationManager的核心方法就几个:
| 方法 | 作用 | 说明 |
|---|---|---|
step() |
单步执行,探索一步 | 适合调试,一步步看路径变化 |
explore(find=..., avoid=...) |
自动探索,直到找到目标地址 | 最常用的方法,find是目标地址,avoid是避开地址 |
run() |
一直执行直到所有路径结束 | 慎用,路径可能爆炸 |
我个人最常用的是explore()。比如,你想找到能到达0x400800这个地址的输入:
simgr = proj.factory.simulation_manager(state)
simgr.explore(find=0x400800)
if simgr.found:
found_state = simgr.found[0]
# 从found_state中提取符号值,得到具体的输入
solution = found_state.solver.eval(flag_symbol)
print(f"找到的输入: {solution}")
else:
print("没有找到能到达目标地址的路径")
这里要注意,simgr.found是一个列表,里面是所有到达目标地址的状态。通常只有一个,但如果有多个路径都能到达,那就有多个。
核心流程总结:
- Project:加载二进制文件
- State:创建初始执行状态
- SimulationManager:管理路径探索
- explore():自动寻找目标路径
- solver.eval():从符号状态中提取具体值
3.5 一个完整的例子:破解简单的密码校验
光说不练假把式。咱们来看一个实际的例子。假设有一个crackme程序,它从stdin读取一个字符串,然后调用check_password函数验证。如果正确,程序会打印"Correct!"并跳转到0x400900;如果错误,打印"Wrong!"并跳转到0x400700。
用Angr破解它:
import angr
# 1. 加载程序
proj = angr.Project('./crackme', auto_load_libs=False)
# 2. 创建初始状态
state = proj.factory.entry_state()
# 3. 创建SimulationManager
simgr = proj.factory.simulation_manager(state)
# 4. 探索,找到能到达0x400900的路径
simgr.explore(find=0x400900, avoid=0x400700)
# 5. 检查结果
if simgr.found:
found_state = simgr.found[0]
# 假设输入存储在stdin缓冲区,我们把它提取出来
# 这里用found_state.posix.dumps(0)获取stdin的内容
password = found_state.posix.dumps(0)
print(f"破解成功!密码是: {password}")
else:
print("破解失败,没有找到有效路径")
嗯,这里要注意一点。上面代码里我用found_state.posix.dumps(0)来获取stdin的内容。但实际项目中,输入可能不是直接从stdin读的,而是通过某个缓冲区。这时候你需要找到那个缓冲区的地址,然后用found_state.memory.load(addr, size)来读取。
我的经验: 在实际逆向中,我经常先用IDA或Ghidra找到目标地址,然后写一个Angr脚本自动跑。Angr最擅长的是处理那些“输入长度固定、校验逻辑复杂”的密码算法。比如一些CTF题目里的白盒AES、自定义哈希函数,用Angr往往能秒杀。
3.6 避坑指南:常见问题
最后,分享几个我踩过的坑:
- 路径爆炸: 如果程序里有大量循环,Angr可能会生成海量路径。解决办法是设置
simgr.use_technique(angr.exploration_techniques.LoopSeer()),或者手动限制探索深度。 - 符号化输入太大: 如果你把整个stdin都符号化了,Angr会非常慢。尽量只符号化关键部分,比如密码输入的那几个字节。
- Z3求解超时: 有时候约束条件太复杂,Z3会算不出来。可以设置超时时间:
state.solver.timeout = 5000(5秒)。
好了,这一章的内容就到这里。Angr的入门其实不难,关键是理解Project、State、SimulationManager这三者的关系。你只要记住:Project是程序,State是快照,SimulationManager是探索器。剩下的就是多写代码,多踩坑。