(本文章纯属虚构,如有雷同纯属巧合)

   这个SEH记录小工具是学习脱壳时的“副产品”。大多数加密壳都少不了用SEH进行反跟踪、反调试,不过前辈们说过“这些连续的SEH给调试者指明了一条通往正确目标的道路”,所以对一个壳的SEH了解应该是比较必要的。但是在OD上不断重复重复再重复地“shift+F9”毕竟是一件比较烦人的事,所以就写了这个小工具,让其自动记录SEH发生异常的地址以及其对应的Handler地址,这样一下子就能知道第几个SEH后就开始处理输入表、第几个SEH后就会跳到OEP,直达代码核心,还能根据Handler地址正确设置断点,这样“Shift+F9”就保证不会跑飞。好,废话少说,下面跟大家来分享。

一,一个脱壳实例
    看雪学院编著的《软件加密技术内幕》第三章第二节有一个Hying前辈写的tElock0.98脱壳机源码,KANXUE编著的《加密与解密(第二版)》第十一章第七节脱壳实例有两个用tElock0.98加了壳的样本,分别是Note_tElock.exe、No-Anti.exe。然后用Hying前辈写的脱壳机去脱这两个壳,Note_tElock.exe脱壳成功,No-Anti.exe脱壳失败。
    这个脱壳机会在第16次SEH时候设置断点保护输入表,第20次SEH时候DUMP文件。用SEHstat.exe(附件中)打开Note_tElock.exe及No-Anti.exe,很快就生成两份SEH数据。如下:
    ----------------------------------------------------------------------------------------------
    Note_tElock.exe
    ImageBase:00400000h  StartAddress:0040DBD6h ProcessId:1256 ThreadId:388
    ...
    ---------------------------------main start-----------------------------------
    Exception 01 Address:0040DA1Dh(-441) Handler:0040DA0E(-456) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 02 Address:0040DA74h(-354) Handler:0040DA58(-382) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 03 Address:0040C08Ch(-6986) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_BREAKPOINT
    Exception 04 Address:0040C090h(-6982) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 05 Address:0040C099h(-6973) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 06 Address:0040C09Eh(-6968) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 07 Address:0040C0A3h(-6963) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 08 Address:0040C0A7h(-6959) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_INT_DIVIDE_BY_ZERO
    Exception 09 Address:0040C6A8h(-5422) Handler:0040C68A(-5452) ExcptionCode:EXCEPTION_ILLEGAL_INSTRUCTION
    Exception 10 Address:0040CAA1h(-4405) Handler:0040CA90(-4422) ExcptionCode:EXCEPTION_INT_DIVIDE_BY_ZERO
    Exception 11 Address:0040CAE4h(-4338) Handler:0040CAC2(-4372) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 12 Address:0040CB27h(-4271) Handler:0040CB03(-4307) ExcptionCode:EXCEPTION_BREAKPOINT
    Exception 13 Address:0040CB67h(-4207) Handler:0040CB41(-4245) ExcptionCode:EXCEPTION_INT_DIVIDE_BY_ZERO
    Exception 14 Address:0040CBA6h(-4144) Handler:0040CB84(-4178) ExcptionCode:EXCEPTION_ACCESS_VIOLATION
    Exception 15 Address:0040CBF0h(-4070) Handler:0040CBC4(-4114) ExcptionCode:EXCEPTION_BREAKPOINT
    Exception 16 Address:0040CE0Dh(-3529) Handler:0040CDFE(-3544) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 17 Address:0040CE49h(-3469) Handler:0040CE2D(-3497) ExcptionCode:EXCEPTION_SINGLE_STEP
    Load Dll   7D590000h C:\WINDOWS\system32\SHELL32.dll
    Load Dll   77F40000h C:\WINDOWS\system32\SHLWAPI.dll
    Load Dll   77180000h C:\WINDOWS\WinSxS\...\comctl32.dll
    Load Dll   5D170000h C:\WINDOWS\system32\comctl32.dll
    Load Dll   76320000h C:\WINDOWS\system32\comdlg32.dll
    Exception 18 Address:0040D6F1h(-1253) Handler:0040D6FF(-1239) ExcptionCode:EXCEPTION_ILLEGAL_INSTRUCTION
    Exception 19 Address:0040D7E1h(-1013) Handler:0040D7D2(-1028) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 20 Address:0040D817h(-959) Handler:0040D7FB(-987) ExcptionCode:EXCEPTION_SINGLE_STEP
    ;这里之后程序就运行了
   
    --------------------------------------------------------------------------------------------------
    No-Anti.exe
    ImageBase:00400000h  StartAddress:0040DBD6h ProcessId:1612 ThreadId:1632
    ...
    ---------------------------------main start-----------------------------------
    Exception 01 Address:0040D9FCh(-474) Handler:0040D9ED(-489) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 02 Address:0040DA37h(-415) Handler:0040DA1B(-443) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 03 Address:0040C08Ch(-6986) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_BREAKPOINT
    Exception 04 Address:0040C090h(-6982) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 05 Address:0040C099h(-6973) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 06 Address:0040C09Eh(-6968) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 07 Address:0040C0A3h(-6963) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 08 Address:0040C0A7h(-6959) Handler:0040C0C5(-6929) ExcptionCode:EXCEPTION_INT_DIVIDE_BY_ZERO
    Exception 09 Address:0040C6A8h(-5422) Handler:0040C68A(-5452) ExcptionCode:EXCEPTION_ILLEGAL_INSTRUCTION
    Exception 10 Address:0040CAA1h(-4405) Handler:0040CA90(-4422) ExcptionCode:EXCEPTION_INT_DIVIDE_BY_ZERO
    Exception 11 Address:0040CAE4h(-4338) Handler:0040CAC2(-4372) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 12 Address:0040CDF0h(-3558) Handler:0040CDE1(-3573) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 13 Address:0040CE28h(-3502) Handler:0040CE0C(-3530) ExcptionCode:EXCEPTION_SINGLE_STEP
    Load Dll   7D590000h C:\WINDOWS\system32\SHELL32.dll
    Load Dll   77F40000h C:\WINDOWS\system32\SHLWAPI.dll
    Load Dll   77180000h C:\WINDOWS\WinSxS\...\comctl32.dll
    Load Dll   5D170000h C:\WINDOWS\system32\comctl32.dll
    Load Dll   76320000h C:\WINDOWS\system32\comdlg32.dll
    Exception 14 Address:0040D6F1h(-1253) Handler:0040D6FF(-1239) ExcptionCode:EXCEPTION_ILLEGAL_INSTRUCTION
    Exception 15 Address:0040D7C8h(-1038) Handler:0040D7B9(-1053) ExcptionCode:EXCEPTION_SINGLE_STEP
    Exception 16 Address:0040D806h(-976) Handler:0040D7EA(-1004) ExcptionCode:EXCEPTION_SINGLE_STEP
    ;这里之后程序就运行了
    
    (说明:Address表示SEH异常发生的地址,Handler表示SEH处理函数的地址,括号里面的数据表示该地址相对程序入口点的偏移,ExcptionCode就是异常类型)
    ---------------------------------------------------------------------------------------------------------
    由上面两份SEH记录很明显可以看出,Note_tElock.exe的SEH很符合Hying前辈的统计数据,所以很顺利就脱了。但是第二份SEH明显就不符合,它总共才发生16次SEH异常而不是20次,在第13次SEH之后就开始处理输入表了,所以这个脱壳机报“locked.exe可能不是TeLock 0.98的外壳。”。
    知道失败原因,我们就可以对源代码进行修改,使它“兼容性”更好一些。由上面数据看来,在本例中根据第几次SEH脱壳可能不是个好主意,我的做法是直接对LoadLibraryA、GetModuleHandleA、VirtualProtectEx下断点(不是INT3哦),当程序第一次执行到LoadLibraryA或GetModuleHandleA时,说明开始处理输入表了,然后在函数返回地址向前80个字节范围内搜索“test  esi, esi;je  ****;”两条指令(不是硬编码偏移值),将函数返回地址直接修改为je ****中的****,这样当LoadLibraryA或GetModuleHandleA执行完毕直接就跳过后面的加密处理代码了(不用修改程序代码哦),而这时的ESI正是原来的输入表地址,可以直接记录下来。当程序执行到VirtualProtectEx时就可以DUMP了(趁PE头还没有被修改),然后大功告成,两个样本程序均能成功脱壳。(附件中有这个tElock0.98_Unpacker.exe)

二、SEH记录小工具的实现

代码:

lea ebx,DBEvent.u       
       .while TRUE
         
               invoke WaitForDebugEvent,addr DBEvent,INFINITE
               
               mov dwDebugOperation,DBG_CONTINUE
               
               .if DBEvent.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT       ;进程结束                     
                    ...                      
               .elseif DBEvent.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT ;进程创建
                       
                       assume ebx:ptr CREATE_PROCESS_DEBUG_INFO
                       
                       mov eax,[ebx].lpStartAddress
                       mov dwStartAddress,eax                       
                       ...
                       invoke _SetBreakPoint,pi.hProcess,dwStartAddress,addr pOldStartAddressCode ;在程序入口点设置INT3断点      
                       
               .elseif DBEvent.dwDebugEventCode == EXIT_THREAD_DEBUG_EVENT    ;线程结束
                       ...                       
               .elseif DBEvent.dwDebugEventCode == CREATE_THREAD_DEBUG_EVENT  ;线程创建
                       ...                       
               .elseif DBEvent.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT       ;DLL装载
                       
                       assume ebx:ptr LOAD_DLL_DEBUG_INFO
                       
                       invoke ReadProcessMemory,pi.hProcess,[ebx].lpImageName,addr pTempBuffer,4,NULL
                       mov eax,dword ptr pTempBuffer
                       invoke ReadProcessMemory,pi.hProcess,eax,addr pDllName,256,NULL
                       ...
                       
               .elseif DBEvent.dwDebugEventCode == UNLOAD_DLL_DEBUG_EVENT     ;DLL卸载
                       ...                                              
               .elseif DBEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT       ;进程异常       
                       
                       assume ebx:ptr EXCEPTION_DEBUG_INFO
                       
                       mov dwDebugOperation,DBG_EXCEPTION_NOT_HANDLED ;对不是预期的异常 通通都不处理
                       
                       invoke _GetExceptionText,[ebx].pExceptionRecord.ExceptionCode,addr szExceptionText
                       
                       .if [ebx].pExceptionRecord.ExceptionCode == EXCEPTION_BREAKPOINT
                               
                               .if bFirstBreakPoint == 1 ;第一次系统断点                                      
                                      invoke lstrcat,addr szExceptionText,addr szNtdllBreakPoint                                  

        
                                      mov dwDebugOperation,DBG_CONTINUE ;继续执行                                 
                                      mov bFirstBreakPoint,0                                      
                               .endif
                               
                               mov eax,[ebx].pExceptionRecord.ExceptionAddress
                               
                               .if eax == dwStartAddress  ;在程序入口点中断 记录一下
                                      
                                      invoke _CleanBreakPoint,pi.hProcess,dwStartAddress,offset pOldStartAddressCode ;清除INT3断点
                                      invoke _ContinueExecute,pi.hThread,dwStartAddress ;INT3之后需要修改EIP
                                      mov dwDebugOperation,DBG_CONTINUE ;继续执行  
                                      
                                      invoke _Hide,pi.hProcess,pi.dwThreadId ;隐藏调试器,目前只能防止IsDebuggerPresent检测 :-)
                                      
                                      invoke _AddRecord,addr szMain
                                      jmp @MyBreakPoint                                      
                                      
                               .endif
                              
                       .endif                                          
                       
                      ;除了自己设置的程序入口断点,其他的异常就开始记录
                       invoke _FindSEHHandler,pi.hProcess,DBEvent.dwThreadId ;查找SEH处理函数地址
                       mov edi,eax
                       mov esi,edi
                       sub esi,dwStartAddress ;计算相对程序入口点偏移
                       
                       mov eax,[ebx].pExceptionRecord.ExceptionAddress ;异常发生地址
                       mov edx,eax
                       sub edx,dwStartAddress ;计算相对程序入口点偏移
                       
                       invoke wsprintf,addr pTempBuffer,addr szExceptionInfo,nExceptionCounter,eax,edx,edi,esi,addr szExceptionText
                       invoke _AddRecord,addr pTempBuffer    ;把记录写到文件中去            
                       
                       inc nExceptionCounter    ;SEH计数器
                       
                       @MyBreakPoint:    
                                                                     
               .endif               
               
               invoke ContinueDebugEvent,DBEvent.dwProcessId,DBEvent.dwThreadId,dwDebugOperation                        
         
         
       .endw

       上面是调试主循环的代码,很简单。在程序创建完毕时对程序入口点设置断点,然后在主程序入口点中断时,提示一下主程序已经开始执行了,并进行调试器隐藏。

代码:

;查找异常处理函数所在地址
_FindSEHHandler proc uses esi edi ebx hProcess,dwThreadID
  
  LOCAL Teb:DWORD,SEHPtrAddress:DWORD
  
  invoke _FindTeb,dwThreadID ;查找该线程对应的Teb
  .if eax == FALSE
    mov eax,-1
    jmp @ExitFindSEHHandler
  .endif
  mov Teb,eax
  
  invoke ReadProcessMemory,hProcess,Teb,addr SEHPtrAddress,4,NULL ;获取当前SEH handler指针所在地址
  invoke ReadProcessMemory,hProcess,SEHPtrAddress,addr pTempBuffer,8,NULL ;获取当前EXCEPTION_REGISTRATION结构
    
  mov eax,dword ptr [pTempBuffer+4] ;得到SEH handler  
  
  
  @ExitFindSEHHandler:
  ret

_FindSEHHandler endp

       上面查找异常处理函数所在地址,也很简单。TEB的开始地方就是当前SEH链的EXCEPTION_REGISTRATION结构地址,EXCEPTION_REGISTRATION的第二部分就是SEH处理函数的地址。

代码:

;查找线程TEB
_FindTeb proc uses esi edi ebx dwThreadID
  
  LOCAL hThread:HANDLE,SelectorEntry:LDT_ENTRY
  
  invoke OpenThread,THREAD_ALL_ACCESS,FALSE,dwThreadID
  .if eax == FALSE
    jmp @ExitFindTeb
  .endif  
  mov hThread,eax
  
  invoke GetThreadContext,hThread,addr Context
  invoke GetThreadSelectorEntry,hThread,Context.regFs,addr SelectorEntry
  
        mov     edx, dword ptr [SelectorEntry.BaseLow]
        and     edx, 0FFFFh
        mov     eax, dword ptr [SelectorEntry.HighWord1.Bytes]
        and     eax, 0FFh
        shl     eax, 10h
        or      edx, eax
        mov     ecx, dword ptr [SelectorEntry.HighWord1.Bytes+3]
        and     ecx, 0FFh
        shl     ecx, 18h
        or      edx, ecx
        mov     ebx, edx
  
        invoke CloseHandle,hThread
       
        mov eax,ebx  
  
  @ExitFindTeb:
  ret

_FindTeb endp

    这上面短短十几行代码,却费了我最多时间。获取线程TEB地址,一开始就想到KTHREAD结构,但是这个结构要在ring0下才能访问,为这个小小程序写一个SYS并要考虑系统兼容性的话,真是杀鸡用牛刀了。后来想起fs:[0]就是TEB地址,即是FS段基地址。由于对保护模式内存管理还是云里雾里,根本不知怎么把FS寄存器选择器数据转为虚拟内存地址,后来跟踪OD才知道原来有GetThreadSelectorEntry这个函数,于是顺便把里面的代码拿过来用,再后来Google GetThreadSelectorEntry,原来网上有不少现成代码(以TEB为关键词却搜不到,奇怪)

代码:

;隐藏调试器 但只能防止IsDebuggerPresent检测 :-)
_Hide proc uses esi edi ebx hProcess,dwThreadID
  
  LOCAL pPatch:BYTE
  
  invoke _FindTeb,dwThreadID
  .if eax == FALSE
    jmp @ExitHide
  .endif
  
  add eax,30h  ;fs 30h偏移 就是PEB    
        
        invoke ReadProcessMemory,hProcess,eax,addr pTempBuffer,4,NULL
        
        mov edx,dword ptr pTempBuffer ;这里就是PEB啦        
        add edx,2  ;PEB+2->BeingDebugged 
        
        mov pPatch,0
        
        invoke WriteProcessMemory,hProcess,edx,addr pPatch,1,NULL ;BeingDebugged 项置0
  
  
  @ExitHide:
  ret

_Hide endp

   这段代码也很简单,找到目标进程的PEB.BeingDebugged直接patch。都是_FindTeb这个函数的功劳啊,让查找其他进程PEB的工作变得Very very easy!