X32下的C++异常处理分析与还原
X32下的c++异常处理
1 |
|
一旦try内的语句有抛出(throw)异常,就会去搜索对应的catch块执行语句
值得注意的是,在C++中,当一个异常被抛出时,程序会在找到第一个匹配的catch块之后处理该异常,然后继续执行后续代码。并且,一旦异常被捕获并处理,程序不会再继续搜索其他的catch块来处理同一个异常。
事件查看器
介绍下事件查看器这个工具
可以用来查看具体是什么异常,但是貌似也可以用调试??
这里手动造一个异常
打开事件查看器,就可以发现具体的错误码和错误偏移,还有路径
值得注意的是,这个偏移量是ROV,相对于ImageBase的偏移
具体分析C++异常过程
测试代码:
1 |
|
当某个函数有try catch的时候,会在函数头注册一个异常回调函数:
例如下面的main函数开头:
分析一下这个代码是干啥的:
1 | .text:00412640 55 push ebp |
push offset __ ehhandler$ _main,这是一个函数指针,放入栈中
mov eax, large fs:0 将异常链表存入eax
后续会有
1 | .text:00412675 8D 45 F4 lea eax, [ebp-0Ch] |
这个就是将__ehhandler $ _main这个函数置为链表头,原来的作为Next指针存着
这样就实现了将函数_ _ehhandler$ _main 挂入异常链表
在含有try的函数结束之后
1 | .text:008527A6 8B 4D F4 mov ecx, [ebp+var_C] |
会有一个注销异常链表的操作,这里就是拿出next,然后覆盖掉原来的异常链表,这样,就恢复了原来的异常链表(即注销)
之前的 push 0FFFFFFFFh,代表try还没开始,也就是说 [ebp-0x4] 位置代表着 trylevel,如果是0xffffffff则代表try块还未开始,如果是0,就代表进入try块
当结束try块以后,还是会将这个trylevel置为-1
如果有try嵌套:
反正也是赋值为非负的一个数
双击__enhandler__$main
在调用函数__CxxFrameHandler之前,会传递一个参数,这里就是eax
其实这就是FuncInfo函数信息表
FuncInfo结构
介绍一下FuncInfo
1 | struct FuncInfo |
其中pUnwindMap和pTryBlockMap分别指向 UnwindMapEntry 和 TryBlockMapEntry 结构
UnwindMapEntry要配合FuncInfo里面的maxState使用。
UnwindMapEntry的作用:栈展开的时候需要执行的函数由UnwindMapEntry表记录
TryBlockMapEntry的作用:这个结构用来判断异常产生在哪一个Try块
UnwindMapEntry结构
这个结构记录了需要执行函数
1 | struct UnwindMapEntry |
由于栈展开存在多个对象,因此以数组的形式记录每个对象的析构信息
toState 项用来判断结构是否属于处于数组中,lpFuncAction用于记录析构函数所在的地址
TryBlockMapEntry结构
在这个结构体中可以知道对应的Try有几个Catch,并且能找到对应的Catch块
TryBlockMapEntry块成员长这样:
1 | struct TryBlockMapEntry |
TryBlockMapEnrty 表结构用于判断异常产生在哪一个try块,tryLow,tryHigh 项用于检查产生的异常是否来源于try块中
最左边的TryLow才是真正的trylevel下标,另外一个TryHigh是用来描述范围的
_msRttiDscr 结构
这个结构用于描述try块中的某一个catch块的信息
1 | struct _msRttiDscr |
具体来说:
nFlag标记用于检查catch块的类型匹配:
如果是 1 :常量 2:变量 4:未知 8:引用
异常的匹配信息存在pType所指向的结构
这个结构便是 TypeDescriptor
TypeDescriptor结构
这是一个记录 异常类型的结构:具体结构长这样:
1 | struct TypeDescriptor |
有了这些信息之后,就可以通过与抛出异常时的信息进行对比,得到对应的表结构
再通过_msRttiDscr结构中的CatchProc得到catch块的首地址
关于throw
抛出异常的工作 由 throw 抛出,在源代码含有throw的函数体中可以找到 __CxxThrowException 这个函数,和之前 _CxxFrameHandler 类似,之前传进去的参数是 FuncInfo,这回是 ThrowInfo
这样可以通过参数,去获取抛出的对象(或者数值)
另外一个参数就是ThrowInfo
每一个throw都对应一个ThrowInfo和一个拷贝的对象。里面包含着对应的信息,包括抛出对象的类型(ThrowInfo),里面放了什么(从拷贝对象可知)
下面是通过ThrowInfo和拷贝对象识别值和类型的过程
但是为什么会有两个RTTI,这是表示CMyException *的类型和void *类型的异常都可以被接收
ThrowInfo结构
1 | struct ThrowInfo |
nFlag为1的时候,表示抛出常量类型的异常; 2 表示抛出变量类型的异常
由于在try块中产生的异常被处理后就不会再返回try块了。因此pDestructor的作用就是记录try块里面的异常对象的析构函数地址,当异常处理完成以后调用异常对象的析构函数
抛出异常所对应的catch块的类型的信息被记录在pCatchTableTypeArray所指向的CatchTableTyoeArray表结构
CatchTableTypeArray结构
1 | struct CatchTableTyoeArray |
ppCatchTableType是一个指向数组的指针,dwCount用来描述数组中元素的个数
CatchTableType中含有含有处理异常时的所需相关信息
CatchTableType结构
CatchTableType中含有含有处理异常时的所需相关信息
1 | struct CatchTableType |
flag用于标记异常对象属于哪一种类型,例如指针,引用,对象等,标记值所代表的含义为:
1:简单类型复制 2:已被捕获 4:有虚表基类复制 8:指针和类型引用复制
当异常类型为对象的时候,由于对象存在基类等相关信息,因此需要将他们也记录下来,thisDisplacement保存了记录基类信息结构的首地址
PMD结构:
1 | struct PMD |
注意注意:
如果Try内有定义对象并且Throw了,那么就要进行析构,Try里面全部对象都要被析构
还原代码的逻辑:
进入一个函数首先看看有没有调用__CxxFrameHandler,和有没有对fs:[0]这个地址进行操作,这个是有异常的标志。
然后一顿操作,把FuncInfo解析出来,有maxState个UnwindMapEntry结构,里面有存析构函数(如果存在析构,具体执行顺序看下标),然后还有dwTryCount个TryBlockMapEntry结构,里面存着Catch块的具体地址(_msRttiDscr 结构)
catch可能不在IDA反编译出来的函数,所以看到try我们需要去自己找对应的catch
解决了这些结构体,就可以看汇编还原代码了。
不要把Catch当成一个函数,而是要把它当成代码块
看见trylevel为0,就可以匹配到对应的catch块了