大家好,我是小杰。又是一个不寻常的周末,昨天我舍友终于做出了他的个人游戏,经过一个月的思考、画图和设计,完成了他的第一个作品。大概的功能都已经出来了,剩下的就是一些优化,新人物新特效之类的了,总的玩法应该不会有什么其他变化啦。——紧接着,今天的重头戏要登场了,他的游戏的结束正是我工作的开始!我做出了我第一个真正意义上的游戏外挂,我周六的时候耗时一天,从利用CE找偏移地址、到找基地址,踩过了好多坑,最后使用C++调用操作系统API进行游戏的注入和内存修改。晚上的时候终于打包完成了预计的功能,Perfect!
今天的文章想听故事有故事,想要干货有干货,硬核知识还是够硬核的,欢迎收藏、点赞加在看,一键三连。
我以为技术无罪,失责当诛,今天从外挂的角度来看程序,也许会有不一样的收获,文章的重点不在于教你怎么做外挂,重点在学从中学到知识,所以话不多说,直接发车。
首先来说一下我使用的工具,我使用的工具很简单,一个耳熟能详的Cheat Engine用来定位数据的内存位置,Visual Studio 2019使用MFC来开发界面,方便外挂的使用,其次谷歌浏览器(最好可以科学上网,别问我为什么,因为我今天遇到的问题我在百度就没查到过,都是用英文在谷歌上搜到的,泪目)
接下来看看Cheat Engine界面长什么样子,具体就不展开介绍了(因为重点不是在于做外挂,而是学知识),非常强大的东西
拿到舍友做的游戏以后,首先先来看看游戏是什么样子的,我们的外挂想实现什么样的功能,**实际可以实现什么样的功能。**下面是游戏的截图:
简单介绍一下游戏的玩法,从上面的图就可以看出,这是一个双人对战游戏,首先它是一个单机游戏,那么所有的数据都是放在本地上的。所以我的机会不就来了!双方可以得分,类似于羽毛球比赛的规则,上面双方有进度条,进度条可以称为怒气,怒气值满了以后可以放技能,技能还是很炫的,不得不说我舍友的创意和水平还是有的,毕竟是第一次做游戏。
玩了一会儿这个游戏之后,我觉得可以做成外挂的点有:
实际上我最后并没有实现无限技能这个功能,我的确找到了那个介于0到1之间 不断上涨的值,但是我锁定后并不会达到我要的效果,而是会出现画面问题,进度条脱离原来的位置接着涨。
可以看一下我最终实现的外挂效果图:
界面上有的已经都实现了,最后的小球模式是根据计划中的第三条是否可触碰小球实现的
游戏的本质和一个应用程序没有什么区别,只不过游戏的资源类型和种类更多一些罢了。游戏再打包发布后是由一个exe和一个甚至多个dll文件还有其他文件组成的。我们这里关心的是exe文件和dll文件,因为我们的函数、变量以及各种逻辑都是主要在这两个里面。
游戏运行的原理就是首先会给程序分配一个内存空间,并不是直接在内存中分配,而是一个虚拟的地址空间,正因为是虚拟的地址空间,所以才可以从程序的角度看我可以拥有超过硬件实际内存的大小(因为段表和页表的存在,所以才得以实现,设计到操作系统原理这里就不展开了,以后也会详细写这块儿内容的)。然而刚才说了exe和dll本质上它们其实是等价的,但是在内存中该怎么存放呢,目前来说都是把exe放到固定的位置。这里又不得不提静态链接库和动态链接库。这两种方式是不同的,所谓静态链接会把库和exe打包在一起,而动态链接则是在外部以dll文件形式存在(这里推荐我之前看过的《程序员的自我修养》这本书写的很硬核,也很有深度,感兴趣的可以去看看)。因此dll里面有相应函数的一些实现,在exe被加载后他们也会动态映射到程序空间的不同地址,比静态链接灵活很多。那么问题就来了,dll每次都被加载到不同的地址空间。回到之前的话题,一个程序的所有东西就这样都放在你的面前了,那么我们需要什么数据,直接就改什么数据不就好了嘛。
外挂的本质在于对数据的修改,我们利用CE这个强大的工具,可以方便的帮我们定位数据在什么地方,也可以直接修改,也可以直接锁定让这个数值不变。但是我们是要制作一个外挂,而不是每次都用CE去找。所以我们需要写一个程序来帮我们做这件事,什么事情呢?就是帮我们找到数据的地址然后修改它,没错就是这么简单。
我们需要思考的问题:
如何找到我们需要的地址,我们以修改分数这个为例来展开说一下(这篇文章的主要目的在于学习知识,而不是一个外挂教程,如果真的感兴趣的话可以从公众号后台加我的联系方式,和我深入探讨,也可以加入我的技术群,一起交流)
首先需要做的就是获取进程,打开CE的进程列表,获取到进程后,才可以读写游戏的内存
紧接着,获取到进程之后首先根据右边积分板中的数值,我们在CE中进行搜索,找到数值为1的8字节的数
注意这里用的是8字节,因为我们看它的内存地址就知道它是个64位的程序,这是个我反复踩的坑,之前一直用的4字节搜的。
首次我们搜索到了68587个地址,这些地址里面都存的是1,那么到底哪个才是真正的地址呢,所以我们需要改变数值,让游戏运行右边得分,我们点再次搜索
此时我们看到数量大大减少,计分板的本质是通过某个地址中的变量来改变数值显示的,所以它一定会随着得分的变化而同步变化,为什么会出现多个变量呢,因为可能游戏中本来就使用了多个变量(比如临时变量,又或者是碰巧数一样而已),总之我们要找的就是最同步变化的那个地址
经过多次重复这个过程,最终我们找到了那个唯一的地址,这个地址就是控制右边记分板数值的变量,很简单吧?
这样找出地址的确很简单,但是我们现在是要写外挂而不是找到这个地址,那么问题就来了。这个地址下次会不会变化呢,显然答案是肯定的,要不我就不会花那么长的时间了。这个地址是在堆中或者在全局区的静态变量,它的地址每次申请和分配都是不一样的,怎么才能每次重新加载游戏,重启游戏后都准确的找到这个地址呢,这就需要知道什么是基地址和偏移地址了。
右击这个地址选择看看是什么写入了这个地址,我们可以先看看下面这个图,然后我再来细说一下,这个图的理解很重要
我们现在得到的这个是一个偏移地址,从图中反汇编的汇编代码中可以看到,mov [rdi+54],eax ,把eax寄存器中的数据写入rdi寄存器中的地址再偏移54之后得到的地址。所以它不是一个固定的地址,每次rdi寄存器的地址是变化的但是这个54的偏移是不会变的。所以,根据rdi寄存器中的数值,我们再去CE中搜索,是哪个地址存放的是rdi里面的这个地址呢,也就是指针的指针
我们最终要找的是基地址,基地址是一个有固定位置或者相对固定位置的地址,而且它与我们这个地址是有关系的,因为经过多次偏移可以最终得到我们现在存放记分板的变量的地址。听起来有点绕是吧,那么我来画个图你就大概明白了。
找基地址的办法还是很多的,最简单的办法就是像刚才那样一次一次的搜索,不断地搜索指向当前这个地址,这种方法非常的繁琐,而且会遇到找错地情况。但是原理讲起来很清楚所以我以这个为例。目前我知道地找基地址地办法还有:**搜索指针映射法、人造指针法、特征码搜索法。**我最终使用地就是搜索指针映射法,再说一遍这篇文章不是外挂制作教程!想研究可以联系我私下讨论。
现在基地址已经找到了,那么接下来要做的事情就是写代码。用程序来帮我们做这个根据基地址找偏移地址并且修改地址中数值地这个过程。
那么现在我们来理一下思路:
我来说说在这个过程中遇到的坑:
游戏是64位程序,我一开新建的是32位的程序,在调用操作系统函数的时候会发生获取不到窗口句柄的情况,经过我不断的网上查阅资料最终得到了两种解决办法。
我一开始采用的就是最原始的办法,手动去找但是真的是特别麻烦,不仅需要自己去记录每次的偏移量是多少,而且每次搜到的都是好几个,我踩坑踩了好久最终我采用了第二种找基地址的办法。能通过第一种办法找到的还是原来的那种比如植物大战僵尸这种老游戏,现代游戏引擎都比较复杂,我舍友这个游戏就是通过Unity引擎做的,因此采用那种真的太累了。当然现在的CE也很智能,难道这就是所谓的“道高一尺魔高一丈”? 哈哈,开个玩笑。
我使用的是C++的MFC开发的,因为手头只有这么个做界面的工具,界面真的超级丑!QT重装电脑被我卸载了,没办法我就去网上找一些界面库,但是找来找去用不了,那些界面库大多都是32位的库,不能放在我64位的程序中!最终没有改成,还是使用这个丑界面将就用吧。
那么接下来简单的看看部分代码吧:
这个代码是用来获取目标程序的模块位置,可以算的上是一个通用的了
unsigned long long dwGetModuleBaseAddress(DWORD dwProcessID, TCHAR* lpszModuleName)
{
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwProcessID);
unsigned long long dwModuleBaseAddress = 0;
if (hSnapshot != INVALID_HANDLE_VALUE)
{
MODULEENTRY32 ModuleEntry32 = { 0 };
ModuleEntry32.dwSize = sizeof(MODULEENTRY32);
if (Module32First(hSnapshot, &ModuleEntry32))
{
do
{
if (_tcscmp(ModuleEntry32.szModule, lpszModuleName) == 0)
{
dwModuleBaseAddress = (unsigned long long)ModuleEntry32.hModule;
break;
}
} while (Module32Next(hSnapshot, &ModuleEntry32));
}
CloseHandle(hSnapshot);
}
return dwModuleBaseAddress;
}
下面这个是用来根据窗口的名字获取程序句柄的代码
hwnd = ::FindWindow(0, L"XXXXXXXXXXXXX");
if (hwnd)
{
GetWindowThreadProcessId(hwnd, &pid);
phandle = OpenProcess(PROCESS_ALL_ACCESS, 0, pid);
if (!phandle) {
AfxMessageBox(_T("未获取到进程句柄!"));
}
}
好了,到这儿差不多结束了,很抱歉的我代码不能开源。首先总不能“游戏未发,外挂先行”吧?其次本文仅供学习并不是外挂教学,有兴趣可以私下联系我。目前这个游戏是单机版,不知道他之后会不会改成网络游戏,改成网络游戏之后数据就放在服务器上了,比如记分板,和人物坐标,我们的外挂也会有好多限制,但是可以对网络封包进行截取和修改,也算多了一个新的思路。希望我们共同学习、共同进步!
最后期待这个游戏的可以再等一等,技能特效和新人物还是很棒的,游戏还未发行,我就不剧透了,原创不易,到现在已经写了四个小时了,欢迎关注,我会写出更多高质量文章!