zzage 发表于 2009-8-12 01:19

利用 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:
:00434E6A 50                      push eax
:00434E6B 64892500000000          mov dword ptr fs:, esp

其中几个被 push 的具体的值可能不同。根据这一点我们就可以在进程中查找上面这部分代码,找到的地方就是入口点。下面来看看具体的代码实现:
.data
g_Delphi_Signsdb55h, 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   db55h, 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
      mov   bl, byte ptr
      cmp   al, bl
      jz      Loop2
      inc   edx
      jmp   Loop1
      
    Loop2:
      inc   ecx
      inc   edx
      cmp   ecx, dwSubStrLen
      jz      exit_1
      mov   al, byte ptr
      mov   bl, byte ptr
      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 endpg_Delphi_Signs 和 g_VC6_Signs 分别对应 Delphi 和 VC6 编译的程序,其中的 0 代表可能不确定的字节。_GetOEP 函数就是具体获得入口点的函数,分别在进程空间中查找每一个特定的

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

2. 中断在入口点

    找到了入口点后,需要中断在入口点处准备 dump 进程,通过 Windows 本身提供的 Debug API 可以实现这一点。
invokeCreateProcess, 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
      invokeWaitForDebugEvent, addr DbgEvent, INFINITE
      .if DbgEvent.dwDebugEventCode==EXIT_PROCESS_DEBUG_EVENT
                ;下面这一行代码很重要,否则被调试进程不会完全退出
                invokeContinueDebugEvent, 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
                              invokeReadProcessMemory, ProcInfo2.hProcess, dwOrgOEP, addr org_code, 1, 0
                              invokeWriteProcessMemory, ProcInfo2.hProcess, dwOrgOEP, addr int3, 1, 0
                              
                        .elseif dwCountBP==2    ;第二次中断,这次是中断在原始入口点,在 OEP 处设置硬件断点
                              invoke_OutputInfo, g_hOutputCtl, CTXT("到达原始入口点")
                              
                              mov   g_context.ContextFlags, CONTEXT_CONTROL
                              invokeGetThreadContext, ProcInfo2.hThread, addr g_context
                              dec   g_context.regEip
                              invokeWriteProcessMemory, ProcInfo2.hProcess, dwOrgOEP, addr org_code, 1, 0
                              invokeSetThreadContext, ProcInfo2.hThread, addr g_context
                              
                              mov   g_context.ContextFlags, CONTEXT_DEBUG_REGISTERS
                              invokeGetThreadContext, ProcInfo2.hThread, addr g_context
                              m2m   g_context.iDr0, dwOEP
                              mov   g_context.iDr7, 1
                              invokeSetThreadContext, ProcInfo2.hThread, addr g_context
                              
                              invokewsprintf, addr buf, CTXT("在 OEP: %08lXh 处设置硬件断点..."), dwOEP
                              invoke_OutputInfo, g_hOutputCtl, addr buf
                        .endif
                        invokeContinueDebugEvent, DbgEvent.dwProcessId, DbgEvent.dwThreadId, DBG_CONTINUE
                        .continue
                .elseif DbgEvent.u.Exception.pExceptionRecord.ExceptionCode==EXCEPTION_SINGLE_STEP
                ;第三次中断,来到真正的入口点,抓取进程,然后终止进程
                        invokewsprintf, addr buf, CTXT("中断在 OEP: %08lXh 处"), dwOEP
                        invoke_OutputInfo, g_hOutputCtl, addr buf
                        invoke_OutputInfo, g_hOutputCtl, CTXT("清除硬件断点...")
                        
                        mov   g_context.ContextFlags, CONTEXT_FULL
                        invokeGetThreadContext, ProcInfo2.hThread, addr g_context
                        mov   g_context.iDr0, 0
                        mov   g_context.iDr7, 0
                        invokeSetThreadContext, ProcInfo2.hThread, addr g_context
                        
                        invoke_OutputInfo, g_hOutputCtl, CTXT("抓取进程...")
                        invoke_Dump, ProcInfo2.hProcess, dwImageBase, dwSizeOfImage, lpMem
                        invokeTerminateProcess, ProcInfo2.hProcess, 0
                        invokeContinueDebugEvent, DbgEvent.dwProcessId, DbgEvent.dwThreadId, DBG_CONTINUE
                        .continue
                .endif
      .endif
      invokeContinueDebugEvent, DbgEvent.dwProcessId, DbgEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED
.endw
invokeCloseHandle, ProcInfo2.hThread
invokeCloseHandle, 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
      invokeReadProcessMemory, hProcess, lpBaseAddress, lpBuffer, dwSize, 0
      popad
      ret
_Dump endp4. 修复输入表

    修复输入表可以利用 ImpREC.dll 来完成,这个也很简单,只需调用一个 RebuildImport 函数就可以搞定。
mov   g_lpRebuildImport, 0
invokeLoadLibrary, CTXT("ImpREC.dll")
.if eax
      mov   ebx, eax
      invokeGetProcAddress, 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

invokeCreateProcess, NULL, addr szFile, NULL, NULL, NULL, NORMAL_PRIORITY_CLASS, \
                     NULL, NULL, addr StartupInfo, addr ProcInfo3
invokeWaitForInputIdle, 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
      invokeDeleteFile, addr g_buffer
      lea   esi, g_buffer
      invokelstrlen, esi
      add   esi, eax
      sub   esi, 4
      invokelstrcpy, esi, CTXT("_.exe")
.endif
invokeTerminateProcess, ProcInfo3.hProcess, 0后记

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

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

asdfslw 发表于 2009-8-14 21:05

怎么连UPX都脱不了啊:(

datochan 发表于 2009-8-16 07:55

跟调试没什么区别,写个静态的脱壳机吧。
这样能学到更多的东西~~

1zm 发表于 2009-8-25 00:57

了不起,支持,啊

zouyelin 发表于 2011-12-11 22:03

页: [1]
查看完整版本: 利用 Debug API 编写一个简单的脱壳机