--- name: re-angr description: angr 符号执行:符号化输入、求解。触发词:angr、符号执行、自动解题、constraint capabilities: [symbolic-execution] --- # 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」整线判断。装前以目标版本的 PyPI `requires-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`) - 装前先升级基础工具:`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)。 1. **输入适配(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) 2. **到达目标地址 / 避开地址建模(find / avoid)**: - 反编译确认**成功路径地址**(校验通过后打印 flag 的地址)与**失败路径地址**(打印 "wrong" 等处,有多个失败点要列全) - 建模:`simgr.explore(find=<入口地址>, avoid=[<失败分支地址>])`(find 可多地址或 lambda `lambda s: b"flag{" in s.posix.dumps(0)`) - find 选地址的技巧:选**校验循环出口**而非程序 exit——找到"经过校验通过分支"的状态即可,不必等打印(见坑 5) - 校验是通过调用函数返回判定(`if (check(input))`)→ find 设在 check 返回后的成功分支地址,avoid 设在失败分支 3. **路径约束与求解(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): ```python for c in flag_sym.chop(8): found.solver.add(0x20 <= c, c <= 0x7e) ``` - 输出到文件:`open('flag.bin','wb').write(flag)`,同时打印 repr 检查(非打印字符常是符号化长度问题,见坑 3) 4. **路径爆炸应对(hook / 限制深度 / 分段)**: - **hook 无关调用**:把与校验无关的重型 / 系统调用替换成轻量过程: ```python 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]] 对已展开的循环体建模 5. **典型模板(find / avoid 模式)**: ```python 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 要求(PyPI `requires-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]]