C++基于SEH二次封装的异常流程与识别
在看代码之前我们先连简单的看下try的处理流程吧
- 函数入口设置回调函数
- 函数的异常抛出使用了__CxxThrowException函数,此函数包含了两个参数,分别是抛出一场关键字的throw的参数的指针,另一个抛出信息类型的指针(ThrowInfo *)。
- 在异常回调函数中,可以得到异常对象的地址和对应ThrowInfo数据的地址以及FunInfo表结构的地址。根据记录的异常类型,进行try块的匹配工作
- 没找到try块怎么办?先调用异常对象的析构函数,然后反汇ExcetionContinueSearch,继续反回到SEH继续执行。
- 找到了try块?通过TryBlockMapEntry结构中的pCatch指向catch信息,用ThrowInfo结构中的异常类型遍历查找相匹配的catch块,比较关键字名称,找到有效的catch块。
- 然后进行栈展开。
- 析构try块中的对象
- 跳转到catch块中执行
- 调用_JumpToContinuation函数,返回catch语句块的结束地址。
上面的步骤,就是典型的异常处理的顺序。
光看文字多无趣,上代码 - 实例分析,我们来跑一遍:
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
|
class CExcepctionBase { public : CExcepctionBase() { printf ( "CExcepctionBase() \r\n" ); } virtual ~CExcepctionBase() { printf ( "~CExcepctionBase()\r\n" ); } }; class CExcepctionDiv0 : public CExcepctionBase { public : CExcepctionDiv0() { printf ( "CExcepctionDiv0()\r\n" ); } virtual ~CExcepctionDiv0(){ printf ( "~CExcepctionDiv0()\r\n" ); }; // 获取错误码 virtual char * GetErrorInfo() { return "CExcepctionDiv0" ; } private : int m_nErrorId ; }; class CExcepctionAccess : public CExcepctionBase { public : CExcepctionAccess() { printf ( "CExcepctionAccess()\r\n" ); } virtual ~CExcepctionAccess(){ printf ( "~CExcepctionAccess()\r\n" ); }; // 获取错误码 virtual char * GetErrorInfo() { return "CExcepctionAccess" ; } }; void TestException( int n) { try { if (n == 1) { throw n; } if (n == 2) { throw 3.0f; } if (n == 3) { throw '3' ; } if (n == 4) { throw 3.0; } if (n == 5) { throw CExcepctionDiv0(); } if (n == 6) { throw CExcepctionAccess(); } if (n == 7) { CExcepctionBase cExceptBase; throw cExceptBase; } } catch ( int n) { printf ( "catch int \n" ); } catch ( float f) { printf ( "catch float \n" ); } catch ( char c) { printf ( "catch char \n" ); } catch ( double d) { printf ( "catch double \n" ); } catch (CExcepctionBase cBase) { printf ( "catch CExcepctionBase \n" ); } catch (CExcepctionAccess cAccess) { printf ( "catch int \n" ); } catch (...) { printf ( "catch ... \n" ); } } int main(){ for ( int i = 0; i < 8; i++) { TestException(i); } return 0; } |
1
2
3
4
5
6
7
8
9
10
11
12
13
|
.text:004011A5 push offset SEH_4011A0 .text:004011AA mov eax, large fs:0 .text:004011B0 push eax .text:004011B1 sub esp, 40h .text:004011B4 push ebx .text:004011B5 push esi .text:004011B6 push edi .text:004011B7 mov eax, ___security_cookie .text:004011BC xor eax, ebp .text:004011BE push eax .text:004011BF lea eax, [ebp+var_C] .text:004011C2 mov large fs:0, eax ...... |
函数开始将异常回调函数压栈,在上文结尾的部分将此函数加入SEH中,这里并不讲解SEH相关信息,除了设置异常回调函数,和参数压栈还设置了security_cookie,防止栈溢出的检查数据,在此同样不予讲述。
我们走进SEH_4011A0看下实现:
1
2
3
|
...... .text:0040CAB1 mov eax, offset stru_40F53C .text:0040CAB6 jmp ___CxxFrameHandler3 |
无疑此项就是编译器产生的异常回调函数。
继续看异常抛出的部分:
1
2
3
4
5
6
7
8
9
10
|
...... .text:004011CE mov [ebp+var_4], 0 .text:004011D5 cmp eax, 1 .text:004011D8 jnz short loc_4011EB .text:004011DA mov [ebp+var_18], eax .text:004011DD push offset __TI1H ;ThrowInfo .text:004011E2 lea eax, [ebp+var_18];获取参数 .text:004011E5 push eax;压栈参数 .text:004011E6 call __CxxThrowException@8 ; _CxxThrowException(x,x) ...... |
熟悉的__CxxThrowException?没错他就是用来抛出异常的函数。
这里的__TI1H就是ThrowInfo结构,那么var_18也就是throw关键字后面跟随的数据。
后面连续的几个throw语句也差不多。
直到抛出对象的时候,代码如下:
1
2
3
4
5
6
7
8
9
10
11
|
...... .text:0040123A loc_40123A: ; CODE XREF: sub_4011A0+81↑j .text:0040123A cmp eax, 5 .text:0040123D jnz short loc_401255 .text:0040123F lea ecx, [ebp+var_34] .text:00401242 call sub_401030 .text:00401247 push offset __TI2?AVCExcepctionDiv0@@ ; .text:0040124C lea ecx, [ebp+var_34] .text:0040124F push ecx .text:00401250 call __CxxThrowException@8 ; _CxxThrowException(x,x) ...... |
这里很在抛出异常之前调用了一个函数sub_401030,这个函数的作用就是设置var_34的值,后面与前面的基本相同。
代码如下:
1
2
3
|
...... .text:00401048 mov dword ptr [esi], offset ??_7CExcepctionDiv0@@6B@ ; const CExcepctionDiv0::`vftable' ...... |
IDA友情提示,这是一个虚表,这个虚表里有两个函数。
这两个函数代码如下:
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
|
.text:004010A0 ; int __thiscall sub_4010A0( void *, char ) .text:004010A0 sub_4010A0 proc near ; DATA XREF: .rdata: const CExcepctionDiv0::`vftable'↓o .text:004010A0 .text:004010A0 arg_0 = byte ptr 8 .text:004010A0 .text:004010A0 push ebp .text:004010A1 mov ebp, esp .text:004010A3 push esi .text:004010A4 mov esi, ecx .text:004010A6 push offset aCexcepctiondiv ; "~CExcepctionDiv0()\r\n" .text:004010AB mov dword ptr [esi], offset ??_7CExcepctionDiv0@@6B@ ; const CExcepctionDiv0::`vftable' .text:004010B1 call _printf .text:004010B6 push offset aCexcepctionbas ; "~CExcepctionBase()\r\n" .text:004010BB mov dword ptr [esi], offset ??_7CExcepctionBase@@6B@ ; const CExcepctionBase::`vftable' .text:004010C1 call _printf .text:004010C6 add esp, 8 .text:004010C9 test [ebp+arg_0], 1 .text:004010CD jz short loc_4010D8 .text:004010CF push esi ; void * .text:004010D0 call ??3@YAXPAX@Z ; operator delete ( void *) .text:004010D5 add esp, 4 .text:004010D8 .text:004010D8 loc_4010D8: ; CODE XREF: sub_4010A0+2D↑j .text:004010D8 mov eax, esi .text:004010DA pop esi .text:004010DB pop ebp .text:004010DC retn 4 .text:004010DC sub_4010A0 endp |
在004010C9地址处做了一个判断,根据传入参数来决定是否释放空间(标准的虚析构函数),因为IDA载入了pdb文件,所以通过IDA的注释可以很清晰的理解这个函数是CExcepctionDiv0的析构函数。
另一个函数代码如下:
1
2
3
4
5
|
.text:00401090 .text:00401090 sub_401090 proc near ; DATA XREF: .rdata:0040D180↓o .text:00401090 mov eax, offset aCexcepctiondiv_1 ; "CExcepctionDiv0" .text:00401095 retn .text:00401095 sub_401090 endp |
在以上的代码来看识别throw语句并不困难,只要找到__CxxThrowException函数就可以找到throw语句了,并根据throw传递的参数,可以断定抛出的数据类型。
来看看catch吧:
1
2
3
4
5
6
7
8
9
|
.text:00401295 loc_401295: ; DATA XREF: .rdata:0040F570↓o .text:00401295 ; catch ( float ) // owned by 4011CE .text:00401295 push offset aCatchFloat ; "catch float \n" .text:0040129A call _printf .text:0040129F add esp, 4 .text:004012A2 mov eax, offset loc_4012A8 .text:004012A7 retn .text:004012A7 ; } // starts at 4011CE .text:004012A7 ; } // starts at 4011A0 |
同样IDA通过pdb文件为我们做出了友好的注释,但是所有的catch语句都会具有以下特点:
--- 没有平衡函数开始的堆栈
-- 返回时将eax赋值为一个地址
通过这两个特点来找到catch语句块是不是很轻松呢,毕竟不平衡堆栈就返回的情况可以说是极少数了吧。
其他的catch我们就不看了,代码都是类似的,那么赋值给eax的地址里面保存了何方神圣?
来看一看:
1
2
3
4
5
6
7
8
9
10
11
|
.text:004012A8 loc_4012A8: ; CODE XREF: sub_4011A0+107↑j .text:004012A8 ; DATA XREF: sub_4011A0+102↑o .text:004012A8 mov ecx, [ebp+var_C] .text:004012AB mov large fs:0, ecx .text:004012B2 pop ecx .text:004012B3 pop edi .text:004012B4 pop esi .text:004012B5 pop ebx .text:004012B6 mov esp, ebp .text:004012B8 pop ebp .text:004012B9 retn |
这样看起来是不是合理多了,没错这个地址的代码就是用来恢复函数开始压入到堆栈的数据(平衡堆栈)。
我们也可以通过以下的规则来找出catch语句块:
1
2
3
4
5
6
|
CATCH0_BEGIN: //IDA中的地址标号 .... //CATCH实现代码 mov eax, CATCH_END ; 函数平衡堆栈的代码 retn PS:如果同一个函数包含多个 catch 语句块,那么后面他们一定时挨着的。 |
避免篇幅庞大,将不在列出后续catch代码。
结构体一揽?从ThrowInfo开始看起吧 结构体详情请看《C++基于SEH二次封装的异常处理 - 数据结构篇》 文末放上结构体总览图:
还记得上文中提过的__TI1H吗,这是IDA为我们生成的名字,他就是我们要找的ThrowInfo,双击进去看看
1
|
__TI1H ThrowInfo <0, 0, 0, 40F5D0h> |
这个结构体是我自己创建的,为了方便观察。
根据ThrowInfo的定义(具体请看我的上一篇文章),第四个参数也就是40F5D0h便是CatchTableTypeArray。
代码如下:
1
2
|
.rdata:0040F5D0 __CTA1H dd 1 ; count of catchable type addresses following .rdata:0040F5D4 dd offset __CT??_R0H@8 ; catchable type 'int' |
1
2
3
|
.rdata:0040F5D8 __CT??_R0H@8 dd CT_IsSimpleType ; DATA XREF: .rdata:0040F5D4↑o .rdata:0040F5D8 ; attributes .rdata:0040F5DC dd offset ??_R0H@8 ; int `RTTI Type Descriptor' |
上面代码的第二个dd是识别错误,它实际上是.H代表的是int类型,IDA为ThrowInfo命名的最后一个字母对应的就是这个类型( __TI1H ),当然除了.H还有其他字母例如:
-- .M = float
-- .D = char
-- .N = double
-- ......
从catch块入手,得到catch语句的信息:
1
2
3
4
5
6
|
.text:00401295 loc_401295: ; DATA XREF: .rdata:0040F570↓o .text:00401295 push offset aCatchFloat ; "catch float \n" .text:0040129A call _printf .text:0040129F add esp, 4 .text:004012A2 mov eax, offset loc_4012A8 .text:004012A7 retn |
1
|
.rdata:0040F570 HandlerType <0, offset ??_R0M@8, -60, offset loc_401295> ; float `RTTI Type Descriptor' |
这个HandlerType实际就是_msRttiDscr,根据结构定义,最后一项就是CatchProc,也就是catch语句块起始处的地址。
实际上在0040F570附近定义了此函数中所有的catch块,可以通过这一个msRttiDscr找到此函数中所有msRttiDscr的信息,也就可以找到所有的catch语句块了。
重点汇集:
识别throw语句并不困难,只要找到__CxxThrowException函数就可以找到throw语句了,并根据throw传递的参数,可以断定抛出的数据类型。
所有的catch语句都会具有以下特点:
--- 没有平衡函数开始的堆栈
-- 返回时将eax赋值为一个地址
----------->分割线
同一个catch相关的msRttiDscr结构汇集在一起,找到一个,全部都可以挖出。
----------->分割线
寻找catch规则:
1
2
3
4
5
6
|
CATCH0_BEGIN: //IDA中的地址标号 .... //CATCH实现代码 mov eax, CATCH_END ; 函数平衡堆栈的代码 retn PS:如果同一个函数包含多个 catch 语句块,那么后面他们一定时挨着的。 |
结构体总览图: