c++ 32位异常还原

本文中的例子下载地址

https://wwmf.lanzout.com/ij4zq18au9yd
密码:2vts

确定try的位置

首先确定try的位置

image

上面明显是一个SEH结构,在c++异常中,state固定在var_4的位置上,这里state初始化位-1,我们将var_4改名为state

image

上图为ida的反编译图,当state赋值为0时,为try的开始,state赋值为-1时表示try块的结束

state除了标识try的范围,还可以用于栈展开

还原case1 - 4

下面我们分析每一个抛出的异常

image

_CxxThrowException的第一个参数为抛出异常的地址,那么这里的异常应该时一个常量3,我们可以跳转到第二个参数ThrowInfo

image

ThrowInfo的数据结构

image

上面是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

image

image

下面看case 5,v7应该保存了这个异常对象的地址,sub_401310是该对象的构造函数,我们先分析这个类的类名,

image

在CatchTableTypeArray中,前面的类是抛出的类(这里是CDiv0Excepction),后面的类是该类的基类(这里为CExceptionBase),所以这里抛出的类为CDiv0Excepction

现在代码可还原为

case 5:
	throw CDiv0Excepction();

然后我们再分析CDiv0Excepction的构造函数

image

经过我们前面的分析,知道CDiv0Excepction有个基类CExceptionBase,所以sub_401360应为CExceptionBase的构造函数,现在将sub_401360重命名为CExceptionBase,看下图

image

这方面可异常关系不大,所以我简略过,下面查看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

image

image

image

看上面我们可以看到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

image

我们可以看到再函数开头有明显的SEH增加节点的行为转到SEH_401000

image

看到这里是FuncInfo的地址,下面转到FuncInfo

image

FuncInfo的结构

image

上面为FuncInfo的结构

上面只有一个TryBlockMapEntry,代表这里只有一个try块,catch的数量为TryBlockMapEntry的第四个成员dwCatchCount,这里有7个catch块,在ida中msRttiDscr表示为HandlerType

msRttiDscr 有几个成员需要关注,pType表明了catch的类型,CatchProc表示catch后执行的函数,dispCatchObjOffset表示catch后异常对象在栈中的偏移(以EBP为基准线)
我们转到第一个catch的 CatchProc

image

上图就是第一个catch的catch函数,由pType可知它是一个int类型的catch,那么上图中var_3C这个变量是什么,请看dispCatchObjOffset的值,如下图

image

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异常

image

大家看上面圈出的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函数(其实随便一个就可以),

image

image

我们可以看到在catch函数中,最后会返回一个地址,这个地址就是catch函数执行完,要跳转的那个地址,我们跟过去

image

看到上面将state设置为-1,代表try ... catch结束了,再接着跳转到loc_40122E

image

这就是这个函数的结尾,可还原为

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中分配资源的操作

image

查看FuncInfo的第三个成员pUnwindMap

image

UnWindMapEntry

image

UnWindMapEntry结构

上面的UnWindMapEntry有三项,该数组的索引为取值,代表state为0,state为1,state为2时,要执行的函数,假如此时state为1,则处理UnWindMapEntry的第二项,执行 loc_412EE0这个函数,因toState为0,所以下一个要执行的UnWindMapEntry为第0项,第0项的lpFunAction为0,所以不执行,toState为-1,所以栈展开到此结束。

我们查看当state为1时,程序执行的情况

image

我们看到这里执行了CAccessExcepction的构造函数,但是异常对象为CAccessExcepction * ,所以CAccessExcepction并没有被当作异常对象进行析构,所以需要借助栈展开进行析构,下面我们查看栈展开时调用的函数内容

image

image

我们可以看到它调用的正是CAccessExcepction的析构函数

异常对象如何复制到catch中

在ThrowInfo中的CatchTableType中的pCopyFunction,若pCopyFunction不为0,c++异常处理函数会调用这个pCopyFunction,将异常对象复制到catch中

image

异常对象如何进行析构

ThrowInfo的第二个成员pDestructor存储了抛出异常的析构函数,若析构函数不为0,c++异常处理函数会调用该析构函数将抛出的异常对象进行析构(不是复制后的catch异常对象,这个对象在catch函数里析构)

image

如何确定异常类型?(Rtti)

上面确定异常类型都是通过ida直接给出的提示

image

那么ida是如何确定异常类型的呢?答案是RTTI(运行时类型标识)

确定catch的类型

以第一个catch为例子,转到msRttiDscr.pType

image

image

这里有三个成员结构为

image

其中第三个成员name是用字符串来表示异常的类型.H代表着int类型

确定throw的异常

throw的TypeDescriptor是在CatchTableType.pTypeInfo

我们以第一个throw为例子

image

image

posted @ 2023-09-13 20:57  乘舟凉  阅读(125)  评论(0编辑  收藏  举报