首页
论坛
课程
招聘
[原创]Windows内核学习笔记之异常(下)
2021-12-31 21:24 11069

[原创]Windows内核学习笔记之异常(下)

2021-12-31 21:24
11069

上篇:Windows内核学习笔记之异常(上)

五.被编译器扩展的SEH

1.概览

在实际开发中,直接通过手动在栈中加入结构化异常处理器的方式存在以下两点明显不足:

  • 需要编写符合SehHandler函数原型的处理函数,要理解_CONTEXT等较复杂的数据结构和概念

  • 因为需要直接操作栈指针,所以无法将登记与销毁异常处理器的代码封装到一个普通的C/C++函数中。这就需要在每个需要保护的程序块前后插入两点嵌入式汇编代码,这会影响程序的简洁性

为了解决上述的问题,编译器对该机制进行了优化,主要完成以下两项任务:

  • 定义必要的关键字来表示异常处理逻辑,供编程人员使用。比如,VC编译器定义了try,except和__finally这3个扩展关键字,运行C和C++程序使用这套关键字来编写异常处理代码

  • 实现对以上关键字的编译,将使用这些关键字编写的异常处理代码与操作系统的SEH机制衔接起来

2.语法

VC编译器优化以后的SEH为程序员提供了如下两种功能:

  • 异常处理功能:用于接收和处理被保护块中的代码所发生的异常

  • 终结处理功能:保证终结处理块始终可以得到执行

A.异常处理

异常处理的语法结构如下:

1
2
3
4
5
6
7
8
__try
{
    // 被保护体,也就是要保护的代码块
}
__except(过滤表达式)
{
    // 异常处理块(exception-handling block)
}

除了try和except关键字以外,Visual C++编译器还提供了如下两个宏来辅助编写异常处理代码:

  • DWORD GetExceptionCode():返回异常代码。只能在过滤表达式或异常处理块中使用这个宏

  • LPEXCEPTION_POINTERS GetExceptionInformation():返回一个指向EXCEPTION_POINTERS结构的指针。只能在过滤表达式中使用这个宏

过滤表达式既可以是常量,函数调用,也可以是用条件表达式或其他表达式,只要表达式的结果为0,1,-1这三个值之一,它们的含义如下:

名称 含义
0 EXCEPTION_CONTINUE_SEARCH 本保护块不处理该异常,让系统继续寻找其他异常保护块
-1 EXCEPTION_CONTINUE_EXECUTION 已经处理异常,让程序回到异常发生点继续执行,如果导致异常的情况没有被消除,那么可能还会再次发生异常
1 EXCEPTION_EXECUTE_HANDLER 这是本保护块预计到的异常,让系统执行本块中的异常处理代码,执行完后会继续执行本异常处理块下面的代码,即except块之后的第一条指令
 

以下是常见的三种编写过滤表达式的方法:

  • 直接使用常量:比如except(EXCEPTION_EXECUTE_HANDLER),或者直接写为except(-1)等

  • 使用条件运算符:比如__except(GetExceptionCode == EXCEPTION_ACCESS_VIOLATION ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)。其含义是,如果发生的异常是非法访问异常,那么久执行异常处理块;否则,就继续搜索其他异常保护块

  • 调用其他函数,通常将GetExceptionCode()得到的异常代码或GetExceptionInformation()得到的异常信息作为参数传给该函数。例如__except(ExcptFilter(GetExceptionInformation()))

可见,可以根据具体需要设计不同复杂度的表达式,可短到一个常熟,可长到编写过滤函数进行一系列操作。

 

以下代码是对于上述内容的样例:

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
#include <cstdio>
#include <Windows.h>
 
DWORD x, y, z;
 
int ExceptFilter(LPEXCEPTION_POINTERS pException)
{
    // 如果是除0异常
    if (pException->ExceptionRecord->ExceptionCode == EXCEPTION_INT_DIVIDE_BY_ZERO)
    {
        y = 10;
        printf("ExceptFilter函数执行,异常已被处理\n");
        return EXCEPTION_CONTINUE_EXECUTION;    // 继续执行出错的代码
    }
 
    return EXCEPTION_CONTINUE_SEARCH;    // 该异常块不处理
}
 
void test1()
{
    __try
    {
        z = x / y;
        printf("在try中输出z的值为:%d\n", z);
    }
    __except (ExceptFilter(GetExceptionInformation()))
    {
        printf("test1的 except块被执行\n");
    }
}
 
void test2()
{
    __try
    {
        z = x / y;
    }
    // 判断是否为除0错误,如果是执行错误处理代码,否则不执行
    __except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
        y = 10;
        z = x / y;
        printf("test2的except被执行,错误已被处理, z的值为:%d\n", z);
    }
}
 
 
int main()
{
    x = 1900, y = 0, z = 0;
    test1();
 
    x = 1900, y = 0, z = 0;
    test2();
 
    return 0;
}

程序的运行结果如下:

 

avatar

B.终结处理

终结处理的语法结构如下:

1
2
3
4
5
6
7
8
__try
{
    // 被保护体(guarded body),也就是要保护的代码块
}
__finally
{
    // 终结代码块
}

终结处理由两部分构造,使用try关键字定义的被保护体和使用_finally关键字定义的终结处理块。终结处理的目标是只要被保护体被执行,那么终结处理块就也会被执行,除非被保护体中的代码终止了当前线程(比如调用ExitThread或ExitProcess退出线程或整个进程)。因为终结处理块的这种特征,终结处理非常适合做状态恢复或资源释放等工作。比如释放被保护块中获得的信号量以防止被保护块内发生意外时因没有释放这个信号量而导致线程死锁的问题。

 

根据被保护块的执行路线,SEH把被保护块的退出(执行完毕)分为正常结束和非正常结束两种。如果被保护块得到自然执行并顺序进入终结处理块,就认为被保护块是正常结束的。如果被保护块是因为发生异常或由于return, goto, break或continue等流程控制语句离开被保护块的,就认为被保护块是非正常结束的。在终结处理块中可以调用以下函数来知道被保护块的退出方式:

1
BOOL AbnormalTermination(void);

如果被保护块正常结束,那么该函数返回FALSE;否则,返回TRUE。该函数只能在终结块中调用。

 

除了上面出现的try和finally关键字,终结处理还有一个关键字leave。该关键字的作用是立即离开(停止执行)被保护块,或者理解为立即跳转到被保护块的末尾(try块的右大括号)。__leave关键字只能出现在被保护体中,使用该关键字的退出属于正常退出。

 

以下代码是上述内容的简单样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <cstdio>
#include <windows.h>
 
int main()
{
    __try
    {
        printf("开始执行__try中的代码\n");
        printf("执行__leave之前的代码\n");
        __leave;
        printf("执行__leave之后的代码\n");
    }
    __finally
    {
        printf("执行__finally的代码\n");
    }
 
    return 0;
}

输出如下:

 

avatar

3.SEH的编译

A.编译器的扩展

不同的编译器对SEH的编译优化结果稍有不同,对于Visual Studio2017来说,其主要做了以下两个方面的修改:

  • 编译器编译try{}catch()结构时,总是将统一的__except_handler3登记为异常处理函数。这样做有利于代码复用和减少目标文件的大小

  • 为了使用统一的异常处理函数(excepthandler3)来满足不同SEH块的需求。在栈上准备EXCEPTION_REGISTRATION_RECORD结构前,编译器产生的代码会压入一个trylevel的整数和一个执行scopetable_entry结构的scopetable指针

总之,编译器会将EXCEPTION_REGISTRATION_RECORD结构扩展为如下的形式:

1
2
3
4
5
6
7
8
struct _EXCEPTION_REGISTRATION
{
    struct _EXCEPTION_REGISTRATION* prev;   
    void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_ REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);   
    struct scopetable_entry * scopetable;
    int trylevel;   
    int _ebp;
};
字段 作用
prev 指向上一个结构体的地址
handler 指向异常处理函数
scopetable 范围表的起始地址
trylevel 这个结构对应的__try块的编号
ebp 栈帧的基地址
 

其中前两个字段是操作系统规定的标准登记结构,后三个字段是编译器扩展的。登记处理函数整数依靠这几个扩展字段来寻找过滤表达式和异常处理块。

B.范围表

为了描述应用程序代码中的tryexcept结构,编译器在编译每个使用此结构的函数时会为其建立一个数组,并存储在模块文件的数据区(通常称为异常处理范围表)中。数组的每个元素是一个scopetable_entry结构,用来描述一个tryexcept结构。

1
2
3
4
5
6
struct scopetable_entry
{
       DWORD      previousTryLevel    //上一个try{}结构编号
       PARPROC    lpfnFilter           //过滤函数的起始地址
       PARPROC    lpfnHandler          //异常处理程序的地址       
};

其中lpfnFilter和lpfnHandler分别用来描述try{}except结构的过滤表达式和异常处理块的起始地址。

C.TryLevel

编译器是以函数为单位来登记异常处理器的,在函数的入口处进行登记,在出口处进行注销。为了确定导致异常的代码是否在保护块中,以及,如果在有多个保护块的情况下,判断属于哪个保护块。编译器为每个try结构进行编号,然后使用一个局部变量来记录当前处于哪个try结构中,这个局部变量称为trylevel,也就是在栈上形成的异常处理结构的trylevel字段。

 

编号从0开始,常量TRYLEVEL_NONE(-1)作为特殊值代表不再任何try结构中。也就是说,trylevel变量被初始化为-1,然后执行到_try结构中时,便将它的编号赋给TryLevel变量。

D.tryexcept()结构的执行

当位于__try{}结构保护块中的代码发生异常时,异常分发函数便会调用_except_handler3这样的处理函数。_except_handler3函数所指向的主要操作如下:

  1. 将第二个参数pRegistrationRecord从系统默认的EXCEPTION_REGISTRATION_RECORD结构强制转换为包含扩展字段的_EXCEPTION_REGISTRATION结构

  2. 先从pRegistrationRecord结构中取出trylevel字段的值并且赋给一个局部变量nTryLevel ,然后根据nTryLevel的值从scopetable字段所 指定的数组中找到一个scopetable_entry结构

  3. 从scopetable_entry结构中取出lpfnFilter字段,如果不为空,则调用这个函数,即评估过滤表达式,如果为空,则跳到第5步

  4. 如果lpfnFilter函数的返回值不等于EXCEPTION_CONTINUE_SEARCH,则准备指向lpfnHanlder字段所指定的函数,并且不再返回。如果过滤表达式返回的是EXCEPTION_CONTINUE_SEARCH,则自然进入(fall through)第五步

  5. 判断scopetable_entry结构的previousTryLevel字段的取值。如果它不等于-1,则将previousTryLevel赋给nTryLevel并返回第2步继续循环;如果previousTryLevel等于-1,那么继续第6步

  6. 返回DISPOSITION_CONTINUE_SEARCH,让系统(RtlDispatchException)继续寻找其他异常处理器

以以下代码为例,体会上述内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
DWORD x = 1900, y = 0, z = 0;
 
void test()
{
    __try
    {
        __try
        {
            z = x / y;
        }
        __except (EXCEPTION_CONTINUE_SEARCH)
        {
 
        }
    }
    __except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
        y = 10;
        z = x / y;
        printf("z = %d\n", z);
    }
}

以下是对test函数的反汇编结果

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
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
void test()
{
00401000  push        ebp                      // 填充ebp
00401001  mov         ebp,esp 
00401003  push        0FFFFFFFFh                // 填充trylevel为-1 
00401005  push        48ADD8h                      // 填充scopetable范围表的起始地址
0040100A  push        offset _except_handler3 (0402968h)    // 填充异常处理函数 
0040100F  mov         eax,dword ptr fs:[00000000h
00401015  push        eax                      // 填充prev
00401016  mov         dword ptr fs:[0],esp              // 将异常处理器挂到fs:[0]中
0040101D  add         esp,0FFFFFFB0h 
00401020  push        ebx 
00401021  push        esi 
00401022  push        edi 
00401023  mov         dword ptr [ebp-18h],esp 
00401026  mov         ecx,offset _D3472036_test@cpp (048F012h
0040102B  call        __CheckForDebuggerJustMyCode (04011D0h
    __try
00401030  mov         dword ptr [ebp-4],0            // 进入try块,将trylevel赋值为0 
    {
        __try
00401037  mov         dword ptr [ebp-4],1              // 进入try块,将trylevel赋值为1
        {
            z = x / y;
0040103E  mov         eax,dword ptr [x (048D000h)]          // 执行try块代码
00401043  xor         edx,edx 
00401045  div         eax,dword ptr [y (048D910h)] 
0040104B  mov         dword ptr [z (048D914h)],eax 
        }
00401050  mov         dword ptr [ebp-4],0              // 离开try块,将trylevel赋值为0
00401057  jmp         test+66h (0401066h)              // 执行跳转,略过except块中内容
        __except (EXCEPTION_CONTINUE_SEARCH)
00401059  xor         eax,eax                      // 对过滤表达式进行运算,这里返回值是0(EXCEPTION_CONTINUE_SEARCH)
$LN16:
0040105B  ret 
$LN13:
0040105C  mov         esp,dword ptr [ebp-18h
        }
0040105F  mov         dword ptr [ebp-4],0              // 离开try块,将trylevel赋值为0
        {
 
        }
    }
00401066  mov         dword ptr [ebp-4],0FFFFFFFFh          // 离开try块,将trylevel赋值为-1
0040106D  jmp         $LN9+39h (04010CFh)              // 执行跳转,略过except块中内容
    __except (GetExceptionCode() == EXCEPTION_INT_DIVIDE_BY_ZERO ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
0040106F  mov         eax,dword ptr [ebp-14h]             
00401072  mov         ecx,dword ptr [eax] 
00401074  mov         edx,dword ptr [ecx] 
00401076  mov         dword ptr [ebp-5Ch],edx 
00401079  cmp         dword ptr [ebp-5Ch],0C0000094h 
00401080  jne         test+8Bh (040108Bh
00401082  mov         dword ptr [ebp-60h],1 
00401089  jmp         test+92h (0401092h
0040108B  mov         dword ptr [ebp-60h],0 
00401092  mov         eax,dword ptr [ebp-60h]        //对过滤表达式进行运算, 这里返回值是1(EXCEPTION_EXECUTE_HANDLER)
$LN17:
00401095  ret 
$LN9:
00401096  mov         esp,dword ptr [ebp-18h
    {
        y = 10;
00401099  mov         dword ptr [y (048D910h)],0Ah          // 执行异常处理代码
        z = x / y;
004010A3  mov         eax,dword ptr [x (048D000h)] 
004010A8  xor         edx,edx 
004010AA  div         eax,dword ptr [y (048D910h)] 
004010B0  mov         dword ptr [z (048D914h)],eax 
        printf("z = %d\n", z);
004010B5  mov         eax,dword ptr [z (048D914h)] 
004010BA  push        eax 
004010BB  push        offset string "z = %d\n" (04701B0h
004010C0  call        printf (0401180h
004010C5  add         esp,8 
        {
 
        }
    }
004010C8  mov         dword ptr [ebp-4],0FFFFFFFFh          // 离开try块,将trylevel赋值为-1
    }
}
004010CF  mov         ecx,dword ptr [ebp-10h
004010D2  mov         dword ptr fs:[0],ecx              // 将异常处理器从fs:[0]中摘除
004010D9  pop         edi 
004010DA  pop         esi 
004010DB  pop         ebx 
004010DC  mov         esp,ebp 
004010DE  pop         ebp 
004010DF  ret

根据上面的代码,可以得出如下结论:

  • 无论是多少个tryexcept结构,在函数中只挂载了一个异常处理器

  • 无论是当进入一个try块,还是离开一个try块,都会修改trylevel的值,来代表目前处于哪个try结构中

  • try块,条件过滤表达式, except块的代码是按顺序保存下来,但是每个块后面都会有跳转语句,避免其顺序执行(jmp, ret)

此时查看scopetable数组的内容:

1
2
3
4
5
6
0x0048ADD8  ff ff ff ff  // 第一个try块的previoustrylevel
0x0048ADDC  6f 10 40 00  // 第一个try块的条件过滤表达式地址
0x0048ADE0  96 10 40 00  // 第一个try块的异常处理块地址
0x0048ADE4  00 00 00 00  // 第二个try块的previoustrylevel
0x0048ADE8  59 10 40 00  // 第二个try块的条件过滤表达式的地址
0x0048ADEC  5c 10 40 00  // 第二个try块的异常处理块地址

可以看到数组中的每个元素都对应了try块的信息,这样就可以让异常处理函数根据每个数组元素保存的数据来找到相应的处理函数。

4.栈展开

相比于tryexcept结构的异常处理,tryfinally结构的终结处理是为了保证finally块中的代码可以执行。比如以下的代码,无论是否将出现异常的代码注释掉,finally块的代码都会被执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
DWORD x = 1900, y = 0, z = 0;
 
void test()
{
    __try
    {
        printf("try块执行\n");
        // z = x / y;
    }
    __finally
    {
        printf("finally代码执行\n");
    }
}

根据以下的反汇编代码可以知道,当使用tryfinally结构的时候,程序会在离开try块之前的代码加入一个call指令,该call指令的地址就是finally块的地址。

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
void test()
{
004010B0  push        ebp 
004010B1  mov         ebp,esp 
004010B3  push        0FFFFFFFFh 
004010B5  push        48AE08h 
004010BA  push        offset _except_handler3 (04029A8h
004010BF  mov         eax,dword ptr fs:[00000000h
004010C5  push        eax 
004010C6  mov         dword ptr fs:[0],esp 
004010CD  add         esp,0FFFFFFB8h 
004010D0  push        ebx 
004010D1  push        esi 
004010D2  push        edi 
004010D3  mov         ecx,offset _D3472036_test@cpp (048F012h
004010D8  call        __CheckForDebuggerJustMyCode (0401210h
    __try
004010DD  mov         dword ptr [ebp-4],0          // 进入try块,trylevel赋值为0
    {
        printf("try块执行\n");
004010E4  push        offset string "try\xbf\xe9\xd6\xb4\xd0\xd0\n" (04701B0h
004010E9  call        printf (04011C0h
004010EE  add         esp,4 
        // z = x / y;
    }
004010F1  mov         dword ptr [ebp-4],0FFFFFFFFh      // 离开try块,trylevel赋值为-1
004010F8  call        test+4Fh (04010FFh)          // 调用finally块中的代码
004010FD  jmp         $LN8 (040110Dh
    __finally
    {
        printf("finally代码执行\n");
004010FF  push        offset string "finally\xb4\xfa\xc2\xeb\xd6\xb4\xd0\xd0\n" (04701BCh
00401104  call        printf (04011C0h
00401109  add         esp,4 
$LN9:
0040110C  ret 
    }
}
0040110D  mov         ecx,dword ptr [ebp-10h
    }
}
00401110  mov         dword ptr fs:[0],ecx 
00401117  pop         edi 
00401118  pop         esi 
00401119  pop         ebx 
0040111A  mov         esp,ebp 
0040111C  pop         ebp 
0040111D  ret

但是出现异常的时候,是无法执行离开try块的代码的。而此时之所以finally块的代码会被执行,就依赖于栈展开机制。

 

此时查看scopetable数组的值,可以看到此时的注册表达式的起始地址为0,异常处理块的起始地址变为finally块的起始地址。此时异常处理函数在执行代码时,根据注册表达式的起始地址为0得知,此时需要将第三个参数作为finally块的地址进行调用。

1
2
3
0x0048AE08  ff ff ff ff 
0x0048AE0C  00 00 00 00 
0x0048AE10  ff 10 40 00        // finally块的地址

而对于以下的代码,程序在输出异常处理块的内容之前会先输出finally块中的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DWORD x = 1900, y = 0, z = 0;
 
void test()
{
    __try
    {
        __try
        {
            z = x / y;
        }
        __finally
        {
            printf("finally代码执行\n");
        }
    }
    __except (EXCEPTION_EXECUTE_HANDLER)
    {
        printf("except代码执行\n");
    }
}

代码的反汇编结果就是异常处理块和终结处理块的组合

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
void test()
{
00401000  push        ebp 
00401001  mov         ebp,esp 
00401003  push        0FFFFFFFFh 
00401005  push        48ADF8h 
0040100A  push        offset _except_handler3 (0402938h
0040100F  mov         eax,dword ptr fs:[00000000h
00401015  push        eax 
00401016  mov         dword ptr fs:[0],esp 
0040101D  add         esp,0FFFFFFB8h 
00401020  push        ebx 
00401021  push        esi 
00401022  push        edi 
00401023  mov         dword ptr [ebp-18h],esp 
00401026  mov         ecx,offset _D3472036_test@cpp (048F012h
0040102B  call        __CheckForDebuggerJustMyCode (04011A0h
    __try
00401030  mov         dword ptr [ebp-4],0          // 进入try块,将trylevel赋值为0
    {
        __try
00401037  mov         dword ptr [ebp-4],1          // 进入try块,将trylevel赋值为1
        {
            z = x / y;
0040103E  mov         eax,dword ptr [x (048D000h)]      // try块中的代码
00401043  xor         edx,edx 
00401045  div         eax,dword ptr [y (048D910h)] 
0040104B  mov         dword ptr [z (048D914h)],eax 
        }
00401050  mov         dword ptr [ebp-4],0      // 离开try块,将trylevel赋值为0
00401057  call        test+5Eh (040105Eh)     // 执行finally块中的代码
0040105C  jmp         test+6Ch (040106Ch
        __finally
        {
            printf("finally代码执行\n");   
0040105E  push        offset string "finally\xb4\xfa\xc2\xeb\xd6\xb4\xd0\xd0\n" (04701B0h// finally块中的代码
00401063  call        printf (0401150h
00401068  add         esp,4 
0040106B  ret 
        }
    }
0040106C  mov         dword ptr [ebp-4],0FFFFFFFFh 
00401073  jmp         $LN7+17h (0401090h
    __except (EXCEPTION_EXECUTE_HANDLER)
00401075  mov         eax, 1              // 条件过滤表达式
00401078  ret 
00401079  mov         esp,dword ptr [ebp-18h
    {
        printf("except代码执行\n");        // except块中的内容
0040107C  push        offset string "except\xb4\xfa\xc2\xeb\xd6\xb4\xd0\xd0\n" (04701C4h
00401081  call        printf (0401150h
00401086  add         esp,4 
        }
    }
00401089  mov         dword ptr [ebp-4],0FFFFFFFFh 
    }
}
00401090  mov         ecx,dword ptr [ebp-10h
00401093  mov         dword ptr fs:[0],ecx 
0040109A  pop         edi 
0040109B  pop         esi 
0040109C  pop         ebx 
0040109D  mov         esp,ebp 
0040109F  pop         ebp 
004010A0  ret

此时查看socpetable数组的值,可以看到外层try块的元素内容没有变化,而内层try块的条件过滤表达式变为0,异常处理块地址也变成了finally块的地址。因此,异常处理函数在执行的时候,就可以根据条件过滤表达式是否为0来判断要不要执行第三个成员保存的函数地址。

1
2
3
4
5
6
0x0048ADF8  ff ff ff ff  // 第一个块的previoustrylevel
0x0048ADFC  75 10 40 00  // 第一个块条件过滤表达式地址
0x0048AE00  79 10 40 00  // 第一个块异常处理地址
0x0048AE04  00 00 00 00  // 第二个块的previoustrylevel
0x0048AE08  00 00 00 00  // 第二个块条件过滤表达式
0x0048AE0C  5e 10 40 00  // 第二个块finally块的地址

异常处理函数就通过这种方式实现了在执行except块的代码之前先执行finally块的代码,结果如下:

 

avatar

六.未处理异常

一个新建进程的起始地址其实并不是PE文件中的AddressOfEntryPoint字段的偏移地址,而是kernel32.dll中BaseProcessStart函数,该函数的伪代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void BaseProcessStart(PPROCESS_START_ROUTINE lpfnEntryPoint)
{
    __try
    {
        NtSetInformationThread(GetCurrentThread(),
                               ThreadQuerySetWin32StartAddress,
                               &lpfnEntryPoint, sizeof(lpfnEntryPoint));
        ExitThread((lpfnEntryPoint)());
    }
    __except(UnhandledExceptionFilter(GetExceptionInformation()))
    {
        ExitThread(GetExceptionCode());
    }
}

根据以上伪代码可以知道,在应用程序的入口函数被调用前,BaseProcessStart会为其先设置一个结构化异常处理器,它是初始线程中最早注册的异常处理器。因为当分发异常时,系统是从最晚注册的异常处理器来查找的,所以这个最早注册的结构化异常处理器是最后得到处理机会的。这样便保证了只有当应用程序自己设计的代码没有处理异常时,这个默认的结构化异常处理器才会得到处理机会。

 

以上代码中的lpfnEntryPoint指向的就是入口函数。在try块中,根据函数的返回值作为参数来调用ExitThread函数退出线程。而如果执行到了异常处理块中,则会将GetExceptionCode函数的返回值作为参数调用ExitThread函数。

 

当程序出现异常的时候,是否要执行异常块中的代码则是由函数UnhandledExceptionFilter决定的,该函数的执行流程如下:

  1. 通过NtQueryInformationProcess查询当前进程是否被调试。如果正在被调试,函数将会返回EXCEPTION_CONTINUE_SEARCH,随后调用ZwRaiseException来进行第二次异常分发

  2. 如果当前进程没有被调试:

    • 查询是否通过SetUnhandlerExceptionFilter注册了处理函数,如果已经注册了,那么就调用已注册的处理函数

    • 如果没有注册处理函数,就会弹出窗口让用户选择是终止程序还是启动即时调试器。如果用户启动了即时调试器,接下来就会由调试器来接管进程,函数就会返回EXCEPTION_EXECUTION_HANDLER

七.参考资料

  • 《软件调试(第二版)》卷一

  • 《软件调试(第二版)》卷二


【公告】看雪招聘大学实习生!看雪20年安全圈的口碑,助你快速成长!

最后于 2021-12-31 21:26 被1900编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回