c++ 32位异常还原
本文中的例子下载地址
https://wwmf.lanzout.com/ij4zq18au9yd
密码:2vts
确定try的位置
首先确定try的位置
上面明显是一个SEH结构,在c++异常中,state固定在var_4的位置上,这里state初始化位-1,我们将var_4改名为state
上图为ida
的反编译图,当state赋值为0时,为try的开始,state赋值为-1时表示try块的结束
state除了标识try的范围,还可以用于栈展开
还原case1 - 4
下面我们分析每一个抛出的异常
_CxxThrowException的第一个参数为抛出异常的地址,那么这里的异常应该时一个常量3,我们可以跳转到第二个参数ThrowInfo
ThrowInfo的数据结构
上面是ThrowInfo的数据结构
可以看到这个异常时一个简单类型异常,并且为int
case 1 - 4 都是差不多的,它们可还原为下面的代码
case 1:
throw 3;
case 2:
throw 3.0f;
case 3:
throw '3';
case 4:
throw 3.0;
还原case 5 - 6
下面看case 5,v7应该保存了这个异常对象的地址,sub_401310是该对象的构造函数,我们先分析这个类的类名,
在CatchTableTypeArray中,前面的类是抛出的类(这里是CDiv0Excepction),后面的类是该类的基类(这里为CExceptionBase),所以这里抛出的类为CDiv0Excepction
现在代码可还原为
case 5:
throw CDiv0Excepction();
然后我们再分析CDiv0Excepction的构造函数
经过我们前面的分析,知道CDiv0Excepction有个基类CExceptionBase,所以sub_401360应为CExceptionBase的构造函数,现在将sub_401360重命名为CExceptionBase,看下图
这方面可异常关系不大,所以我简略过,下面查看CDiv0Excepction和CExceptionBase的虚表,最终可还原为
class CExceptionBase{
public:
virtual char * toString() = 0;
}
class CDiv0Excepction : public CExceptionBase{
public:
CDiv0Excepction(){
printf("CExcepctionDiv0()\r\n");
}
virtual char * toString(){
return "div zero excepction";
}
}
case 6 和 case5是差不多的 ,最终可还原为
class CAccessExcepction : public CExceptionBase{
public:
CAccessExcepction(){
printf("CAccessExcepction()\r\n");
}
virtual char * toString(){
return "access excepction";
}
}
case 6:
throw CAccessExcepction();
还原case 7
下面看case 7
看上面我们可以看到case 7抛出的是一个指针CAccessExcepction *
,(所有的对象指针都继承void *),所以下面可还原为
case 7:
throw new CAccessExcepction();
最终try块可还原为
class CExceptionBase{
public:
virtual char * toString() = 0;
}
class CDiv0Excepction : public CExceptionBase{
public:
CDiv0Excepction(){
printf("CExcepctionDiv0()\r\n");
}
virtual char * toString(){
return "div zero excepction";
}
}
class CAccessExcepction : public CExceptionBase{
public:
CAccessExcepction(){
printf("CAccessExcepction()\r\n");
}
virtual char * toString(){
return "access excepction";
}
}
int sub_401000(int n){
try{
switch(n){
case 1:
throw 3;
case 2:
throw 3.0f;
case 3:
throw '3';
case 4:
throw 3.0;
case 5:
throw CDiv0Excepction();
case 6:
throw CAccessExcepction();
case 7:
throw new CAccessExcepction();
}
catch...
}
还原前面4个catch
下面还原catch
我们可以看到再函数开头有明显的SEH增加节点的行为转到SEH_401000
看到这里是FuncInfo的地址,下面转到FuncInfo
FuncInfo的结构
上面为FuncInfo的结构
上面只有一个TryBlockMapEntry,代表这里只有一个try块,catch的数量为TryBlockMapEntry的第四个成员dwCatchCount,这里有7个catch块,在ida中msRttiDscr表示为HandlerType
msRttiDscr 有几个成员需要关注,pType表明了catch的类型,CatchProc表示catch后执行的函数,dispCatchObjOffset表示catch后异常对象在栈中的偏移(以EBP为基准线)
我们转到第一个catch的 CatchProc
上图就是第一个catch的catch函数,由pType可知它是一个int类型的catch,那么上图中var_3C这个变量是什么,请看dispCatchObjOffset的值,如下图
dispCatchObjOffset为-60,换算为16进制,则为-3C,所以var_3C刚好为EBP-3C,这就是异常对象
这个catch函数可还原为
catch(int a){
printf("catch int %d\r\n",a);
}
前面四个可还原为
catch(int a){
printf("catch int %d\r\n",a);
}
catch(float a){
printf("catch float %f\r\n",a);
}
catch(char a){
printf("catch char %c\r\n",a);
}
catch(double a){
printf("catch double %f\r\n",a);
}
还原剩下的catch异常
现在还原剩下的catch异常
大家看上面圈出的catch异常,可能会认为这个异常类型为CExceptionBase,但是注意看msRttiDscr 的第一个成员nFlag的值为8,这代表这是一个引用类型的异常,所以这个catch异常可还原为
catch(CExceptionBase& a){
printf("catch error %s\r\n",a.toString());
}
第6个catch异常可还原为
catch(CAccessExcepction* a){
printf("catch error %s\r\n",a->toString());
}
最后一个异常的类型为0,这个的意思是任何异常都匹配,这个catch异常可还原为
catch(...){
printf("catch ...\r\n");
}
还原程序的结尾
最后我们这个程序的结尾
我们找最后一个catch异常的catch函数(其实随便一个就可以),
我们可以看到在catch函数中,最后会返回一个地址,这个地址就是catch函数执行完,要跳转的那个地址,我们跟过去
看到上面将state设置为-1,代表try ... catch结束了,再接着跳转到loc_40122E
这就是这个函数的结尾,可还原为
printf("Test end!\r\n")
整个函数的还原
最后整理一下整个函数的还原情况
#include <iostream>
class CExceptionBase{
public:
virtual char * toString() = 0;
};
class CDiv0Excepction : public CExceptionBase{
public:
CDiv0Excepction(){
printf("CExcepctionDiv0()\r\n");
}
virtual char * toString(){
return "div zero excepction";
}
};
class CAccessExcepction : public CExceptionBase{
public:
CAccessExcepction(){
printf("CAccessExcepction()\r\n");
}
virtual char * toString(){
return "access excepction";
}
};
int sub_401000(int n){
try{
switch(n){
case 1:
throw 3;
case 2:
throw 3.0f;
case 3:
throw '3';
case 4:
throw 3.0;
case 5:
throw CDiv0Excepction();
case 6:
throw CAccessExcepction();
case 7:
throw new CAccessExcepction();
}
}
catch(int a){
printf("catch int %d\r\n",a);
}
catch(float a){
printf("catch float %f\r\n",a);
}
catch(char a){
printf("catch char %c\r\n",a);
}
catch(double a){
printf("catch double %f\r\n",a);
}
catch(CExceptionBase& a){
printf("catch error %s\r\n",a.toString());
}
catch(CAccessExcepction* a){
printf("catch error %s\r\n",a->toString());
}
catch(...){
printf("catch ...\r\n");
}
printf("Test end!\r\n");
}
int main(){
sub_401000(1);
}
额外知识
c++ state在栈展开的作用
栈展开是当异常发生后,回收在try中分配资源的操作
查看FuncInfo的第三个成员pUnwindMap
上面的UnWindMapEntry有三项,该数组的索引为取值,代表state为0,state为1,state为2时,要执行的函数,假如此时state为1,则处理UnWindMapEntry的第二项,执行 loc_412EE0这个函数,因toState为0,所以下一个要执行的UnWindMapEntry为第0项,第0项的lpFunAction为0,所以不执行,toState为-1,所以栈展开到此结束。
我们查看当state为1时,程序执行的情况
我们看到这里执行了CAccessExcepction的构造函数,但是异常对象为CAccessExcepction * ,所以CAccessExcepction并没有被当作异常对象进行析构,所以需要借助栈展开进行析构,下面我们查看栈展开时调用的函数内容
我们可以看到它调用的正是CAccessExcepction的析构函数
异常对象如何复制到catch中
在ThrowInfo中的CatchTableType中的pCopyFunction,若pCopyFunction不为0,c++异常处理函数会调用这个pCopyFunction,将异常对象复制到catch中
异常对象如何进行析构
ThrowInfo的第二个成员pDestructor存储了抛出异常的析构函数,若析构函数不为0,c++异常处理函数会调用该析构函数将抛出的异常对象进行析构(不是复制后的catch异常对象,这个对象在catch函数里析构)
如何确定异常类型?(Rtti)
上面确定异常类型都是通过ida直接给出的提示
那么ida是如何确定异常类型的呢?答案是RTTI(运行时类型标识)
确定catch的类型
以第一个catch为例子,转到msRttiDscr.pType
这里有三个成员结构为
其中第三个成员name是用字符串来表示异常的类型.H代表着int类型
确定throw的异常
throw的TypeDescriptor是在CatchTableType.pTypeInfo
我们以第一个throw为例子