利用 Debug API 编写一个简单的脱壳机

作者: 一块三毛钱
邮件: zhongts@163.com
日期: 2005.2.22


    脱壳的一般步骤是:查找入口点,中断在入口点,dump 进程,修复输入表。大家一般借助调试器来完成这几步。下面我就来介绍如何通过编程实现一个简单的脱壳机,自动完成上面的

几个步骤。

1. 查找入口点

    查找入口点可以利用现有的工具来完成,如 PEiD、PE-Scan 等。通过对 PEiD 中的 GenOEP 插件的逆向工程我们可以找到如下方法来查找入口点。这种方法的根据就是每个编译器编译

出来的程序在入口点处的代码通常是一样的。比如说 VC6 编译的程序,入口点处的部分代码一般都是下面这个样子:

:00434E55 55                      push ebp
:00434E56 8BEC                    mov ebp, esp
:00434E58 6AFF                    push FFFFFFFF
:00434E5A 68302E4500              push 00452E30
:00434E5F 68A83F4300              push 00433FA8
:00434E64 64A100000000            mov eax, dword ptr fs:[00000000]
:00434E6A 50                      push eax
:00434E6B 64892500000000          mov dword ptr fs:[00000000], esp

其中几个被 push 的具体的值可能不同。根据这一点我们就可以在进程中查找上面这部分代码,找到的地方就是入口点。下面来看看具体的代码实现:

.data
g_Delphi_Signs  db  55h, 8Bh, 0ECh, 83h, 0C4h, 0, 53h, 0B8h, 0, 0, 0, 0, 0E8h, 0, 0,
                    0, 0, 8Bh, 1Dh, 0, 0, 0, 0, 8Bh, 3h, 0E8h, 0, 0, 0, 0, 8Bh, 3h
g_VC6_Signs     db  55h, 8Bh, 0ECh, 6Ah, 0FFh, 68h, 0, 0, 0, 0, 68h, 0, 0, 0, 0, 64h,
                    0A1h, 0, 0, 0, 0, 50h, 64h, 89h, 25h, 0, 0, 0, 0

.code
_GetOEP proc lpMem:DWORD, dwLen:DWORD
        LOCAL   dwOEP
        
        pushad
        invoke  _InString, lpMem, dwLen, addr g_Delphi_Signs, 32
        .if eax
                jmp     exit_1
        .endif
        
        invoke  _InString, lpMem, dwLen, addr g_VC6_Signs, 29
        .if eax
                jmp     exit_1
        .endif
        
        jmp     exit_0
        
exit_1:
        mov     dwOEP, eax
        popad
        mov     eax, dwOEP
        ret
exit_0:
        popad
        xor     eax, eax
        ret
_GetOEP endp

_InString proc lpszStr:DWORD, dwStrLen:DWORD, lpszSubStr:DWORD, dwSubStrLen:DWORD
        LOCAL   dwPos
        
        pushad
        mov     eax, dwStrLen
        .if eax < dwSubStrLen
                jmp     exit_0
        .endif
        sub     eax, dwSubStrLen
        mov     dwStrLen, eax
        
        mov     esi, lpszStr
        mov     edi, lpszSubStr
        xor     edx, edx
        
    Loop1: 
        cmp     edx, dwStrLen
        jz      exit_0
        xor     ecx, ecx
        mov     al, byte ptr [edi+ecx]
        mov     bl, byte ptr [esi+edx]
        cmp     al, bl
        jz      Loop2
        inc     edx
        jmp     Loop1
        
    Loop2: 
        inc     ecx
        inc     edx
        cmp     ecx, dwSubStrLen
        jz      exit_1
        mov     al, byte ptr [edi+ecx]
        mov     bl, byte ptr [esi+edx]
        cmp     al, bl
        jz      Loop2
        test    al, al
        jz      Loop2
        sub     edx, ecx
        inc     edx
        jmp     Loop1
        
exit_1:
        sub     edx, ecx
        mov     dwPos, edx
        popad
        mov     eax, dwPos
        ret
        
exit_0:
        popad
        xor     eax, eax
        ret
_InString endp

g_Delphi_Signs 和 g_VC6_Signs 分别对应 Delphi 和 VC6 编译的程序,其中的 0 代表可能不确定的字节。_GetOEP 函数就是具体获得入口点的函数,分别在进程空间中查找每一个特定的

入口点特征代码,如果能找到就说明找到了入口点。查找特征代码又是由函数 _InString 来完成的,具体实现看看代码就清楚了。

2. 中断在入口点

    找到了入口点后,需要中断在入口点处准备 dump 进程,通过 Windows 本身提供的 Debug API 可以实现这一点。

invoke  CreateProcess, 0, addr szFile, 0, 0, 0, DEBUG_PROCESS + DEBUG_ONLY_THIS_PROCESS, 
                0, 0, addr StartupInfo, addr ProcInfo2
.if !eax
        invoke  _OutputInfo, g_hOutputCtl, CTXT("不能创建进程!!!")
        jmp     l_exit
.endif
.while TRUE
        invoke  WaitForDebugEvent, addr DbgEvent, INFINITE
        .if DbgEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
                ;下面这一行代码很重要,否则被调试进程不会完全退出
                invoke  ContinueDebugEvent, DbgEvent.dwProcessId, DbgEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
                .break
        .elseif DbgEvent.dwDebugEventCode==EXCEPTION_DEBUG_EVENT
                .if DbgEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_BREAKPOINT
                        inc     dwCountBP
                        .if dwCountBP==1        ;第一次中断时在原始入口点处设置断点
                                invoke  _OutputInfo, g_hOutputCtl, CTXT("在原始入口点设置断点...")
                                mov     int3, 0CCh
                                invoke  ReadProcessMemory, ProcInfo2.hProcess, dwOrgOEP, addr org_code, 1, 0
                                invoke  WriteProcessMemory, ProcInfo2.hProcess, dwOrgOEP, addr int3, 1, 0
                                
                        .elseif dwCountBP==2    ;第二次中断,这次是中断在原始入口点,在 OEP 处设置硬件断点
                                invoke  _OutputInfo, g_hOutputCtl, CTXT("到达原始入口点")
                                
                                mov     g_context.ContextFlags, CONTEXT_CONTROL
                                invoke  GetThreadContext, ProcInfo2.hThread, addr g_context
                                dec     g_context.regEip
                                invoke  WriteProcessMemory, ProcInfo2.hProcess, dwOrgOEP, addr org_code, 1, 0
                                invoke  SetThreadContext, ProcInfo2.hThread, addr g_context
                                
                                mov     g_context.ContextFlags, CONTEXT_DEBUG_REGISTERS
                                invoke  GetThreadContext, ProcInfo2.hThread, addr g_context
                                m2m     g_context.iDr0, dwOEP
                                mov     g_context.iDr7, 1
                                invoke  SetThreadContext, ProcInfo2.hThread, addr g_context
                                
                                invoke  wsprintf, addr buf, CTXT("在 OEP: %08lXh 处设置硬件断点..."), dwOEP
                                invoke  _OutputInfo, g_hOutputCtl, addr buf
                        .endif
                        invoke  ContinueDebugEvent, DbgEvent.dwProcessId, DbgEvent.dwThreadId, DBG_CONTINUE
                        .continue
                .elseif DbgEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
                ;第三次中断,来到真正的入口点,抓取进程,然后终止进程
                        invoke  wsprintf, addr buf, CTXT("中断在 OEP: %08lXh 处"), dwOEP
                        invoke  _OutputInfo, g_hOutputCtl, addr buf
                        invoke  _OutputInfo, g_hOutputCtl, CTXT("清除硬件断点...")
                        
                        mov     g_context.ContextFlags, CONTEXT_FULL
                        invoke  GetThreadContext, ProcInfo2.hThread, addr g_context
                        mov     g_context.iDr0, 0
                        mov     g_context.iDr7, 0
                        invoke  SetThreadContext, ProcInfo2.hThread, addr g_context
                        
                        invoke  _OutputInfo, g_hOutputCtl, CTXT("抓取进程...")
                        invoke  _Dump, ProcInfo2.hProcess, dwImageBase, dwSizeOfImage, lpMem
                        invoke  TerminateProcess, ProcInfo2.hProcess, 0
                        invoke  ContinueDebugEvent, DbgEvent.dwProcessId, DbgEvent.dwThreadId, DBG_CONTINUE
                        .continue
                .endif
        .endif
        invoke  ContinueDebugEvent, DbgEvent.dwProcessId, DbgEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invoke  CloseHandle, ProcInfo2.hThread
invoke  CloseHandle, ProcInfo2.hProcess
mov     ProcInfo2.hProcess, 0

关键技术就是要在入口点处设置一个硬件断点,从而中断在入口点处准备 dump 进程。这里要设置硬件断点而不能设置一个 int3 断点的原因是我们设置断点的时候外壳还没有解密程序代码

。如果我们在入口点处写入一个 0CCh 字节来设置一个 int3 断点,当外壳把程序代码解密后,入口点处的 0CCh 字节又会被解密后的代码覆盖,所以 int3 断点不起作用。

3. dump 进程

    中断在入口点处了就可以 dump 进程,这个代码很简单

_Dump proc hProcess:DWORD, lpBaseAddress:DWORD, dwSize:DWORD, lpBuffer:DWORD
        pushad
        invoke  ReadProcessMemory, hProcess, lpBaseAddress, lpBuffer, dwSize, 0
        popad
        ret
_Dump endp

4. 修复输入表

    修复输入表可以利用 ImpREC.dll 来完成,这个也很简单,只需调用一个 RebuildImport 函数就可以搞定。

mov     g_lpRebuildImport, 0
invoke  LoadLibrary, CTXT("ImpREC.dll")
.if eax
        mov     ebx, eax
        invoke  GetProcAddress, ebx, CTXT("RebuildImport")
        .if eax
                mov     g_lpRebuildImport, eax
        .else
                invoke  _OutputInfo, g_hOutputCtl, CTXT("不能从 ImpREC.dll 中引入 RebuildImport 函数")
                invoke  _OutputInfo, g_hOutputCtl, CTXT("脱壳后的文件不能重建输入表!!!")
        .endif
.else
        invoke  _OutputInfo, g_hOutputCtl, CTXT("找不到 ImpREC.dll 文件")
        invoke  _OutputInfo, g_hOutputCtl, CTXT("脱壳后的文件不能重建输入表!!!")
.endif

invoke  CreateProcess, NULL, addr szFile, NULL, NULL, NULL, NORMAL_PRIORITY_CLASS, \
                       NULL, NULL, addr StartupInfo, addr ProcInfo3
invoke  WaitForInputIdle, ProcInfo3.hProcess, -1
invoke  _OutputInfo, g_hOutputCtl, CTXT("重建输入表...")
mov     ecx, dwOEP
sub     ecx, dwImageBase
lea     eax, g_buffer

push    eax
push    5
push    0
push    ecx
push    ProcInfo3.dwProcessId
call    g_lpRebuildImport       ;调用 ImpREC.dll 中的 RebuildImport 函数重建输入表

.if eax==0
        invoke  _OutputInfo, g_hOutputCtl, CTXT("重建输入表失败!!!")
.else
        invoke  DeleteFile, addr g_buffer
        lea     esi, g_buffer
        invoke  lstrlen, esi
        add     esi, eax
        sub     esi, 4
        invoke  lstrcpy, esi, CTXT("_.exe")
.endif
invoke  TerminateProcess, ProcInfo3.hProcess, 0

后记

    除了上面介绍的几个步骤外,还有文件修正,文件结构优化等可以参考本文附件中给出的代码。这里实现的只不过是一个很简单的脱壳机,对付不了几个壳。在下是一个菜鸟,文章很简

单可能还有很多错误,只是希望这篇文章能够对大家有一点点帮助。谢谢!

附件:EasyUnpack v0.1.1.zip