利用 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
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!