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
 
这个函数就很简单了直接返回字符串“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开始看起吧 结构体详情请看《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'
 
这个结构体的第二项是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命名的最后一个字母对应的就是这个类型( __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
在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语句块了。

 

重点汇集:

 

 

          识别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语句块,那么后面他们一定时挨着的。
 

 

结构体总览图:

 

 





posted on 2019-12-31 14:51  活着的虫子  阅读(382)  评论(0编辑  收藏  举报

导航