符号执行与 angr
2025/8/25大约 9 分钟
符号执行与 angr
本文适合
已掌握基本逆向工程的学习者。学完你能:为简单 CrackMe 配置 angr 的符号输入、目标地址、失败地址和约束,并用求出的输入回到原程序验证
一句话判断
当目标二进制的成功地址和失败地址明确,输入从 stdin、argv 或固定内存进入,且校验逻辑不依赖复杂外部环境时,可以尝试用 angr 自动探索成功路径。
angr 适合“找一条能到成功分支的路径”,不适合盲目丢给所有复杂程序。
题目中常见信号
- 程序是简单 CrackMe,输入固定长度,成功/失败字符串清晰。
- IDA/Ghidra 中能确定
find成功地址和avoid失败地址。 - 输入来自 stdin、argv、scanf/read/fgets 或固定内存缓冲区。
- 校验逻辑主要是算术、异或、比较和分支。
- 没有复杂文件系统、网络、多线程、反调试、时间随机或标准加密黑盒。
- 手动读约束较烦,但路径数量还可控。
核心概念
angr 做的事可以拆成四步:
- 加载程序:识别架构和地址空间。
- 创建状态:把输入设置为符号变量。
- 探索路径:向成功地址走,避开失败地址。
- 求解输入:对找到的状态求解符号变量。
最重要的是建好初始状态和约束。输入长度、换行符、可打印范围、目标地址和 avoid 地址错了,angr 结果就会跑偏。
最小分析流程
- 静态定位成功地址和失败地址,优先用输出字符串 xref。
- 确认输入来源:stdin、argv、内存、文件还是函数参数。
- 创建固定长度符号输入,并限制字符范围。
- 设置
find和avoid,必要时 hook 无关函数。 simgr.explore()后求解输入。- 用真实程序运行该输入,验证是否进入成功分支。
最小验证示例
import angr
import claripy
p = angr.Project("./crackme", auto_load_libs=False)
flag_len = 16
flag = claripy.BVS("flag", flag_len * 8)
state = p.factory.entry_state(stdin=flag)
for i in range(flag_len):
c = flag.get_byte(i)
state.solver.add(c >= 0x20)
state.solver.add(c <= 0x7e)
simgr = p.factory.simulation_manager(state)
simgr.explore(find=0x401234, avoid=0x401260)
if simgr.found:
found = simgr.found[0]
sol = found.solver.eval(flag, cast_to=bytes)
print(sol)然后验证:
printf '%s' '求出的输入' | ./crackme如果程序输出成功,才算 angr 求解闭环完成。
常见利用 / 解题路线
路线总览:
- stdin 路线:输入来自标准输入时,用
entry_state(stdin=sym)。 - argv 路线:输入来自命令行参数时,用
entry_state(args=["./bin", sym_arg])。 - 内存路线:输入在固定地址时,用
state.memory.store(addr, sym)。 - find/avoid 路线:通过成功/失败字符串 xref 定位地址,提高探索效率。
- hook 路线:hook
strlen、strcmp、随机数、时间、复杂库函数或反调试函数。 - 分段路线:对路径爆炸的程序,先手动进到 check 函数或用
call_state从目标函数开始。
什么是符号执行
符号执行是一种程序分析技术,它使用符号值(而非具体值)来执行程序,记录程序的路径约束,然后使用约束求解器找到满足特定路径的输入。
符号执行 vs 具体执行
具体执行:
input = 123
if input > 100:
print("big") # 执行这条路径
else:
print("small")
符号执行:
input = symbol
if input > 100:
print("big") # 记录约束: input > 100
else:
print("small") # 记录约束: input <= 100符号执行的优势
1. 自动化路径探索
2. 自动生成满足约束的输入
3. 可以处理复杂的条件分支
4. 适合 CTF 中的逆向题angr 基础
什么是 angr
angr 是一个开源的二进制分析框架,支持符号执行、程序分析、漏洞发现等功能。
安装 angr
# 使用 pip 安装
pip install angr
# 或者使用 angr-dev(开发版)
git clone https://github.com/angr/angr.git
cd angr
pip install -e .angr 基本概念
import angr
import claripy
# Project:加载二进制文件
p = angr.Project('./binary', auto_discover=False)
# State:程序状态
state = p.factory.entry_state()
# Simulation Manager:管理多个状态
simgr = p.factory.simulation_manager(state)angr 基本用法
最简单的例子
import angr
# 加载二进制文件
p = angr.Project('./crackme', auto_discover=False)
# 创建初始状态
state = p.factory.entry_state()
# 创建仿真管理器
simgr = p.factory.simulation_manager(state)
# 执行到找到目标地址
simgr.explore(find=0x401234)
# 检查是否找到
if simgr.found:
found = simgr.found[0]
print(f"找到解: {found.posix.dumps(0)}")
else:
print("未找到解")指定输入位置
import angr
import claripy
p = angr.Project('./crackme', auto_discover=False)
# 创建符号输入
input_size = 32
input_bvs = claripy.BVS('input', input_size * 8)
# 创建初始状态,指定 stdin 为符号输入
state = p.factory.entry_state(stdin=input_bvs)
# 或者指定内存中的符号值
# state = p.factory.entry_state()
# state.memory.store(0x601000, input_bvs)
simgr = p.factory.simulation_manager(state)
simgr.explore(find=0x401234)
if simgr.found:
found = simgr.found[0]
# 求解具体值
solution = found.solver.eval(input_bvs, cast_to=bytes)
print(f"解: {solution}")添加约束
import angr
import claripy
p = angr.Project('./crackme', auto_discover=False)
# 创建符号输入
input_bvs = claripy.BVS('input', 8 * 8) # 8 字节
state = p.factory.entry_state(stdin=input_bvs)
# 添加约束:输入必须是可打印字符
for i in range(8):
byte = input_bvs.get_byte(i)
state.solver.add(byte >= 0x20)
state.solver.add(byte <= 0x7e)
simgr = p.factory.simulation_manager(state)
simgr.explore(find=0x401234)
if simgr.found:
found = simgr.found[0]
solution = found.solver.eval(input_bvs, cast_to=bytes)
print(f"解: {solution}")angr 高级用法
多个目标地址
import angr
p = angr.Project('./crackme', auto_discover=False)
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)
# 指定多个目标地址
simgr.explore(find=[0x401234, 0x401256])
if simgr.found:
for found in simgr.found:
print(f"找到解: {found.posix.dumps(0)}")避免特定地址
import angr
p = angr.Project('./crackme', auto_discover=False)
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)
# 避免特定地址(如错误处理)
simgr.explore(find=0x401234, avoid=0x401200)
if simgr.found:
found = simgr.found[0]
print(f"找到解: {found.posix.dumps(0)}")使用钩子
import angr
# 钩子函数
def hook_printf(state):
# 忽略 printf 调用
pass
p = angr.Project('./crackme', auto_discover=False)
# 注册钩子
p.hook_symbol('printf', hook_printf)
state = p.factory.entry_state()
simgr = p.factory.simulation_manager(state)
simgr.explore(find=0x401234)处理库函数
import angr
# 使用 angr 的 SimProcedures 替代库函数
p = angr.Project('./crackme', auto_discover=False)
# 自动替代常见库函数
p = angr.Project('./crackme', auto_discover=False, use_sim_procedures=True)
# 或者手动替代
from angr.sim_procedures import libc
p.hook_symbol('printf', libc.printf.Printf())
p.hook_symbol('scanf', libc.scanf.Scanf())angr 实战示例
示例 1:简单的 CrackMe
import angr
import claripy
def solve_crackme():
p = angr.Project('./crackme', auto_discover=False)
# 创建符号输入
flag = claripy.BVS('flag', 32 * 8)
# 创建初始状态
state = p.factory.entry_state(stdin=flag)
# 添加约束:输入以 flag{ 开头
for i, c in enumerate(b'flag{'):
state.solver.add(flag.get_byte(i) == c)
# 添加约束:输入以 } 结尾
state.solver.add(flag.get_byte(31) == ord('}'))
# 创建仿真管理器
simgr = p.factory.simulation_manager(state)
# 执行到成功地址
simgr.explore(find=0x401234)
if simgr.found:
found = simgr.found[0]
solution = found.solver.eval(flag, cast_to=bytes)
print(f"Flag: {solution.decode()}")
else:
print("未找到解")
solve_crackme()示例 2:多分支验证
import angr
import claripy
def solve_multi_branch():
p = angr.Project('./multi_branch', auto_discover=False)
# 创建符号输入
input_data = claripy.BVS('input', 16 * 8)
state = p.factory.entry_state(stdin=input_data)
# 添加可打印字符约束
for i in range(16):
byte = input_data.get_byte(i)
state.solver.add(byte >= 0x20)
state.solver.add(byte <= 0x7e)
simgr = p.factory.simulation_manager(state)
# 执行到成功地址
simgr.explore(find=0x401234)
if simgr.found:
found = simgr.found[0]
solution = found.solver.eval(input_data, cast_to=bytes)
print(f"解: {solution.decode()}")
else:
print("未找到解")
solve_multi_branch()示例 3:内存中的符号值
import angr
import claripy
def solve_memory():
p = angr.Project('./memory_check', auto_discover=False)
state = p.factory.entry_state()
# 在内存中创建符号值
input_addr = 0x601000
input_data = claripy.BVS('input', 16 * 8)
state.memory.store(input_addr, input_data)
# 添加约束
for i in range(16):
byte = input_data.get_byte(i)
state.solver.add(byte >= 0x30)
state.solver.add(byte <= 0x39) # 数字
simgr = p.factory.simulation_manager(state)
simgr.explore(find=0x401234)
if simgr.found:
found = simgr.found[0]
solution = found.solver.eval(input_data, cast_to=bytes)
print(f"解: {solution.decode()}")
solve_memory()angr 常见问题
1. 状态爆炸
问题:分支太多导致状态爆炸
解决:
1. 使用 avoid 避免不重要的路径
2. 使用 hook 简化复杂函数
3. 限制探索深度
4. 使用 unicorn 引擎加速2. 约束求解超时
问题:约束太复杂导致求解超时
解决:
1. 简化约束
2. 添加更多约束缩小搜索空间
3. 使用更强大的求解器
4. 分段求解3. 库函数处理
问题:库函数行为复杂
解决:
1. 使用 sim_procedures
2. 手动 hook 库函数
3. 忽略不重要的库函数angr 工具
angr CLI
# 使用 angr CLI 运行脚本
python solve.py
# 使用 angr 的 REPL
python -m angrangr Management
# 安装 angr Management
pip install angr-management
# 启动 GUI
angr-managementCTF 中的符号执行
适用场景
1. 复杂的条件分支
2. 多层嵌套的验证逻辑
3. 需要满足多个约束
4. 手动分析困难的程序不适用场景
1. 有反调试保护
2. 有环境检测
3. 依赖外部输入(如时间、随机数)
4. 程序行为依赖外部状态常见失败原因
- 以为 angr 能解决所有逆向题:复杂环境、哈希/AES、巨大循环、多线程都可能让 angr 不适合。
find地址错:不要用输出函数地址当成功地址,要用进入成功分支后的具体基本块。- 忘记
avoid:没有避开失败路径时,状态会浪费在大量无意义分支。 - 输入长度不对:stdin 常包含换行,必要时给符号输入末尾加
\n或约束。 - 不加字符范围:搜索空间过大,且可能求出不可打印输入。
auto_load_libs=True导致慢:CTF CrackMe 通常先关库加载,必要时 hook。- 求解后不验证:angr 模型和真实运行可能不同,必须用原程序确认。
做题时的归类问题
- 程序有大量条件分支但只需找到一条正确路径,先回到 符号执行与angr。
- 手动分析太复杂,需要自动化求解输入,先回到 符号执行与angr。
- 程序验证逻辑是多层嵌套的 if-else,先回到 符号执行与angr。
- 需要找到满足特定条件的输入,但不知道具体值,先回到 符号执行与angr。
- 程序有反调试但没有环境检测,先回到 符号执行与angr。
迷你案例
一个 crackme 输出 Good 或 Bad。IDA 中 Good 的 xref 地址是 0x401234,Bad 的 xref 地址是 0x401260。输入来自 read(0, buf, 16)。
写 angr 脚本创建 16 字节 stdin,限制为可打印字符,设置:
simgr.explore(find=0x401234, avoid=0x401260)求出 b'angr_is_useful!'。回到原程序:
printf 'angr_is_useful!' | ./crackme输出 Good。这个案例的闭环是:字符串定位地址 -> 建符号 stdin -> find/avoid 探索 -> 求解 -> 原程序验证。