16 KiB
name, description, capabilities
| name | description | capabilities | |
|---|---|---|---|
| re-angr | angr 符号执行:符号化输入、求解。触发词:angr、符号执行、自动解题、constraint |
|
angr 符号执行(符号化输入 / 自动求解)
何时使用 / 何时不用
- 用:CTF 逆向题——输入在长循环 / 深比较链里逐字节校验(校验通过地址已知或可定位),人工逆推繁琐易错
- 用:已知"输入必须到达某地址 / 避开某地址",想把路径条件交给求解器(find / avoid 模式)
- 用:输入来源复杂(argv / 文件 / 标准输入),需要按通道符号化后求解
- 不用:简单 XOR / 明文比较(一屏伪代码内)——人工更快(re-binary-core)
- 不用:校验含不可符号化内容(环境相关值、随机数、未符号化输入)——会无解或误解(见坑 3)
- 不用:需要精确约束集合而非一条路径时——纯约束求解用 re-z3 更轻(见坑 2)
- 注意:符号执行是把双刃剑——先人工定位关键校验函数缩小符号化范围(见坑 2);运行验证走沙箱(re-analyze/platform-tips 最高原则)
工具准备
参考 re-analyze/platform-tips 最高原则——用求解结果运行目标验证时默认沙箱;angr 本身是进程内仿真器,不接触真实系统调用,求解过程无需沙箱。
angr(pip 安装)
pip install angr(Linux / macOS / Windows 均可;Windows 下 pip 装 win32 wheel,WSL 内安装 Linux 版亦可)- Python 版本兼容性(重点):angr 9.3.0 起要求 Python 3.12+(
requires-python >=3.12;当前主线 10.x 同);9.2.x 不是固定区间——下限随补丁版本抬高(9.2.91 要求 ≥3.8,9.2.203 已要求 ≥3.10),不能按「9.2.x」整线判断。装前以目标版本的 PyPIrequires-python为准(pip index versions angr看可用版本),再按系统 Python 选:- 系统自带 Python 3.12+(Ubuntu 24.04 默认 3.12)→ 直接
pip install angr(当前主线 10.x;Python >=3.12) - 系统为 Python 3.11 及以下 →
pip install "angr<9.3"(9.2.x 线),别硬装 9.3+ - 多版本并存用 venv 隔离:
python3.12 -m venv ~/venvs/angr && source ~/venvs/angr/bin/activate(或brew install python@3.12/py -3.12 -m venv angr-venv)
- 系统自带 Python 3.12+(Ubuntu 24.04 默认 3.12)→ 直接
- 装前先升级基础工具:
pip install --upgrade pip setuptools wheel(避免原生组件编译失败) - Linux 建议补
binutils(angr 处理 / 重写二进制会调 objcopy):apt install binutils - 验证:
python3 -c "import angr; print(angr.__version__)"
python3
- Linux:
apt install python3(多数自带);macOS:brew install python;Windows: 官方安装包 /choco install python - 验证:
python3 --version
目标二进制
- 原始样本副本(先备份:符号执行不修改文件,但建模要反复对照,见坑 5)+ re-triage 初勘产物(架构 / 位数 / 是否静态链接 / 是否带壳)
- 验证:
file target确认架构与位数(arm 目标 angr 支持,注意字节序)
操作步骤
按顺序执行,每步记录结果(地址 / 约束 / 求解脚本 / flag,证据路径见 re-triage)。先人工后自动化:符号化范围越小越稳(见坑 2)。
-
输入适配(Input Adapter):确定符号源(symbol source):
-
反编译定位输入读取点:
read/scanf/fgets/getline/main(int argc, char **argv)的 argv 使用处(re-ghidra / re-ida / re-radare2) -
按符号源类型选择接入方式(CTF 的 stdin 只是其中一种——真实目标输入可能是网络包/文件映射/JNI 参数/自定义字节码):
符号源 接入方式 stdin(标准输入) project.factory.full_init_state(stdin=claripy.BVS('in', 32*8))argv(命令行参数) project.factory.full_init_state(args=["./target", claripy.BVS("arg1", 64*8)])file(文件输入) state.fs.insert('/tmp/in', angr.SimFile('in', content=claripy.BVS('in', 32*8)))——fopen 后即符号化;按 fd 操作用state.posix.fd[0](fd 0=stdin;fd 1/2 是 stdout/stderr,勿混)memory(mmap/堆/全局缓冲区) 直接符号化目标内存区: state.memory.store(addr, claripy.BVS('buf', n*8))——程序从该地址读入即符号化(mmap 文件、解密缓冲、共享内存通用)network buffer(网络包/recv) hook recv/read:按调用约定取 buf 参数(x86-64 SysV 是 RSI=buf、RDX=len),把符号字节写进该地址,返回值是 ssize_t 字节数而不是 buffer 指针——返回符号长度才能让if (recv(...) > 0)一类控制流可变。照"返回符号指针"实现会把后续控制流全部建歪jni argument(Android native 入参) 对 JNIEnv方法参数做符号化:从GetByteArrayElements/GetStringUTFChars返回处符号化(配合 re-android-native/re-frida 确认入参形态)custom VM bytecode(自定义 VM 指令流) 把字节码缓冲区整体符号化 + 约束操作码范围(0x00-0x0F 类)——VM 逆向的符号执行路径(re-deobfuscate 联动) -
记下:符号化通道 + 字节数 + 读取点地址(后续 find / hook 用);N 字节长度对照读取点逻辑确认(见坑 3)
-
-
到达目标地址 / 避开地址建模(find / avoid):
- 反编译确认成功路径地址(校验通过后打印 flag 的地址)与失败路径地址(打印 "wrong" 等处,有多个失败点要列全)
- 建模:
simgr.explore(find=<入口地址>, avoid=[<失败分支地址>])(find 可多地址或 lambdalambda s: b"flag{" in s.posix.dumps(0)) - find 选地址的技巧:选校验循环出口而非程序 exit——找到"经过校验通过分支"的状态即可,不必等打印(见坑 5)
- 校验是通过调用函数返回判定(
if (check(input)))→ find 设在 check 返回后的成功分支地址,avoid 设在失败分支
-
路径约束与求解(solver):
- 找到状态后,约束即该状态路径上累积的条件:
found = simgr.found[0] - 求解:直接对符号变量求值(不依赖 posix 插件):
flag = found.solver.eval(inp, cast_to=bytes)(inp 即步骤 1 创建的 stdin/argv 符号)或按符号变量名求:found.solver.eval(flag_sym, cast_to=bytes) - 多解时按需加约束(长度 / 可打印字符,见坑 4):
for c in flag_sym.chop(8): found.solver.add(0x20 <= c, c <= 0x7e) - 输出到文件:
open('flag.bin','wb').write(flag),同时打印 repr 检查(非打印字符常是符号化长度问题,见坑 3)
- 找到状态后,约束即该状态路径上累积的条件:
-
路径爆炸应对(hook / 限制深度 / 分段):
- hook 无关调用:把与校验无关的重型 / 系统调用替换成轻量过程:
project.hook(addr_of_sleep_or_memset_impl, angr.SIM_PROCEDURES["stubs"]["ReturnUnconstrained"]())angr.SIM_PROCEDURES自带库(libc.sleep、linux_kernel等)优先:project.hook_symbol("sleep", angr.SIM_PROCEDURES["libc"]["sleep"])(sleep 属 libc 类目,不是 posix) - 限制探索规模:
simgr = proj.factory.simgr(state, veritesting=True)(veritesting 合并路径,长循环题常用,见坑 2);simgr.explore(find=..., avoid=..., num_find=1)找到即停;stash上限 /lazy_solves选项控制 - 分段探索:长循环把入口地址 hook 住,先探索到循环边界,再对循环体单独符号化展开(人工定位循环不变量后缩小范围)
- 仍爆炸 → 回到步骤 1 缩小符号化范围 / 结合人工分析(见坑 2),或换 re-z3 对已展开的循环体建模
- hook 无关调用:把与校验无关的重型 / 系统调用替换成轻量过程:
-
典型模板(find / avoid 模式):
import angr, claripy p = angr.Project("./target", auto_load_libs=False) # 关库加载,快且稳 inp = claripy.BVS("inp", 32 * 8) # 32 字节符号化输入 state = p.factory.full_init_state(stdin=inp) simgr = p.factory.simgr(state, veritesting=True) simgr.explore(find=0x401100, avoid=[0x401200, 0x401300]) # 示意地址:按目标实际入口/分支替换 if simgr.found: sol = simgr.found[0].solver.eval(inp, cast_to=bytes) print(sol) # 直接保存进证据目录 else: print("no path found") # 排查:地址错 / 输入长度 / 未符号化- 模板参数化:
find/avoid/ 输入长度 / 符号化通道写进脚本头部注释,逐个换参跑(见坑 5)
- 模板参数化:
验证:沙箱内(re-sandbox)用求解出的输入原样跑目标(stdin 重定向 ./target < flag.bin 或按 argv / 文件通道),必须打印 flag{...};与 re-z3 / 人工还原结果交叉对照(见坑 4)。
跨域联合
-
re-address-space:mapped_base/rebase 与目标地址对齐
-
re-ctf:本技能是 re-ctf 网关工作流第 3 步的自动化解题路径(逐字节长循环校验题)
-
re-binary-core:前置工作台——反编译定位输入读取点 / 成功失败分支地址(re-ghidra / re-ida / re-radare2);re-triage 初勘决定架构 / 位数 / 壳
-
re-deobfuscate:混淆先还原再符号执行(花指令 / 平坦化函数直接符号执行会路径爆炸)
-
re-z3:姊妹技能——无循环 / 已人工展开的约束集合用 z3 更轻更快;angr 求解慢时对约束子集转 z3
-
re-crypto-decrypt:其「工具准备」将 angr 列为可选的解密仿真方案(还原算法失败时符号化执行解密函数)
-
re-sandbox:求解输入的运行验证沙箱(re-analyze/platform-tips 最高原则)
-
re-gdb / re-tracing:动态交叉验证(断点看校验分支实际走向,与 angr 路径结论对照)
常见坑与陷阱
- 路径爆炸:现象——
explore跑十几分钟状态数飞涨,内存吃满;原因——符号化范围过大(整段程序都符号化)、无关分支(strlen / memcpy 展开、错误处理分支)被逐一探索、长循环不合并;对策——hook 无关系统调用(步骤 4)、veritesting=True合并路径、num_find=1找到即停、缩小符号化输入范围(步骤 1 只符号化校验真正读取的字节);仍不行就分段探索或转人工分析 - 复杂校验慢 → 结合人工分析:现象——一个看似简单的题 angr 几分钟没结果;原因——校验链中夹着查表 / 随机数 / 系统调用副作用,符号执行在这些点低效;对策——先人工反编译定位关键校验函数,把符号化入口设到校验函数入口(
blank_state+ 手动设置寄存器 / 内存),跳过前面无关代码;校验是纯等式集合时直接换 re-z3 - 未符号化输入 → 无解:现象——
explore秒回no path found,或求解出的值跑原程序不通过;原因——输入没被符号化(读取的是真实 stdin / 文件内容)、符号化字节数 < 实际读取长度(fgets(buf, 0x40)却只符号化 16 字节)、输入含运行时才能确定的量(随机数 / 时间戳);对策——对照反编译确认读取点与长度(步骤 1),stdin 长度按读取上限符号化;随机值点用 hook 固定(hook_symbol("rand", ...))再符号化 - python 版本兼容:现象——
pip install angr报编译错误 / import 即崩(ImportError指向 C 扩展);原因——angr 9.3.0+ 官方要求 Python 3.12+,9.2.x 线下限随补丁版本抬高(9.2.91 要求 ≥3.8、9.2.203 已要求 ≥3.10),在过旧解释器上装新版(或在 3.13 上装无 wheel 的旧依赖)会现场编译甚至失败;对策——查目标版本的 Python 要求(PyPIrequires-python):9.3+ 用 3.12+,9.2 按具体补丁版的元数据选解释器,建独立 venv 安装(工具准备节);装前pip install --upgrade pip setuptools wheel - find 地址选错 → 求解出的"flag"跑不通:现象——求解成功但输出含非打印字符 / 程序不打印 flag;原因——find 设在错误处理循环(失败也经过)、多失败点只 avoid 了一个、符号化长度与程序读取不一致;对策——find 选校验循环出口(用反编译确认唯一成功路径),avoid 列全所有失败分支;输出先
repr()检查再原样重放(见步骤 3 验证);拿多个解逐一跑目标验证 - PIE 基址偏移 → find 地址填错:现象——按 Ghidra 显示的地址(如
0x00101231)填 find,angr 永远找不到;原因——PIE 二进制 Ghidra 按 0x100000 基址显示,angr 从 0x400000 装载;对策——地址换算(差 0x300000 之类固定偏移),或先查 angr 内模块实际装载基址再定 find 地址,脚本头部注释标明换算关系 - 自定义 VM/重度混淆题直接符号执行低效:现象——校验包在自定义字节码 VM(Tigress 等)里,angr 状态爆炸 / 求解无果;原因——VM 的 dispatch 循环反复符号化 + 混淆路径分支(先消 opaque predicate,见 re-deobfuscate);对策——VM handler 用 Unicorn 实例化仿真(angr 只负责外层控制流),或探索到死路径后导出每条路径的 SMT 方程交给 re-z3 生成输入(Tigress 案例达到 100% 分支覆盖)
- SimProcedure 摘要不准 → 结果诡异:现象——
explore结果明显违反直觉(错误返回值、绕过校验),或同一逻辑换种跑法结果不一致;原因——angr 用 Python 摘要(SimProcedure)替代库函数以避免路径爆炸(strlen/malloc 等),但摘要有 bug 或不完整;对策——行为异常时优先怀疑 SimProcedure:禁用对应摘要(注意输入约束不紧时可能路径爆炸)或用project.hook按实际场景自定义摘要(如针对单一格式串自写 scanf hook) - 除零约束缺失 → 求解出 4294967295 类怪值:现象——求解出的输入里出现
0xFFFFFFFF之类,跑原程序必挂;原因——Z3 对a/b(分母为 0)求值为全 1(4294967295),angr 只在部分路径(VEX 侧出口)拦截除零,SimProcedure/自定义分析代码可能漏过去;对策——涉及除法的符号表达式显式加"分母非零"约束,别依赖 angr 自动处理 - 未初始化寄存器/内存被自动符号化 → 求解空间虚胖:现象——求解极慢或结果含垃圾值,日志有 unconstrained 告警;原因——
entry_state对未初始化的寄存器/内存生成未约束符号变量,把无关状态也符号化了;对策——按告警设置状态选项ZERO_FILL_UNCONSTRAINED_MEMORY/ZERO_FILL_UNCONSTRAINED_REGISTERS(或SYMBOL_FILL_*抑制告警),或显式设置已知初值,缩小求解空间 - cmovxx 不会自动分裂分支(ollvm 后继还原):现象——angr 模拟 ollvm 真实块找后继时 cmov 分支丢失,后继关系不完整;原因——angr 对 cmov 类指令通过积累约束实现而非分裂两个状态;对策——手动分裂:
state.copy()两份(一份执行 Move:setattr(regs, dst, getattr(regs, src)),一份不执行),并手动跳过 cmov 指令(state.regs.ip += ins.size)防止 angr 再次执行,再各自 simgr 步进找后继 - 分发器 hook 要 unhook + 地址偏移校准:现象——hook 主分发器末条指令跳真实块后执行又跳回分发器死循环,或 angr 解码出混乱指令;原因——hook 未卸掉(执行回来再次进 hook);capstone 从错误边界解码(指令错位);对策——跳到目标块后立即 unhook;解码混乱时调整 hook 地址(经验偏移如 -6,逐位试并输出汇编对照确认,见下条方法)
- ollvm 真实块识别与 patch 顺序:现象——静态插件(D-810)面对变异 ollvm(双循环头、汇聚块与循环头合并)乏力;原因——静态分析难处理变体;对策——动态执行找真实顺序:BFS 找循环头(回到已访问块即循环头)→ 循环头两前驱=序言+汇聚块(多前驱者为汇聚块)→ 汇聚块前驱们=真实块,另找 ret 块(无后继且末指令 retn);patch 顺序固定:先收集全部无用块列表(含起止地址)→ 再 patch 控制流 → 最后按先前列表 nop 无用块(先 patch 再找无用块会找错)
- API 速查(装载/状态/求解/hook 各族)与组合套路见 commands;版本差异、求解语义与装载边界见 gotchas