对于每个Cracker,补丁是再熟悉不过的东西了。补丁的形式大致有两种,一种是文件补丁,一种是
内存补丁。两者的根本区别在于文件补丁对程序的部分代码是永久性修改,而内存补丁却是在程序代码映射到内存的时候才做出修改,所以内存补丁的一大好处就是保持了程序的完整性,却能使用到修改后的程序。尤其对现在的一些加壳
软件,内存补丁的作用越来越明显。
破解调试用到的OD就附带了一个内存补丁的功能。载入OD,对某个语句进行修改,然后按“Ctrl+P”就可以看到OD的内存补丁管理窗口。我们可以通过键盘上的空格键对补丁进行激活或者禁用。OD这个功能对一些破解
新手来说是非常有用的,当对关键跳转不确定的时候,直接用个内存补丁运行一下判断是不是真正的关键跳转,如图1所示。但是OD这个功能还有一点小小的不足,就是每次F9运行后要重新载入,然后再找回那个补丁的位置。一两次还好,如果次数多了也挺烦人的。现在我们就用Win32汇编来实现OD这个功能,要有自己的GUI界面,而且不像OD每补一次就重新载入那么麻烦。
初步分析
要修改程序的内存,必须要有足够的权限来打开这个程序的进程,然后才可以通过API函数读写要修改程序的内存数据。确定了总的思路后,我们开始分步来实现。
首先是打开进程,我们可以用CreatProcess函数创建对象程序的进程,然后用ReadProcessMemory和WriteProcessMemory来读取和写入进程的内存地址。界面用输入字符的形式来实现,这里就涉及了字符转16进制数值的问题,我们可以用GetDlgItemText获得字符串,然后进行简单的ASCII码加减来实现转换。
下面简单介绍一下最核心的两个函数,这两个函数都在kernel32.lib输出库里面。ReadProcessMemory函数用来读取指定进程的空间的数据,此空间必须是可以访问的,否则读取操作会失败!函数原型如下。
BOOL ReadProcessMemory(
HANDLE hProcess,
//目标进程句柄,必须要足够的权限才能打开
LPCVOID lpBaseAddress,
//读取数据的起始地址
LPVOID lpBuffer,
//存放数据的缓存区地址
D
word nSize,
//要读取的字节数
LPD
word lpNumberOfBytesRead
//实际读取数存放地址
);
BOOL WriteProcessMemory(
HANDLE hProcess,
//要写进程的句柄,也是要足够权限才能写入
LPVOID lpBaseAddress,
//写内存的起始地址
LPVOID lpBuffer,
//写入数据的地址
D
word nSize,
//要写的字节数
LPD
word lpNumberOfBytesWritten
//实际写入的字节数
);
编写代码
我们用一个最简单的例子分析一下。test.exe是一个非常简单的测试程序,双击运行就会出现“Sorry”提示,如图2所示。用OD逆向这个程序,我们发现在00401004那里有个je跳转是跳到“Sorry”提示的,如图3所示。如果我们把je改为jnz或者nop掉,就能成功爆破这个程序了。该句是“74 15”,我们只要改为“75 15”或者“90 90”就可以了。分析完最基本的流程,下面是编写核心程序代码,该代码的作用就是创建进程和读写内存的。
图2
图3
.data?
dbOldByte db 2 dup(?)
stStartUp STARTUPINFO <?>
stProcInfo PROCESS_INFORMATION <?>
PATCH_POSITION dd ?
;想爆破的地址
dbPatch dd ?
;爆破前的指令
dbPatched dd ?
;爆破后的指令
.const
szExecFilename db "test.exe",0
;定义文件名
szErrExec db "无法装载执行文件",0
szErrVersion db "执行文件的版本不正确,无法修正",0
//以下为核心子程序,用来修改内存代码
_ProcMemory proc
//创建进程
invoke GetStartupInfo,addr stStartUp
invoke CreateProcess,offset szExecFilename,0,0,0,0,
NORMAL_PRIORITY_CLASS or CREATE_SUSPENDED,0,0,
offset stStartUp,offset stProcInfo
.if eax ;判断eax == 1
;读进程内存并验证内容是否正确
invoke ReadProcessMemory,stProcInfo.hProcess,PATCH_POSITION,
addr dbOldByte,2,NULL
.if eax
mov ax,
word ptr dbOldByte
.if ax ==
word ptr dbPatch
invoke WriteProcessMemory,stProcInfo.hProcess,
PATCH_POSITION,addr dbPatched,2,0
invoke ResumeThread,stProcInfo.hThread
.else
invoke TerminateProcess,stProcInfo.hProcess,-1
invoke MessageBox,0,addr szErrVersion,0,
MB_OK or MB_ICONSTOP
.endif
invoke CloseHandle,stProcInfo.hProcess
invoke CloseHandle,stProcInfo.dwThreadId
.endif
.else
;不能创建进程时显示出错提示
invoke MessageBox,NULL,addr szErrExec,NULL,MB_OK or MB_ICONSTOP
.endif
ret
_ProcMemory endp
完成了核心代码之后,就要考虑GUI界面部分了,要由用户输入想要修改的地址和修改的内容。我们先用RadAsm创建一个对话框,添加三个编辑框和一个按钮,如图4所示。首先是限制用户输入的字符串,因为只能输入16进制的字符串,所以我们就用窗体子类化来实现。下面的子程序就是实现窗体子类化的。
szAllowedChar db "0123456789ABCDEFabcdef",08h
;定义有效按键,08h为退格键
lpOldProcEdit dd ?
//编辑框的新窗体过程
_ProcEdit proc uses ebx edi esi hWnd,uMsg,wParam,lParam
mov eax,uMsg
.if uMsg == WM_CHAR
;只接受我们需要的WM_CHAR字符信息
mov eax,wParam
mov edi,offset szAllowedChar
mov ecx,sizeof szAllowedChar
repnz scasb
;当ZF=0或比较结果不相等,且CX/ECX<>0时重复
;使用scasb指令查表
.if ZERO? ;判断零标志位是否被置位
.if al > 9
;判断是否输入字母,因为表中大于9的肯定是字母
and al,not 20h
;将字母转换为大写,not为取反。小写从61h开始,大写从41h开始,只要第六位是0,就肯定是大写了
.endif
;CallWindowProc是将WM_CHAR消息转发给主窗体
invoke CallWindowProc,lpOldProcEdit,hWnd,uMsg,eax,lParam
ret
.endif
.else
invoke CallWindowProc,lpOldProcEdit,hWnd,uMsg,wParam,lParam
ret
.endif
xor eax,eax
ret
_ProcEdit endp
但是我们有三个输入框,这样要是每个都子类化的话就显得麻烦了,干脆用超类化为这三个输入框建立一个新的类好了。我们用下面的子程序来实现。
//控件超类化,建立新的Edit类,限制输入位数和输入字符
_SuperCla