由于生计问题,我不得不写一段关于异常处理的代码,我的程序是多线程的,我的目的是要在某个线程发生异常的时候结束本线程,保存出错信息,重新启动一个新线程增加程序稳定性,于是呼打开vs 2005就开工了,新建一个测试程序


void __stdcall SetCatchProc(void* pCProc)
{
 __asm
 {
  push    pCProc
  push    fs:[0]      
  mov     fs:[0],esp
 }
}

static int __stdcall CatchProc(EXCEPTION_RECORD* pExcept,void* pErr,void* pContext,void*)
{
 if (pExcept->ExceptionCode == EXCEPTION_BREAKPOINT)
 {
  printf("catched");
  return 0;
 }
 printf("catched");
 return 1;
}


int _tmain(int argc, _TCHAR* argv[])

 SetCatchProc(CatchProc);
__asm int 3
  char p1[255];
 gets(p1);
 return 0;
}


但是无论如何程序也无法捕获异常,但是打开编译器/EHa选项后使用try catch(...)确可以正常捕获异常,不使用try catch(...)的原因是因为虽然能捕获所有异常,但是并不能获取异常信息,虽然release编译模式下可以在catch处理分段上使用__asm mov lpExcept,dword ptr [esp+8] 来取得一个EXCEPTION_RECORD结构指针,但是我并没有找到任何文档关于这个参数的说明,这个只是我使用od最终后发现的,并且在Debug编译模式下代码不能单单的这一句就能完成,于是呼我不得不开动od
跟踪到如下代码的int3处F8
  push    dword ptr [ebp-118]
  push    dword ptr fs:[0]
  mov     dword ptr fs:[0], esp
  int3
od带我们进入异常处理领空

7C958554    8B1C24          mov     ebx, dword ptr [esp]
7C958557    51              push    ecx
7C958558    53              push    ebx                              ; kernel32.GetProcessHeap
7C958559    E8 D88F0000     call    7C961536   //很容易可以看出这里是异常处理核心,如果返回结果不为0那么表示异常处理,调用ZwContinue继续执行程序,否则抛出异常
7C95855E    0AC0            or      al, al
7C958560    74 0C           je      short 7C95856E
7C958562    5B              pop     ebx                              ; 0012FB5C
7C958563    59              pop     ecx                              ; 0012FB5C
7C958564    6A 00           push    0
7C958566    51              push    ecx
7C958567    E8 23E8FFFF     call    ZwContinue
7C95856C    EB 0B           jmp     short 7C958579
7C95856E    5B              pop     ebx                              ; 0012FB5C
7C95856F    59              pop     ecx                              ; 0012FB5C
7C958570    6A 00           push    0
7C958572    51              push    ecx
7C958573    53              push    ebx                              ; kernel32.GetProcessHeap
7C958574    E8 C6F1FFFF     call    ZwRaiseException
进入7C961536


7C961536    8BFF            mov     edi, edi
7C961538    55              push    ebp
7C961539    8BEC            mov     ebp, esp
7C96153B    83EC 5C         sub     esp, 5C
7C96153E    56              push    esi
7C96153F    FF75 0C         push    dword ptr [ebp+C]
7C961542    8B75 08         mov     esi, dword ptr [ebp+8]
7C961545    56              push    esi
7C961546    C645 FF 00      mov     byte ptr [ebp-1], 0
7C96154A    E8 94010000     call    7C9616E3
7C96154F    84C0            test    al, al
7C961551    0F85 406A0200   jnz     7C987F97
7C961557    53              push    ebx
7C961558    57              push    edi
7C961559    8D45 F8         lea     eax, dword ptr [ebp-8]
7C96155C    50              push    eax
7C96155D    8D45 08         lea     eax, dword ptr [ebp+8]
7C961560    50              push    eax
7C961561    E8 2073FFFF     call    7C958886
7C961566    E8 3773FFFF     call    7C9588A2
7C96156B    8BD8            mov     ebx, eax
7C96156D    33FF            xor     edi, edi
7C96156F    83FB FF         cmp     ebx, -1               //之前做一些准备工作,ebx为seh异常处理链表头,这里是在笑颜链表头是否在线程堆栈领空
7C961572    74 6E           je      short 7C9615E2
7C961574    3B5D 08         cmp     ebx, dword ptr [ebp+8]
7C961577  ^ 0F82 28A1FDFF   jb      7C93B6A5
7C96157D    8D43 08         lea     eax, dword ptr [ebx+8]
7C961580    3B45 F8         cmp     eax, dword ptr [ebp-8]           ; Exceptio.0040A880
7C961583  ^ 0F87 1CA1FDFF   ja      7C93B6A5
7C961589    F6C3 03         test    bl, 3
7C96158C  ^ 0F85 13A1FDFF   jnz     7C93B6A5
7C961592    8B43 04         mov     eax, dword ptr [ebx+4] //这里取出异常处理函数入口,并判断入口是否在堆栈里面,防止益出攻击
7C961595    3B45 08         cmp     eax, dword ptr [ebp+8]
7C961598    72 09           jb      short 7C9615A3
7C96159A    3B45 F8         cmp     eax, dword ptr [ebp-8]           ; Exceptio.0040A880
7C96159D  ^ 0F82 02A1FDFF   jb      7C93B6A5
7C9615A3    50              push    eax
7C9615A4    E8 51000000     call    7C9615FA //这里就是这次事故核心,这里是效验函数合法性
7C9615A9    84C0            test    al, al
7C9615AB  ^ 0F84 F4A0FDFF   je      7C93B6A5
7C9615B1    FF73 04         push    dword ptr [ebx+4]
7C9615B4    8D45 F4         lea     eax, dword ptr [ebp-C]
7C9615B7    50              push    eax
7C9615B8    FF75 0C         push    dword ptr [ebp+C]
7C9615BB    53              push    ebx
7C9615BC    56              push    esi
7C9615BD    E8 2E71FFFF     call    7C9586F0                         ; 调用seh处理
既然事故核心函数出来了,当然要跟进去看看
7C9615FA    8BFF            mov     edi, edi
7C9615FC    55              push    ebp
7C9615FD    8BEC            mov     ebp, esp
7C9615FF    83EC 34         sub     esp, 34
7C961602    A1 30779B7C     mov     eax, dword ptr [7C9B7730]
7C961607    53              push    ebx
7C961608    56              push    esi
7C961609    8B75 08         mov     esi, dword ptr [ebp+8]
7C96160C    8945 FC         mov     dword ptr [ebp-4], eax           ; Exceptio.00401000
7C96160F    57              push    edi
7C961610    8D45 F8         lea     eax, dword ptr [ebp-8]
7C961613    50              push    eax                              ; Exceptio.00401000
7C961614    8D45 EC         lea     eax, dword ptr [ebp-14]
7C961617    50              push    eax                              ; Exceptio.00401000
7C961618    33DB            xor     ebx, ebx
7C96161A    56              push    esi
7C96161B    895D F4         mov     dword ptr [ebp-C], ebx
7C96161E    E8 58000000     call    7C96167B   //这里从pe 头的directories目录的loadconfig取出一个地址线性数组指针,和数组大小,下面的代码就是根据这个数组判断seh处理函数是否在这些数据当中,也就是是否合法,这样也是为了安全性,防止不合法的seh处理函数被调用,一开始我也纳闷,因为找了好多资料都都没有提到seh异常处理会效验处理函数的合法性
7C961623    3BC3            cmp     eax, ebx
7C961625    8945 F0         mov     dword ptr [ebp-10], eax          ; Exceptio.00401000
7C961628    0F84 30690200   je      7C987F5E
7C96162E    8B7D F8         mov     edi, dword ptr [ebp-8]
7C961631    3BFB            cmp     edi, ebx
7C961633    0F84 25690200   je      7C987F5E
7C961639    83F8 FF         cmp     eax, -1
7C96163C    0F84 07690200   je      7C987F49
7C961642    2B75 EC         sub     esi, dword ptr [ebp-14]
7C961645    33D2            xor     edx, edx                         ; ntdll.KiFastSystemCallRet
7C961647    3BFB            cmp     edi, ebx
7C961649    0F8C 04690200   jl      7C987F53
7C96164F    8D0C17          lea     ecx, dword ptr [edi+edx]
7C961652    D1F9            sar     ecx, 1
7C961654    8B1C88          mov     ebx, dword ptr [eax+ecx*4]
7C961657    3BF3            cmp     esi, ebx
7C961659  ^ 0F82 1EA0FDFF   jb      7C93B67D
7C96165F  ^ 0F87 E56EFDFF   ja      7C93854A
7C961665    B0 01           mov     al, 1
7C961667    8B4D FC         mov     ecx, dword ptr [ebp-4]
7C96166A    5F              pop     edi                              ; ntdll.7C9615A9
7C96166B    5E              pop     esi                              ; ntdll.7C9615A9
7C96166C    5B              pop     ebx                              ; ntdll.7C9615A9
7C96166D    E8 358FFFFF     call    7C95A5A7
7C961672    C9              leave
7C961673    C2 0400         retn    4

下面代码是xp sp2代码,更加好理解,win2003 的代码从效率上优化过,以至于我这286的头脑理解不过来
7C9579C8    8BFF            mov     edi, edi                         ; Exceptio.00401000
7C9579CA    55              push    ebp
7C9579CB    8BEC            mov     ebp, esp
7C9579CD    51              push    ecx                              ; Exceptio.0040DC70
7C9579CE    FF75 08         push    dword ptr [ebp+8]                ; Exceptio.00400000
7C9579D1    E8 738EFDFF     call    RtlImageNtHeader
7C9579D6    F640 5F 04      test    byte ptr [eax+5F], 4
7C9579DA    0F85 CF7C0100   jnz     7C96F6AF
7C9579E0    8D45 FC         lea     eax, [ebp-4]
7C9579E3    50              push    eax                              ; Exceptio.0040DAE8
7C9579E4    6A 0A           push    0A
7C9579E6    6A 01           push    1
7C9579E8    FF75 08         push    dword ptr [ebp+8]                ; Exceptio.00400000
7C9579EB    E8 668EFDFF     call    RtlImageDirectoryEntryToData
7C9579F0    85C0            test    eax, eax                         ; Exceptio.0040DAE8
7C9579F2    0F84 D8010000   je      7C957BD0
7C9579F8    8B4D FC         mov     ecx, [ebp-4]
7C9579FB    85C9            test    ecx, ecx                         ; Exceptio.0040DC70
7C9579FD    0F84 CD010000   je      7C957BD0
7C957A03    83F9 40         cmp     ecx, 40
7C957A06  ^ 0F85 361DFEFF   jnz     7C939742
7C957A0C    8338 48         cmp     dword ptr [eax], 48
7C957A0F    0F82 BB010000   jb      7C957BD0
7C957A15    8B48 40         mov     ecx, [eax+40]                    ; Exceptio.0040DC70
7C957A18    85C9            test    ecx, ecx                         ; Exceptio.0040DC70
7C957A1A    0F84 B0010000   je      7C957BD0
7C957A20    8378 44 00      cmp     dword ptr [eax+44], 0
7C957A24    0F84 A6010000   je      7C957BD0
7C957A2A    8B55 0C         mov     edx, [ebp+C]
7C957A2D    890A            mov     [edx], ecx                       ; Exceptio.0040DC70
7C957A2F    8B40 44         mov     eax, [eax+44]
7C957A32    8B4D 10         mov     ecx, [ebp+10]
7C957A35    8901            mov     [ecx], eax                       ; Exceptio.0040DAE8
7C957A37    C9              leave
7C957A38    C2 0C00         retn    0C

很容易我们就能看出这个函数的意图,就是取LoadConfig第40字节开始为数组头指针,44字节开始dowrd 为数组长度
我查阅了一些关于pe头的文档上面都只提到loadconfig的用途和目的不明确,不过我想msdn上面应该有详细介绍的,可惜本人英语水平实在是烂到了极点,所以不想在晚上了还被这个折磨,由此我们可以看出LoadConfig与seh异常处理是有关系的,而且vs2005连接器好象还没有关闭生成loadconfig的选项,此时只要我们用一个工具将pe文件的loadconfig目录清0,程序运行就又可以正常捕获异常了.当然你也许会建议我使用__try __except或者指定catch异常类型等等,不过既然问题已经来了,那么我们至少应该尝试着去解决.
既然问题已经找到了,那么我们得尝试去解决,我们想到了最简单的办法就是清零LoadConfig,但是我们如何保证LoadConfig没有其他用途呢,况且每次编译都要这样还不麻烦死,于是呼我们可以寻求更简单一点的方法,就是hook,我们使用try catch(...)后,程序会自动在函数开始注册一个seh处理链,这个注册过程优先于所有用户代码,所以我们可以简单的写一个函数来达到目的,我的函数代码如下,这样即使hook失败了,我们依然可以使用catch(...)处理过程,但是这样以来,代码发生异常后就只会回调函数pCProc


int    __stdcall CatchProc(EXCEPTION_RECORD* pExcept,void* pErr,void* pContext,void* p_Dis)
{
    if (pExcept->ExceptionCode == EXCEPTION_BREAKPOINT)
    {
        printf("catched");
        return 0;
    }
    printf("catched");
    return 1;
}
 

bool __stdcall SetCatchProc(void*    pCProc)
{
    char* p_ExpProcEntry;
    __asm
    {
        mov eax,fs:[0]
        add eax,4
        mov eax,dword ptr [eax]
        mov p_ExpProcEntry,eax
    }
    DWORD p_oldProtect = 0;
    if (VirtualProtect(p_ExpProcEntry,5,PAGE_EXECUTE_READWRITE,&p_oldProtect))
    {
        *(BYTE*)p_ExpProcEntry = 0xE9;
        *(DWORD*)(p_ExpProcEntry+1)=(char*)pCProc-p_ExpProcEntry-5;
        return true;
    }
    return false;
}

当然可以简单的使用__try __except或者指定catch异常类型来捕获异常,不过我觉得使用seh异常获得的信息丰富些.