首页
社区
课程
招聘
[原创]通过DLL注入魔改植物大战僵尸(1)——准备工作
发表于: 2020-12-17 18:11 8195

[原创]通过DLL注入魔改植物大战僵尸(1)——准备工作

2020-12-17 18:11
8195

最近接触了各种各样的 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

 
 
#include "windows.h"
 
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;
}
#include "windows.h"
 
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
#include <Windows.h>
 
int main(){
    LoadLibraryA("patch.dll");
}
// test.cpp
#include <Windows.h>
 
int main(){
    LoadLibraryA("patch.dll");
}
 
#include <windows.h>
#define PATCH_VERSION L"1.0"
 
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;
}
#include <windows.h>
#define PATCH_VERSION L"1.0"
 
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')) # EP: 0021EBF2h -> 0021EBE7h
    DLLNameOffset = 0x00272A60 # db 'DBGHELP.DLL',0
    writeBytes(DLLNameOffset,b'patch.dll\x00')
    writeBytes(EP,bytes([0x68,0x60,0x2A,0x67,0x00])) # push offset aPatchDll
    writeBytes(EP + 5,bytes([0xFF,0x15,0xA4,0x20,0X65,0x00])) # call LoadLibraryA
 
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')) # EP: 0021EBF2h -> 0021EBE7h
    DLLNameOffset = 0x00272A60 # db 'DBGHELP.DLL',0
    writeBytes(DLLNameOffset,b'patch.dll\x00')
    writeBytes(EP,bytes([0x68,0x60,0x2A,0x67,0x00])) # push offset aPatchDll
    writeBytes(EP + 5,bytes([0xFF,0x15,0xA4,0x20,0X65,0x00])) # call LoadLibraryA
 
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");
}
#define PATCH_VERSION L"1.0"
 
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编辑 ,原因:
收藏
免费 3
支持
分享
最新回复 (6)
雪    币: 1041
活跃值: (733)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
求教大佬逆向该怎么学习
2020-12-17 18:37
0
雪    币: 14303
活跃值: (10776)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
3
鸭子咯咯哒 求教大佬逆向该怎么学习
C语言先得知道 然后汇编吧 一般就是自己写程序自己逆
本人也是菜鸡
——Rimao
2020-12-18 10:57
0
雪    币: 10
活跃值: (45)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
大佬哪本书有讲hook和DLL注入的知识呢
2020-12-21 19:33
0
雪    币: 14303
活跃值: (10776)
能力值: ( LV12,RANK:360 )
在线值:
发帖
回帖
粉丝
5
cTdN 大佬哪本书有讲hook和DLL注入的知识呢
李承远的《逆向工程核心原理》挺不错,另外网上也有很多博客和帖子的
2020-12-21 22:34
0
雪    币: 224
活跃值: (237)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
太好了 找到玩植物的同行了
2020-12-29 09:28
0
雪    币: 224
活跃值: (237)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
hook还不错
2020-12-29 09:29
0
游客
登录 | 注册 方可回帖
返回
//