最近接触了各种各样的 Hook 方法比如 IAT hook、inline hook,又想起暑假那会通过进程注入把PVZ魔改了一通,还是挺好玩的。于是最近又重新开始研究PVZ的魔改,这回采用的方法是DLL注入 ,力求复刻95版植物大战僵尸的各种功能(95版的魔改方法貌似是直接Patch原文件,而不是DLL注入)。研究过程中借鉴了很多前人的基础,同时融入了自己的一些思考,欢迎大家共同讨论!
P.S. GitHub地址贴在最后了噢~
开始魔改PVZ之前,需要你有一些知识储备:
有关进程、虚拟内存和PE文件的加载过程我之前写的有关进程注入的博客里有讲过,可以参考一下:链接 。工具的使用当然是熟能生巧了,至于其他知识网上也有很多资料。
为了突出重点这篇文章就不会对这些基础知识做过多讲解了。
首先得先为后面的魔改打下一些基础,所以我们先来实现一些基础的功能:
我们在 VS 2019 中创建好项目,并写好一个简单的DLL:
为了方便我把VS自动生成的一些文件都删掉了(pch.h、framework.h、pch.cpp),然后修改一下编译选项: 预编译头改为“不使用预编译头”: 生成调试信息改为“否”,不然除了DLL文件还给你生成一大堆别的文件: 文件名和输出目录也可以改一下: 选择 生成->生成解决方案 就编译好了一个简单的DLL。 先写一段代码来测试我们的DLL是否能够加载成功: 通过 LoadLibraryA 函数来显式链接DLL。
运行一下: OK!
修改一下代码,再编译:
现在要让PVZ加载我们的DLL,也就是说我们要让PVZ调用 LoadLibraryA 函数,且参数得是"patch.dll"。 PVZ一个硕大的文件不可能不导入外部DLL,所以我们先参考参考:
所有的 Windows API 函数的调用协议都是 _stdcall ,也就是参数从右至左入栈,且由被调用函数清理数据,这样我们就不用在 call 指令后再 add esp, 4h 了。 他这个调用流程比较复杂,我们可以简化一下,我的思路是在程序的入口点(Entry Point,EP)之前 Patch 一段汇编代码:
并修改程序的 EP 为 push 指令的 RVA。 用 Python 来写 Patch 脚本:
主要 Patch 了三个地方:
Patch 之后相当于我们让程序在启动之前先导入了我们的DLL。但问题是原先 EP 前面的内容被我们覆盖了,在导入DLL后,我们需要给他还原一下:
在DLL被加载时修复一下即可:
将 patch.dll 放在PVZ目录下,运行被 Patch 后的 PlantsVsZombies.exe 看看效果: DLL加载成功了! 并且游戏能正常运行: 第一个目标达成!
95版拾取单个阳光的数量是50(原版是25),我们首先实现一下这个功能。
通过Cheat Engine(CE)找到了修改阳光数量的汇编指令,这些地址可以自行调试,也可以去一些论坛上找到前人的总结(比如吾爱破解): E9 是 mov ecx, data 的 opcode 后面接的是 data 的值(小端存储),我们只需要把 E9 后面的 19 00 00 00 修改为 32 00 00 00 即可。
有了之前研究进程注入的基础,写起来就很容易了:
其中 WriteDword的实现:
因为我们的DLL被加载到了PVZ进程的虚拟内存 中,因此我们是可以直接通过指针访问和修改PVZ的内存的。 但由于 DEP(数据执行保护, Data Execution Prevention) 机制的存在,代码所在的.text段是默认是无法修改的,因此我们需要通过 Windows API 中的 VirtualProtect 函数修改内存页保护属性。 SetMemWriteable 函数将内存页的保护属性设为可执行、可读、可写 。 然后就可以通过指针修改内存了。
在DLL函数被加载时进行注入:
编译,打开PVZ查看效果: 第二个目标也达成了! 庆祝庆祝,接下来我们开始本篇文章的最后一个目标。
在目标2中我们实现了对PVZ的汇编指令修改,但是这种方法只能实现小幅度修改,一些很复杂的功能召唤特定数量、特定种类的僵尸可能需要很多条汇编指令才能实现。这时就需要PVZ主动调用我们DLL的导出函数,来实现一些复杂的功能,我把这个过程称之为回调 。
要调用导出函数,首先得有导出函数,我们写一个测试的导出函数出来:
调用这个函数时会弹出一个对话框,我们可以把游戏暂停的窗口替换成这个对话框。 这是原效果: 通过一些方法我们找到了打开游戏暂停窗口的地址: 这一块有8个字节,我们调用导出函数只需要5个字节,完美。
CALL_BACK 是函数指针,在DLL加载时已经被重定位了,因此指针的值就是函数的真实地址。 其中 WriteCall 的实现:
E8 是 call 指令的机器码,后面接4字节的偏移量。 编译,进游戏测试: 至此本篇文章的三个目标就完美达成了,下一篇文章的内容还没想好,很多功能我还在研究中,到时再看看嘛呜呜。
最后祭出 GitHub 仓库:bluesadi/PVZPatch ,正在火热更新中。
[游戏安全] 植物大战僵尸之代码注入与魔改 by Rimao
BOOL
APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if
(ul_reason_for_call
=
=
DLL_PROCESS_ATTACH) {
MessageBox(NULL, L
"DLL is patched to process!"
, L
"Success"
, MB_OK);
}
return
TRUE;
}
BOOL
APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if
(ul_reason_for_call
=
=
DLL_PROCESS_ATTACH) {
MessageBox(NULL, L
"DLL is patched to process!"
, L
"Success"
, MB_OK);
}
return
TRUE;
}
/
/
test.cpp
int
main(){
LoadLibraryA(
"patch.dll"
);
}
/
/
test.cpp
int
main(){
LoadLibraryA(
"patch.dll"
);
}
BOOL
APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if
(ul_reason_for_call
=
=
DLL_PROCESS_ATTACH) {
WCHAR TITLE[
100
]
=
{
0
};
wsprintf(TITLE, L
"PVZ Patch (version %s)"
, PATCH_VERSION);
MessageBox(NULL, L
"DLL is patched to PVZ process!"
, TITLE, MB_OK);
}
return
TRUE;
}
BOOL
APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if
(ul_reason_for_call
=
=
DLL_PROCESS_ATTACH) {
WCHAR TITLE[
100
]
=
{
0
};
wsprintf(TITLE, L
"PVZ Patch (version %s)"
, PATCH_VERSION);
MessageBox(NULL, L
"DLL is patched to PVZ process!"
, TITLE, MB_OK);
}
return
TRUE;
}
push esi
mov esi, ds:LoadLibraryA
push offset aDbghelpDll ;
"DBGHELP.DLL"
call esi ; LoadLibraryA
push esi
mov esi, ds:LoadLibraryA
push offset aDbghelpDll ;
"DBGHELP.DLL"
call esi ; LoadLibraryA
push offset aPatchDll ;
"patch.dll"
call ds:LoadLibraryA
push offset aPatchDll ;
"patch.dll"
call ds:LoadLibraryA
PVZFile
=
open
(
'PlantsVsZombies.exe'
,
'rb+'
)
def
writeBytes(offset,data):
PVZFile.seek(offset)
PVZFile.write(data)
def
patch():
EP
=
0x0021EBE7
EPOffset
=
0x120
writeBytes(EPOffset,EP.to_bytes(
4
,
'little'
))
DLLNameOffset
=
0x00272A60
writeBytes(DLLNameOffset,b
'patch.dll\x00'
)
writeBytes(EP,bytes([
0x68
,
0x60
,
0x2A
,
0x67
,
0x00
]))
writeBytes(EP
+
5
,bytes([
0xFF
,
0x15
,
0xA4
,
0x20
,
0X65
,
0x00
]))
if
__name__
=
=
"__main__"
:
patch()
print
(
'Success!'
)
PVZFile
=
open
(
'PlantsVsZombies.exe'
,
'rb+'
)
def
writeBytes(offset,data):
PVZFile.seek(offset)
PVZFile.write(data)
def
patch():
EP
=
0x0021EBE7
EPOffset
=
0x120
writeBytes(EPOffset,EP.to_bytes(
4
,
'little'
))
DLLNameOffset
=
0x00272A60
writeBytes(DLLNameOffset,b
'patch.dll\x00'
)
writeBytes(EP,bytes([
0x68
,
0x60
,
0x2A
,
0x67
,
0x00
]))
writeBytes(EP
+
5
,bytes([
0xFF
,
0x15
,
0xA4
,
0x20
,
0X65
,
0x00
]))
if
__name__
=
=
"__main__"
:
patch()
print
(
'Success!'
)
void Repair() {
DWORD EP
=
0x0021EBE7
;
WriteBytes(EP,
11
,
0xB8
,
0xFF
,
0x00
,
0x00
,
0x00
,
0xE8
,
0xA0
,
0xBC
,
0x00
,
0x00
,
0xC3
);
DWORD DLLNameOffset
=
0x00272A60
;
WriteString(DLLNameOffset,
"DBGHELP.DLL"
);
}
void Repair() {
DWORD EP
=
0x0021EBE7
;
WriteBytes(EP,
11
,
0xB8
,
0xFF
,
0x00
,
0x00
,
0x00
,
0xE8
,
0xA0
,
0xBC
,
0x00
,
0x00
,
0xC3
);
DWORD DLLNameOffset
=
0x00272A60
;
WriteString(DLLNameOffset,
"DBGHELP.DLL"
);
}
BOOL
APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) {
if
(ul_reason_for_call
=
=
DLL_PROCESS_ATTACH) {
WCHAR TITLE[
100
]
=
{
0
};
wsprintf(TITLE, L
"PVZ Patch (version %s)"
, PATCH_VERSION);
MessageBox(NULL, L
"DLL is patched to PVZ process!"
, TITLE, MB_OK);
Repair();
}
return
TRUE;
}
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2020-12-18 16:49
被34r7hm4n编辑
,原因: