第4章:Angr基础入门:Angr架构解析、安装与配置、加载二进制文件、基础API使用
好,咱们正式开始接触Angr这个大家伙。说实话,我第一次看到Angr的源码时,心里也犯过嘀咕——这玩意儿也太复杂了吧?但用久了你会发现,它的架构设计其实非常优雅。说白了,Angr就是一个用Python写的二进制分析框架,核心思想是把二进制程序抽象成一个数学表达式,然后通过约束求解器来探索所有可能的执行路径。
4.1 Angr架构解析
Angr的架构,我习惯把它分成三层来看:
- 底层引擎:负责模拟CPU执行指令,包括VEX中间语言翻译、指令模拟等
- 中间层:提供符号执行的核心能力,比如状态管理、路径探索、约束求解
- 上层API:我们平时调用的那些接口,比如
project.factory、simgr这些
嗯,这里有个关键点你要记住:Angr并不是直接模拟x86指令,而是先把二进制指令翻译成VEX中间语言(一种架构无关的IR)。为什么要多此一举?因为这样一套引擎就能支持x86、ARM、MIPS等多种架构。我在项目中遇到过需要同时分析x86和ARM固件的情况,Angr这套设计帮我省了不少事。
核心组件一览:
- Project:整个分析的入口,加载二进制文件后的一切操作都通过它
- State:表示程序在某个时刻的状态,包括寄存器、内存、文件描述符等
- SimEngine:模拟引擎,负责执行指令并生成新的状态
- SimManager:路径管理器,控制探索策略(比如DFS、BFS)
- Solver:约束求解器,底层用的是Z3
4.2 安装与配置
安装Angr其实很简单,但有几个坑我得提前告诉你。我曾经在Windows上折腾了一整天,最后发现还是Linux最省心。
推荐方式:使用pip安装
pip install angr
如果你在Linux上,这条命令基本就够了。但如果你用的是Windows,我建议你用WSL或者Docker。为什么?因为Angr依赖的pyvex、unicorn等组件在Windows上编译起来很麻烦。我个人习惯在Ubuntu 20.04上跑,从来没出过问题。
小技巧:安装完成后,可以跑一下这个命令验证是否成功:
python -c "import angr; print(angr.__version__)"
如果能正常输出版本号,说明安装成功了。
依赖问题处理:
- 如果遇到
libpython相关的错误,试试sudo apt install python3-dev - 如果Z3安装失败,可以单独装:
pip install z3-solver - 内存不够?Angr分析大程序时确实吃内存,建议至少4GB RAM
注意:Angr的版本迭代很快,不同版本的API可能有差异。我建议你固定一个版本使用,比如pip install angr==9.2。我曾经因为升级版本导致之前写的脚本全废了,血的教训啊。
4.3 加载二进制文件
加载二进制文件是Angr的第一步,也是最基础的一步。说白了,就是告诉Angr:「嘿,帮我分析这个程序」。
基本用法:
import angr
# 加载一个二进制文件
project = angr.Project('./test_binary', auto_load_libs=False)
这里有个参数auto_load_libs=False,我建议你一开始就加上。为什么?因为Angr默认会尝试加载所有依赖的动态库,比如libc、libm这些。你想想看,光是libc就有几万个函数,全加载进来分析,内存直接爆炸。我在分析一个CTF题目时就吃过这个亏,程序本身才几KB,结果Angr加载了半小时还没完。
加载后的基本信息:
print("架构:", project.arch)
print("入口点:", hex(project.entry))
print("文件名:", project.filename)
输出示例:
架构: <Arch AMD64 (LE)>
入口点: 0x401060
文件名: ./test_binary
| 属性 | 说明 | 常用场景 |
|---|---|---|
| project.arch | 目标架构信息 | 判断是x86还是ARM |
| project.entry | 程序入口点地址 | 从哪开始执行 |
| project.loader | CLE加载器对象 | 查看加载的段、符号表 |
| project.factory | 工厂对象 | 创建状态、块、模拟管理器 |
4.4 基础API使用
好了,加载完二进制文件,咱们来看看怎么用Angr干活。我挑几个最常用的API给你讲讲。
4.4.1 创建初始状态
# 创建一个初始状态,从入口点开始
state = project.factory.entry_state()
# 或者从指定地址开始
state = project.factory.blank_state(addr=0x401234)
entry_state()会模拟程序从入口点开始执行,而blank_state()则是创建一个「空白」状态,你可以手动设置寄存器、内存的值。我个人更喜欢用blank_state,因为控制力更强。比如在分析一个函数时,我可以直接设置参数寄存器的值。
4.4.2 符号变量
符号执行的核心就是符号变量。说白了,就是把一个具体的值替换成一个「符号」,让Angr去探索所有可能的值。
# 创建一个32位的符号变量
sym_var = state.solver.BVS('input', 32)
# 把符号变量写入寄存器
state.regs.eax = sym_var
# 或者写入内存
state.memory.store(0x1000, sym_var)
嗯,这里要注意BVS是BitVectorSymbol的缩写,表示一个位向量符号。第二个参数是位宽,32就是32位,对应一个int。我在做漏洞分析时,经常用符号变量来表示用户输入,然后看程序会走到哪些分支。
4.4.3 模拟执行
# 创建模拟管理器
simgr = project.factory.simulation_manager(state)
# 探索,直到找到某个地址
simgr.explore(find=0x401337)
# 查看找到的状态
if simgr.found:
found_state = simgr.found[0]
print("找到了!")
# 获取符号变量的具体值
solution = found_state.solver.eval(sym_var)
print("解:", hex(solution))
这段代码是Angr最经典的用法。你想想看,我们只需要告诉Angr「帮我找到地址0x401337」,它就会自动探索所有路径,直到找到能到达那个地址的输入。这不比手动逆向快多了?
常用探索方法:
explore(find=addr):找到能到达addr的路径explore(avoid=addr):避开addr路径explore(find=addr, avoid=addr2):组合使用run():一直执行直到结束step():单步执行
4.4.4 约束求解
约束求解是Angr的杀手锏。当你有了符号变量,又探索到了目标状态,就可以让Z3求解器帮你算出具体的值。
# 添加约束
state.solver.add(state.regs.eax > 0x100)
state.solver.add(state.regs.eax < 0x200)
# 求解
if state.solver.satisfiable():
solution = state.solver.eval(state.regs.eax)
print("满足条件的值:", hex(solution))
else:
print("无解")
我曾经用这个技巧解过一个CTF的逆向题。程序要求输入一个字符串,经过一系列变换后等于某个固定值。我直接把输入设成符号变量,然后让Angr自动求解,几秒钟就拿到了flag。要是手动逆向,估计得折腾半天。
避坑指南:约束越多,求解越慢。我建议你只添加必要的约束,别一股脑全加上。另外,eval()默认只返回一个解,如果你想要多个解,可以用eval_n()或eval_atleast()。
4.5 一个完整的例子
最后,我给你一个完整的例子,把上面这些API串起来。假设我们有一个程序,它读取一个整数,如果等于0x1337就输出"Correct"。
import angr
# 加载二进制文件
project = angr.Project('./crackme', auto_load_libs=False)
# 创建初始状态
state = project.factory.entry_state()
# 创建模拟管理器
simgr = project.factory.simulation_manager(state)
# 探索,找到输出"Correct"的地址
# 假设"Correct"字符串的地址是0x401234
simgr.explore(find=0x401234)
if simgr.found:
found = simgr.found[0]
# 假设输入存储在0x600000
input_addr = 0x600000
input_val = found.memory.load(input_addr, 4)
solution = found.solver.eval(input_val)
print(f"正确的输入是: {hex(solution)}")
else:
print("没找到路径")
你看,核心代码也就十几行。Angr的强大之处就在于,它把复杂的符号执行过程封装成了简单的API调用。你不需要理解Z3的底层原理,也不需要手动管理路径探索,Angr都帮你搞定了。
嗯,这一章的内容就到这。下一章我们会深入Angr的State对象,看看怎么精细控制程序状态。到时候我会分享一些我在实际漏洞分析中积累的技巧,保证让你大开眼界。