初探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
}
__try: __try块中包含可能触发异常的代码。如果代码抛出异常,则交由__except块处理。

__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
#include <Windows.h>
#include <iostream>
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
}
对于这段代码而言,在异常过滤器中自定义了一个函数exception_memory_access_violation, 以GetExceptionInformation()的返回值作为参数,返回值是一个异常过滤器的值,所以也可以直接在__except块的参数中写入异常过滤器的值,如__except (EXCEPTION_EXECUTE_HANDLER) 。 具体而言,GetExceptionInformation()函数返回__try块中产生的异常值(也就是产生异常的原因),据此我们可以实现对异常的过滤。 下面看一些操作系统的异常值:
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
EXCEPTION_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
栈溢出时引发该异常。
也可以从ida里面看到

SEH

举例来讲,如果__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 点进去很容易可以看见主逻辑,但是你这样解密之后,就会发现粗来一堆乱码

Fake_code

然后肯定感觉到出题人包藏东西了,动调会发现不断地报异常,SEH无疑了

查看汇编,可以发现

Fake_code

如果你有一点点汇编基础,就可以知道,如果这是一个正数,__try块中就会触发除0异常,进入__except块处理该异常,由于ida不能反编译这一块的内容,我们只能硬读汇编,由于这不是我们这篇文章的重点,简要略过简要略过

Fake_code

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
11
enc = [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函数只是一个简单的获取输入的函数:

The_Magic

TlsCallback_1函数可以看到真正的加密逻辑,但是如果你按照这个逻辑来写脚本,最终还是一串乱码,。。。

The_Magic

和上一道题的逻辑类似,动调出异常,汇编查看:

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
.text:00007FF795001860 ; ---------------------------------------------------------------------------
.text:00007FF795001860
.text:00007FF795001860 loc_7FF795001860: ; CODE XREF: TlsCallback_1:loc_7FF79500193C↓j
.text:00007FF795001860 mov eax, [rsp+168h+var_148]
.text:00007FF795001864 inc eax
.text:00007FF795001866 mov [rsp+168h+var_148], eax
.text:00007FF79500186A
.text:00007FF79500186A loc_7FF79500186A: ; CODE XREF: TlsCallback_1+23E↑j
.text:00007FF79500186A cmp [rsp+168h+var_148], 20h ; ' '
.text:00007FF79500186F jg loc_7FF795001941
.text:00007FF795001875 movsxd rax, [rsp+168h+var_148]
.text:00007FF79500187A lea rcx, input
.text:00007FF795001881 movzx eax, byte ptr [rcx+rax]
.text:00007FF795001885 add eax, 208h
.text:00007FF79500188A movsxd rcx, [rsp+168h+var_148]
.text:00007FF79500188F lea rdx, input
.text:00007FF795001896 mov [rdx+rcx], al
.text:00007FF795001899 movsxd rax, [rsp+168h+var_148]
.text:00007FF79500189E mov [rsp+168h+var_118], rax
.text:00007FF7950018A3 lea rcx, input
.text:00007FF7950018AA mov [rsp+168h+var_120], rcx
.text:00007FF7950018AF call rand
.text:00007FF7950018B4 add eax, 3E4h
.text:00007FF7950018B9 mov rcx, [rsp+168h+var_120]
.text:00007FF7950018BE mov rdx, [rsp+168h+var_118]
.text:00007FF7950018C3 movzx ecx, byte ptr [rcx+rdx]
.text:00007FF7950018C7 xor ecx, eax
.text:00007FF7950018C9 mov eax, ecx
.text:00007FF7950018CB movsxd rcx, [rsp+168h+var_148]
.text:00007FF7950018D0 lea rdx, input
.text:00007FF7950018D7 mov [rdx+rcx], al
.text:00007FF7950018DA
.text:00007FF7950018DA loc_7FF7950018DA: ; DATA XREF: .rdata:00007FF795003950↓o
.text:00007FF7950018DA ; __try { // __except at loc_7FF795001903
.text:00007FF7950018DA movsxd rax, [rsp+168h+var_148]
.text:00007FF7950018DF lea rcx, input
.text:00007FF7950018E6 movzx eax, byte ptr [rcx+rax]
.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 ecx
.text:00007FF7950018FD mov [rsp+168h+var_128], eax
.text:00007FF795001901 jmp short loc_7FF79500193C
.text:00007FF795001901 ; } // starts at 7FF7950018DA
.text:00007FF795001903 ; ---------------------------------------------------------------------------
.text:00007FF795001903
.text:00007FF795001903 loc_7FF795001903: ; DATA XREF: .rdata:00007FF795003950↓o
.text:00007FF795001903 ; __except(loc_7FF795002CE0) // owned by 7FF7950018DA
.text:00007FF795001903 movsxd rax, [rsp+168h+var_148]
.text:00007FF795001908 lea rcx, input
.text:00007FF79500190F movzx eax, byte ptr [rcx+rax]
.text:00007FF795001913 add eax, 7Bh ; '{'
.text:00007FF795001916 movsxd rcx, [rsp+168h+var_148]
.text:00007FF79500191B lea rdx, unk_7FF7950051E0
.text:00007FF795001922 mov ecx, [rdx+rcx*4]
.text:00007FF795001925 sub ecx, 1C8h
.text:00007FF79500192B xor eax, ecx
.text:00007FF79500192D movsxd rcx, [rsp+168h+var_148]
.text:00007FF795001932 lea rdx, input
.text:00007FF795001939 mov [rdx+rcx], al
.text:00007FF79500193C
.text:00007FF79500193C loc_7FF79500193C: ; CODE XREF: TlsCallback_1+2E1↑j
.text:00007FF79500193C jmp loc_7FF795001860

啊!如此美妙的汇编~ 首先看,__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 ecx
这个加密循环的汇编也不是很难理解??~或许吧,。 好的,不多说,默认大家是可以看懂汇编的~,因为SEH部分分析完了嘻嘻 按照这个汇编逻辑我们可以写出解密代码:
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
#include <iostream>
#include <cstring>

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;
}
# 小插曲,SEH深入分析 ### SEH链 SEH以链的形式存在。第一个异常处理中未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。 SEH链是由_EXCEPTION_REGISTRATION_RECORD结构体组成的链表, **_EXCEPTION_REGISTRATION_RECORD结构体定义如下
1
2
3
4
5
6
7
typedef 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
7
EXCEPTION_DISPOSITION __cdecl _except_handler
(
EXCEPTION_RECORD *pRecord,
EXCEPTION_REGISTRATION_RECORD *pFrame,
CONTEXT *pContext,
PVOID pValue
);
异常处理函数接受4个参数输入,这4个参数保存着一些与异常相关的信息。 第2个参数EXCEPTION_REGISTRATION_RECORD即是上文提到过的SEH链结构体,第4个参数是OS保留,可以忽略,下面会分析第1和第3个参数。 异常处理函数返回名为EXCEPTION_DISPOSITION的枚举类型,它由系统调用,是一个回调函数。
EXCEPTION_RECORD结构体定义
1
2
3
4
5
6
7
8
typedef 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;
ExceptionCode指出异常类型,即上文提到过的一些对应的异常值。 ExceptionAddress表示发生异常的代码地址
CONTEXT结构体定义 异常处理函数接受的第三个参数是指向CONTEXT结构体的指针,它用来备份CPU的值。 多线程环境下,每个线程内部都有一个CONTEXT结构体。 CPU离开当前线程转而运行其他线程时,CPU寄存器的值被保存到当前线程的CONTEXT结构体中,当CPU返回该线程时,使用保存在CONTEXT结构体中的值来覆盖CPU各寄存器的值,以此来保证多线程安全。 CONTEXT结构体的定义如下:
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
typedef 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;
一部分,具体请看微软文档 下面着重看一下Eip的值: 一般来讲Eip成员应该存储触发异常后的代码地址,即触发异常时的Eip值。 具体而言,当某一句代码触发异常,那么Eip的值应该指向这句代码的结束地址。这样当SEH处理完毕异常后,程序可以回到原来的地方,继续执行正常的代码。 但是, 在异常处理函数中可能将参数传递过来的CONTEXT.Eip设置为其他地址,然后返回处理函数。 这样之前暂停的线程会执行新的EIP地址处的代码(反调试中经常使用这个技术)。
返回值 EXCEPTION_DISPOSITION定义** typedef enum _EXCEPTION_DISPOSITION { ExceptionContinueExecution = 0, //已经处理了异常,回到异常触发点继续执行 ExceptionContinueSearch = 1, //没有处理异常,继续遍历异常链表 ExceptionNestedException = 2, //OS内部使用 ExceptionCollidedUnwind = 3 //OS内部使用 }EXCEPTION_DISPOSITION; ### SEH内部函数 结构化异常处理提供了两个可用于 try-except 语句的内部函数: GetExceptionCode 返回 (一个32位整数) 的代码,也就是上文提到过的异常原因相对应的异常值。 GetExceptionInformation 返回一个指向 EXCEPTION_POINTERS 结构的指针,该结构包含有关异常的其他信息。 EXCEPTION_POINTERS定义 EXCEPTION_POINTERS 是 Windows 平台上一个常见的数据结构,用于描述异常发生时的上下文。它包含异常记录和线程上下文的信息,定义如下:
1
2
3
4
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
ExceptionRecord上文已经介绍过了

ContextRecord: 类型:PCONTEXT (指向 CONTEXT 结构的指针) 描述:保存异常发生时的线程上下文,包括寄存器值、堆栈指针、程序计数器等。 常用字段: Eip/Rip(指令指针,用于 x86/x64 架构) Esp/Rsp(栈指针) Ebp/Rbp(基址指针) 寄存器状态(如 Eax, Ebx 等) 下面我们来看一道题: # miniL2021 0oooop

0oooop

进入一看,内存访问错误~,真是友好的粗体人 我们打开汇编一看,又是SEH,

0oooop

这里__except(loc_6F2356)的意思就是跳转到指定的处理代码块( loc_6F2356)。 类似与刚开始示例中exception_memory_access_violation函数的位置 跟进loc_412356的__except filter,然后跟进sub_411131,此函数经过一次跳转到sub_411DD0。反编译结果如下:

0oooop

看到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 6F2330
下面两句是整数除0异常触发代码,
1
2
.text:006F2346                 mov     edx, 0
.text:006F234B div edx
try块结束地址是.text:006F234B,所以这个数就是0x4B。

后面的几句代码跳转也可以说明这个就是EIP的值

1
2
3
4
if
*(_DWORD *)(*(_DWORD *)(a2 + 4) + 184) += 54;
else
*(_DWORD *)(*(_DWORD *)(a2 + 4) + 184) += 63;
也是在CONTEXT结构体那里强调过,可以通过修改CONTEXT的Eip来控制程序返回的地址。 我们有理由相信这两个操作是为了转向不同的结果:即为我们打印失败或成功的信息。所以这里应当修改的是Eip的值,也就是控制程序返回不同的地址。

好,但是这样做很麻烦,我们可以借助ida来帮我们分析, if ( **(_DWORD )a2 != EXCEPTION_INT_DIVIDE_BY_ZERO ), 我们已经知道(_DWORD **)a2是_EXCEPTION_RECORD 里面的ExceptionCode; 上文提及过,结构化异常处理内部函数只有GetExceptionCode和GetExceptionInformation。 代码中对a2解引用两次才得到ExceptionCode,所以我们可以推知代码访问使用的函数应该是GetExceptionInformation,而a2则是此函数的返回值类型。 具体而言,站在异常的角度来看,a2的数据类型应该是_EXCEPTION_POINTERS *类型的。 修改a2的类型之后,一切都变得如此明了~

0oooop

当然还有内存访问异常,在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

RE5

当然,TEA里面也有一个类似的异常处理 main函数处理的是整数除0异常,TEA里面有一个整数除0异常,循环里面有内存访问异常

RE5

可以看到, 分支 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
8
LONG __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;
}
和分支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
#include <iostream>
#include <cstring>
#include <iomanip>
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
12
typedef 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
7
struct _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
5
typedef 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