首页
社区
课程
招聘
[原创]C++基于SEH二次封装的异常流程与识别
2019-10-20 01:04 4597

[原创]C++基于SEH二次封装的异常流程与识别

2019-10-20 01:04
4597

在茫茫的汇编中,怎么来识别try结构呢?

在看代码之前我们先连简单的看下try的处理流程吧

  • 函数入口设置回调函数
  • 函数的异常抛出使用了__CxxThrowException函数,此函数包含了两个参数,分别是抛出一场关键字的throw的参数的指针,另一个抛出信息类型的指针(ThrowInfo *)。
  • 在异常回调函数中,可以得到异常对象的地址和对应ThrowInfo数据的地址以及FunInfo表结构的地址。根据记录的异常类型,进行try块的匹配工作
  • 没找到try块怎么办?先调用异常对象的析构函数,然后反汇ExcetionContinueSearch,继续反回到SEH继续执行。
  • 找到了try块?通过TryBlockMapEntry结构中的pCatch指向catch信息,用ThrowInfo结构中的异常类型遍历查找相匹配的catch块,比较关键字名称,找到有效的catch块。
  • 然后进行栈展开。
  • 析构try块中的对象
  • 跳转到catch块中执行
  • 调用_JumpToContinuation函数,返回catch语句块的结束地址。

上面的步骤,就是典型的异常处理的顺序。

光看文字多无趣,上代码 - 实例分析,我们来跑一遍:

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;
}
先来看看函数开始的代码吧:
.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看下实现:

......
.text:0040CAB1                 mov     eax, offset stru_40F53C
.text:0040CAB6                 jmp     ___CxxFrameHandler3

无疑此项就是编译器产生的异常回调函数。

继续看异常抛出的部分:

......
.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语句也差不多。

直到抛出对象的时候,代码如下:

......
.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的值,后面与前面的基本相同。

代码如下:

......
.text:00401048                 mov     dword ptr [esi], offset ??_7CExcepctionDiv0@@6B@ ; const CExcepctionDiv0::`vftable'
......

IDA友情提示,这是一个虚表,这个虚表里有两个函数。

这两个函数代码如下:

.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的析构函数。


另一个函数代码如下:

.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吧:

.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的地址里面保存了何方神圣?

来看一看:

.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语句块:

CATCH0_BEGIN: //IDA中的地址标号
....		  //CATCH实现代码
mov eax, CATCH_END ; 函数平衡堆栈的代码
retn

PS:如果同一个函数包含多个catch语句块,那么后面他们一定时挨着的。

避免篇幅庞大,将不在列出后续catch代码。


结构体一揽?从ThrowInfo开始看起吧 结构体详情请看《C++基于SEH二次封装的异常处理 - 数据结构篇》 文末放上结构体总览图:

还记得上文中提过的__TI1H吗,这是IDA为我们生成的名字,他就是我们要找的ThrowInfo,双击进去看看

__TI1H          ThrowInfo <0, 0, 0, 40F5D0h>

这个结构体是我自己创建的,为了方便观察。

根据ThrowInfo的定义(具体请看我的上一篇文章),第四个参数也就是40F5D0h便是CatchTableTypeArray。

代码如下:

.rdata:0040F5D0 __CTA1H         dd 1                    ; count of catchable type addresses following
.rdata:0040F5D4                 dd offset __CT??_R0H@8 ; catchable type 'int'

这个结构体的第二项是pTypeInfo,指向异常类型结构TypeDescriptor,双击进去看看:
.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语句的信息:
.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给我们标出来的注释,这个注释代表此地址的引用位置,双击进去看看:
.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规则:

CATCH0_BEGIN: //IDA中的地址标号
....		  //CATCH实现代码
mov eax, CATCH_END ; 函数平衡堆栈的代码
retn

PS:如果同一个函数包含多个catch语句块,那么后面他们一定时挨着的。




结构体总览图:










[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2019-10-20 08:49 被Hasic编辑 ,原因:
收藏
点赞2
打赏
分享
最新回复 (2)
雪    币: 6124
活跃值: (4051)
能力值: ( LV6,RANK:80 )
在线值:
发帖
回帖
粉丝
黑洛 1 2019-10-20 14:18
2
0
不错不错
雪    币: 432
活跃值: (141)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
kanxueqwe 2019-11-20 14:25
3
0
你好,请问为什么使用OD调试时候,对于中文系统nseh = "\xeb\x06\x90\x90" 装入内存时候\xeb\x06会变成一个字节\3F,从而不能出发漏洞
游客
登录 | 注册 方可回帖
返回