ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

符号执行技术解析:从核心原理到KLEE、angr实战应用

符号执行技术解析:从核心原理到KLEE、angr实战应用 1. 从“黑盒”到“白盒”为什么我们需要符号执行在软件开发和安全测试的日常里我们最熟悉的测试方法是什么单元测试、集成测试、模糊测试Fuzzing。这些方法本质上都是“黑盒”或“灰盒”测试。我们准备一堆输入数据扔给程序然后观察输出是否符合预期或者程序会不会崩溃。这个过程就像在黑暗中摸索效率很大程度上取决于测试用例的质量和运气。你可能会覆盖到80%的常见路径但那些隐藏在复杂条件分支、特定输入组合下的“犄角旮旯”里的Bug往往就成了漏网之鱼。符号执行Symbolic Execution提供了一种截然不同的思路。它不关心具体的输入值而是把输入当作一个“符号”比如一个叫x的未知数。然后它沿着程序的每一条可能的执行路径像解数学题一样去推导程序的行为。它会记录下程序执行过程中所有影响路径走向的条件比如if (x 10)并把这些条件收集起来形成一个“路径约束”。最终它会问一个问题存在什么样的具体输入值可以满足这条路径的约束条件从而让程序走到这里如果存在它就能通过求解器Solver自动生成一个具体的测试用例。举个例子你有一个函数func(int a, int b)里面有几个嵌套的if-else。传统的测试你需要绞尽脑汁想(a5, b3)、(a-1, b100)等各种组合。而符号执行工具会直接说“要走到最里面那个printf(“critical bug!”)分支需要满足a 100 b 0 a b 42。我这里有一个解a105, b-63。” 它直接把那个能触发隐藏分支的“神奇”输入给你构造出来了。这就是符号执行的核心魅力路径覆盖的完备性与测试用例的自动生成。它特别擅长处理那些逻辑复杂、分支繁多的代码比如协议解析器、文件格式处理器、加密解密函数等这些正是传统测试和模糊测试难以深入覆盖的领域。对于安全研究员来说它是挖掘深层漏洞如整数溢出、缓冲区越界、逻辑错误的利器对于开发者而言它是提升代码质量、验证程序健壮性的强大工具。然而“入坑”这个词用得非常贴切。符号执行并非银弹它有着著名的“路径爆炸”问题对复杂程序的分析可能永远也跑不完。工具链的搭建、环境配置、对底层原理的理解都构成了不低的学习门槛。但一旦掌握它为你打开的将是一扇通往程序分析新世界的大门。接下来我们就一步步来搭建这个新世界的脚手架。2. 核心概念拆解符号、执行与约束求解要玩转符号执行必须吃透它的三个核心概念符号、执行引擎和约束求解器。这就像木匠的锯、刨、凿各有各的用处组合起来才能干活。2.1 符号程序输入的“代数变量”在符号执行中程序的输入如函数的参数、读取的文件、网络数据包不再是一个具体的值如数字5或字符串“hello”而被提升为一个符号。你可以把它理解为一个代数中的变量比如x、y、input_buf。当程序读取这个符号时它并不知道x是5还是10它只知道存在这么一个量。程序后续所有基于这个输入的计算都会变成基于这个符号的符号表达式。例如如果程序执行y x 10那么y的值就不是一个具体数字而是一个符号表达式x 10。如果接着执行if (y 20)那么条件就变成了符号表达式(x 10) 20。这种抽象的力量在于它同时代表了所有可能的具体值。一次符号执行理论上可以探索所有由这些符号化输入所决定的执行路径。2.2 符号执行引擎沿着所有路径“分身”前进这是符号执行的大脑。它模拟程序的执行但处理的是符号和符号表达式而不是具体值。它的工作方式可以想象成在每一个条件分支点如if、switch、循环执行引擎都会“分裂”成两个或多个副本每个副本代表一条可能的路径并记录下选择该路径所需满足的条件即路径约束。假设我们有如下代码片段和符号输入xint func(int x) { int y x * 2; // y 2 * x (符号表达式) if (y 10) { // 路径约束: (2*x) 10 // 路径 A if (x 5) { // 路径约束: (2*x)10 x5 // 路径 A1 return 1; } else { // 路径 A2 return 2; } } else { // 路径 B return 3; } }符号执行引擎会这样工作从入口开始y被赋值为符号表达式2*x。遇到第一个if (y 10)。引擎分裂路径A假设条件为真添加约束(2*x) 10到其路径约束集合。路径B假设条件为假添加约束(2*x) 10到其路径约束集合。在路径A中继续执行遇到第二个if (x 5)。引擎再次分裂路径A1添加约束(2*x)10 x5。路径A2添加约束(2*x)10 x5。最终引擎会探索出四条理论路径A1, A2, B以及可能因约束无解而被剪枝的路径并为每条路径生成一组路径约束。2.3 约束求解器从抽象约束到具体测试用例路径约束是一组关于符号的数学逻辑表达式如(2*x)10 x5。但我们需要的是能真正喂给程序运行的具体输入值。这时就需要约束求解器出场了比如 Z3、STP、Boolector。求解器的任务就是回答是否存在一组具体的值满足这组路径约束如果存在它就找出一个解通常是一个解如果不存在约束互相矛盾如x10 x5则说明这条路径在现实中是不可达的可以被安全地忽略。接上例对于路径A1的约束(2*x)10 x5化简x 5 x 5。这显然矛盾无解。这意味着路径A1是不可达的通过符号执行我们自动发现了一段“死代码”。这是它强大的代码理解能力体现。对于路径A2的约束(2*x)10 x5化简为x 5 x 5即x 5。求解器可以轻松给出一个解例如x 6。于是我们就自动生成了一个能覆盖路径A2的测试用例func(6)。对于路径B的约束(2*x) 10即x 5。求解器可以给出x 3作为测试用例。通过这三者的配合符号执行系统实现了“分析路径 - 收集约束 - 求解用例”的自动化闭环。理解了这个闭环你就掌握了符号执行最基本的工作原理。接下来我们看看如何选择实现这些原理的工具。3. 工具链选型从研究原型到工业级应用符号执行领域经过多年发展已经形成了多层次、多语言支持的工具生态。选择合适的起点工具能让你事半功倍。我们可以将这些工具大致分为三类经典研究原型、活跃开源框架、以及集成化商业/高级工具。3.1 经典入门之选KLEE如果你是第一次接触符号执行KLEE几乎是无可争议的起点。它基于 LLVM 编译器基础设施这意味着它支持任何能编译到 LLVM IR 的语言主要是 C/C。KLEE 并非一个“黑盒”工具它需要你对程序进行一些简单的“插桩”准备。为什么从 KLEE 开始成熟稳定诞生于斯坦福大学有大量论文、教程和案例支撑社区遇到的大部分坑都有记载。设计经典它的工作流程源码 - LLVM IR - 符号执行是许多后续工具参考的范式。功能全面支持内存模型、文件系统、环境变量等外部状态的符号化能处理比较真实的程序。教育意义强通过使用 KLEE你能最直观地理解符号执行引擎、约束求解器它默认集成 STP是如何协同工作的。它的工作流程通常如下用clang -emit-llvm将你的 C 程序编译成 LLVM 字节码文件.bc。编写一个简单的“驱动”程序harness用 KLEE 提供的特殊函数如klee_make_symbolic来标记哪些输入应该是符号化的。用klee命令执行这个.bc文件KLEE 引擎开始探索路径。分析 KLEE 输出的报告它会在klee-out-*目录下为每条可达路径生成具体的测试用例文件.ktest你可以用ktest-tool来查看内容。一个简单的例子// test.c #include klee/klee.h int main() { int x; klee_make_symbolic(x, sizeof(x), “x”); // 告诉KLEEx是符号 if (x 100) { klee_assert(0); // 如果x100触发一个“错误” } return 0; }编译并运行 KLEE 后它会专门生成一个使x 100的测试用例帮助你发现断言失败的条件。这正是漏洞挖掘的雏形。3.2 现代化活跃框架angr如果你主要关注二进制程序分析尤其是逆向工程、漏洞挖掘或者你的目标程序没有源码那么angr是你的首选。它是一个基于 Python 的二进制分析平台符号执行是其核心功能之一。angr 的优势在于无需源码直接对二进制文件如 ELF、PE进行分析这对安全研究至关重要。高度可编程提供了极其丰富的 API你可以像写 Python 脚本一样精细地控制符号执行的策略、钩住hook特定函数、动态修改执行状态。集成化环境除了符号执行还集成了 CFG控制流图恢复、反编译、污点分析等功能形成一个强大的分析套件。活跃的社区在 CTF夺旗赛和安全研究社区非常流行有大量现成的脚本和解决方案可以参考。angr 的典型工作流import angr # 加载二进制程序 proj angr.Project(‘./target_binary’, auto_load_libsFalse) # 设置符号执行的起点通常是main函数 state proj.factory.entry_state() # 创建符号执行管理器 simgr proj.factory.simulation_manager(state) # 设置探索目标例如找到某个特定地址的输出 simgr.explore(find0x400844) # 假设这是成功输出的地址 # 如果找到了 if simgr.found: found_state simgr.found[0] # 求解出到达该状态所需的输入 solution found_state.posix.dumps(0) # 获取标准输入的解 print(“Found input:”, solution)angr 的强大也带来了较高的学习曲线你需要理解 VEX IR它的中间语言、状态State模型等概念。但对于复杂的、无源码的二进制程序分析它是目前最强大的开源工具。3.3 其他工具与选型考量Triton另一个强大的二进制程序动态分析框架符号执行能力突出API 设计更底层适合需要极度定制化的高级用户。S2E (Selective Symbolic Execution)一个全系统符号执行平台可以符号化整个虚拟机包括操作系统内核用于分析设备驱动或内核漏洞复杂度极高更像一个研究系统。商用工具如 KLEE 的商业版、某些 SAST 工具中的模块提供了更好的图形界面、性能优化和工程化支持但通常价格不菲。选型建议初学者有 C 源码从KLEE开始理解核心概念。安全研究员分析二进制文件主攻angr。追求极致定制化分析研究Triton。学术研究或分析系统软件了解S2E。选定工具后真正的“坑”才刚刚开始。配置环境、理解工具对目标程序的要求是实践的第一步也是劝退很多人的一步。4. 环境搭建与第一个符号化程序让我们以最经典的KLEE为例手把手走通从安装到运行第一个符号化程序的完整流程。这个过程在 Linux 环境下进行最为顺畅推荐 Ubuntu 20.04/22.04。4.1 KLEE 环境搭建依赖与编译KLEE 的安装方式有多种这里介绍通过源码编译的方式虽然步骤稍多但最有利于理解其组成和后续排错。步骤 1安装基础依赖sudo apt-get update sudo apt-get install -y build-essential curl libcap-dev libncurses5-dev python3-minimal python3-pip unzip cmake步骤 2安装 LLVMKLEE 的基石KLEE 对 LLVM 版本有特定要求例如 KLEE 2.3 对应 LLVM 11.x。最好查看 KLEE 官方文档的推荐版本。# 以 LLVM 11 为例 wget https://github.com/llvm/llvm-project/releases/download/llvmorg-11.1.0/llvm-11.1.0.src.tar.xz tar -xf llvm-11.1.0.src.tar.xz cd llvm-11.1.0.src mkdir build cd build cmake -DCMAKE_BUILD_TYPERelease -DLLVM_ENABLE_PROJECTS“clang;clang-tools-extra” -DLLVM_TARGETS_TO_BUILD“X86” -DLLVM_ENABLE_ASSERTIONSOn .. make -j$(nproc) # 利用多核编译耗时较长 sudo make install注意编译 LLVM 是整个过程最耗时的一步可能需要半小时以上取决于机器性能。确保磁盘空间充足约10-20GB。步骤 3安装约束求解器STPKLEE 默认使用 STP 作为求解器。git clone https://github.com/stp/stp.git cd stp mkdir build cd build cmake .. make -j$(nproc) sudo make install步骤 4编译安装 KLEEgit clone https://github.com/klee/klee.git cd klee mkdir build cd build # 注意指定 LLVM 的安装路径如果安装在默认的 /usr/local则如下配置 cmake -DENABLE_SOLVER_STPON -DENABLE_POSIX_RUNTIMEON -DLLVM_CONFIG_BINARY/usr/local/bin/llvm-config .. make -j$(nproc) # 不建议直接 make install因为可能污染系统。我们后续通过 build 目录下的 bin 使用即可。至此KLEE 的主要组件就编译好了。你可以通过./klee --help来验证。4.2 准备第一个符号化程序我们写一个简单的、有分支的程序并用 KLEE 来探索。程序源码 (simple.c):#include klee/klee.h int check_password(int pwd) { if (pwd 123456) { return 1; // 密码正确 } else { return 0; // 密码错误 } } int main() { int input; // 关键声明 input 为符号变量。参数分别是变量地址大小名字仅用于调试 klee_make_symbolic(input, sizeof(input), “input”); int result check_password(input); // 我们可以让 KLEE 断言一个不可能的情况来寻找触发该情况的输入 // 例如我们“错误地”断言 result 永远为 0KLEE 就会寻找使 result 为 1 的输入 klee_assume(result 0); // 注意klee_assume 是假设条件为真并在此基础上继续探索。 // 更常用的可能是 klee_assert(result 1)当断言失败时 KLEE 会报告错误。 // 这里我们换一种方式用 if 和 klee_assert 来演示。 // 重写 main 函数逻辑 // if (result 1) { // klee_assert(0); // 触发一个错误报告 // } // return 0; }让我们修改成一个更典型的测试用例我们想找到那个“正确密码”。修改后的simple.c:#include klee/klee.h int check_password(int pwd) { if (pwd 123456) { return 1; } else { return 0; } } int main() { int input; klee_make_symbolic(input, sizeof(input), “input”); int result check_password(input); if (result 1) { klee_assert(0); // 当密码正确时我们“断言失败”让 KLEE 标记这条路径为有趣 } return 0; }4.3 编译、运行与分析结果步骤 1编译成 LLVM 字节码使用clang必须是和你编译的 KLEE 相匹配的版本来编译。# 假设你的 clang 在 /usr/local/bin /usr/local/bin/clang -I /path/to/klee_src/include -emit-llvm -c -g -O0 -Xclang -disable-O0-optnone simple.c -o simple.bc-I指定 KLEE 头文件路径就是之前 git clone 的 klee 源码目录下的include/。-emit-llvm输出 LLVM IR。-c只编译不链接。-g包含调试信息方便 KLEE 输出更易读的信息。-O0 -Xclang -disable-O0-optnone禁用优化但保留某些分析所需的信息。步骤 2使用 KLEE 执行符号执行/path/to/klee_build/bin/klee --libcuclibc --posix-runtime simple.bc--libcuclibc使用 KLEE 内置的简化 C 库模型因为我们的程序调用了klee_assert。--posix-runtime启用 POSIX 运行时模型以支持更多系统调用。运行后KLEE 会开始探索。对于这个简单程序它会很快结束。输出会显示探索了多少条路径以及是否发现了断言失败。步骤 3解读输出与生成测试用例KLEE 的输出目录通常是klee-out-*。我们查看最新的一个ls klee-last/你应该能看到一些.ktest文件。这些就是 KLEE 生成的测试用例数据。我们使用ktest-tool来查看触发klee_assert(0)即密码正确的那个用例/path/to/klee_build/bin/ktest-tool klee-last/test000001.ktest假设test000001.ktest是对应断言失败的那个文件。输出中你会看到object ‘input’的值它应该就是123456或者其内存表示的字节。恭喜你你已经用符号执行自动“破解”了这个简单的密码检查程序这个过程虽然简单却完整展示了符号执行的威力自动探索路径并生成触发特定分支包括错误分支的输入。然而当你把目标从几十行的小程序换成成千上万行的真实项目时挑战才真正开始。5. 直面核心挑战路径爆炸与性能调优当你兴冲冲地将 KLEE 或 angr 对准一个稍具规模的真实程序时很可能遇到两种情况要么程序很快跑完但只探索了皮毛比如只执行了初始化代码要么程序一直运行内存占用不断攀升直到你不得不终止它。这就是符号执行著名的“路径爆炸”问题也是其从学术界走向工业应用的最大障碍。5.1 理解路径爆炸为什么它会发生程序中的路径数量理论上与分支点的数量成指数关系。一个包含n个独立条件分支的程序就可能产生2^n条路径。循环则更可怕一个循环m次的循环体如果内部有分支路径数可能是组合爆炸。符号执行引擎试图探索所有这些路径其状态空间会迅速膨胀到无法管理。例如一个简单的网络报文解析函数可能对每个字节的每个比特位都有不同的处理逻辑其路径数对于符号执行来说就是天文数字。5.2 实战调优策略与“爆炸”共舞我们无法彻底消除路径爆炸但可以通过一系列策略来管理它将分析引导到我们最关心的代码区域。策略一限制分析深度与资源这是最直接的方法。给符号执行引擎戴上“缰绳”。深度限制 (--max-depthin angr,--max-timein KLEE)限制从入口点开始执行的指令条数或符号执行的时间。适用于只想分析程序开头部分如初始化、参数解析的场景。内存与时间限制设定硬性上限防止工具跑飞。例如在 angr 中可以通过simgr.run()的n参数限制活跃状态数量或监控内存使用。状态合并 (State Merging)当两条路径的状态非常相似时例如只有某个中间变量的值不同但后续执行不再依赖该变量可以考虑将它们合并以减少状态总数。KLEE 和 angr 都支持某种形式的状态合并但这需要谨慎使用因为过度合并可能掩盖重要的程序行为差异。策略二选择性符号化 (Selective Symbolization)并非所有输入都需要符号化。将分析资源集中在最关键的数据上。只符号化感兴趣的部分例如在分析一个图像处理器的漏洞时可能只将文件头部的某些字段设为符号而文件体数据设为具体值。在 KLEE 中这意味着只在klee_make_symbolic中标记关键变量。使用具体值引导 (Concolic Execution)混合具体执行与符号执行。先使用一组随机或特定的具体输入运行程序记录下走过的路径及其约束。然后通过取反路径上的某个条件约束让求解器生成一个能走向新分支的输入。接着用这个新输入再次具体执行如此迭代。这种方法能有效探索与已知路径相邻的新区域而不是盲目地全局搜索。angr 的ExplorationTechnique中可以实现类似策略。策略三智能搜索策略 (Search Strategies)符号执行引擎在众多待探索的路径状态中先探索哪一条默认策略如深度优先 DFS、广度优先 BFS可能效率很低。我们需要更聪明的策略覆盖导向 (Coverage-guided)优先探索那些能执行到新代码即之前从未执行过的指令或基本块的路径。这模仿了 AFL 等模糊测试器的思想能快速增加代码覆盖率。angr 的Explorer组件支持这种策略。距离导向 (Distance-guided)如果我们有一个目标地址例如一个已知存在漏洞的函数调用点可以优先探索在控制流图上距离目标更近的路径。angr 的BFSSearcher结合 CFG 可以粗略实现更精细的可以使用Dijkstra等图搜索算法。符号值启发式优先探索那些涉及符号值参与更多约束条件的路径或者约束条件更“复杂”的路径这些路径可能通向更有趣、更隐蔽的程序逻辑。策略四函数摘要与模型 (Function Summaries/Models)对于复杂的库函数如malloc,strcpy,printf或系统调用完全符号化其内部实现是不必要且低效的。我们可以为其创建摘要或模型。摘要记录函数对符号状态的影响如它读取了哪些符号内存修改了哪些内存返回了什么符号表达式而不真正执行其每一行代码。这需要深厚的功底。模型用一个简化的、等价的实现来替换原函数。例如为strlen提供一个模型它遍历符号化字符串直到遇到符号化的 ‘\0’并返回一个符号表达式表示的长度。KLEE 和 angr 都支持用户自定义函数模型angr 中称为Hook。# 在 angr 中 Hook 一个函数 proj.hook(0x401230) # 假设这是目标函数地址 def custom_puts(state): # 模拟 puts 行为例如直接返回不做实际输出 # 操作 state 中的寄存器或内存 state.regs.rax 1 # 假设返回成功合理地对标准库和复杂第三方库函数进行建模能极大提升分析速度和稳定性。策略五并行与分布式执行对于超大型程序单机资源可能不足。将符号执行任务分布式化是一个研究方向。可以将不同的路径探索任务分发到多台机器上执行。KLEE 有实验性的分布式支持但配置复杂。更常见的做法是手动将程序分割成不同的功能模块进行独立分析。在实际操作中这些策略需要组合使用并经过反复试验和调优。没有放之四海而皆准的配置你需要像调优数据库查询一样根据目标程序的特点来调整你的符号执行“查询计划”。接下来我们看看如何将这套方法论应用到更贴近实战的场景中。6. 实战案例挖掘一个简单的缓冲区溢出漏洞理论说再多不如动手实践。让我们用一个经典的栈缓冲区溢出漏洞程序作为目标尝试使用 angr 来自动化地生成一个能触发崩溃的输入即 Exploit 中的 PoC。这个案例将串联起工具使用、策略选择和结果分析的完整流程。6.1 目标程序分析我们有一个简单的 32 位 ELF 程序vuln其源码可能如下// vuln.c #include stdio.h #include string.h void vulnerable_function(char *input) { char buffer[64]; strcpy(buffer, input); // 经典的栈溢出漏洞点 } int main(int argc, char **argv) { if (argc 1) { vulnerable_function(argv[1]); printf(“Input processed.\n”); } else { printf(“Please provide an argument.\n”); } return 0; }用gcc -m32 -fno-stack-protector -z execstack -o vuln vuln.c编译禁用栈保护便于演示。我们的目标是不进行人工逆向仅通过 angr 的符号执行自动找到一个输入字符串使其在strcpy时覆盖返回地址导致程序控制流被劫持通常表现为段错误 Segmentation Fault。6.2 angr 脚本编写定义目标与约束我们并不需要 angr 精确地覆盖返回地址并跳转到 shellcode那太复杂了。一个更实际的目标是让程序执行到strcpy之后但还没正常返回main之前就发生崩溃。我们可以把目标地址设定在vuln函数末尾之后或者直接寻找导致非法内存访问的状态。更精准的方法是我们让 angr 探索程序并检查在执行过程中是否发生了“不受约束的指令指针跳转”即程序计数器跳转到了一个符号化的地址这通常意味着返回地址被符号化数据覆盖了。angr 的SimProcedures和状态检查可以帮我们做到这一点。编写 angr 脚本 (exploit_vuln.py):import angr import claripy def main(): # 加载二进制文件 proj angr.Project(‘./vuln’, auto_load_libsFalse) # 不自动加载动态库简化分析 # 创建符号化输入。我们不知道多长会溢出所以创建一个较长的符号化比特向量。 # 假设我们生成一个 200 字节的符号化字符串。 input_len 200 # claripy.BVS 创建符号化比特向量。‘input’是名字每个字节8位共 input_len 个字节。 symbolic_input claripy.BVS(‘input’, 8 * input_len) # 8 bits per byte # 设置初始状态从程序的 main 函数开始并设置命令行参数。 # argv 是一个列表[程序名 我们的符号化输入] state proj.factory.entry_state(args[‘./vuln’, symbolic_input]) # 我们添加一个约束输入的字符串应以换行符或空字符结尾吗对于strcpy不需要它会一直复制直到遇到源字符串的‘\0’。 # 但为了更现实我们可以约束最后一个字节为‘\0’或者不约束让求解器自由发挥。 # 这里我们不额外约束让 angr 去探索所有可能。 # 创建模拟管理器 simgr proj.factory.simulation_manager(state) # 定义我们的“目标”寻找一个崩溃的状态。 # angr 内置了检测常见漏洞的机制我们可以使用 ‘explore’ 的 ‘avoid’ 和 ‘find’ 参数或者手动检查。 # 一个简单策略我们让 angr 自由探索然后过滤出那些因为非法内存访问而错误终止的状态。 def is_crash_state(state): # 检查状态是否以错误结束例如发生了内存访问违规 return state.has_crashed or state.addr in state.history.jump_targets and not proj.loader.find_object_containing(state.addr) # 使用 explore 的 find 回调函数 def find_crash(state): return is_crash_state(state) # 运行探索设置一定的步数限制防止无限循环 simgr.explore(step_funclambda lsm: lsm, findfind_crash, num_find1) # 检查结果 if len(simgr.found) 0: print(“[] Found a potential crash state!”) found_state simgr.found[0] # 获取导致崩溃的输入 # 我们需要获取 argv[1] 的值。它存储在内存中。 # 更直接的方式从标准输入读取的符号化数据这里我们是从命令行参数传入的。 # 在初始状态设置时符号化输入被放入了 argv[1] 对应的内存区域。 # 我们可以通过求解约束来获取一个具体值。 solution found_state.solver.eval(symbolic_input, cast_tobytes) print(f“[] Exploit input (hex): {solution.hex()}“) print(f”[] Exploit input (string,可能含不可打印字符): {repr(solution)}“) # 验证用这个输入实际运行程序看是否崩溃 import subprocess try: # 注意需要处理可能包含空字节的输入这里简单用原始字节传递 subprocess.run([‘./vuln’, solution], checkTrue, timeout2) print(“[-] Program did not crash with this input. Might be a false positive.”) except subprocess.CalledProcessError as e: print(“[] Verified! Program crashed with exit code”, e.returncode) except subprocess.TimeoutExpired: print(“[?] Program timed out.”) else: print(“[-] Could not find a crash state within the exploration limits.”) # 可以尝试检查 ‘deadended’ 或 ‘errored’ 状态集合 if len(simgr.errored) 0: print(“[-] Some states encountered errors during execution.”) if __name__ “__main__”: main()6.3 运行分析与结果解读运行脚本python3 exploit_vuln.py。可能的过程与输出angr 开始符号执行探索从main开始的所有路径。由于输入是符号化的strcpy的行为也变成了符号化它将符号化数据复制到固定大小的缓冲区buffer上。angr 的内存模型会跟踪这次复制。当复制的符号化数据长度超过buffer的大小64字节时angr 会模拟写入栈上buffer之后的内存区域包括保存的基址指针EBP和返回地址。当函数vulnerable_function尝试通过ret指令返回时它会从栈上弹出返回地址。如果这个地址现在是符号化的因为被我们的输入覆盖了那么下一条指令的地址就是符号化的。angr 会认为这是一个“符号化指令指针”Symbolic IP这通常被视为一个不可继续执行的状态因为不知道具体跳到哪里该状态可能被标记为errored或满足我们的is_crash_state条件。脚本在simgr.errored或通过自定义检查找到这样的状态然后求解出导致该状态的具体输入值。输出的solution可能是一长串非打印字符的字节序列。你可以用hexdump -C查看其内容很可能会发现其中一段对应着覆盖返回地址的部分在小端序 32 位系统上通常是第 644 到 648 字节的位置被覆盖成了某个非法地址。6.4 案例总结与技巧这个案例展示了符号执行在漏洞挖掘中的基本范式目标定义明确你要找什么崩溃、到达特定地址、触发特定条件。符号化输入确定哪些程序输入需要被符号化命令行参数、文件、网络数据。路径探索与状态检查驱动符号执行引擎运行并检查状态是否满足目标。约束求解与用例生成从目标状态反推出能到达该状态的具体输入。实战技巧起点选择不一定从entry_state开始。如果程序有复杂的初始化可以直接从漏洞函数附近开始proj.factory.blank_state(addr0x804844)并手动设置好上下文寄存器、内存这能节省大量分析时间。避免环境依赖对于文件操作、网络连接等使用 angr 的Hook或 SimProcedure 来模拟返回符号化或确定值避免分析陷入操作系统细节。处理外部函数对libc函数进行合理的建模hook至关重要。angr 已经内置了许多常用库函数的 SimProcedure如strcpy,malloc它们比实际执行库代码快得多且行为更确定。耐心与迭代第一次运行往往不成功。可能需要调整搜索策略、添加/移除约束、修改状态检查函数。这是一个反复试验的过程。通过这个案例你应该能感受到符号执行并非全自动的“漏洞魔术棒”而是一个需要安全研究员精心设计和引导的程序分析框架。它的价值在于能处理复杂的路径条件自动生成触发深层逻辑的输入这是传统模糊测试难以做到的。然而将其成功应用于大型、复杂的真实世界软件仍然充满挑战这需要我们深入理解工具的脾性和目标程序的结构。
返回列表