第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的架构:

Angr 核心架构图 上层:分析模块 路径探索 · 漏洞检测 · 符号化参数注入 · 约束求解 我们日常使用的 API(entry_state, simulation_manager 等) 中层:符号执行引擎 维护符号状态 · 处理路径分支 · 管理约束条件 核心:SimulationManager 管理所有活跃路径 底层:加载与翻译 CLE(加载二进制) · PyVEX(翻译为 VEX IR) 把机器码变成 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是一个列表,里面是所有到达目标地址的状态。通常只有一个,但如果有多个路径都能到达,那就有多个。

核心流程总结:

  1. Project:加载二进制文件
  2. State:创建初始执行状态
  3. SimulationManager:管理路径探索
  4. explore():自动寻找目标路径
  5. 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是探索器。剩下的就是多写代码,多踩坑。


专注资料整理