angr 符号执行(符号化输入 / 自动求解)

SkillDev tools

angr 符号执行:符号化输入、求解。触发词:angr、符号执行、自动解题、constraint

Available today. Use it from your connected AI after setup.

Connect ahel once, and every AI you use reads what you have installed.

Then ask your AI: use the angr 符号执行(符号化输入 / 自动求解) skill

What this skill tells your AI

The instructions your AI receives, as published by dslsdzc/rev-skills in .claude/skills/re-angr/SKILL.md and read by ahel’s review.

何时使用 / 何时不用

  • 用: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);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(最新 9.3.x 线)
    • 系统为 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):
      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 无关调用:把与校验无关的重型 / 系统调用替换成轻量过程:
      project.hook(addr_of_sleep_or_memset_impl, angr.SIM_PROCEDURES["stubs"]["ReturnUnconstrained"]())
      
      angr.SIM_PROCEDURES 自带库(libc.sleeplinux_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 模式)

    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]]

Signals

GitHub stars
57
Forks
8
Last commit
Sep 2026

ahel review

  • K1binfo
    installs-packages

Automated review, not a security audit. Ruleset v1+k2.

Advanced
Catalog kind
skill
Gateway key
re-angr
Source
github.com/dslsdzc/rev-skills