资讯动态

angr符号执行入门:从约束求解到自动逆向的实践笔记

发布时间:2026/10/18 21:18:45 来源:尧图企业网站定制
开始碰 angr 之前我一度觉得符号执行是某种黑魔法好像一行脚本下去程序自己就会把“正确输入”交出来听起来实在像骗人。后来真正把它用在逆向、CTF reverse 题以及分析某些陌生二进制上才慢慢摸清楚它到底能做什么、不能做什么。这篇 angr 学习笔记1我会按自己的学习路径来写先解释 angr 要解决的核心问题再讲环境准备和三个关键概念最后用一个 Demo 完整跑一遍搜索流程并把常见问题一起列出来。目标读者是刚开始接触二进制分析、听说符号执行但还没系统上手的人如果你已经有经验可以直接跳到第三节以后看实操部分。1. 先搞清楚angr在解决什么问题1.1 从“试输入”到“问方程”以前拿到一个带校验的程序最直接的做法是动态调试给不同输入观察程序走到哪个分支、寄存器变成什么值。对简单比较来说这很快但一旦逻辑复杂起来问题就来了输入空间太大一个一个试不现实分支太多人工盯不住反混淆或者代码膨胀之后手跟汇编更是要命。angr 的思路不是“多跑几次输入”而是把程序跑成一组数学约束。它先把指令翻译成一种中间表示再让“符号变量”代替真实输入参与执行。程序每遇到一个分支条件比如x 42或者mem[0] ! A它就往当前状态里加一个约束。到最后它用约束求解器问有没有一组真实输入能让这些约束全部成立如果有这个输入就是我们要找的值。这个思路和“动态调试多试几次”完全不同动态试输入像在迷宫入口逐个试钥匙符号执行更像是把迷宫的墙、门、岔路全部写成方程式然后直接求解出一条通路。它不是靠运气碰而是靠逻辑推。1.2 符号执行里的“符号变量”怎么理解很多刚接触的人会被“符号变量”这个词吓到。其实可以把它理解成“一个还没确定值的变量”比如x 42是具体值而x 符号整数表示“现在不知道它是什么但只要未知数集合里存在一个合适的值”。程序操作这个符号变量时会记录下它受到的限制。举个最简单的例子。假设程序里有这样一个检查if (input * 3 42) { // correct }传统逆向会想input 是多少手工一算就知道 14。但如果是连续几十个条件比如input[0] A、input[1] B、input[2] 3…… 人算起来就烦了。angr 做的事情就是把每个条件变成一条约束最后构造成一个方程组让求解器去解。需要注意一点符号执行不等于“把所有可能的路径都跑一遍”。它仍然是沿着程序控制流走的但因为变量是符号化的同一段代码可以同时覆盖很大一片输入空间。路径爆炸依旧是它的天敌后面我会专门讲怎么绕开。1.3 它适合什么场景又不太适合什么场景我自己的使用场景主要有三类CTF 的 reverse 题经常需要从一段复杂校验逻辑里反推出一个字符串分析别人给的二进制想快速知道某个函数的输入格式还有自动化的辅助解压和解码逻辑比如程序里有字符变换、加解密用符号执行可以在没有完整理解算法的情况下直接得到正确输入。但 angr 不是万能钥匙。对特别大的程序比如整个系统、复杂 GUI、大量循环和递归直接做全符号执行很容易爆炸。它更适合“目标明确的小范围分析”。在实际做题或分析时我一般先用静态反汇编把范围缩小到一个函数、一段比较逻辑再让 angr 进场而不是一开始就在入口跑全程序。记住这一点能帮你省下大量时间。2. 装好环境从venv到第一个能加载的二进制2.1 Python版本和虚拟环境angr 是 Python 框架依赖比较多安装前我建议先建一个独立虚拟环境。这里说的“独立”不是可选项是强烈建议。因为你可能在别的项目里用了某个库而 angr 对依赖有自己的一套要求直接在系统环境里安装稍不留神就把环境搞乱了。我常用的安装命令大概是这样的python3 -m venv angr-env source angr-env/bin/activate pip install angr如果你的网络下载慢可以把 pip 默认源换成一个镜像源这个根据你所在地区选一个快的就行没必要死磕默认源。装完之后先确认一下版本python -c import angr; print(angr.__version__)angr 的大版本之间接口差异比较大网上很多旧教程用的是state.se新版已经改成state.solver。所以你看到任何代码之前第一件事是确认自己的 angr 版本和文章里的版本是否一致否则照抄很容易报错。2.2 第一次加载二进制文件环境装好后可以先做一个最小验证。比如随便拿一个系统自带的程序或者自己刚编译的测试程序import angr p angr.Project(./demo, auto_load_libsFalse) print(p.arch.name) print(hex(p.entry))第一行Project(./demo)就是让 angr 去解析这个二进制文件。auto_load_libsFalse是我个人很常用的参数意思是不要自动去加载外部动态链接库。原因很简单很多分析场景根本不需要完整执行 libc自动加载库会让过程变慢还会带来大量无关路径。等你真的需要某个库函数时再单独去 hook 或者手动加载也不迟。运行后如果打印出了架构名称和入口地址说明加载基本成功。如果报错通常说明文件格式、架构或者二进制本身有点特殊比如是一个纯裸机固件没有标准文件头。这个阶段不要急着写复杂的分析脚本先把“加载”这一步跑通再说。2.3 环境问题速查现象常见原因处理思路安装过程报依赖编译错误Python 版本过新或过旧换一个 LTS 版本 Python 重新建虚拟环境import angr 提示缺少动态库底层依赖没装全查看报错信息按提示补系统依赖包加载二进制很慢尝试加载了外部库设置auto_load_libsFalse网上代码大量报错angr 大版本 API 不同确认版本把state.se改为state.solver等这些小问题看起来很初级但我在实际操作中真的被它们卡过不少次。所以宁可最开始多花十分钟把环境弄干净也不要等到跑题目时再来排查。3. 核心概念Project、State、SimulationManager3.1 Project把二进制文件抬上工作台Project是 angr 的入口对象。它代表“一个被加载到内存解析状态的二进制程序”你可以通过它拿到架构、入口地址、加载器信息、符号表等。常见用法有这些import angr p angr.Project(./demo, auto_load_libsFalse) print(p.arch) print(p.entry) print(p.loader.main_object)p.arch会告诉你程序运行在什么架构上p.entry是程序入口p.loader.main_object对应主模块的信息在分析加壳或者多模块场景时特别有用。你可以把Project理解成一个工作台。它本身不会帮你跑程序只是把文件解析好准备好后续分析的原料。真正的运行状态要从State开始。3.2 State某一瞬间的机器快照State表示程序在某一个时刻的状态包括寄存器、内存、文件描述符、符号变量、约束条件等。angr 执行每一个指令都是在改变 State。创建一个初始 State 有三种常见方式entry_state()从程序入口开始执行。blank_state()创建一个“空”状态再从任意地址开始执行。full_init_state()从初始化函数开始模拟更完整的过程。最简单的是entry_state()state p.factory.entry_state()对于大多数 CTF 题目来说从入口开始已经够了。但如果你已经知道关键函数地址也可以先创建blank_state()然后手动把 PC 设置到目标地址甚至手动设置寄存器和内存。这一步有点像调试器里“跳到某条指令”的操作能帮你绕过大量无关代码。State 最核心的一点是它内部的值不一定都是具体数字还可以是符号位向量。符号位向量本质上就是“一个还没确定的变量”比如arg1 state.solver.BVS(arg1, 8 * 32)这表示创建了一个 32 字节的符号变量名字叫arg1。为什么是8 * 32因为一个字节 8 位32 字节就是 256 位刚好可以放一段长度不超过 32 的字符串。符号变量存在的意义是它暂时代表所有可能的输入后续的约束会逐步把它限定到特定的值。3.3 SimulationManager替你批量管理路径程序遇到分支时一个 State 会分裂成多个 State。比如if (x 42)这个分支一个 State 带着约束x 42另一个带着约束x ! 42。这样执行下去State 会越来越多路径会像树一样展开。SimulationManager通常写作simgr就是用来管理这些 State 的。它的核心操作是exploresimgr p.factory.simgr(state) simgr.explore(find0x401234, avoid0x401240)这条命令的意思是把初始 State 放进去让路径不断往前跑凡是走到地址0x401234的路径都算“找到”凡是走到0x401240的路径都算“避开”不再往深走。你可以不用手动管理每个 State只告诉 angr“我要去哪、我不要去哪”剩下的路径筛选交给simgr。这是 angr 比单纯手写符号执行引擎舒服很多的地方。4. 第一个实战脚本从Demo程序里找正确输入4.1 先准备一个尽量简单的Demo为了讲清楚流程我假设本机有一个名叫demo的小程序主函数逻辑大致等价于这样一段 C 代码int main(int argc, char **argv) { const char *s argv[1]; if (s[0] A s[1] B s[2] 3 s[3] 4 s[4] \0) { /* correct 分支 */ return 0; } /* wrong 分支 */ return 1; }注意这里省略了输入长度检查实际写代码时为了安全应该补上。我这里只是为了把“符号执行”这个过程压缩到最简单程序逐个字符比较正确时走一个分支错误时走另一个分支。真实分析中我不会一上来就写 angr 脚本而是先用反汇编工具打开程序找到correct分支和wrong分支的地址。这个步骤非常关键因为explore里的find和avoid需要具体地址没有地址就只能靠猜效率极低。你可以在反汇编工具里盯着主函数看到cmp和jne指令附近的两个目标地址记录下来。4.2 用angr自动搜索路径拿到地址之后写这样一个 Python 脚本import angr import claripy p angr.Project(./demo, auto_load_libsFalse) # 创建符号输入先假设 argv[1] 最多 32 字节 arg1 claripy.BVS(arg1, 8 * 32) # 从入口开始args 里的第一个参数是程序名第二个是我们要解的符号参数 state p.factory.entry_state(args[./demo, arg1]) simgr p.factory.simgr(state) # 这里两个地址必须换成你自己反汇编看到的地址 simgr.explore(find0x401234, avoid0x401240) if simgr.found: found simgr.found[0] answer found.solver.eval(arg1, cast_tobytes) print(正确输入是:, answer.strip(b\x00)) else: print(没找到检查地址或约束是否合理)我来解释一下这段脚本的关键点。第一我们用claripy.BVS(arg1, 8 * 32)创建符号变量长度设为 32 字节足够容纳短字符串。第二entry_state(args[./demo, arg1])是在告诉 angr当程序启动时argv[0]是./demoargv[1]是一个符号变量arg1。第三simgr.explore会自动向前探索凡是命中find地址的状态会被收进found列表。4.3 找到状态之后怎么取答案found是一个 State 列表代表所有满足条件的执行路径。每个 State 里都保存了arg1目前受到的约束。我们可以用found.solver.eval(arg1, cast_tobytes)让求解器从这些约束里求出一个具体值。cast_tobytes是常用的参数它会尝试把约束解转换成字节串。很多时候解出来的字节后面会有多余的\x00所以我习惯再加一个.strip(b\x00)。如果simgr.found是空的先不要急着怀疑 angr 不行大概率是下面几个原因之一地址写错了find和avoid设反了符号输入的字节数不够或者程序入口和你预期不一致。把这些逐个排查一遍通常都能找到问题。有一点要强调上面的地址0x401234、0x401240只是示例。每个人编译出来的二进制地址都不一样绝对不要抄这段代码去跑自己的程序。真实使用中一切以你反汇编看到的地址为准。5. 常见问题与排查技巧实录5.1 加载阶段文件加载不了、路径不对最常见的加载问题就是Project找不到文件。这里要注意./demo是相对路径而 angr 会按当前工作目录去找文件所以命令行所在目录和文件位置必须一致。还有一个经常被忽略的问题程序如果有很多外部库调用auto_load_libsFalse可能导致某些函数地址无法解析。这时候可以临时把auto_load_libs改成True试试或者手动 hook。但不要一上来就改成True否则路径爆炸、加载缓慢都会找上门。我的习惯是先开auto_load_libsFalse跑一遍如果报错说某地址无法执行再去检查那个地址是不是外部函数。如果是再决定是否 hook。5.2 搜索阶段跑得慢、跑飞了、找不到目标explore跑太慢绝大多数时候是路径爆炸。程序里只要有几个循环状态就会迅速膨胀。比如while (i 20)这种循环符号执行会在每一轮分裂子路径数量指数级增长再强的工作站也顶不住。缩短搜索范围的办法有两个方向。第一尽量把find和avoid设置得离关键判断点更近不要从整个程序的入口开始跑太远。第二把循环控制变量先手动设成具体值或者用 angr 的 hook 能力替换掉循环函数。这些都涉及一点逆向功底但确实是绕不开的。在写explore之前我建议先问自己三个问题目标地址到底在哪程序输入是通过命令行参数、文件还是标准输入传进来的有没有明显的外部函数调用这三个问题都确认了再动笔写脚本成功率会高很多。5.3 求解阶段约束对了但结果不是想要的很多时候路径找到了found.solver.eval返回的却不是人话。这不是 angr 解错了而是约束没有限制到位。比如你把arg1设成 32 字节的符号变量但程序只比较前四个字符没有检查第五个字符\0那么求出来的解可能是AB34后面跟一堆随机字节甚至因为符号变量在内存里的长度和字符串语义不完全一致输出的形式会很怪。解决办法是在脚本里手动补约束。你可以从found状态里检查已有约束也可以直接创建状态后自己加found.add_constraints(arg1.get_byte(4) 0) answer found.solver.eval(arg1, cast_tobytes)另外如果求解器解了很久都没反应可能是约束太复杂也可能是符号变量位数太大。把符号变量的长度设成刚好够用不要随便设成超大值能省很多时间。5.4 问题排查速查表症状可能原因排查方向加载文件报错路径错误、格式特殊确认文件路径查看p.loader.main_object找不到任何路径find/avoid 地址不对反汇编核对目标地址找到路径但输出乱码约束不足、长度过大手动补约束缩小符号变量长度搜索极慢路径爆炸、循环过多缩小搜索范围hook 或剪枝API 报错angr 版本不同检查版本调整新旧接口这张表不是万能药但能帮你快速定位问题出在加载、搜索还是求解阶段。我经常见到初学者卡在一个地方反复改参数其实只要判断出“当前是第几个阶段的问题”解决起来就清晰很多。6. 下一步怎么走6.1 先补哪些基础才不会越学越虚angr 是工具不是武功本身。如果你只记 API遇到新题目还是会懵。我个人建议同步补三块基础汇编指令至少要能看懂常见架构里的比较、跳转、栈操作C 语言很多分析目标的原始逻辑就是 C 写的编译原理里的控制流图和数据流分析概念这决定了你能否理解 angr 内部正在做什么。符号执行的门槛不在“安装 angr”而在“你能不能把一个真实问题抽象成符号变量和约束”。这个抽象能力只能靠多看反汇编、多写小 Demo 慢慢练。6.2 个人踩过的坑和习惯我自己踩过最大的坑是过早依赖 angr 去“硬解”结果脚本写了一大堆连问题出在哪都不知道。后面我改成“先静态分析再符号执行”的顺序先把目标函数看明白确认输入从哪里来、正确分支在哪里、有哪些明显的循环和外部调用然后用最短路径写 angr 脚本。另一个习惯是每个脚本里我会先打印几个关键信息比如版本、入口地址、是否找到路径。这些输出看着简单但在排查时价值很大能避免你对着一个空列表发呆。如果你刚开始学建议拿一道非常简单的 CTF reverse 题练手先不追求满分脚本而是跑通一个“输入符号化 - 路径搜索 - 约束求解”的最小闭环。把这个闭环跑熟之后再慢慢加复杂约束和 hook就不会觉得 angr 那么玄学了。这篇 angr 学习笔记1先写到这里后面我会继续记录更多实战思路包括约束求解细节、常用 hook 手法和路径爆炸的几种处理办法。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑