SHE学习笔记
初探SEH,仅仅是入门~ # 简介and闲话
作为一个根本没有接触过SEH的人,上来就做相关题目,不出所料被暴打了呜呜
不过初次接触很多新的概念是会感觉陌生和难以理解,翻来覆去多看几遍,对照着题目练习,慢慢就能接受了
用两天时间也就差不多可以大致了解一些相关的异常处理了
至于不过这篇文章只是一些比较简单的应用,而且只涉及了SEH,(因为我太菜了,动调也是ida,主要是不太会用OD和X64..)
对于VEH、UEH、VCH等异常处理还没有研究过,SEH栈展开之类的高级内容也许以后学了会再写一篇
## 下面介绍一下SEH: 功能
SEH实际包含两个主要功能:结束处理(termination
handling)和异常处理(exception handling)
每当你建立一个try块,它必须跟随一个finally块或一个except块。
一个try块之后不能既有finally块又有except块。但可以在try-except块中嵌套try-finally块,反过来
也可以。 __try,__finally关键字用来标出结束处理程序两段代码的轮廓
不管保护体(try块)
是如何退出的。不论你在保护体中使用return,还是goto,或者是longjump,结束处理程序
(finally块)都将被调用。 在try使用__leave关键字会引起跳转到try块的结尾
TIB结构:在用户模式下,TIB(ThreadInformationBlock)位于TEB的头部。而TEB是操作系统为了保存每个线程的私有数据创建的,每个线程都有自己的TEB。(这个没有理解也没关系,下面我详细说一下)
画一个流程图也许会更好理解 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23+---------+ +----------------+ +---------------+
| 发生异常 +--->+ TIB +----->+ Next +--+
| | | fs:[0] | +---------------+ | +------------------+
+---------+ +----------------+ | Handler +-------------->+ 异常处理函数 |
+---------------+ | | ... |
| | retn |
+----------+ +------------------+
|
+-------v-------+
| Next +--+
+---------------+ | +------------------+
| Handler +-------------->+ 异常处理函数 |
+---------------+ | | ... |
| | retn |
+----------+ +------------------+
|
+-------v-------+
| FFFFFFh |
+---------------+ +------------------+
| Handler +-------------->+ 异常处理函数 |
+---------------+ | ... |
| retn |
+------------------+
next是下一个链的地址。如果next的值是FFFFFFh,表示是链表的最后一个节点,该节点的回调函数是系统设置的一个终结处理函数,所有无人值守的异常都会到达这里。
异常处理函数可以是自定义的函数,系统有一个默认的函数,但我们可以自定义一个异常处理函数,让它来处理。
但是得先安装自定义函数才能使用。 下面看一个小例子,虽然不是我自己写的
1
2
3
4
5
6
7基本结构
__try {
// 受保护的代码
}
__except ( /*异常过滤器exception filter*/ ) {
// 异常处理程序exception handler
}
__except: __except块中是用户定义的处理异常的代码。
exception filter: exception filter称为异常过滤器。顾名思义,它的作用是对异常进行过滤。
异常过滤器只有三个值(定义在Windows的Excpt.h中):
EXCEPTION_CONTINUE_EXECUTION(-1) 在发生异常的地方继续执行。
EXCEPTION_CONTINUE_SEARCH (0) 异常无法识别。继续搜索下一个处理程序。
EXCEPTION_EXECUTE_HANDLER (1)
异常被识别。通过执行控制转移到异常处理程序__except复合语句,然后继续执行__except块。
异常过滤器决定了是否处理当前异常,即是否执行__except块中的代码(异常处理程序exception
handler)。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int main()
{
char* mem = 0;
std::cout << "Hello World!\n";
__try {
*mem = 0; //throw exception
}
__except (exception_memory_access_violation(GetExceptionInformation())) //handler
{
puts("Memory error in except");
}
}
int exception_memory_access_violation(LPEXCEPTION_POINTERS p_exinfo)
{
if (p_exinfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION)
{
return EXCEPTION_EXECUTE_HANDLER; //handle this exception
}
else return EXCEPTION_CONTINUE_SEARCH; //Do not handle this exception
}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
37
38
39
40EXCEPTION_ACCESS_VIOLATION 0xC0000005
程序企图读写一个不可访问的地址时引发的异常。例如企图读取0地址处的内存。
EXCEPTION_ARRAY_BOUNDS_EXCEEDED 0xC000008C
数组访问越界时引发的异常。
EXCEPTION_BREAKPOINT 0x80000003
触发断点时引发的异常。
EXCEPTION_DATATYPE_MISALIGNMENT 0x80000002
程序读取一个未经对齐的数据时引发的异常。
EXCEPTION_FLT_DENORMAL_OPERAND 0xC000008D
如果浮点数操作的操作数是非正常的,则引发该异常。所谓非正常,即它的值太小以至于不能用标准格式表示出来。
EXCEPTION_FLT_DIVIDE_BY_ZERO 0xC000008E
浮点数除法的除数是0时引发该异常。
EXCEPTION_FLT_INEXACT_RESULT 0xC000008F
浮点数操作的结果不能精确表示成小数时引发该异常。
EXCEPTION_FLT_INVALID_OPERATION 0xC0000090
该异常表示不包括在这个表内的其它浮点数异常。
EXCEPTION_FLT_OVERFLOW 0xC0000091
浮点数的指数超过所能表示的最大值时引发该异常。
EXCEPTION_FLT_STACK_CHECK 0xC0000092
进行浮点数运算时栈发生溢出或下溢时引发该异常。
EXCEPTION_FLT_UNDERFLOW 0xC0000093
浮点数的指数小于所能表示的最小值时引发该异常。
EXCEPTION_ILLEGAL_INSTRUCTION 0xC000001D
程序企图执行一个无效的指令时引发该异常。
EXCEPTION_IN_PAGE_ERROR 0xC0000006
程序要访问的内存页不在物理内存中时引发的异常。
EXCEPTION_INT_DIVIDE_BY_ZERO 0xC0000094
整数除法的除数是0时引发该异常。
EXCEPTION_INT_OVERFLOW 0xC0000095
整数操作的结果溢出时引发该异常。
EXCEPTION_INVALID_DISPOSITION 0xC0000026
异常处理器返回一个无效的处理的时引发该异常。
EXCEPTION_NONCONTINUABLE_EXCEPTION 0xC0000025
发生一个不可继续执行的异常时,如果程序继续执行,则会引发该异常。
EXCEPTION_PRIV_INSTRUCTION 0xC0000096
程序企图执行一条当前CPU模式不允许的指令时引发该异常。
EXCEPTION_SINGLE_STEP 0x80000004
标志寄存器的TF位为1时,每执行一条指令就会引发该异常。主要用于单步调试。
EXCEPTION_STACK_OVERFLOW 0xC00000FD
栈溢出时引发该异常。
举例来讲,如果__try块中产生整数除零异常,那么GetExceptionInformation()函数返回0xC0000094自定义过滤器中将这个值与预先设定好的异常值比较。 在此例中,这个异常值是EXCEPTION_ACCESS_VIOLATION,即0xC0000005。如果异常值相等,那么就返回EXCEPTION_EXECUTE_HANDLER,进而执行exception handler,也就是使用当前__except块处理异常,否则就返回EXCEPTION_CONTINUE_SEARCH,即继续搜索下一个异常处理程序。 所以,只有__try块中产生读写不可访问地址异常时,__except块才会处理该异常。也就是说,除了EXCEPTION_ACCESS_VIOLATION这个异常,__except都不会处理。 也就是说,__try块中产生整数除零异常并不会触发异常处理 下面,我用几道题来具体说明一下: # moectf2022 Fake_code 点进去很容易可以看见主逻辑,但是你这样解密之后,就会发现粗来一堆乱码
然后肯定感觉到出题人包藏东西了,动调会发现不断地报异常,SEH无疑了
查看汇编,可以发现
如果你有一点点汇编基础,就可以知道,如果这是一个正数,__try块中就会触发除0异常,进入__except块处理该异常,由于ida不能反编译这一块的内容,我们只能硬读汇编,由于这不是我们这篇文章的重点,简要略过简要略过
except块中对应的代码即key = (97 * key + 101) % 233; key ^= 0x29; loc_140001212即input[i] ^= box[key];
main函数已经编译出来,这样就可以写代码了 1
2
3
4
5
6
7
8
9
10
11enc = [30,112,122,110,234,131,158,239,150,226,178,213,153,187,187,120,185,61,110,56,66,194,134,255,99,189,250,121,163,109,96,148,179,66,17,195,144,137,189,239,212,151,248,123,139,11,45,117,126,221,203]
box = [172, 4, 88, 176, 69, 150, 159, 46, 65, 21, 24, 41, 177, 51, 170, 18, 13, 137, 230, 250, 243, 196, 189, 231, 112, 138, 148, 193, 133, 157, 163, 242, 63, 130, 142, 215, 3, 147, 61, 19, 5, 107, 65, 3, 150, 118, 227, 177, 138, 74, 34, 85, 196, 25, 245, 85, 166, 31, 14, 97, 39, 203, 31, 158, 90, 122, 227, 21, 64, 148, 71, 222, 0, 1, 145, 102, 183, 205, 34, 100, 245, 165, 156, 104, 165, 82, 134, 189, 176, 221, 118, 40, 171, 22, 149, 197, 38, 44, 246, 57, 190, 0, 165, 173, 227, 147, 158, 227, 5, 160, 176, 29, 176, 22, 11, 91, 51, 149, 164, 9, 22, 135, 86, 31, 131, 78, 74, 60, 85, 54, 111, 187, 76, 75, 157, 177, 174, 229, 142, 200, 251, 14, 41, 138, 187, 252, 32, 98, 4, 45, 128, 97, 214, 193, 204, 59, 137, 197, 139, 213, 38, 88, 214, 182, 160, 80, 117, 171, 23, 131, 127, 55, 43, 160, 29, 44, 207, 199, 224, 229, 73, 201, 250, 107, 192, 152, 102, 153, 146, 0, 2, 212, 117, 70, 34, 5, 53, 209, 75, 197, 173, 224, 142, 69, 59, 80, 21, 181, 46, 133, 48, 137, 84, 18, 222, 241, 90, 240, 43, 167, 27, 74, 38, 93, 152, 212, 161, 190, 209, 77, 126, 56, 222, 11, 10, 84, 184, 115, 109, 173, 140, 30, 217, 49, 95, 86, 126, 189, 72, 50, 152, 46, 62, 235, 162, 29]
key = 0x19
index = 0
lst = []
for i in range(len(enc)):
index = (0x7f * index + 0x66) % 0xff
if index >> 7 == 0:
key = (97 * key + 101) % 233
key ^= 0x29
print(chr(enc[i] ^ box[key]), end='')
TSCTF-J2023 The_Magic
这道题需要一些TLS知识,这里不细说,感兴趣的可以自己去了解了解 TlsCallback_0函数只是一个简单的获取输入的函数:
TlsCallback_1函数可以看到真正的加密逻辑,但是如果你按照这个逻辑来写脚本,最终还是一串乱码,。。。
和上一道题的逻辑类似,动调出异常,汇编查看:
1 | .text:00007FF795001860 ; --------------------------------------------------------------------------- |
啊!如此美妙的汇编~
首先看,__try块中产生的整数除零异常,和上一题不能说十分相似,只能说一模一样
1
2
3
4
5
6.text:00007FF7950018EA sar eax, 7
.text:00007FF7950018ED mov [rsp+168h+var_130], eax
.text:00007FF7950018F1 mov eax, 1
.text:00007FF7950018F6 cdq
.text:00007FF7950018F7 mov ecx, [rsp+168h+var_130]
.text:00007FF7950018FB idiv ecx1
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
37
38
39
using namespace std;
int Seed = 150;
int getrand()
{
Seed = (Seed + 1029) * 3847 % 5665;
return Seed;
}
int main()
{
int enc[100] = { 'T', 'S', 'C', 'T', 'F', '-', 'J', '{', 111, 186, 117, 71, 44, 54, 93, 43, 179, 68, 158, 25, 145, 107, 229, 146, 220, 128, 220, 203, 195, 73, 179, 12, 7, '}' };
int Xor[100] = { 611, 613, 709, 611, 591, 589, 599, 597, 4732, 2686, 1877, 2929, 2678, 7275, 5979, 3657, 3879, 8962, 7445, 2694, 3384, 3710, 4938, 2832, 2329, 4215, 3152, 4911, 6525, 3445, 2071, 5765, 3700, 3156, 4178, 7234, 3437, 2437, 5236, 7265, 5391, 6257, 5756, 3426, 3446, 7432, 3871, 3442, 2200, 589 };
int i, j;
printf("TSCTF-J{");
for(i = 8; i <= 32; i++)
{
int randint = getrand();
for(j = 32; j < 126; j++)
{
int input = j;
input = (unsigned char)((input + 520) ^ (randint + 996));
if(input >> 7 == 0)
{
input = (unsigned char)((input + 123) ^ (Xor[i] - 456));
}
if(input == enc[i])
{
cout << char(j);
}
}
}
printf("}");
return 0;
}1
2
3
4
5
6
7typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
//指向下一个 EXCEPTION_REGISTRATION_RECORD
PEXCEPTION_DISPOSITION Handler;
//指向异常处理函数
} EXCEPTION_REGISTRATION_RECORD,*PEXCEPTION_REGISTRATION_RECORD;1
2
3
4
5
6
7EXCEPTION_DISPOSITION __cdecl _except_handler
(
EXCEPTION_RECORD *pRecord,
EXCEPTION_REGISTRATION_RECORD *pFrame,
CONTEXT *pContext,
PVOID pValue
);1
2
3
4
5
6
7
8typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; // 异常代码(如 0xC0000005 表示访问冲突)
DWORD ExceptionFlags; // 异常标志(如 0 表示异常是第一次发生)
struct _EXCEPTION_RECORD* ExceptionRecord; // 链接下一个异常记录
PVOID ExceptionAddress; // 触发异常的地址
DWORD NumberParameters; // 附加信息的参数数量
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 附加参数
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;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
32typedef struct _CONTEXT {
DWORD ContextFlags;
DWORD Dr0; //0x04
DWORD Dr1; //0x08
DWORD Dr2; //0x0c
DWORD Dr3; //0x10
DWORD Dr6; //0x14
DWORD Dr7; //0x18
FLOATING_SAVE_AREA FloatSave;
DWORD SegGs; //0x88
DWORD SegFs; //0x90
DWORD SegEs; //0x94
DWORD SegDs; //0x98
DWORD Edi; //0x9c
DWORD Esi; //0xa0
DWORD Ebx; //0xa4
DWORD Edx; //0xa8
DWORD Ecx; //0xac
DWORD Eax; //0xb0
DWORD Ebp; //0xb4
DWORD Eip; //0xb8
DWORD SegCs; //0xbc MUST BE SANITIZED
DWORD EFlags; //0xc0 MUST BE SANITIZED
DWORD Esp; //0xc4
DWORD SegSs; //0xc8
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;1
2
3
4typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
ContextRecord: 类型:PCONTEXT (指向 CONTEXT 结构的指针) 描述:保存异常发生时的线程上下文,包括寄存器值、堆栈指针、程序计数器等。 常用字段: Eip/Rip(指令指针,用于 x86/x64 架构) Esp/Rsp(栈指针) Ebp/Rbp(基址指针) 寄存器状态(如 Eax, Ebx 等) 下面我们来看一道题: # miniL2021 0oooop
进入一看,内存访问错误~,真是友好的粗体人 我们打开汇编一看,又是SEH,
这里__except(loc_6F2356)的意思就是跳转到指定的处理代码块( loc_6F2356)。 类似与刚开始示例中exception_memory_access_violation函数的位置 跟进loc_412356的__except filter,然后跟进sub_411131,此函数经过一次跳转到sub_411DD0。反编译结果如下:
看到a2是一个很奇怪的数,我们对他进行M去转成枚举类型,可以知道a2就是EXCEPTION_INT_DIVIDE_BY_ZERO,即整数除0异常。
继续注意,可以看到这个加密在对a2进行异或加减的时候,里面有一个操作*(_DWORD
*)(a2 + 4) + 184,
由于184的16进制是0xb8,对照上面写的CONTEXT结构体后面的注释,可以知道这个就是Eip的值,即触发异常代码的下一句代码的地址值,取它的最后一字节来异或。
返回去看__try块, 1
2
3
4
5
6
7
8
9.text:006F2330 ; __try { // __except at loc_6F2377
.text:006F2330 mov [ebp+ms_exc.registration.TryLevel], 0
.text:006F2337 lea ebx, [ebp+Str]
.text:006F233D xor eax, eax
.text:006F233F db 3Eh
.text:006F233F mov dword ptr [eax], 0
.text:006F2346 mov edx, 0
.text:006F234B div edx
.text:006F234B ; } // starts at 6F23301
2.text:006F2346 mov edx, 0
.text:006F234B div edx
后面的几句代码跳转也可以说明这个就是EIP的值 1
2
3
4if:
*(_DWORD *)(*(_DWORD *)(a2 + 4) + 184) += 54;
else:
*(_DWORD *)(*(_DWORD *)(a2 + 4) + 184) += 63;
好,但是这样做很麻烦,我们可以借助ida来帮我们分析, if ( **(_DWORD )a2 != EXCEPTION_INT_DIVIDE_BY_ZERO ), 我们已经知道(_DWORD **)a2是_EXCEPTION_RECORD 里面的ExceptionCode; 上文提及过,结构化异常处理内部函数只有GetExceptionCode和GetExceptionInformation。 代码中对a2解引用两次才得到ExceptionCode,所以我们可以推知代码访问使用的函数应该是GetExceptionInformation,而a2则是此函数的返回值类型。 具体而言,站在异常的角度来看,a2的数据类型应该是_EXCEPTION_POINTERS *类型的。 修改a2的类型之后,一切都变得如此明了~
当然还有内存访问异常,在TLS里面,这个涉及另一种机制VEH,由于不是本文重点,就不说了 我是不会承认是因为我太菜了不会的,呜呜
一点小细节
我之前说__except块中的代码(也就是异常处理程序except handler)中的内容是不能被IDA反编译出来的。 但在miniL这道题中,我们是看反编译出来的结果,为什么会这样? 来看一下__except 的内容:
__except ( /异常过滤器exception filter/ ) { // 异常处理程序exception handler }
注意__except块中有两个重要的成员: 异常过滤器exception filter 异常处理程序exception handler moectf以及TSCTF都将核心代码写入exception handler,也就是__except块大括号包裹的内容,这里的东西是不能够被IDA反编译出来的,所以我们只能通过阅读汇编获得程序逻辑。 而miniL的题目则是将核心代码写入exception filter。由于异常过滤器实际上也是一个函数,所以能够被IDA识别并且反编译。
简而言之,如果出题人想考验选手阅读汇编代码的能力,那么就将代码直接写在exception handler中。 如果出题人不想为难选手嗯怼汇编,就把代码写入exception filter函数中,或者在exception handler中调用一个写入核心加密过程的函数。
2024鹏城杯 RE5
点进去,一个人畜无害的TEA加密,但是你这样解密之后,仍旧是一串乱码,hhhhhh 当然,你粗略的看一遍汇编就知道这也是个SEH
当然,TEA里面也有一个类似的异常处理 main函数处理的是整数除0异常,TEA里面有一个整数除0异常,循环里面有内存访问异常
可以看到, 分支 1: EXCEPTION_ACCESS_VIOLATION 访问冲突异常(EXCEPTION_ACCESS_VIOLATION),通常是非法访问内存(如读写空指针)。
处理: 计算堆栈上的某个地址(Esp + 96),并将一个随机数存储到该地址。 修改 EIP(指令指针)以跳过异常触发的指令(增加 2),让程序继续执行而不会反复触发异常。 返回 -1,表示异常已被处理。
分支 2: EXCEPTION_INT_DIVIDE_BY_ZERO 整数除零异常(EXCEPTION_INT_DIVIDE_BY_ZERO),通常是尝试除以零。
处理: 使用 srand(0) 重置随机数生成器的种子为 0。 调用 sub_401000(),
注册了一个名为 Handler
的自定义向量化异常处理程序,并返回注册成功后的句柄。
修改堆栈上的某个值(Esp + 84)为 2。 修改
EIP(指令指针)以跳过异常触发的指令(增加 2),避免循环触发。 返回
-1,表示异常已被处理。 1
2
3
4
5
6
7
8LONG __stdcall Handler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
if ( ExceptionInfo->ExceptionRecord->ExceptionCode != EXCEPTION_INT_DIVIDE_BY_ZERO )
return 0;
*(_DWORD *)(ExceptionInfo->ContextRecord->Esp + 80) = 3;
ExceptionInfo->ContextRecord->Eip += 2;
return -1;
}
这样一步步的调试,跟踪地址,我们可以发现这个TEA原本的样貌(具体分析自己去动调跟一下吧,我觉得我说的很清楚了):
key[]被改成了{2, 2, 3, 3}; sum -= 1640531527; 被改成了: sum +=
rand();其中srand = 0 下面就可以写脚本了: 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
37
38
39
40
41
42
43
44
45
46
47
48
using namespace std;
void decrypt(unsigned int* data, unsigned int* key)
{
unsigned int v0 = data[0], v1 = data[1];
unsigned int roundsum[32],sum;
for (int round = 0; round < 32; round++)
{
roundsum[round] = rand();
if (round > 0)
{
roundsum[round] += roundsum[round - 1];
}
}
for (int i = 0; i < 0x20; ++i)
{
sum = roundsum[31 - i];
v1 -= (key[3] + (v0 >> 5)) ^ (sum + v0) ^ (key[2] + 16 * v0);
v0 -= (key[1] + (v1 >> 5)) ^ (sum + v1) ^ (*key + 16 * v1);
}
data[0] = v0;
data[1] = v1;
}
int main()
{
srand(0);
unsigned int sum = 0, delta = 1640531527;
unsigned int key[5] = { 2,2,3,3 };
int i = 0;
unsigned int enc[] = {
0xEA2063F8, 0x8F66F252,
0x902A72EF, 0x411FDA74,
0x19590D4D, 0xCAE74317,
0x63870F3F, 0xD753AE61
};
for (int i = 0; i < 4; i++)
{
decrypt(&enc[i * 2], key);
}
printf("\n%s", (char *)enc);
return 0;
}
另一个小插曲:TEB
下面要介绍VC++编译器对SEH所做的增强版本,在这之前,先说明一些关于TEB(Thread
Environment Block,线程环境块)的知识。在这里只讲解与SEH相关的内容。
TEB成员众多,此处我们只需要了解_NT_TIB。 SEH 机制依赖其中的 _NT_TIB
结构,_NT_TIB 中的 ExceptionList 字段是异常处理链的起点。
**_NT_TIB结构** 1
2
3
4
5
6
7
8
9
10
11
12typedef struct _NT_TIB {
struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList;
PVOID StackBase;
PVOID StackLimit;
PVOID SubSystemTib;
union {
PVOID FiberData;
DWORD Version;
};
PVOID ArbitraryUserPointer;
struct _NT_TIB *Self;
} NT_TIB;
ExceptionList:指向 _EXCEPTION_REGISTRATION_RECORD 结构的链表头,表示当前线程的 SEH 链。 StackBase 和 StackLimit:定义了当前线程的栈边界。 Self:指向自身的指针,用于访问当前线程的 TEB。
TEB访问方法 Ntdll.NtCurrentTeb() 用户模式下使用此函数访问TEB,Ntdll.NtCurrentTeb()返回当前线程的TEB结构体的地址。
FS段寄存器 FS:[0]指向SEH起始地址。
我们知道,原始的 SEH 机制通过 _EXCEPTION_REGISTRATION_RECORD
链表实现,每个注册的异常处理器都会挂载到这个链表上。 而VC++ 编译器增强的
SEH 机制在 _EXCEPTION_REGISTRATION
结构中添加了额外字段,用于支持更复杂的异常处理功能。
**_EXCEPTION_REGISTRATION** 1
2
3
4
5
6
7struct _EXCEPTION_REGISTRATION {
struct _EXCEPTION_REGISTRATION *prev; // 指向上一个异常记录
void *handler; // 异常处理函数指针
struct scopetable_entry *scopetable; // 指向 scopetable 的数组
int trylevel; // 当前 try 块的索引
int _ebp; // 栈帧指针
};
新增功能: scopetable:一个数组,存储所有 __try 块的过滤函数(filter)和终止函数(handler)。 每个 __try 块对应一个数组元素。 trylevel:当前处于第几个 __try 块。 _ebp:保存当前栈帧,用于返回到函数上下文。
SCOPETABLE SCOPETABLE 是一个辅助结构,用于管理多个
__try 块的处理逻辑。 1
2
3
4
5typedef struct _SCOPETABLE {
DWORD previousTryLevel; // 前一个 try 块的索引
DWORD lpfnFilter; // 当前 try 块的过滤函数 (__except 的条件表达式)
DWORD lpfnHandler; // 当前 try 块的终止函数 (__finally 块的逻辑)
} SCOPETABLE, *PSCOPETABLE;
所以, 按照原始的设计,每一个__try/__except(__finally) 都应该对应一个 EXCEPTION_REGISTRATION。 但是VS实际实现中,每个使用 __try/__except(__finally) 的函数,不管其内部嵌套或反复使用多少 __try/__except(__finally),都只注册一遍,即只将一个 EXCEPTION_REGISTRATION 挂入当前线程的异常链表中。 MSC(Microsoft Visual C++ 编译器) 提供一个处理函数,即 EXCEPTION_REGISTRATION::handler 被设置为 MSC 的某个函数,而不是我们自己提供的 __except 代码块,我们自己提供的多个 __except 块被存储在 EXCEPTION_REGISTRATION::scopetable 数组中。 下面看个题来理解一下: # SCTF2019 creakme




