SEH笔记

Last updated on January 30, 2024 am

浅谈SEH

​ 从去年的moectf到今年的moectf,以及平日的刷题都遇到了SEH的题目,但是一直没有系统的学习总结。正巧最近也在看《加密与解密》这本书,也看到了SEH相关内容。就借着今年的moectf来总结一下SEH吧!

SEH

什么是SEH

​ SEH全称Structured Exception Handling,即结构化处理异常。是一种当代码产生异常提供纠错机会的机制,比如访问非法地址,发生除0错误就去修正地址、修正除数等。换句话说,也就是发生异常的时候系统会调用一个用户自定义的回调函数,此回调函数的主要作用就是修正错误,让代码重新正常运行。

不妨先看看这个回调函数样子

1
2
3
4
5
EXCEPTION_DISPOSITION
__cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord,
void * EstablisherFrame,
struct _CONTEXT *ContextRecord,
void * DispatcherContext);

​ 这个函数就是在发生异常的时候操作系统会调用的异常处理函数,它有四个参数,指示了一些与异常相关的信息。可能会有疑问,即我怎么这四个参数的值是多少呢?其实在异常发生的时候,操作系统就会自动把这个四个参数压入栈中,也因此需要__cdecl来声明此函数的调用约定。

它的返回值是枚举类型

1
2
3
4
5
6
7
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution = 0, //已经处理了异常,回到异常触发点继续执行
ExceptionContinueSearch = 1, //没有处理异常,继续遍历异常链表
ExceptionNestedException = 2, //OS内部使用
ExceptionCollidedUnwind = 3 //OS内部使用
}EXCEPTION_DISPOSITION;

现在就来看看这四个参数是什么

     函数的第一个参数是一个指向**EXCEPTION_RECORD**结构的指针,定义如下
1
2
3
4
5
6
7
8
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常代码 比如0xc0000005
DWORD ExceptionFlags; //异常标志
struct _EXCEPTION_RECORD *ExceptionRecord; //指向另一个此结构的指针
PVOID ExceptionAddress; //异常发生地址
DWORD NumberParameters; //下面数组的元素个数
DWORD ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //附加信息
} EXCEPTION_RECORD;

​ 函数的第二个参数是一个指向establisher帧结构的指针。它是SEH中一个至关重要的参数,但是现在可以忽略它

​ 回调函数的第三个参数是一个指向CONTEXT结构的指针,定义如下(该结构定义随着Windows系统不同也会改变。此结构保存异常发生时一些上下文环境

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
typedef struct _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;
} CONTEXT;

回调函数的第四个参数被称为DispatcherContext。它暂时也可以被忽略。

​ 现在我们大概知道回调函数会有四个参数,这些参数会由操作系统自动压栈,并且这些参数还会为我们提供异常的重要信息,现在还有一个问题就是,操作系统去哪里找这个回调函数?

​ 首先我们应该想到一个问题,即异常处理函数不可能只有一个,毕竟有那么多不同的异常,每个异常都需要专门的函数去处理,因此操作系统选择把这些异常处理函数以链表的形式存储,也就是EXCEPTION_REGISTRATION_RECORD结构,此结构定义如下

1
2
3
4
typedef struct _EXCEPTION_REGISTRATION_RECORD{
struct _EXCEPTION_REGISTRATION_RECORD *Next; //指向下一个结构的指针
PEXCEPTION_ROUTINE Handler; //当前异常处理函数的地址
}EXCEPTION_REGISTRATION_RECORD;

因此只要找到这个EXCEPTION_REGISTRATION_RECORD结构,就可以找到异常处理函数,那么这个结构在哪呢?

​ 这就不得不提到TIB(Thread Information Block,线程信息块)了,它位于TEB(Thread Environment Block,线程环境块)的头部。TEB是操作系统为了保存每个线程的私有数据而创建的,也就是说每个线程都有自己独立的TEB。不同系统的TIB结构也会不同,但我们也不需要具体知道TIB结构,因为**_EXCEPTION_REGISTRATION_RECORD**结构总位于TIB结构头部。而在32位系统上,fs:[0]总是指向TIB结构。

一句话说明,我们只需要去fs:[0]就可以找到此链表了。

下面就来试试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
40
41
42
43
44
45
46
47
48
49
#include <windows.h>
#include <stdio.h>
DWORD scratch;
EXCEPTION_DISPOSITION
__cdecl
_except_handler(struct _EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT* ContextRecord,
void* DispatcherContext)
{
unsigned i;
// 指明是我们让流程转到我们的异常处理程序的
printf("Hello from an exception handler\n");
// 改变CONTEXT结构中EAX的值,以便它指向可以成功进写操作的位置
ContextRecord->Eax = (DWORD)&scratch;
// 告诉操作系统重新执行出错的指令
return ExceptionContinueExecution;
}


int main()
{
DWORD handler = (DWORD)_except_handler;//当然也可以把这个异常处理函数任意命名
//安装我们自己的SEH
__asm
{
// 创建EXCEPTION_REGISTRATION结构:
// 注意压栈顺序,请细细体会这个链表的形成过程
push handler // handler函数的地址
push FS : [0] // 前一个handler函数的地址
mov FS : [0] , ESP // 安装新的EXECEPTION_REGISTRATION结构
}
__asm
{
mov eax, 0 // 将EAX清零
mov[eax], 1 // 写EAX指向的内存从而故意引发一个错误
}
printf("After writing!\n");

//卸载我们自己的SEH
__asm
{
// 移去我们的EXECEPTION_REGISTRATION结构
mov eax, [ESP] // 获取前一个结构
mov FS : [0] , EAX // 安装前一个结构
add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈
}
return 0;
}

这里使用了几句简单的汇编语言,以便和之前所说的对应。实际上__try操作也是类似的,编译器也会大概如此翻译。

再次总结一下目前已经知道的东西:在异常发生时,操作系统会去fs:[0]找**_EXCEPTION_REGISTRATION_RECORD**结构,此结构是异常处理函数的链表,即fs:[0]存放的是下一个异常处理函数结构的地址,fs:[4]存放的就是当前异常处理函数地址。此链表也就是所谓的SEH链,操作需要此SEH链来遍历所有异常处理函数,来找到可以处理此异常的函数。

初探展开

如下是第二段demo,此demo中我们选择不处理异常,看看会发生什么吧!

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
66
#include <windows.h>
#include <stdio.h>
EXCEPTION_DISPOSITION
__cdecl _except_handler(
struct _EXCEPTION_RECORD* ExceptionRecord,
void* EstablisherFrame,
struct _CONTEXT* ContextRecord,
void* DispatcherContext)
{
printf("Home Grown handler: Exception Code: %08X Exception Flags %X",
ExceptionRecord->ExceptionCode, ExceptionRecord->ExceptionFlags);

if (ExceptionRecord->ExceptionFlags & 1)
printf(" EH_NONCONTINUABLE");
if (ExceptionRecord->ExceptionFlags & 2)
printf(" EH_UNWINDING");
if (ExceptionRecord->ExceptionFlags & 4)
printf(" EH_EXIT_UNWIND");
if (ExceptionRecord->ExceptionFlags & 8) // 注意这个标志
printf(" EH_STACK_INVALID");
if (ExceptionRecord->ExceptionFlags & 0x10) // 注意这个标志
printf(" EH_NESTED_CALL");
printf("\n");

// 我们不想处理这个异常,让其它函数处理吧
return ExceptionContinueSearch;
}


void HomeGrownFrame(void)
{
DWORD handler = (DWORD)_except_handler;
__asm
{
// 创建EXCEPTION_REGISTRATION结构:
push handler // handler函数的地址
push FS : [0] // 前一个handler函数的地址
mov FS : [0] , ESP // 安装新的EXECEPTION_REGISTRATION结构
}

*(PDWORD)0 = 0; // 写入地址0,从而引发一个错误
printf("I should never get here!\n");

__asm
{
// 移去我们的EXECEPTION_REGISTRATION结构
mov eax, [ESP] // 获取前一个结构
mov FS : [0] , EAX // 安装前一个结构
add esp, 8 // 把我们EXECEPTION_REGISTRATION结构弹出堆栈
}
}


int main()
{
__try
{
HomeGrownFrame();
}
__except (EXCEPTION_EXECUTE_HANDLER)
{
printf("Caught the exception in main()\n");
}

return 0;
}

此demo主要流程就是在函数HomeGrownFrame()中安装一个SEH并且引发一个错误,但是我们并不处理这个错误,而是交给main()函数中的__except来处理。当尝试运行时,会发现如下的输出

1
2
3
Home Grown handler: Exception Code: C0000005 Exception Flags 0
Home Grown handler: Exception Code: C0000027 Exception Flags 2 EH_UNWINDING
Caught the Exception in main()

应当令人惊讶的是,_except_handler函数似乎调用了两次,而且结果还不相同。

​ 比较一下以“Home Grown Handler”开头的两行,就会看出它们之间有明显的区别。第一次异常标志是0,而第二次是2。这就涉及到展开(Unwinding)。实际上,当一个异常处理回调函数拒绝处理某个异常时,它会被再一次调用。但是这次回调并不是立即发生的。

​ 当异常发生的时候,操作系统会第一次遍历**_EXCEPTION_REGISTRATION_RECORD结构链表**,直到找到处理此异常的函数,然后第二次遍历此链表直到处理此异常的结点,在这两次调用中,操作系统都会调用里面的异常处理函数(由于异常处理函数通常会设置一些过滤操作,所以并不是每次都会执行真正的修复部分),唯一的区别就是第二次调用中异常标志被设置为2。这个值被定义为EH_UNWINDING。

​ 那为什么需要调用两次呢?实际上是操作系统给这个函数做最后的清理,一个绝好的例子是C++类的析构函数。当一个函数的异常处理程序拒绝处理某个异常时,通常执行流程并不会正常地从那个函数退出。现在,想像一个定义了 一个C++类的实例作为局部变量的函数。C++规范规定析构函数必须被调用。这带EH_UNWINDING标志的第二次回调就给这个函数一个机会去做一些类似于调用析构函数和__finally块之类的清理工作。

​ 粗暴地来说,展开就是二次执行异常处理函数,并做一些清理。

编译器层面的SEH

​ 虽然使用几句简单的汇编也可以安装SEH结构,但是在使用上多少还是有不便,于是乎Visual为此进行了设置语法结构,即简单的__try{} __except{}结构,它的模式是这样的

1
2
3
4
5
6
__try {
// 这里是被保护的代码
}
__except (过滤器表达式) {
// 这里是异常处理程序代码
}

​ 在汇编层面实际上与之前的是一样的,也就是在进入一个__try块之前一定会看到类似的语句

1
MOV DWORD PTR FS:[00000000],ESP

​ 也就是安装SEH到fs:[0]处,与之前的知识是一致的

​ 但实际上,Visual的__try结构是扩展了的,它往里面_EXCEPTION_REGISTRATION_RECORD结构添了点新东西。

1
2
3
4
5
6
7
8
9
struct _EXCEPTION_REGISTRATION_RECORD{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)( PEXCEPTION_RECORD,
PEXCEPTION_REGISTRATION,
PCONTEXT,
PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
};

​ 此结构的第一个和第二成员与之前的是一样的,后面三个域:scopetable(作用域表)、trylevel和_ebp是新增加的。scopetable域指向一个scopetable_entry结构数组,而trylevel域实际上是这个数组的索引。最后一个域_ebp,是EXCEPTION_REGISTRATION结构创建之前栈帧指针(EBP)的值。

​ 还有一个结构与此相关,也是Visual真正使用的结构CPPEH_RECORD

1
2
3
4
5
struct CPPEH_RECORD{
DWORD old_esp;
EXCEPTION_POINTERS *exc_ptr;
struct _EXCEPTION_REGISTRATION_RECORD registration;
}

​ 此处的old_esp是真正开始执行正常函数操作时的esp

​ 同时保留一个DWORD来存储EXCEPTION_POINTERS结构,这个结构其实就是调用GetExceptionInformation所返回的指针,如下是此结构的定义

1
2
3
4
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord;
PCONTEXT ContextRecord;
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

​ 说了这么多不如直接来看看Visual是怎么做的吧!

​ 下面给出源码和进入__try之前的反汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
__try
{

}
__except (EXCEPTION_EXECUTE_HANDLER)
{

}

return 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
push        ebp  
mov ebp,esp
push 0FFFFFFFEh //压入TryLevel
push 539228h //压入ScopTable
push offset _except_handler4 (0533DB0h) //压入_except_handler4
mov eax,dword ptr fs:[00000000h]
push eax //压入Next指针

//开辟新空间 做一些安全检查
add esp,0FFFFFF38h
push ebx
push esi
push edi
lea edi,[ebp-18h]
xor ecx,ecx
mov eax,0CCCCCCCCh
rep stos dword ptr es:[edi]
mov eax,dword ptr [__security_cookie (053A004h)]
xor dword ptr [ebp-8],eax
xor eax,ebp
push eax
//安全检查结束

lea eax,[ebp-10h]
mov dword ptr fs:[00000000h],eax //把上一个SEH地址给现在这个SEH结构
mov dword ptr [ebp-18h],esp //保存现在的ESP值

//一些安全检查
mov ecx,offset _EF1F479B_SEH@cpp (053C0A2h)
call @__CheckForDebuggerJustMyCode@4 (053132Fh)

​ 这里给出栈的分布图,就会更加明朗了

​ 现在安装SEH后的栈分布图知道了,就可以具体研究那些新增成员的含义了。

​ 如下是_SCOPETABLE_ENTRY结构定义

1
2
3
4
5
typedef struct _SCOPETABLE_ENTRY{
DWORD EnclosingLevel; //上一层__try块
PVOID FilterFunc; //过滤表达式
PVOID HandlerFunc; //__except代码或__finally代码
}

​ 这里的FilterFunc就是except里面的过滤表达式,它在这里充当异常筛选的作用,可以完成任何想要完成的工作,只要最后反回的结果符号要求即可,其返回值

1
2
3
#define EXCEPTION_EXECUTE_HANDLER 1
#define EXCEPTION_CONTINUE_SEARCH 0
#define EXCEPTION_CONTINUE_EXECUTION -1

HandlerFunc就是之前的异常处理函数了。

​ 那么编译器为什么要选择对结构进行扩展呢?其原因是编译器并不会对每个__try块都生成一个EXCEPTION_REGISTRATION_RECORD结构,而是不管有多层嵌套调用的__try块都只生成一个EXCEPTION_REGISTRATION_RECORD结构并挂入线程的异常链表。对于那些不同__except块代码则用一个代理函数处理,这个代理函数就是扩展结构_EXCEPTION_REGISTRATION_RECORD_except_handler4函数,而我们自己写的__except代码则被存储在scopetable数组中。

​ 换句话说,编译器将多层的__try块进行抽象,使用_except_handler4来处理所有的__except代码,在scopetable数组中存储了相应层级的FilterFuncHandlerFunc,而扩展结构中的TryLevel就是这个数组的索引,_except_handler4就可以通过这些结构来调用相应的函数。

再谈展开

​ 前面提及展开就是给函数第二次机会,让他清理一下自己的东西,并且把SEH的链表头设置为当前的SEH。这一部分是操作系统自动完成的,当然我们也可以手动进行展开操作,微软提供一个APIRtlUnwind可以完成这个操作,这个函数声明如下

1
RtlUnwind(VirtualTargetFrame,TargetPC,ExceptionRecord,ReturnValue)

参数说明:

VirtualTargetFrame:展开时最后SEH停止与回调函数对应的EXCEPTION_REGISTRATION_RECORD指针,即希望在哪个回调函数前展开调用停止

TargetPC:调用RtlUnwind返回后应执行指令的地址,如果0,则自然返回RtlUnwind调用后下一条指令

ExceptionRecord:当前异常EXCEPTION_RECORD结构

ReturnValue:返回值,通常不使用

​ 实际上大多时候并不需要自己手动调用,微软也并没有十分公开此API,由操作系统自动调用即可。

例子

以今年moectf为例来解说一下吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __cdecl main_0(int argc, const char **argv, const char **envp)
{
char v4; // [esp+0h] [ebp-100h]

__CheckForDebuggerJustMyCode(&unk_5EC063);
puts("Welcome to moectf2023!!! Now you find YunZh1Jun's revenge!!!");
puts("Do you know TEA(an encryption algorithm)? Do you know unwind in SEH? ");
puts("I believe you can understand them! So let me check your flag~");
sub_5E10CD("Input:", v4);
sub_5E13CA("%64s", (char)&flag);
MEMORY[0] = 0;
sub_5E10FF();
puts("Right flag! Have fun in moectf2023~");
return 0;
}

​ 这是主函数的反汇编,如果使用IDA打开会发现大红大红的MEMORY[0] = 0,实际上就是故意引发错误来使用SEH,IDA并不会把__except内容给反汇编出来,所以必须看汇编代码,如下是相关的汇编代码

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
.text:005E5908 ;   __try { // __except at loc_5E5928
.text:005E5908 mov [ebp+ms_exc.registration.TryLevel], 0
.text:005E590F mov large dword ptr ds:0, 0
.text:005E590F ; } // starts at 5E5908
.text:005E5919 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:005E5920 jmp short loc_5E597A

.text:005E5928 loc_5E5928:
.text:005E5928 ; __except(loc_5E5922) // owned by 5E5908
.text:005E5928 mov esp, [ebp+ms_exc.old_esp]
.text:005E592B push offset aDx3906 ; "DX3906"
.text:005E5930 push offset flag
.text:005E5935 call enc
.text:005E593A add esp, 8
.text:005E593D push offset aDoctor3 ; "doctor3"
.text:005E5942 push offset dword_5EA580
.text:005E5947 call enc
.text:005E594C add esp, 8
.text:005E594F push offset aFux1aoyun ; "FUX1AOYUN"
.text:005E5954 push offset unk_5EA588
.text:005E5959 call enc
.text:005E595E add esp, 8
.text:005E5961 push offset aR3verier ; "R3verier"
.text:005E5966 push offset dword_5EA590
.text:005E596B call enc
.text:005E5970 add esp, 8
.text:005E5973 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh

​ 其中//所注释的语句都是IDA自动识别添加的,可以通过这几个注释语句很明显的看到__try{} __except{}结构,而__except括号中的代码就应该是异常筛选函数了,这题没有使用异常筛选函数,均是直接处理异常。

​ 所以loc_5E5928处代码就是相关的异常处理函数了,此异常的主要代码就在这里面,所使用的加密算法就是普通的TEA,这不是本文主要分析内容且相关加解密也很简单就不再赘述。

​ 继续往下看

1
2
3
4
5
6
7
8
9
10
11
12
13
loc_5E597A:
.text:005E597A ; __try { // __except at loc_5E5995
.text:005E597A mov [ebp+ms_exc.registration.TryLevel], 1
.text:005E5981 call sub_5E10FF
.text:005E5981 ; } // starts at 5E597A
.text:005E5986 mov [ebp+ms_exc.registration.TryLevel], 0FFFFFFFEh
.text:005E598D jmp short loc_5E5A01

loc_5E5995:
.text:005E5995 ; __except(loc_5E598F) // owned by 5E597A
.text:005E5995 mov esp, [ebp+ms_exc.old_esp]
.text:005E5998 mov [ebp+var_20], 0
.text:005E599F jmp short loc_5E59AA

​ 显然此时的关键函数是__try块里的sub_5E10FF,跟进去查看

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
.text:005E1820 var_E4= dword ptr -0E4h
.text:005E1820 var_10= byte ptr -10h
.text:005E1820 var_C= dword ptr -0Ch
.text:005E1820 var_4= dword ptr -4
.text:005E1820
.text:005E1820 push ebp
.text:005E1821 mov ebp, esp
.text:005E1823 sub esp, 0D0h
.text:005E1829 push ebx
.text:005E182A push esi
.text:005E182B push edi
.text:005E182C lea edi, [ebp+var_10]
.text:005E182F mov ecx, 4
.text:005E1834 mov eax, 0CCCCCCCCh
.text:005E1839 rep stosd
.text:005E183B mov eax, ___security_cookie
.text:005E1840 xor eax, ebp
.text:005E1842 mov [ebp+var_4], eax
.text:005E1845 mov [ebp+var_C], offset sub_5E12FD
.text:005E184C push [ebp+var_C]
.text:005E184F push large dword ptr fs:0
.text:005E1856 mov large fs:0, esp
.text:005E185D int 3 ; Trap to Debugger
.text:005E185E mov eax, [esp+0E4h+var_E4]
.text:005E1861 mov large fs:0, eax
.text:005E1867 add esp, 8
.text:005E186A pop edi
.text:005E186B pop esi
.text:005E186C pop ebx
.text:005E186D mov ecx, [ebp+var_4]
.text:005E1870 xor ecx, ebp ; StackCookie
.text:005E1872 call j_@__security_check_cookie@4 ; __security_check_cookie(x)
.text:005E1877 add esp, 0D0h
.text:005E187D cmp ebp, esp
.text:005E187F call j___RTC_CheckEsp
.text:005E1884 mov esp, ebp
.text:005E1886 pop ebp
.text:005E1887 retn

​ 让我们把目光集中到地址5E1845处

1
2
3
4
5
.text:005E1845 mov     [ebp+var_C], offset sub_5E12FD
.text:005E184C push [ebp+var_C]
.text:005E184F push large dword ptr fs:0
.text:005E1856 mov large fs:0, esp
.text:005E185D int 3 ; Trap to Debugger

​ 这几句汇编是否觉得很熟悉,这不就是SEH安装流程嘛!。先回调函数压栈,再压入当前的fs:[0],然后再将上一个SEH地址存储到fs:[0],最后引发一个断点异常,所以此时关键就是那个回调函数sub_5E12FD

​ 现在就来分析一下这个回调函数吧

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
.text:005E1B50 push    ebp
.text:005E1B51 mov ebp, esp
.text:005E1B53 sub esp, 0C0h
.text:005E1B59 push ebx
.text:005E1B5A push esi
.text:005E1B5B push edi
.text:005E1B5C mov edi, ebp
.text:005E1B5E xor ecx, ecx
.text:005E1B60 mov eax, 0CCCCCCCCh
.text:005E1B65 rep stosd
.text:005E1B67 mov ecx, offset unk_5EC063
.text:005E1B6C call j_@__CheckForDebuggerJustMyCode@4 ; __CheckForDebuggerJustMyCode(x)
.text:005E1B71 push offset aDx3906 ; "DX3906"
.text:005E1B76 push offset dword_5EA598
.text:005E1B7B call enc
.text:005E1B80 add esp, 8
.text:005E1B83 push offset aDoctor3 ; "doctor3"
.text:005E1B88 push offset unk_5EA5A0
.text:005E1B8D call enc
.text:005E1B92 add esp, 8
.text:005E1B95 push offset aFux1aoyun ; "FUX1AOYUN"
.text:005E1B9A push offset unk_5EA5A8
.text:005E1B9F call enc
.text:005E1BA4 add esp, 8
.text:005E1BA7 push offset aR3verier ; "R3verier"
.text:005E1BAC push offset dword_5EA5B0
.text:005E1BB1 call enc
.text:005E1BB6 add esp, 8
.text:005E1BB9 mov eax, 1
.text:005E1BBE pop edi
.text:005E1BBF pop esi
.text:005E1BC0 pop ebx
.text:005E1BC1 add esp, 0C0h
.text:005E1BC7 cmp ebp, esp
.text:005E1BC9 call j___RTC_CheckEsp
.text:005E1BCE mov esp, ebp
.text:005E1BD0 pop ebp
.text:005E1BD1 retn

​ 这个处理函数所执行的操作也是TEA加密,但是我们关注最后的返回值是1,也即ExceptionContinueSearch,也就是说操作系统认为这个处理函数没有真正处理异常,这就需要向外去找__except块代码来处理,但是在调用__except之前,操作系统还会再调用以一次这个函数,也就是我们之前提及的展开,可以把一段代码与初探展开的代码比较,会发现有异曲同工之妙。所以这一块涉及的密文需要两次TEA解密,这也是此题真正的考点。

​ 后续的内容都比较简单了,就是比较密文,相同即可得到flag,只要按部就班根据TEA解密就行了,不过需要注意的是后半部分的flag需要两次TEA解密,而且TEA使用的K数组是DWORD型的。

​ 完结!

参考资料:

1.《加密与解密》—— Windows下异常处理

2.深入解析结构化异常

3.《逆向工程核心原理》—— SEH部分


SEH笔记
http://example.com/2023/08/23/SEH/
Author
yring
Posted on
August 23, 2023
Licensed under