93、漏洞利用开发(三):SEH覆盖、SafeSEH绕过

SEH覆盖,老牌漏洞利用技术了。我记得刚入行那会儿,这几乎是Windows漏洞利用的标配。你想想看,一个精心构造的异常处理链,就能让程序乖乖跳转到你的shellcode。但后来微软加了SafeSEH,这条路就没那么好走了。

今天我们就来聊聊SEH覆盖的原理,以及如何绕过SafeSEH。我个人习惯把这类技术分成两个阶段:先理解它为什么能工作,再思考怎么绕过保护。

SEH是什么?为什么会被利用?

SEH(Structured Exception Handling)是Windows的异常处理机制。说白了,就是程序出错了,系统会去查一个链表,找到对应的处理函数。这个链表存放在栈上,每个节点包含两个指针:下一个异常处理结构的地址,以及当前异常处理函数的地址。

关键点:SEH链存储在栈上,而栈是我们可以控制的。如果我们能覆盖掉异常处理函数的指针,当异常发生时,程序就会执行我们指定的地址。

典型的SEH覆盖流程是这样的:

  1. 构造一个超长缓冲区,覆盖掉栈上的SEH结构
  2. 把异常处理函数指针改成指向我们的shellcode或跳板指令
  3. 触发一个异常(比如访问非法内存),让系统去调用被篡改的异常处理函数

我在项目中遇到过好几次,新手最容易犯的错误是:覆盖了SEH但忘了触发异常。你想想看,如果程序正常执行完,根本不会走异常处理流程,你覆盖得再漂亮也没用。

经典SEH覆盖示例

来看一个简单的漏洞程序:

// vuln.c
#include <windows.h>
#include <stdio.h>

void vulnerable(char *input) {
    char buf[256];
    strcpy(buf, input);  // 没有边界检查!
}

int main() {
    char buffer[512];
    printf("Enter input: ");
    gets(buffer);
    vulnerable(buffer);
    return 0;
}

这个程序用strcpy复制用户输入,没有检查长度。当我们输入超过256字节的数据时,就会覆盖栈上的返回地址和SEH链。

在Windows XP SP2之前,我们只需要覆盖返回地址就行了。但之后微软加了GS编译选项和SafeSEH,情况就变了。

SafeSEH的原理

SafeSEH是微软在Visual Studio 2003引入的缓解措施。它的核心思想是:在程序加载时,把所有合法的异常处理函数地址记录在一张表中。当异常发生时,系统会检查要调用的处理函数是否在这张表里。

如果不在,系统就直接终止程序,不会执行你的shellcode。

避坑指南:我曾经以为SafeSEH是万能的,直到发现它只保护了PEB中注册的异常处理函数。如果你能找到一个不在SafeSEH保护范围内的模块,或者利用堆上的异常处理,还是可以绕过的。

绕过SafeSEH的几种思路

绕过SafeSEH,说白了就是让系统调用一个不在黑名单里的地址。我总结了几种常见手法:

绕过方式 原理 适用条件
模块内跳板 在非SafeSEH模块中找pop pop ret指令序列 目标程序加载了未启用SafeSEH的DLL
堆喷射+SEH 在堆上布置大量NOP+shellcode,SEH指向堆地址 堆地址可预测,且DEP未开启
伪造SEH链 构造一个指向栈上shellcode的SEH节点 需要精确控制栈布局
利用异常处理嵌套 通过多层异常处理绕过检查 程序有复杂的异常处理逻辑

我个人最常用的是第一种——模块内跳板。为什么呢?因为很多老旧DLL没有启用SafeSEH,比如一些第三方库或者系统自带的旧版本DLL。你只需要找到这些模块,在里面搜索pop pop ret指令序列就行了。

实战:寻找跳板指令

pop pop ret是SEH利用中的经典跳板。为什么是这三个指令?因为异常处理函数被调用时,栈上保存了异常记录结构。执行两次pop可以跳过这个结构,然后ret会从栈上取出下一个地址作为返回地址——这个地址就是我们覆盖的shellcode地址。

在未启用SafeSEH的模块中搜索pop pop ret

# 使用Immunity Debugger的搜索功能
!mona find -s "\x5f\x5e\xc3" -m non_safeseh.dll
# 或者用Windbg
s 0 L?80000000 5f 5e c3

找到地址后,我们把它填入SEH的处理函数指针位置。当异常触发时,程序会先执行pop pop ret,然后跳转到我们的shellcode。

注意:不同版本的Windows和编译器,pop pop ret的字节码可能不同。比如32位和64位系统就不一样。我建议你在目标系统上实际测试一下,不要直接拿网上的地址用。

绕过SafeSEH的完整流程

嗯,这里要注意一个细节:SafeSEH检查的是异常处理函数的地址是否在合法表中。但如果我们能控制异常处理函数指针指向一个已经存在于合法表中的地址,那就能绕过检查。

具体怎么做呢?我举个例子:

  1. 找到目标程序加载的一个DLL,它启用了SafeSEH
  2. 在这个DLL中,找到一个call [esp+offset]jmp [reg]之类的指令
  3. 这个指令的地址在SafeSEH表中是合法的
  4. 把SEH处理函数指针改成这个地址
  5. 当异常发生时,系统检查通过,执行这个指令,然后间接跳转到我们的shellcode

说白了,就是借刀杀人。用合法的地址去执行非法的代码。

知识体系图

下面这张图展示了SEH覆盖和SafeSEH绕过的核心逻辑:

SEH覆盖与SafeSEH绕过知识体系 缓冲区溢出 → 覆盖SEH链 触发异常 → 系统检查SEH处理函数 无SafeSEH保护 直接执行shellcode 有SafeSEH保护 需要绕过 漏洞利用成功 需要绕过SafeSEH 模块内跳板 | 堆喷射 | 伪造SEH链

实际项目中的经验

我在做漏洞利用开发时,遇到过最头疼的情况是:目标程序启用了SafeSEH,而且所有加载的模块都启用了SafeSEH。这时候怎么办?

有两个思路:

  • 找未启用SafeSEH的模块:用工具扫描所有加载的DLL,看有没有漏网之鱼。我记得有一次,目标程序加载了一个旧版的COM组件,那个组件就没开SafeSEH。
  • 利用堆地址:如果DEP没开,可以在堆上布置shellcode,然后让SEH指向堆地址。虽然堆地址不如栈地址好预测,但通过堆喷射可以大大提高成功率。

个人经验:我建议你在做SEH利用时,先检查目标程序加载的所有模块。用!mona modules命令可以快速查看哪些模块启用了SafeSEH。找到那个没开保护的模块,你的工作就完成了一半。

总结

SEH覆盖是漏洞利用中的经典技术,虽然SafeSEH增加了难度,但并非不可绕过。关键在于理解异常处理机制的本质,以及找到保护措施的盲区。我个人觉得,做漏洞利用开发最重要的不是记住多少技巧,而是理解底层原理。原理通了,绕过方法自然就出来了。

好了,这一讲就到这里。记住,实战中多测试、多验证,纸上谈兵是搞不定漏洞利用的。


公众号:蓝海资料掘金营,微信deep3321