X32下的c++异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

int main(int argc)
{
try {
if (argc == 1)
throw 1;
}

catch (int e)
{
cout << "catch int " << e << endl;
}

catch (...)
{
cout << "catch all" << endl;
}

return 0;
}

一旦try内的语句有抛出(throw)异常,就会去搜索对应的catch块执行语句

值得注意的是,在C++中,当一个异常被抛出时,程序会在找到第一个匹配的catch块之后处理该异常,然后继续执行后续代码。并且,一旦异常被捕获并处理,程序不会再继续搜索其他的catch块来处理同一个异常。

事件查看器

介绍下事件查看器这个工具

1718600434405

可以用来查看具体是什么异常,但是貌似也可以用调试??

这里手动造一个异常

1718600575669

打开事件查看器,就可以发现具体的错误码和错误偏移,还有路径

1718600561100

值得注意的是,这个偏移量是ROV,相对于ImageBase的偏移

具体分析C++异常过程

1719219097725

测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;
int main()
{
try
{
throw 1;
}

catch (int e)
{
cout << "触发int异常\n" << endl;
}

catch (float e)
{
cout << "触发了float异常\n" << endl;
}

return 0;
}

当某个函数有try catch的时候,会在函数头注册一个异常回调函数:

例如下面的main函数开头:

1718710227685

1719061935153

分析一下这个代码是干啥的:

1
2
3
4
5
6
7
8
9
10
11
.text:00412640 55                            push    ebp
.text:00412641 8B EC mov ebp, esp
.text:00412643 6A FF push 0FFFFFFFFh
.text:00412645 68 00 77 41 00 push offset __ehhandler$_main
.text:0041264A 64 A1 00 00 00 00 mov eax, large fs:0
.text:00412650 50 push eax
.text:00412651 51 push ecx
.text:00412652 81 EC 08 01 00 00 sub esp, 108h
.text:00412658 53 push ebx
.text:00412659 56 push esi
.text:0041265A 57 push edi

push offset __ ehhandler$ _main,这是一个函数指针,放入栈中

mov eax, large fs:0 将异常链表存入eax

后续会有

1
2
.text:00412675 8D 45 F4                      lea     eax, [ebp-0Ch]
.text:00412678 64 A3 00 00 00 00 mov large fs:0, eax

这个就是将__ehhandler $ _main这个函数置为链表头,原来的作为Next指针存着

这样就实现了将函数_ _ehhandler$ _main 挂入异常链表

在含有try的函数结束之后

1
2
.text:008527A6 8B 4D F4                      mov     ecx, [ebp+var_C]
.text:008527A9 64 89 0D 00 00 00 00 mov large fs:0, ecx

会有一个注销异常链表的操作,这里就是拿出next,然后覆盖掉原来的异常链表,这样,就恢复了原来的异常链表(即注销)

之前的 push 0FFFFFFFFh,代表try还没开始,也就是说 [ebp-0x4] 位置代表着 trylevel,如果是0xffffffff则代表try块还未开始,如果是0,就代表进入try块

当结束try块以后,还是会将这个trylevel置为-1

1719062524688

如果有try嵌套:

1719062762747

反正也是赋值为非负的一个数

双击__enhandler__$main

在调用函数__CxxFrameHandler之前,会传递一个参数,这里就是eax

其实这就是FuncInfo函数信息表

1718710265704

FuncInfo结构

介绍一下FuncInfo

1
2
3
4
5
6
7
8
9
10
struct FuncInfo
{
magicNumber dd ;? //编译器生成的固定数字
maxState dd ? ;//最大栈展开数的下标值,也就是trylevel最大不能超过maxState,同时也是栈展开最大的次数
pUnwindMap dd ? ;//指向栈展开函数表的指针,指向UnwindMapEntry表结构
dwTryCount dd ? ;//一个函数里面的Try块的数量
pTryBlockMap dd ? ;//Try块的列表,指向TryBlockMapEntry表的结构
}

//64位程序下会额外多几个成员

其中pUnwindMap和pTryBlockMap分别指向 UnwindMapEntry 和 TryBlockMapEntry 结构

UnwindMapEntry要配合FuncInfo里面的maxState使用。

UnwindMapEntry的作用:栈展开的时候需要执行的函数由UnwindMapEntry表记录

TryBlockMapEntry的作用:这个结构用来判断异常产生在哪一个Try块

UnwindMapEntry结构

这个结构记录了需要执行函数

1
2
3
4
5
struct UnwindMapEntry
{
DWORD toState;//栈展开数下标值,即Trylevel,到时候看范围就知道
DWORD lpFuncAction;//展开执行的函数
}

由于栈展开存在多个对象,因此以数组的形式记录每个对象的析构信息

toState 项用来判断结构是否属于处于数组中,lpFuncAction用于记录析构函数所在的地址

TryBlockMapEntry结构

在这个结构体中可以知道对应的Try有几个Catch,并且能找到对应的Catch块

TryBlockMapEntry块成员长这样:

1
2
3
4
5
6
7
8
struct TryBlockMapEntry
{
DWORD tryLow; //try块的最小状态索引,用于范围检查(trylevel的最小索引)
DWORD tryHigh; //try块的最大状态索引,用于范围检查(trylevel的最大索引)
DWORD catchHigh; //catch块的最高状态索引,用于范围检查(trylevel的上限)
DWORD dwCatchCount; //catch块的个数
DOWRD pCatchHandlerArray; //catch块的描述,指向_msRttiDscr表结构
}

TryBlockMapEnrty 表结构用于判断异常产生在哪一个try块,tryLow,tryHigh 项用于检查产生的异常是否来源于try块中

1719197846739

最左边的TryLow才是真正的trylevel下标,另外一个TryHigh是用来描述范围的

_msRttiDscr 结构

这个结构用于描述try块中的某一个catch块的信息

1
2
3
4
5
6
7
struct _msRttiDscr
{
DWORD nFlag; //用于Catch块的匹配检查
DWORD pType; //catch块要捕捉的类型,指向TypeDescriptor表结构,如果是零,就代表所有类型,即catch all
DWORD dispCatchObjOffset; //用于定位异常对象在当前ESP中的偏移位置
DWORD CatchProc; //catch块的首地址,可以用来定位catch
}

具体来说:

nFlag标记用于检查catch块的类型匹配:

如果是 1 :常量 2:变量 4:未知 8:引用

异常的匹配信息存在pType所指向的结构

这个结构便是 TypeDescriptor

TypeDescriptor结构

这是一个记录 异常类型的结构:具体结构长这样:

1
2
3
4
5
6
struct TypeDescriptor
{
DWORD Hash; //类型名称的Hash数值
DWORD spare;//保留
DWORD name; //类型名称
}

有了这些信息之后,就可以通过与抛出异常时的信息进行对比,得到对应的表结构

再通过_msRttiDscr结构中的CatchProc得到catch块的首地址

关于throw

抛出异常的工作 由 throw 抛出,在源代码含有throw的函数体中可以找到 __CxxThrowException 这个函数,和之前 _CxxFrameHandler 类似,之前传进去的参数是 FuncInfo,这回是 ThrowInfo

1719065341932

这样可以通过参数,去获取抛出的对象(或者数值)

另外一个参数就是ThrowInfo

每一个throw都对应一个ThrowInfo和一个拷贝的对象。里面包含着对应的信息,包括抛出对象的类型(ThrowInfo),里面放了什么(从拷贝对象可知)

下面是通过ThrowInfo和拷贝对象识别值和类型的过程


1719152317760

但是为什么会有两个RTTI,这是表示CMyException *的类型和void *类型的异常都可以被接收

ThrowInfo结构

1
2
3
4
5
6
7
struct ThrowInfo
{
DWORD nFlag; //抛出异常类型标记
DWORD pDestructor; //异常对象的析构函数地址
DWORD pForwardCompat;//未知
DOWRD pCatchTableTypeArray //catch块类型表,指向CatchTableTyoeArray表结构
}

nFlag为1的时候,表示抛出常量类型的异常; 2 表示抛出变量类型的异常

由于在try块中产生的异常被处理后就不会再返回try块了。因此pDestructor的作用就是记录try块里面的异常对象的析构函数地址,当异常处理完成以后调用异常对象的析构函数

抛出异常所对应的catch块的类型的信息被记录在pCatchTableTypeArray所指向的CatchTableTyoeArray表结构

CatchTableTypeArray结构

1
2
3
4
5
struct CatchTableTyoeArray
{
DWORD dwCount; //CatchTableType 数组包含的元素个数
DWORD ppCatchTableType; //catch块的类型信息,类型为CatchTableType**
}

ppCatchTableType是一个指向数组的指针,dwCount用来描述数组中元素的个数

CatchTableType中含有含有处理异常时的所需相关信息

CatchTableType结构

CatchTableType中含有含有处理异常时的所需相关信息

1
2
3
4
5
6
7
8
struct CatchTableType
{
DWORD flag;
DWORD pTypeInfo;//指向异常类型的结构,指向TypeDescriptor表结构
DWORD thisDisplacement; //基类信息
DWORD sizeorOffset; //类的大小
DWORD pCopyFunction; //复制构造函数的指针
}

flag用于标记异常对象属于哪一种类型,例如指针,引用,对象等,标记值所代表的含义为:

1:简单类型复制 2:已被捕获 4:有虚表基类复制 8:指针和类型引用复制

当异常类型为对象的时候,由于对象存在基类等相关信息,因此需要将他们也记录下来,thisDisplacement保存了记录基类信息结构的首地址

PMD结构:

1
2
3
4
5
6
struct PMD
{
DWORD dwOffsetToThis; //基类偏移
DWORD dwOffsetToVBase; //虚基类偏移
DWORD dwOffsetToVbTable; //基类虚表偏移
}

注意注意:

如果Try内有定义对象并且Throw了,那么就要进行析构,Try里面全部对象都要被析构

还原代码的逻辑:

1719198058819

进入一个函数首先看看有没有调用__CxxFrameHandler,和有没有对fs:[0]这个地址进行操作,这个是有异常的标志。

然后一顿操作,把FuncInfo解析出来,有maxState个UnwindMapEntry结构,里面有存析构函数(如果存在析构,具体执行顺序看下标),然后还有dwTryCount个TryBlockMapEntry结构,里面存着Catch块的具体地址(_msRttiDscr 结构)

catch可能不在IDA反编译出来的函数,所以看到try我们需要去自己找对应的catch

解决了这些结构体,就可以看汇编还原代码了。

不要把Catch当成一个函数,而是要把它当成代码块

1719218558733

看见trylevel为0,就可以匹配到对应的catch块了