第一个angr脚本:加载二进制文件,探索函数,获取执行路径
好,咱们正式开始动手了。
前面几章聊了不少理论,你可能已经有点手痒了。今天我们就来写第一个真正的angr脚本。别担心,代码量很少,核心逻辑就几行。我带你一步步走,保证你能跑起来。
准备工作:环境确认
在写脚本之前,先确认你的angr装好了。打开终端,输入:
python -c "import angr; print(angr.__version__)"
如果没报错,说明环境OK。如果报错,回头看看第二章的安装步骤。我个人习惯用Python 3.8以上版本,angr对3.10、3.11支持得也不错。
加载二进制文件
angr的第一步,就是把目标二进制文件“吞”进去。它内部会解析ELF、PE或者Mach-O格式,构建出一个完整的程序状态模型。
来看代码:
import angr
# 加载一个二进制文件
proj = angr.Project('./test_binary', auto_load_libs=False)
这里有个参数auto_load_libs=False,我建议你一开始就加上。为什么?因为angr默认会尝试加载所有依赖的动态库,比如libc.so、libstdc++.so。这在分析复杂程序时很有用,但初学者很容易被海量的库函数搞晕。关掉它,我们只分析目标程序本身,清爽很多。
我在项目中遇到过好几次,开着自动加载库,结果angr卡在libc的某个内部函数里出不来。后来我养成了习惯:先关掉,等需要分析库函数时再手动加载。
探索函数:获取函数列表
加载完二进制,我们来看看里面有哪些函数。angr把二进制文件解析后,会生成一个CFG(控制流图)的骨架,我们可以直接查询函数信息。
# 获取程序的入口点
print("入口点:", hex(proj.entry))
# 获取所有函数(需要先构建CFG)
cfg = proj.analyses.CFGFast()
for func_addr, func in cfg.functions.items():
print(f"函数: {func.name} @ {hex(func_addr)}")
这里调用了CFGFast分析。它会快速扫描二进制,识别出所有函数边界和调用关系。注意,这个分析不是符号执行,它只是静态解析。但它是后续符号执行的基础——你得知道程序有哪些函数,才能决定从哪里开始分析。
嗯,这里要注意:CFGFast不是100%准确的。遇到混淆过的代码或者手写汇编,它可能会漏掉一些函数。不过对于大多数普通编译的程序,它已经够用了。
获取执行路径:从入口到目标
好,重头戏来了。我们想模拟程序从入口开始执行,看看它走了哪些路径。angr的核心思想就是:把程序的执行看作状态机,每个状态包含寄存器、内存、文件描述符等信息。符号执行引擎会探索所有可能的分支。
下面这个例子,我们让angr从入口开始执行,直到遇到第一个分支点:
# 创建一个初始状态
state = proj.factory.entry_state()
# 创建模拟管理器
simgr = proj.factory.simulation_manager(state)
# 探索一步(执行到下一个分支点)
simgr.step()
# 查看当前有多少个活跃状态
print("活跃状态数:", len(simgr.active))
# 如果有多条路径,打印每个状态的指令地址
for i, s in enumerate(simgr.active):
print(f"路径 {i}: 当前指令 @ {hex(s.addr)}")
运行这段代码,你会看到类似这样的输出:
活跃状态数: 2
路径 0: 当前指令 @ 0x400510
路径 1: 当前指令 @ 0x400520
这说明程序在入口处遇到了一个条件分支(比如if-else),angr同时探索了两条路径。每个路径都是一个独立的状态,拥有自己的寄存器值和内存快照。
你想想看,这比传统调试器强在哪?调试器一次只能跟一条路,而angr能同时走所有路。这就是符号执行的威力。
核心概念:状态与模拟管理器
刚才代码里出现了两个关键对象:state和simgr。我简单解释一下:
- State(状态):代表程序在某一时刻的快照。包含CPU寄存器、内存、文件系统、符号变量等。你可以把它理解成一个“平行宇宙”中的程序瞬间。
- SimulationManager(模拟管理器):管理所有状态的容器。它负责调度、合并、剪枝状态。常用的状态列表有
active(活跃)、deadended(结束)、errored(出错)等。
我个人习惯把simgr想象成一个“状态池”。你往里丢一个初始状态,它自动帮你分裂、推进、回收。你只需要关心最终结果就行。
核心流程总结
加载二进制 → 创建初始状态 → 创建模拟管理器 → 探索执行 → 分析结果
可视化:angr执行流程
下面这张图展示了angr从加载到探索的完整流程。我特意画成了SVG,方便你理解各个组件的关系。
从图中可以看到,angr把二进制加载成Project,然后创建初始状态,交给SimulationManager去探索。遇到分支时,状态一分为二,各自沿着不同路径前进。这就是符号执行最核心的机制。
避坑指南
我曾经踩过的坑:
- 忘记关自动加载库:第一次跑angr时,我加载了一个简单的"Hello World"程序,结果angr花了5分钟还没初始化完。后来发现它在疯狂解析libc。关掉
auto_load_libs后,秒开。 - 状态爆炸:如果你让angr一直跑下去,不设任何限制,它可能会生成成千上万个状态。我建议先用
simgr.explore(find=目标地址)来引导探索,而不是无脑step。 - CFG分析耗时:对于大型二进制(比如几十MB),
CFGFast也可能跑很久。可以先试试CFGEmulated,它更轻量。
完整示例:跑一个真实程序
光说不练假把式。我们找一个真实的简单程序来测试。假设有一个test_binary,它从命令行读一个参数,如果参数是"secret",就打印"Access granted",否则打印"Access denied"。
我们用angr来探索所有可能的执行路径:
import angr
proj = angr.Project('./test_binary', auto_load_libs=False)
state = proj.factory.entry_state()
simgr = proj.factory.simulation_manager(state)
# 探索,直到找到"Access granted"的路径
simgr.explore(find=lambda s: b"Access granted" in s.posix.dumps(1))
if simgr.found:
found_state = simgr.found[0]
print("找到了!路径状态:")
print(f" 指令地址: {hex(found_state.addr)}")
# 获取标准输出
print(f" 输出内容: {found_state.posix.dumps(1)}")
else:
print("没有找到目标路径")
这段代码的关键在于find参数。它接受一个回调函数,angr会在每个状态上调用这个函数,如果返回True,就把该状态标记为"found"。这里我们检查标准输出中是否包含"Access granted"字符串。
你可能会问:如果程序需要输入怎么办?别急,后面章节会专门讲符号化输入。今天先感受一下angr的基本工作流。
小结
今天我们做了三件事:加载二进制、获取函数列表、探索执行路径。代码加起来不到20行,但背后涉及了angr最核心的抽象:Project、State、SimulationManager。
说白了,angr就是一个程序状态模拟器。你给它一个起点,它帮你走完所有可能的路。至于怎么从这些路中找到你想要的——那是后面章节要讲的内容。
好,今天的课就到这里。去动手试试吧,把代码敲一遍,看看你的二进制文件能探索出多少条路径。
小提示:如果你手头没有合适的二进制文件,可以用gcc编译一个简单的C程序:
// test.c
#include <stdio.h>
int main(int argc, char **argv) {
if (argc > 1) {
printf("有参数\n");
} else {
printf("无参数\n");
}
return 0;
}
编译:gcc -o test_binary test.c
然后用angr加载它,看看能不能探索出两条路径。
公众号:蓝海资料掘金营,微信deep3321