您的位置:首页 > 编程语言 > C语言/C++

vc++6对windows SEH扩展分析

2015-08-11 18:02 387 查看
相传FS:[0]指向线程的异常处理链表。汇编高手们为了向链表中添加异常处理节点,通常会如下编码:

push ExceptHandler  1)
mov eax,fs:[0]      2)
push eax            3)
mov fs:[0],esp      4)
真是简单的4句话,够小鸟们领悟半天了。首先看异常处理块的定义:

struct EXCEPTION_REGISTRATION
{ 
    void* prev;    链表的下一个节点
    void* handler;
};
prev在结构的低4字节,handler在结构的高4字节。对于push操作,每次使esp减4,上面的1-3)正好对应在堆栈中生成一个EXCEPTION_REGISTRATION结构;同时fs:[0]保存了前一个链表结构的起始地址。好,4)是把新生成的结构加入链表:esp指向整个结构的起始地址(起始地址是整个结构中地址最小的域,因此不就是prev的位置么?)。如此fs:[0]

就一直是链表头。这种做法是数据结构中链表的头部插入法~

结构中prev是结构体指针,而handler指向异常处理函数的地址。对于vc++6.0这个函数名为:__except_handler3。函数原型如下:

int __except_handler3(
     struct _EXCEPTION_RECO,
     struct EXCEPTION_REGIST,
     struct _CONTEXT *pContex,
     void * pDispatcherContext );
函数作用么,用来异常的展开和分发(分发是网上专业的学名,其实就是调用自己设置的异常过滤函数和异常终止函数)。其实,到这整个windows的SEH体系就全部展现出来了:一个异常链表,链表中的异常处理节点和节点中处理函数的原型。但是,vc++6.0扩展了这个体系:

首先,扩展SEH链中的节点类型为:

struct _EXCEPTION_REGISTRATION
{
  struct _EXCEPTION_REGISTRATION *prev;
  void *handler;
  struct scopetable_entry *scopetable;
  int trylevel;
  int _ebp;
};
既然说是扩展了,原有的结构必须保留,要不然windows都不认为这是一个SEH节点。win原生的SEH节点是通过诺干push指令在堆栈中形成的;而扩展的节点是在原生的结构尾部追加了3个域,这3个域在内存中的地址必然比原生SEH节点的几个域更高(远离堆栈顶),因此他们在操作上一定是:先push 3个扩展域,然后在依次push handler和prev。好,都说到加强型SEH节点会依次执行push ebp - push trylevel 等指令,有没有觉得哪些地方似曾相识?没觉得的话再把这句话重头读一遍。就是push
ebp那条指令。

一般程序会在什么时候主动push ebp?当然就是函数入口,为了形成栈帧~这有点跳跃了,先看下各个域的定义。ebp如前所述,保存函数栈帧;trylevel,是一个数组索引,指向下面马上要提到的scopetable数组。下面就是最重要的域--scopetable:他是一个结构体数组,数组项记录了单个函数中(注意是单个函数,不是函数嵌套)每层try块的trylevel/filter/finall。当发生异常进入异常处理函数__exception_handler3时,通过scopetable数组定位函数中每个try块。

typedef struct _SCOPETABLE
{
   DWORD previousTryLevel; //定位前一个try块的索引值
   DWORD lpfnFilter; //当前try块的过滤函数
   DWORD lpfnHandler; //当前try块的终止函数
}SCOPETABLE, *PSCOPETABLE;
看到这,你可能已糊涂了,一会出现EXCEPTION_REGISTRATION!trylevel,一会又出现_SCOPETABLE!previousTryLevel。他们之间有什么区别和联系?说来话长,EXCEPTION_REGISTRATION!trylevel用于指明一个函数将使用scopetable数组中哪个元素,每进入一个try块,trylevel加一。初始时为-1,代表当前函数没有任何try块;而_SCOPETABLE!previousTryLevel,前面说过了,指向前一个try块。可能这样说还是太抽象,来一个具象的例子:读者对线性表应该有印象吧?线性表不像链表具有指向下一个结构的结构指针,但是他仍然可以轻松的找到稀疏数组中的下一个元素。他是怎么做到的?每个数组元素都有一个数组索引域,指向下一个元素在数组中的位置。到此知道_SCOPETABLE!previousTryLevel的作用了吧?

写到这又出现一个新的问题,就是scopetable数组项如何确定previousTryLevel的值?首先程序编译时,编译器肯定知道了当前函数中try的嵌套层数(这个,你可以认为我忽悠你的,但是后面我能找到佐证),由编译器来为每个scopetable数组项的previousTryLevel域赋值。空口无凭,我只能拿出代码:

#include <windows.h>

int main()
{
    int a=0x55;
    __try
    {
        __try
        {
            a=0xaa;
        }
        __finally
        {
            MessageBox(NULL,"","",MB_OK);
        }
    }
    __finally
    {
        a=0x00;
    }
    return 0;
}

调试看看这段代码的行为,以下代码是list文件的输出,没有经过链接过程:

COMDAT _main
_main	PROC NEAR					; COMDAT

; 4    : {
//接下去5个push生成_EXCEPTION_REGISTRATION结构
  00000	55		 push	 ebp
  00001	8b ec		 mov	 ebp, esp
  00003	6a ff		 push	 -1
  00005	68 00 00 00 00	 push	 OFFSET FLAT:__sehtable$_main
  0000a	68 00 00 00 00	 push	 OFFSET FLAT:__except_handler3
  0000f	64 a1 00 00 00
	00		 mov	 eax, DWORD PTR fs:__except_list
  00015	50		 push	 eax
  00016	64 89 25 00 00
	00 00		 mov	 DWORD PTR fs:__except_list, esp
  0001d	83 c4 b4	 add	 esp, -76		; ffffffb4H
  00020	53		 push	 ebx
  00021	56		 push	 esi
  00022	57		 push	 edi
  00023	8d 7d a4	 lea	 edi, DWORD PTR [ebp-92]
  00026	b9 11 00 00 00	 mov	 ecx, 17			; 00000011H
  0002b	b8 cc cc cc cc	 mov	 eax, -858993460		; ccccccccH
  00030	f3 ab		 rep stosd

; 5    : 	int a=0x55;

  00032	c7 45 e4 55 00
	00 00		 mov	 DWORD PTR _a$[ebp], 85	; 00000055H

; 6    : 	__try

  00039	c7 45 fc 00 00
	00 00		 mov	 DWORD PTR __$SEHRec$[ebp+20], 0 //修改_EXCEPTION_REGISTRATION!trylevel=0

; 8    : 		__try

  00040	c7 45 fc 01 00
	00 00		 mov	 DWORD PTR __$SEHRec$[ebp+20], 1 //修改_EXCEPTION_REGISTRATION!trylevel=1

; 10   : 			a=0xaa;

  00047	c7 45 e4 aa 00
	00 00		 mov	 DWORD PTR _a$[ebp], 170	; 000000aaH

; 11   : 		}

  0004e	c7 45 fc 00 00
	00 00		 mov	 DWORD PTR __$SEHRec$[ebp+20], 0
  00055	e8 02 00 00 00	 call	 $L41903
  0005a	eb 1e		 jmp	 SHORT $L41904
$L41901:
$L41903:

; 14   : 			MessageBox(NULL,"","",MB_OK);

  0005c	8b f4		 mov	 esi, esp
  0005e	6a 00		 push	 0
  00060	68 00 00 00 00	 push	 OFFSET FLAT:??_C@_00A@?$AA@ ; `string'
  00065	68 00 00 00 00	 push	 OFFSET FLAT:??_C@_00A@?$AA@ ; `string'
  0006a	6a 00		 push	 0
  0006c	ff 15 00 00 00
	00		 call	 DWORD PTR __imp__MessageBoxA@16
  00072	3b f4		 cmp	 esi, esp
  00074	e8 00 00 00 00	 call	 __chkesp
$L41902:
  00079	c3		 ret	 0
$L41904:

; 16   : 	}
这段代码给人最直观的印象是:首先,一个函数体内部只生成一个_EXCEPTION_REGISTRATION结构,无论内部try块嵌套多少次。其次,_EXCEPTION_REGISTRATION结构在函数入口处即已生成,在函数内部仅在进入一层try块时才修改_EXCEPTION_REGISTRATION!trylevel的值。最后,ide在遇到try块终止会自动插入call/jmp语句跳转到finall块中执行终止语句。还没完,前面提到“scopetable数组项如何确定previousTryLevel的值”,这里还没解决。下面集中处理这个问题。

前面的篇幅已经阐明,_EXCEPTION_REGISTRATION扩展结构中保存有scopetable数组地址,数组是过滤函数和终止函数的集合。而通过反汇编main,可以看到在入口处程序压入scopetable数组地址,那么我们可以顺着这个藤摸到scopetable这个瓜。

3:    int main()
4:    {
00401010   push        ebp
00401011   mov         ebp,esp
00401013   push        0FFh
00401015   push        offset string ""+4 (00422020)
0040101A   push        offset __except_handler3 (004011e4)
0040101F   mov         eax,fs:[00000000]
scopetable的地址是0x422020,这个十有八九是PE文件.rodata节,ollydbg验证了此事:

Memory map, item 20
 Address=00422000
 Size=00002000 (8192.)
 Owner=unwind   00400000
 Section=.rdata
 Type=Imag 01001002
 Access=R
 Initial access=RWE
恩,rodata节的内容,只有在编译链接阶段由ide来定义节中数据,这个至少可以推知在编译阶段ide确实已经知道scpoetable数组的规模。其次,跟踪到内存0x422020处,可以得到如下数据:

00422020  FF FF FF FF 00 00 00 00 98 10 40 00 00 00 00 00  ....?@.....
00422030  00 00 00 00 6C 10 40 00                          ....l@.
其中0x401098 和0x40106c正好是代码中两个_finall块的起始地址:

12:           __finally
13:           {
14:               MessageBox(NULL,"","",MB_OK);
0040106C   mov         esi,esp


17:       __finally
18:       {
19:           a=0x00;
00401098   mov         dword ptr [ebp-1Ch],0
$L41898:
0040109F   ret
这也就是说,ide通过rodata中的数据可以在链接阶段清楚了解到scopetable数组内容。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: