WIN32汇编语言教程:第13章 进程控制 · 13.3 进程调试(5)
程序的开头用equ语句定义补丁的地址00401004h,并在dbPatch常量中定义补丁位置的原先代码,在dbPatched常量中定义了要写入的补丁内容。
程序首先使用CreateProcess函数创建Test.exe进程,如果创建成功,则先用ReadProcessMemory函数读出补丁处的原来内容,并验证是否和我们保留的相同。因为用户可能将补丁程序用在不同版本的目标程序中,这时候补丁就会写入错误的地址,这可能会引起目标程序崩溃,首先验证补丁处的原有内容是否正确,就可以防止发生这种错误的发生。通过了内容的校验以后,程序再使用WriteProcessMemory函数将补丁写入正确的位置。
虽然程序很简单,但是读者还要注意两个细节:首先是创建进程的时候最好使用CREATE_SUSPENDED标志,这样目标进程的主线程一开始是被挂起的,可以防止补丁程序在执行补丁代码的过程中被Windows打断,然后切换到目标进程去的情况,当补丁完成以后,使用ResumeThread函数恢复目标进程的执行就可以了;第二点是完成补丁以后,不要忘记使用CloseHandle语句将CreateProcess返回的进程句柄和线程句柄关闭。
例子中使用了CreateProcess函数来创建目标进程,对于使用这种方法得到的目标进程句柄,父进程拥有全部权限,可以自由读写子进程的地址空间。如果使用OpenProcess去获取运行中的目标进程的句柄时,不要忘了指定PROCESS_VM_READ和PROCESS_VM_WRITE权限,否则补丁操作会失败。
13.3.3 调试API的使用
对于上面的内存补丁例子,读者可能会说:这种东西还好意思拿出来亮相,又没有实用价值!只要用16进制编辑器去Test.exe中查找数据串“74h、15h”然后直接改成“90h、90h”就一劳永逸了,何必这样兴师动众写一个补丁程序呢?的确如此,不过使用这个例子是为了介绍ReadProcessMemory和WriteProcessMemory函数的用法,而且可以用来继续引出本节中的例子程序。
稍微接触过加密解密的读者肯定见过“加壳程序”和“脱壳程序”这两个名词。“加壳”指将可执行文件的代码和数据经过某种变换后存储,并在原来的可执行文件中添加一段用于还原的代码,这样在执行程序的时候,这些代码会自动将原来可执行文件的代码和数据还原并执行,用户并不会感觉到程序被改动过,这段用于还原的代码就像是一层壳附在原来文件外面,所以对文件进行变换处理的程序就被叫做“加壳程序”。
“加壳”的原因有两个:压缩和加密。压缩程序可以将可执行文件的内容压缩存储,这样文件占用的磁盘空间可以缩小,这时“壳代码”就是解压缩代码;而加密程序则是为了保证可执行文件的内容不被随便修改(就像上面讲的用16进制编辑器去修改关键代码),这时“壳代码”就是解密代码,一般解密代码中同时包含有反跟踪模块。现在的大部分加密软件同时有压缩的功能,如Aspack,PECompact和ASProtect等软件。在被加壳的文件中,原来的代码和数据已经面目全非了,用16进制编辑器去寻找特征码是根本无法找到的,所以无法用修改文件的方法进行静态补丁。
要对加过壳的软件进行修改必须首先将它脱壳,但大部分的加壳软件都无法用简单的方法去对付,除了一些加密程度不高的“壳”可以利用逆算法完全恢复原来的文件外,有很多“壳”是无法用逆运算对付的。虽然可以用Soft-ICE等跟踪软件跟踪到壳代码的内部,并一直跟踪到壳代码将原来的可执行文件恢复为止,然后再手工去改动内存中的关键代码,但用户不可能为了执行程序而每次都用调试器去载入可执行文件,并且花一段时间去跟踪并做“手工补丁”。
既然无法用算法将文件还原,尝试从已经正常运行的进程中拷贝代码会怎么样呢?这就是ProcDump之类的软件做的事情,这些软件等待壳代码将可执行文件在内存中恢复,然后从内存中拷贝已还原的代码并写成一个可执行文件,但效果并不那么令人满意。
在这种情况下,内存补丁技术就又派上用途了,这时的关键就在于补丁程序必须有调试器的功能,可以模拟手工使用Soft-ICE等软件进行跟踪的过程,一直跟踪到“壳代码”执行完毕,可执行文件的代码被完全恢复以后再去打内存补丁,Win32中的调试API就可以让我们做到这一点。
1. 改进后的内存补丁程序
首先来改造Test.exe,将它加在一层壳里面,为了简化操作,我们使用Upx压缩软件将它压缩。现在用W32Dasm将压缩后的Test.exe反汇编,可以发现入口地址已经不一样了,入口处的代码也不一样了。下面就是Upx生成的动态解压缩代码:
//******************** Program Entry Point ********
:00405120 60 pushad
:00405121 BE00504000 mov esi, 00405000
:00405126 8DBE00C0FFFF lea edi, dword ptr [esi+FFFFC000]
:0040512C 57 push edi
:0040512D 83CDFF or ebp, FFFFFFFF
:00405130 EB10 jmp 00405142
:00405132 90 nop
:00405133 90 nop
...
:0040526E 61 popad
:0040526F E98CBDFFFF jmp 00401000
注意:整段代码的最后有一句jmp 00401000,00401000h就是Test.exe程序原来的入口地址,实际上执行到这里的时候,解压缩代码已经结束,可执行文件原来的代码和数据已经被恢复。我们要做的事情就是使用调试API将程序执行到解压缩代码结束的地方,再进行内存补丁。
改进后的补丁程序放在所附光盘的Chapter13\Patch2目录中。Patch2.asm文件如下:
.586
.model flat, stdcall
option casemap :none ; case sensitive
;####################################################################
; Include 数据
;####################################################################
include windows.inc
include user32.inc
include kernel32.inc
includelib user32.lib
includelib kernel32.lib
;####################################################################
BREAK_POINT1 equ 00405120h
BREAK_POINT2 equ 00401000h
PATCH_POSITION equ 00401004h
;####################################################################
; 数据段
;####################################################################
.data?
align dword
stCT CONTEXT <?>
stDE DEBUG_EVENT <?>
stStartUp STARTUPINFO <>
stProcInfo PROCESS_INFORMATION <>
szBuffer db 1024 dup (?)
.const
dbPatched db 90h,90h
dbInt3 db 0cch
dbOldByte db 60h
szExecFilename db 'Test.exe',0
szErrExec db '无法装载执行文件!',0
;####################################################################
; 代码段
;####################################################################
.code
Start:
;********************************************************************
; 创建进程
;********************************************************************
invoke GetStartupInfo,addr stStartUp
invoke CreateProcess,offset szExecFilename,NULL,NULL,NULL,\
NULL,DEBUG_PROCESS or DEBUG_ONLY_THIS_PROCESS,NULL,\
NULL,offset stStartUp,offset stProcInfo
.if !eax
invoke MessageBox,NULL,addr szErrExec,NULL,\
MB_OK or MB_ICONSTOP
invoke ExitProcess,NULL
.endif
;********************************************************************
; 调试进程
;********************************************************************
.while TRUE
invoke WaitForDebugEvent,addr stDE,INFINITE
.break .if stDE.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT
;********************************************************************