C++基于SEH二次封装的异常流程与识别

在茫茫的汇编中,怎么来识别try结构呢?

在看代码之前我们先连简单的看下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
129

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
12

......
.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

这个函数就很简单了直接返回字符串“CExcepctionDiv0”。

在以上的代码来看识别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开始看起吧:

还记得上文中提过的__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'

这个结构体的第二项是pTypeInfo,指向异常类型结构TypeDescriptor,双击进去看看:

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命名的最后一个字母对应的就是这个类型,当然除了.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

在loc_401295的右侧我们看到IDA给我们标出来的注释,这个注释代表此地址的引用位置,双击进去看看:

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语句块了。

Author: BarretGuy
Link: https://basicbit.cn/2018/11/12/2018-11-14-C++异常处理的流程与识别/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.