3、第一个angr脚本:加载二进制文件、获取入口点、打印基本块信息
好,咱们正式开始写第一个angr脚本。
说实话,很多初学者一上来就想着搞复杂的符号执行、路径探索,结果被各种API搞懵了。我个人习惯是,先让脚本跑起来,哪怕只是打印个入口点地址,也比对着文档发呆强。
3.1 环境准备:确认angr已安装
在写脚本之前,先确认你的环境没问题。打开终端,输入:
python -c "import angr; print(angr.__version__)"
如果输出版本号,比如9.2.58,那就OK。如果报错,说明没装好。我建议用Python 3.8到3.10之间的版本,太新的Python有时会有兼容性问题。
conda install -c conda-forge angr。我在Windows上踩过这个坑,折腾了半小时才搞定。
3.2 加载二进制文件
angr的核心对象叫Project。你可以把它理解成一个“二进制文件的管理器”。它负责解析文件格式、加载指令、管理符号表等等。
写法很简单:
import angr
# 加载一个二进制文件
proj = angr.Project('./test_binary', auto_load_libs=False)
这里有个参数auto_load_libs=False,我强烈建议你加上。为什么呢?因为angr默认会尝试加载所有依赖的动态链接库,比如libc.so、ld-linux.so等等。这一加载就是几百兆,而且很多库我们根本用不上。我在分析一个嵌入式固件时,忘了关这个选项,结果angr加载了整整两分钟,最后还报了一堆符号找不到的错误。关了它,清爽多了。
3.3 获取入口点
入口点(Entry Point)是程序开始执行的第一条指令地址。对于ELF文件,就是_start函数的位置。
获取入口点有两种方式:
# 方式一:通过Project对象
entry = proj.entry
print(f"入口点地址: {hex(entry)}")
# 方式二:通过loader模块
entry_from_loader = proj.loader.main_object.entry
print(f"Loader给出的入口点: {hex(entry_from_loader)}")
这两种方式结果是一样的。我个人习惯用proj.entry,因为更简洁。
_start,而是某个特定的复位向量。这时候proj.entry可能不准,需要手动指定基址。这个我们后面章节会讲。
3.4 获取基本块信息
基本块(Basic Block)是程序控制流的最小单位。它是一段顺序执行的指令序列,只有一个入口和一个出口。
angr提供了CFG(控制流图)来获取基本块信息。但生成CFG比较耗时,我们先用一个轻量级的方法:
# 获取入口点处的基本块
block = proj.factory.block(proj.entry)
print(f"基本块地址: {hex(block.addr)}")
print(f"基本块大小: {block.size} 字节")
print(f"包含指令数: {block.instructions}")
# 打印指令列表
for insn in block.capstone.insns:
print(f" 0x{insn.address:x}: {insn.mnemonic} {insn.op_str}")
这里用到了capstone反汇编引擎。angr内部集成了capstone,所以我们可以直接调用block.capstone.insns来获取反汇编结果。
3.5 完整脚本示例
把上面的代码拼起来,就是一个完整的angr脚本:
import angr
def analyze_binary(file_path):
# 加载二进制文件
print(f"[*] 正在加载: {file_path}")
proj = angr.Project(file_path, auto_load_libs=False)
# 获取入口点
entry = proj.entry
print(f"[*] 入口点: {hex(entry)}")
# 获取入口点基本块
block = proj.factory.block(entry)
print(f"[*] 基本块信息:")
print(f" 地址: {hex(block.addr)}")
print(f" 大小: {block.size} 字节")
print(f" 指令数: {block.instructions}")
# 打印反汇编
print(f"\n[*] 反汇编代码:")
for insn in block.capstone.insns:
print(f" 0x{insn.address:x}: {insn.mnemonic} {insn.op_str}")
return proj
if __name__ == "__main__":
proj = analyze_binary("./test_binary")
3.6 运行结果解读
假设我们分析的是一个简单的C程序,输出可能长这样:
[*] 正在加载: ./test_binary
[*] 入口点: 0x400520
[*] 基本块信息:
地址: 0x400520
大小: 16 字节
指令数: 4
[*] 反汇编代码:
0x400520: xor ebp, ebp
0x400522: mov r9, rdx
0x400525: pop rsi
0x400526: mov rdx, rsp
看到没?入口点0x400520处的代码是典型的_start函数开头。它先清零ebp,然后保存参数。这就是程序启动时的标准流程。
3.7 核心知识体系
为了让你更直观地理解本章的知识结构,我画了一张图:
这张图把整个流程串起来了:加载 → 获取入口点 → 获取基本块 → 读取属性。每一步都有对应的API,你照着写就行。
3.8 常见问题与避坑
- 加载时间过长:检查是否忘了加
auto_load_libs=False。我曾经分析一个Go语言编译的二进制,没关这个选项,等了5分钟还没加载完。 - 入口点地址异常:比如打印出来是
0x0或者一个很大的地址。这通常是因为文件格式不标准,或者基址没设置对。可以试试proj.loader.main_object.min_addr看看加载基址。 - 基本块为空:如果
block.instructions为0,说明angr没有正确反汇编。可能是架构识别错了,检查一下proj.arch属性。
- angr的
Project是入口,所有操作都基于它 proj.entry获取入口点,proj.factory.block()获取基本块- 基本块对象包含地址、大小、指令数、反汇编等属性
- 记得关闭
auto_load_libs,否则加载会非常慢
好了,第一个angr脚本就写完了。别看它简单,这是你进入angr世界的第一步。后面我们会在这个基础上,逐步加入符号执行、路径探索、约束求解等高级功能。嗯,慢慢来,不着急。