SEH
windows异常处理程序,存储在栈中
SEH链
SEH链实际上就是一个链表,其基本单位是一个个的节点
每个节点都是由一个后向指针(指针域)和一个函数指针(值域)组成
1 | typedef struct _EXCEPTION_REGISTRATION_RECORD { |
Next不必多说,Handler是一个有固定格式的函数,其函数接口长这样
1 | EXCEPTION_DISPOSITION myHandler(//返回值为枚举类型,表明本函数处理结束后程序的走向 |
这四个参数会由OS传给异常处理函数
1 | typedef struct _EXCEPTION_RECORD { |
返回值是一个枚举类型,在excpt.h中可以找到其定义
1 | // Exception disposition return values |
SEH链画在图上相当于
最后一个节点的next指向FFFFFFFF,即-1,表明SEH链结束
访问进程SEH链
TEB+0x00存放的是NtTib结构体基地址
这个NtTib长啥样呢?
1 | typedef struct _NT_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 | PUSH &myHandler ;seh节点的函数地址压栈 |
当前节点成为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 | start() |
在main之前___tmainCRTStartup
会首先调用__cinit
进行注册,然后才会调用main
也就是说,这是运行库注册的
注册新的seh节点
注意此时只是注册,其中的异常处理函数也是一个回调函数,不会立刻执行,需要确实发现异常的时候才会执行
main函数首先插入了一个seh节点,其异常处理函数在0x40105A处,这个函数干了啥呢?
1 | .text:0040105A loc_40105A: ; DATA XREF: _main↑o |
这个函数检查了当前是否处于调试状态,根据该状态将不同的函数地址,写到esi+0xB8h指向的地方
这是个什么地方呢?Context.Eip,干啥用的呢?留作后话
当我们的seh节点注册完毕之后
1 | esp=0019FF28 |
此时ExceptionList已经指向0x19FF28了,栈中观察这个位置
后向指针也已经指向了刚才的0x40105A节点.现在的SEH链长这样
1 | ExceptionList@0x386000-> |
如果发生异常,首先会调用这个链最头上的myNode@0x19FF28
中的异常处理函数
od已经自动识别出seh链长啥样了,od->查看->seh链
引发异常
在注册完我们的seh节点之后,有两句故意找茬的指令,按理说c语言是敲不出这种指令的,实际上是用内联汇编写的
1 | .text:00401017 xor eax, eax |
把1写到0x0位置,显然往未注册过的内存写东西会寄,触发EXCEPTION_ACCESS_VIOLATION(0xc0000005)异常
执行异常处理函数
触发异常之后,程序会跳转到第一个seh节点中注册的异常处理函数,期间经过了很多库函数调用,导致栈顶移动了很远的距离
从0x19FF28到0x19F320一共0xc08个字节
此时控制已经转移到myHandler@0x40105A,这就是main函数一开始注册的seh节点中的异常处理函数
此时栈帧的情况
myHandler@0x40105A的返回地址是NTDLL库函数
返回地址往上的4个双字就是myHandler的参数了
1 | EXCEPTION_DISPOSITION myHandler(//返回值为枚举类型,表明本函数处理结束后程序的走向 |
参数 | 参数地址 | 参数指向 |
---|---|---|
pRecord | 0x19F324 | 0x19F420 |
pFrame | 0x19F328 | 0x19FF28 |
Context | 0x19F32C | 0x19F470 |
pValue | 0x19F330 | 0x19F3AC |
参数指向的地址都在栈中,看来抬栈0xc08个字节其中的作用就有准备参数
Record@0x19F420
Record@0x19F420长这样
1 | typedef struct _EXCEPTION_RECORD {//0x50个字节 |
偏移 | 成员 | 值 | 意义 |
---|---|---|---|
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 | typedef struct _EXCEPTION_REGISTRATION_RECORD {//8字节 |
Context@0x19F470
1 | typedef struct DECLSPEC_NOINITALL _CONTEXT {//0x2cc个字节 |
1 | ==> 0019F470 |0001007F |
其中eip指向0x401019,这还是发生内存异常的那条指令
1 | .text:00401019 mov dword ptr [eax], 1 |
myHandler@0x40105A
分析完了函数以及返回地址,现在将焦点放到该异常处理函数身上
此时的堆栈
1 | 0019F320 77B08BF2 返回到 ntdll.77B08BF2 ;返回地址 |
把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 | .text:004011AB mov dword_40AF88, eax |
一眼顶针,原来是返回到___tmainCRTStartup
了
栈溢出修改seh函数指针
SEH节点都是在栈帧中的,缓冲区溢出可以修改SEH节点中的异常处理函数地址,改成shellcode
书上给出的有漏洞的例子
1 |
|
在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 | char shellcode[]= |
前面的字节都填充成0x90即nop指令,一是为了充当pad,二是为了增加命中率,只要是控制调到这些nop指令中,就会顺序执行到shellcode
最后三个字节是0x12FE94,而shellcode的基地址就是0x12FE94,也就是说将shellcode的基地址溢出到了seh节点中的异常处理函数指针上了,
就算是写0x12FE95,0x12FE96等等也都可以,nop已经把靶阔的很大了
然后除以0异常,而刚才被溢出的seh节点又恰好在seh链上首当其冲
因此shellcode就被当作异常处理函数调用了