游戏外挂开发原理初探——植物大战僵尸内存挂为例

Posted 思源湖的鱼

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了游戏外挂开发原理初探——植物大战僵尸内存挂为例相关的知识,希望对你有一定的参考价值。

前言

对外挂有点兴趣,做番了解

一、游戏外挂开发原理

1、外挂分类

外挂现在大体上来讲分为三种,分别是模拟按键,WPE封包和内存挂

  • 模拟键盘的,鼠标的,这种就叫做按键模拟,也叫脚本精灵
  • 修改数据包的,这种就叫做WPE封包外挂,这种外挂相对而言比较稳定,但是对于技术要求难度也比较高,因为修改WPE封包,你需要知道和了解加密解密算法等等一系列的知识
  • 修改本地内存的,这种相对而言比较常见,市场上面比较常见的也是这种游戏外挂,相对而言技术要求也不是太高,资料也比较齐全,比较大众

修改服务器内存的比较少见,技术太高一般人没有办法入手

2、内存挂

按键模拟网上脚本还是多的
WPE封包留待之后学习吧
本篇初探主要学习下内存挂

其实,制作内存挂也不是很难,步骤就这么几步

  • 找游戏数据内存地址,偏移地址
  • 修改内存地址的值,达到外挂的目的
  • 用编程语言写出一个程序,使得外挂方便于携带传输,同时也方便于下次玩游戏直接使用

其实第三步主要是方便使用,方便贩卖等等,很多时候对于一个高手而言,根本不需要写出来,记录下内存地址,偏移地址以后,下次上游戏直接工具里面修改就是了。但是如果经常玩某个游戏,可以写出来,用不着每次玩游戏都去修改。

二、开发一个内存挂——以植物大战僵尸为例

1、找游戏内存地址

(1)内存基址和物理地址

找游戏内存地址,偏移地址,这可能是整个游戏外挂开发中最难的一步,因为一个大型的网络游戏,可能一个技能或者血条的内存基址可能会偏移很多次,最后才得到一个内存地址也叫物理地址

那么,这里有个问题来了,我们为什么要找技能或者血条的内存基址?不是直接修改物理地址就行了吗?这样是不行的,因为物理地址是会变动的;举个例子,每次关闭游戏重新启动以后,游戏进程是会变动的,同样的,游戏代码在内存中的存放位置,因为游戏结束以后,释放掉了内存地址,这个内存地址可能会被其他程序所占用,所以是会变动的;当你关闭游戏重新进入以后,游戏的技能或者血条在内存中的物理地址就变了。但是,游戏技能或者血条的内存基址是不会变动的,在内存中的物理地址不管怎么变动,内存基址都不会变动

那么内存基址和物理地址之间是一种什么样的关系呢?看下面这个公式:

物理地址 = 一级偏移地址 + 偏移地址
一级偏移地址 = 二级偏移地址 + 偏移地址
二级偏移地址 = 三级偏移地址 + 偏移地址
三级偏移地址 = 游戏技能基址 + 偏移地址

如上公式,一个游戏的物理地址是等于这个游戏的基址加上偏移地址的,所以,想要每次重新进入游戏都能够找到准确的物理地址进行修改内存地址的值,达到外挂的目的的话,就必须找到这个物理地址当前对应的偏移地址和基址,只有这样才能够确保每次登陆游戏都能够修改物理地址的值。

(2)用CE找内存基址

首先,介绍一款找游戏内存基址的工具Cheat Engine简称CE,Cheat Engine是一款内存修改编辑工具 ,它允许修改游戏或软件内存数据,以得到一些其他功能。它包括16进制编辑,反汇编程序,内存查找工具。与同类修改工具相比,它具有强大的反汇编功能,且自身附带了外挂制作工具,可以用它直接生成外挂。

下面,打开植物大战僵尸,打开CE,在CE中找到植物大战僵尸,附加到CE中去,如图所示:


游戏附加到CE以后,看自己想修改哪儿的内容,就去找对应的内存基址,怎么找呢?

比如修改无限阳光吧;首先,看一下最开始的阳光是多少?比如这里,开局阳光是50,就在CE中数值一项输入50,扫描类型选择精确数值在数值类型选择4字节然后点击首次搜索,如图所示:


如上图所示,出来了这么多的地址,究竟哪一个是我们想要的物理地址呢?接下来就需要在游戏中改动一下阳光的值,然后点击再次搜索,结果就会出来了,如图所示:



如上第二幅图所示,我们想要的物理地址就出来了,把这个地址添加到地址列表中去,然后修改一下阳光的值,确认一下是不是想要的地址,不过这里要注意,在修改的时候,我们需要点击列表地址中我们拉下来的这一行的数值这一栏,其他是修改地址和字节类型用的,不要乱动,如图所示:

如上图所示,这就是我们想要的物理地址,这里我们可以锁定这个内存地址的值,一直是100或者200或者更多。只需要点击前面激活这一栏选中就行了,从而达到锁定阳光值的效果

但是当关闭游戏,重新进入,这个地址就没用了,又需要重新找。所以,这里就需要找到真正的阳光值的基址和偏移地址,那么,怎么找呢?我们找到这个物理地址以后,就右键,选择找出是什么改写了这个地址也就是快捷键F6这个名字每个版本的汉化效果都不一样的,这里就会弹出一个窗口,是空白,然后我们再到游戏中改变阳光的值,看看这个空白窗口中出现的代码,如图所示:



如上图三所示,分析这两条代码,看见最终都指向了一个指针数值17C88118

  • 首先分析一下mov [edi+00005560],esi这条指令,我们在详细信息中看见,寄存器esi中的代码是000026DE这个值换算成十进制刚好是9950,那么这个9950是怎么来的呢?其实就是刚刚我在做上一步的时候,说了要变动游戏中的阳光值,所以我先直接把阳光值修改成了10000,然后种了一个石头,刚好是50,所以阳光值发生了变化,这条指令mov [edi+00005560],esi就是把寄存器esi9950的阳光值放入edi+00005560这个地址中去,这个地址是谁呢?这个地址就是刚刚找出来的物理地址,刚好满足前面的公式:物理地址 = 阳光基址地址 + 偏移地址
  • 接下看看add [eax+00005560],ecx这条指令,add指令是相加的意思,前面的mov是赋值或者移动的意思,这条代码的意思就是,把ecx寄存器中的值加入[eax+00005560]这个地址中的值当中去,再看看寄存器eax的地址是多少?刚好又是17C88118这个指针数值,那么,我们就可以确定一个事情,那就是17C88118这个指针数值,肯定是上一级的地址的指针数值,那么上一级要么是基址,要么还是一个偏移地址,这个需要进一步来确认
  • 17C88118这个指针数值,再次放到CE中搜索一波,看看这个值对应的地址是多少,看看出来的是一个动态地址还是静态地址(在CE中,黑色的地址表示动态地址,绿色的表示静态地址),这里这个值是十六进制的,所以在搜索的时候,需要勾选搜索框前面的hex,这里填好以后点击新的扫描,首次扫描如图所示:


点击再次扫描,把这些红色的过滤掉,然后看看还剩下的地址有哪些,如图所示:


多次点击再次扫描以后发现不管怎么点击都剩下来了52个结果,然后看了一下这些结果中并没有绿色的静态地址,并且其他地址开头几位都比较像,唯独如图所示两个地址比较特殊,这两个可能就是我们需要的地址了

百度了一波,结果所说的是这种开头比较像的是数组,也就是说,遇到这种,基本都不是我们需要的地址,直接放弃,把这两个特殊的拉进下边的地址列表,开始分析,双击地址这一栏,在出来的更改地址窗口中复制下这个地址,然后再勾选指针选项,在出来的内容框中填写入刚刚复制的地址,在上面的内容框中写入刚刚找到的00005560这个偏移地址,得到的结果刚好是阳光的物理地址,最后的阳光值也正好和当前的阳光值一样,并且,这两个地址都是一样的,而且都是动态地址,如图所示:



如上图所示,这两个地址加上偏移地址都可以得到阳光物理地址,右键-找出是什么访问了这个地址,也就是快捷键的F5

进去以后也是和前面一样出来一个窗口,但是这两个窗口里面的内容就不一样了,第一个地址0264A400在窗口中会出现内容,但是第二个地址0956BD3C在出现的窗口里面是一片空白,所以,这两个地址中,0264A400才是我们要找的真正的一级偏移地址,如图所示:



如上第二幅图所示,找到了真正的一级偏移地址0264A400后,访问这个地址的窗口中看这些代码指令,通过对比,发现这些代码都指向同一个指针数值02649C98且都有偏移地址00000768,如图所示:


如上图所示,既然所有的代码都指向了同一个指针数值02649C98那么就继续按照前面找一级偏移地址的办法,丢到CE中,继续新的十六进制搜索,搜索指针数值02649C98以后得到如下图所示的结果:


如上图所示,一共搜索到923个地址,而且还有几个绿色的静态地址,这里我们就可以尝试一下了,看看这几个绿色的静态地址究竟是不是我们想要的阳光基址

同样的原理,我们把几个绿色的静态地址拉到地址列表中,在地址这一栏双击,在出来的更改地址窗口中复制下这个地址,然后再勾选指针选项,在出来的内容框中填写入刚刚复制的地址,在上面的内容框中写入刚刚找到的00000768这个偏移地址,然后再点击添加偏移在出来的内容框中输入刚刚第一次找到的00005560这个偏移地址,得到的结果刚好是阳光物理地址和阳光当前的值,并且这几个静态地址的结果都是一样的。这里需要说明一下,为什么这里要填写两个偏移地址,因为刚刚第一次找到的是一级偏移地址,这次是二级偏移地址了,所以,这里

阳光物理地址 = 一级偏移地址 + 偏移地址
一级偏移地址 = 阳光基址 + 偏移地址
阳光物理地址 =阳光基址 + 第一次的偏移地址 + 第二次的偏移地址




如上四幅图所示,四个静态地址最终结果都是一样的,都可以用作阳光基址,那么就随便取一个就行。这样,阳关的基址和偏移地址就找到了,阳光的基址和偏移地址如下:

阳光基址:006A9EC0
一级偏移:00000768
二级偏移:00005560

同样的道理,还可以找植物冷却时间,不过找冷却时间思路不太一样,因为冷却时间我们不知道具体的值,可以通过找未知的初始化值的方式来找,也可以通过字节的方式来找

2、修改游戏内存地址的值

这一步就简单了,比如阳光的值,找到了以后,我们每次上游戏,直接在CE的地址列表框中手动添加地址,把像上面我们验证这几个基址的时候一样,把基址,偏移地址填写好,然后在数值这一栏双击,修改数值成为我们想要的阳光值就可以了,这一步就不过多提及

3、编写游戏外挂

编写外挂只要找出来想要的功能的内存基址,偏移等,直接在程序中写上读取和写入内存地址就行了,下面就是植物大战僵尸的外挂的易语言源码,简单的分析一波:

.版本 2

.程序集 窗口程序集_启动窗口
.程序集变量 进程ID, 整数型
.程序集变量 a, 整数型

.子程序 __启动窗口_创建完毕, , , 窗口创建完毕就利用时钟事件获取阳光值并输出

进程ID = 取进程ID (“PlantsVsZombies.exe”)
时钟1.时钟周期 = 999

.子程序 _时钟9_周期事件

标签提示信息.左边 = 标签提示信息.左边 - 2
.如果真 (标签提示信息.左边 = -300)
    标签提示信息.左边 = 300
.如果真结束

.子程序 _时钟1_周期事件, , , 获取阳光值并输出
.局部变量 读阳光基址, 整数型
.局部变量 读偏移地址一, 整数型
.局部变量 阳光物理地址, 整数型

读阳光基址 = 读内存整数型 (进程ID, 十六到十 (“006A9EC0”), )
读偏移地址一 = 读内存整数型 (进程ID, 读阳光基址 + 十六到十 (“768”))
阳光物理地址 = 读内存整数型 (进程ID, 读偏移地址一 + 十六到十 (“5560”), )
标签当前阳光值.标题 = “当前阳光值:” + 到文本 (阳光物理地址)

.子程序 _编辑框输入阳光值_获得焦点, , , 设置输入框在被获取焦点以后情况编辑框内容

编辑框输入阳光值.内容 = “”

.子程序 _按钮增加阳光_被单击, , , 增加阳光值
.局部变量 取阳光基址, 整数型
.局部变量 取偏移地址一, 整数型
.局部变量 取阳光物理地址, 整数型

取阳光基址 = 读内存整数型 (进程ID, 十六到十 (“006A9EC0”), )
取偏移地址一 = 读内存整数型 (进程ID, 取阳光基址 + 十六到十 (“768”))
取阳光物理地址 = 读内存整数型 (进程ID, 取偏移地址一 + 十六到十 (“5560”), )
a = 到整数 (编辑框输入阳光值.内容)
写内存整数型 (进程ID, 取偏移地址一 + 十六到十 (“5560”), 取阳光物理地址 + a)

.子程序 _按钮减少阳光_被单击
.局部变量 取阳光基址, 整数型
.局部变量 取偏移地址一, 整数型
.局部变量 取阳光物理地址, 整数型

取阳光基址 = 读内存整数型 (进程ID, 十六到十 (“006A9EC0”), )
取偏移地址一 = 读内存整数型 (进程ID, 取阳光基址 + 十六到十 (“768”))
取阳光物理地址 = 读内存整数型 (进程ID, 取偏移地址一 + 十六到十 (“5560”), )
a = 到整数 (编辑框输入阳光值.内容)
写内存整数型 (进程ID, 取偏移地址一 + 十六到十 (“5560”), 取阳光物理地址 - a)

.子程序 _按钮锁定阳光值_被单击, , , 锁定阳光值

时钟2.时钟周期 = 100

.子程序 _时钟2_周期事件, , , 锁定阳光值时钟事件
.局部变量 锁定阳光基址, 整数型
.局部变量 锁定偏移地址一, 整数型
.局部变量 锁定阳光物理地址, 整数型

锁定阳光基址 = 读内存整数型 (进程ID, 十六到十 (“006A9EC0”), )
锁定偏移地址一 = 读内存整数型 (进程ID, 锁定阳光基址 + 十六到十 (“768”))
锁定阳光物理地址 = 读内存整数型 (进程ID, 锁定偏移地址一 + 十六到十 (“5560”), )
写内存整数型 (进程ID, 锁定偏移地址一 + 十六到十 (“5560”), 1000)

.子程序 _按钮取消锁定阳光_被单击, , , 取消锁定阳光值

时钟2.时钟周期 = 0

.子程序 _按钮锁定冷却1_被单击, , , 锁定冷却1

时钟3.时钟周期 = 100

.子程序 _时钟3_周期事件, , , 锁定冷却1时钟事件
.局部变量 锁定冷却一基址, 整数型
.局部变量 锁定冷却一偏移地址一, 整数型
.局部变量 锁定冷却一偏移地址二, 整数型
.局部变量 锁定冷却一物理地址, 整数型

锁定冷却一基址 = 读内存整数型 (进程ID, 十六到十 (“006A9F38”), )
锁定冷却一偏移地址一 = 读内存整数型 (进程ID, 锁定冷却一基址 + 十六到十 (“768”))
锁定冷却一偏移地址二 = 读内存整数型 (进程ID, 锁定冷却一偏移地址一 + 十六到十 (“144”), )
锁定冷却一物理地址 = 读内存整数型 (进程ID, 锁定冷却一偏移地址二 + 十六到十 (“70”), )
写内存整数型 (进程ID, 锁定冷却一偏移地址二 + 十六到十 (“70”), 1)

.子程序 _按钮取消锁定冷却1_被单击, , , 取消锁定冷却1

时钟3.时钟周期 = 0

.子程序 _按钮锁定冷却2_被单击, , , 锁定冷却2

时钟4.时钟周期 = 100

.子程序 _时钟4_周期事件, , , 锁定冷却2时钟事件
.局部变量 锁定冷却二基址, 整数型
.局部变量 锁定冷却二偏移地址一, 整数型
.局部变量 锁定冷却二偏移地址二, 整数型
.局部变量 锁定冷却二物理地址, 整数型

锁定冷却二基址 = 读内存整数型 (进程ID, 十六到十 (“006A9F78”), )
锁定冷却二偏移地址一 = 读内存整数型 (进程ID, 锁定冷却二基址 + 十六到十 (“768”))
锁定冷却二偏移地址二 = 读内存整数型 (进程ID, 锁定冷却二偏移地址一 + 十六到十 (“144”), )
锁定冷却二物理地址 = 读内存整数型 (进程ID, 锁定冷却二偏移地址二 + 十六到十 (“C0”), )
写内存整数型 (进程ID, 锁定冷却二偏移地址二 + 十六到十 (“C0”), 1)

.子程序 _按钮取消锁定冷却2_被单击, , , 取消锁定冷却2

时钟4.时钟周期 = 0

.子程序 _按钮锁定冷却3_被单击, , , 锁定冷却3

时钟5.时钟周期 = 100

.子程序 _时钟5_周期事件, , , 锁定冷却3时钟事件
.局部变量 锁定冷却三基址, 整数型
.局部变量 锁定冷却三偏移地址一, 整数型
.局部变量 锁定冷却三偏移地址二, 整数型
.局部变量 锁定冷却三物理地址, 整数型

锁定冷却三基址 = 读内存整数型 (进程ID, 十六到十 (“006AA00C”), )
锁定冷却三偏移地址一 = 读内存整数型 (进程ID, 锁定冷却三基址 + 十六到十 (“768”))
锁定冷却三偏移地址二 = 读内存整数型 (进程ID, 锁定冷却三偏移地址一 + 十六到十 (“144”), )
锁定冷却三物理地址 = 读内存整数型 (进程ID, 锁定冷却三偏移地址二 + 十六到十 (“110”), )
写内存整数型 (进程ID, 锁定冷却三偏移地址二 + 十六到十 (“110”), 1)

.子程序 _按钮取消锁定冷却3_被单击, , , 取消锁定冷却3

时钟5.时钟周期 = 0

.子程序 _按钮锁定冷却4_被单击, , , 锁定冷却4

时钟6.时钟周期 = 100

.子程序 _时钟6_周期事件, , , 锁定冷却4时钟事件
.局部变量 锁定冷却四基址, 整数型
.局部变量 锁定冷却四偏移地址一, 整数型
.局部变量 锁定冷却四偏移地址二, 整数型
.局部变量 锁定冷却四物理地址, 整数型

锁定冷却四基址 = 读内存整数型 (进程ID, 十六到十 (“006AA00C”), )
锁定冷却四偏移地址一 = 读内存整数型 (进程ID, 锁定冷却四基址 + 十六到十 (“768”))
锁定冷却四偏移地址二 = 读内存整数型 (进程ID, 锁定冷却四偏移地址一 + 十六到十 (“144”), )
锁定冷却四物理地址 = 读内存整数型 (进程ID, 锁定冷却四偏移地址二 + 十六到十 (“160”), )
写内存整数型 (进程ID, 锁定冷却四偏移地址二 + 十六到十 (“160”), 1)

.子程序 _按钮取消锁定冷却4_被单击, , , 取消锁定冷却4

时钟6.时钟周期 = 0

.子程序 _按钮锁定冷却5_被单击, , , 锁定冷却5

时钟7.时钟周期 = 100

.子程序 _时钟7_周期事件, , , 锁定冷却5始终事件
.局部变量 锁定冷却五基址, 整数型
.局部变量 锁定冷却五偏移地址一, 整数型
.局部变量 锁定冷却五偏移地址二, 整数型
.局部变量 锁定冷却五物理地址, 整数型

锁定冷却五基址 = 读内存整数型 (进程ID, 十六到十 (“006AA00C”), )
锁定冷却五偏移地址一 = 读内存整数型 (进程ID, 锁定冷却五基址 + 十六到十 (“768”))
锁定冷却五偏移地址二 = 读内存整数型 (进程ID, 锁定冷却五偏移地址一 + 十六到十 (“144”), )
锁定冷却五物理地址 = 读内存整数型 (进程ID, 锁定冷却五偏移地址二 + 十六到十 (“1B0”), )
写内存整数型 (进程ID, 锁定冷却五偏移地址二 + 十六到十 (“1B0”), 1)

.子程序 _按钮取消锁定冷却5_被单击, , , 取消锁定冷却5

时钟7.时钟周期 = 0

.子程序 _按钮锁定冷却6_被单击, , , 锁定冷却6

时钟8.时钟周期 = 100

.子程序 _时钟8_周期事件, , , 锁定冷却6时钟事件
.局部变量 锁定冷却六基址, 整数型
.局部变量 锁定冷却六偏移地址一, 整数型
.局部变量 锁定冷却六偏移地址二, 整数型
.局部变量 锁定冷却六物理地址, 整数型

锁定冷却六基址 = 读内存整数型 (进程ID, 十六到十 (“006AA00C”), )
锁定冷却六偏移地址一 = 读内存整数型 (进程ID, 锁定冷却六基址 + 十六到十 (“768”))
锁定冷却六偏移地址二 = 读内存整数型 (进程ID, 锁定冷却六偏移地址一 + 十六到十 (“144”), )
锁定冷却六物理地址 = 读内存整数型 (进程ID, 锁定冷却六偏移地址二 + 十六到十 (“200”), )
写内存整数型 (进程ID, 锁定冷却六偏移地址二 + 十六到十 (“200”), 1)

.子程序 _按钮取消锁定冷却6_被单击, , , 取消锁定冷却6

时钟8.时钟周期 = 0

在外挂软件启动以后,读取植物大战僵尸的游戏进程ID,然后读取阳光的物理地址,加以输出显示,然后在启动窗口创建完毕的时候还加了一个时钟,达到实时监控阳光值的目的,然后设置了几个按钮,分别是增加阳光、锁定阳光、锁定冷却时间和减少阳光、取消锁定阳光、取消锁定冷却时间:

  • 增加阳光
    增加阳光就定义了一个全局变量,变量中获取输入框中的值,然后让阳光物理地址里面的值加上输入的值,就达到了增加阳光的目的
  • 减少阳光
    减少阳光和增加阳关的写法一模一样,只不过把阳光物理地址里面的值设置为了减去输入的值而已,这样就达到了减少阳关的目的
  • 锁定阳光
    锁定阳光定义了一个时钟,设置时钟周期为999毫秒,然后设置了阳光固定值1000。当按钮锁定阳光被单击以后,就执行这个时钟的时钟周期,也就是每隔999毫秒执行一次时钟下面的代码,也就是执行在阳光物理地址中改阳光的值为1000,这样就达到了锁定阳光的目的
  • 取消锁定阳光
    取消锁定阳光其实就是把锁定阳光这个时钟的时钟周期设置为0,当取消锁定阳光的按钮被单击以后,就执行这个时钟周期事件,时钟下面的代码也就不执行了,达到了取消阳光锁定的目的

后面的锁定冷却时间和取消锁定冷却时间的原理和上面锁定阳光和取消锁定阳光的原理一样,这里针对每个栏位的植物做了单独的冷却锁定和取消锁定,所以代码比较长,其实在找植物冷却时间的时候,可以配合OD,直接找到冷却时间前面的循环干掉,这样所有的植物栏的冷却时间都没了

结语

不管是单机游戏,还是网络游戏,原理都一样,只不过,可能游戏类型不同,所需要的东西也不一样,比如射击类游戏,就需要D3D技术来做人物透视的定位等等

以上是关于游戏外挂开发原理初探——植物大战僵尸内存挂为例的主要内容,如果未能解决你的问题,请参考以下文章

植物大战僵尸——任意阳光修改

植物大战僵尸游戏内存地址

原生JS实现的h5小游戏-植物大战僵尸

Python开发植物大战僵尸游戏(附github项目地址)

前端小游戏——植物大战僵尸

植物大战僵尸外挂