作者:问号哥
前言:
首先跟大家说说我对游戏辅助的理解。什么是游戏辅助呢?一是通过修改程序中的数据达到变态功能,比如变态血量,变态射速,飞天遁地;二是通过修改游戏程序代码的执行顺序,实现变态功能。比如jmp掉死亡判定,子弹不减,子弹无后座等等。本篇文章主要以steam中的PixelStrike3D为例实现子弹无限,子弹无后座,子弹连发,以及方框透视。由于笔者的水平有限,其中一些功能可能不是很完善,但主要是提供一种思路,望大佬见谅。
1. 子弹不减:
要实现子弹无限,首先找到子弹的地址,这里使用强大的内存搜索引擎ce搜索。
首先用ce附加游戏;
为什么要选用4字节呢?大概是根据前辈的经验,一般fps的子弹数为整数型也就是4字节,
以2字节存放子弹的游戏不是没有,但是市面上基本看不到了。
点击搜索后可以看到内存中所有值为29的地址,由于我们只要其中一个真是表示主武器子弹的地址所以要使用再次扫描,继续筛选。
再次筛选,首先要改变主武器的子弹数量,也就是在游戏里开一枪,然后在ce中输入此时的子弹数量,点击再次搜索
此时我们可以看到只剩下2个地址了,在重复上述操作,我们就可以得到一个地址
尝试修改看看
我们发现我们更改后游戏里面的值并没有发生变化,进游戏里看枪试试,
发现我们子弹99999了,说明子弹数的显示实在开枪后调用的,之前屏幕上的子弹是另一个内存地址存放的,当我们开枪游戏会调用我们真实的子弹数,减去消耗的子弹数,赋值给显示子弹的内存。这就是我们之前在首次搜索子弹时的干扰,这些干扰有可能是真实值在调用函数时的形参,或者是一个无关紧要的临时存储,或者是用来校验数据是否异常,的临时变量。
但是当我们退出游戏,重新开始一句游戏的时候,我们的找到的地址没用了。
这是为什么呢?
这里用c++的一段代码给大家看看原因
#include
#include
class武器
{
public:
武器();
DWORD弹药数;
};
武器::武器() {
DWORD弹药数 = 30;
}
class人物
{
public:
人物();
武器* 主武器;
};
人物::人物()
{
主武器 = new武器;
}
class游戏
{
public:
游戏();
人物* 人物的地址;
};
游戏::游戏()
{
人物的地址 = new人物;
}
int main() {
游戏* 一把新的游戏 = new游戏;
printf("武器的地址:%x", &(一把新的游戏->人物的地址->主武器->弹药数));
}
这段代码的大致意思是当我们开启新一轮的游戏后,每次游戏都会重新示例化人物对象,当初始化人物对象时又会初始化武器对象。以下为2此运行的结果
可以看到每次运行的结果都不一样,
那么我们如何找到一个能够长久使用的地址呢,这就引进了基址和偏移和概念。
为什么我们能使用基址加偏移的方式得到我们想要的地址呢?
这是因为游戏有对应的执行流程,就如同前面所展示的代码一样,要先有游戏,才能有人物,再有武器,通过基址我们可以得到最先创建的对象(一般第一个对象的存放地址固定),读取对象中下一个对象的内容,通过游戏对象,我们可以找到人物对象的地址,通过人物对象,我们可以找到武器对象的地址,再在武器对象中读取我们想要的子弹地址。
什么是基址呢?准确的是就是模块的地址加上一定的偏移就是基址,在ce中这样的地址一般为绿色,这里给出一些样本供大家参考
SHELL32.dll+679AB0
MSCTF.dll+11D148
用x64dbg附加游戏
可以看到对应的模块
以及模块对应的地址
由于模块的地址是我们可以读取到的,所以我们就可以得到基址
通过上面的关系式可以得到第一个表达式的地址为
0x7ffe223d00000+0x679ab0
给大家看一个常见的基址加偏移的找地址方法
先通过最下排读取基地址中只想下一个对象的地址
这里【】是读取括号中地址的值的意思
【"GameAssembly.dll"+0663F378】的值为0x1ee456832d0,就是第一个对象的地址,通过【0x1ee456832d0+0x18】读取到下一个对象的地址,如此反复,得到最后1ecf018f6ac地址(注意最后一个没有读取值,如果读取值就是我们想要的值)
那么知道了寻址方法,我们该如何通过基址+偏移的方式得到我们想要的子弹地址呢;
目前我所知道的有2中方法,1.通过分析汇编代码,寻找地址的来源(高手用的)。2.通过指针扫描,穷举找到我们想要基址加偏移的组合(懒人专用).
这里我介绍第二种方法:
先来讲讲原理吧,指针扫描就是通过分析代码中所有的调用,比如我们对子弹地址进行扫描,那么ce就会去查找所有对子弹地址的调用,在根据子弹地址的调用找子弹地址调用的调用,直至找到调用是基址为止。hang
知道了原理我们如何使用这个方法呢
首先我们要先找到子弹的地址(因为它是会变的,接下来称之为动态地址)
,点击指针扫描
由于这个游戏是x64位游戏,u3d的偏移又比所以我尝试之后推荐如图配置
然后点击确定,选择安装路径,重点来了,你选择的路径一定要是全英文路径,不能有一点中文,否则ce不认识,就不给你保存了!!!我当初这里卡了一下午
耐心等待,如果怕时间不够就把程序挂起,随便弄个调试器,这游戏没啥反调试用sharpodx64保护一下调试器即可,百度即可下载
Ok,这些就是穷举出来的所有地址,其中肯定有一部分干扰,所以我们要改变地址的值,然后通过筛选,筛选掉一部分干扰,找到能用的一个地址,再给大家说一个经验,类似与人物属性的一般会放在UnityPlayer.dll中,所以这个基址的可能性会大一点。
这里说下不小心关掉指针扫描器怎么办
我们退出出本局游戏,然后再进入游戏,点击指针扫描器
然后点击确定,就吧无效的干扰排除了,保存为1.1
此时我们的子弹为30,那就吧30全都找出来
保存为1.1.1
可以看到还有不少,我们再重启整个游戏,再进行一次如上操作
我们可以看到只剩下几个了,这几个是比较稳定的,随便一个估计都能拿来用,这里我选择之前我用过的
双击它,添加到ce主界面,我们就可以稳定快捷的修改我们的子弹数了A.A。
悄悄说一句,游戏检测比较弱,联机也能999.绿色游戏,绿色游戏。
那么接下来我们来实现无限子弹
有3个思路:
1. 修改子弹到一个很大的数,一局游戏打不完即可,比如999
2. 类似与ce的锁值功能,不断向子弹的地址写入30
3. 把子弹减少的指令nop掉,让我们开枪不减子弹
思路实现2(适合有windows编程基础的选手):
这里我给出部分核心代码,没有给出的函数,大家可以根据对应的中文名自己百度相应功能的函数
//"UnityPlayer.dll"+01A255E8
#define子弹偏移数 8
ULONGLONG人物坐标偏移[子弹偏移数] = { 0x01A255E8,0x0,0x10,0x28,0x28,0x50,c0,a0 };
ULONGLONG getOtherProcessAddr_byOff(HANDLEhProcess, intnumOfOff, ULONGLONGoffs[], ULONGLONGbase) {
base += offs[0];
/*printf("获取到的地址:%llx\n", base);*/
ULONGLONG tmp = 0;
for (size_t i = 1; i < numOfOff; i++)
{
BOOL bret = ReadProcessMemory(hProcess, (LPVOID)base, &base, 8, 0);
if (!bret) {
return 0;
}
base += offs[i];
/*printf("获取到的地址:%llx\n", base);*/
}
returnbase;
}
BOOL无限子弹进程(LPARAM游戏句柄) {
#define子弹偏移数 8
ULONGLONG子弹偏移[子弹偏移数] = { 0x01A255E8,0x0,0x10,0x28,0x28,0x50,0xc0,0xa0 };
ULONGLONG UnityPlayer = 获取模块地址(进程pid, "UnityPlayer.dll");
ULONGLONG子弹地址=getOtherProcessAddr_byOff(游戏句柄, 子弹偏移数, 子弹偏移, UnityPlayer);
while (true) {
DWORD要写入的子弹数 = 666;
WriteProcessMemory(游戏句柄, 子弹地址, &要写入的子弹数, 4, 0);
}
}
int main() {
/*游戏* 一把新的游戏 = new 游戏;
printf("武器的地址:%x", );
system("pause");*/
HANDLE进程句柄 = 获取进程句柄("游戏的名字");
CreateRemoteThread(游戏句柄, 0, 0, (LPTHREAD_START_ROUTINE)无限子弹进程, 游戏句柄, 0, 0);
}
思路实现3通过改汇编指令实现无限子弹,这个方法也是本人最喜欢的一种,简单粗暴又好使
如何找到修改子弹的汇编指令的地址呢?只要通过对子弹地址下改写断点,拦截改写,记录下访问的地址就可以了,ce很贴心的提供了对应的功能
选第二个,我们探究的这个地址的值,只与该地址有关,而非这个指针
完成后进游戏打两枪
原来是这货把我们的子弹变少了,点击旁边的反汇编程序按钮,进入ce的反汇编窗口
那现在我们只要不让这条指令执行即可,2中思路,跳过他--把它上一行的jne改为jmp
改为
进游戏看看,完美实现,
第二种干掉他
进游戏同样实现我们的无限子弹
那么代码如何实现呢,看看这条指令的地址
只要把GameAssembly.dll+0x148bb85后面的6个字节改为90 90 90 90 90 90 即可,篇幅有限,这里只用第二种思路实现
ULONGLONG模块地址 = (ULONGLONG)获取模块地址("GameAssembly.dll");
PBYTE子弹硬编码地址 = (PBYTE)(模块地址 + 0x148bb85);
//输入a开启无限子弹
if (getchar()==’a’) {
//获取旧的硬编码以便还原
memcpy_s(旧的子弹硬编码, 6, 子弹硬编码地址, 6);
//改变一下内存的读写权限
DWORD子弹地址读写权限 = 0;
VirtualProtectEx(GetCurrentProcess(), 子弹硬编码地址, 5, PAGE_EXECUTE_READWRITE, &子弹地址读写权限);
memset(子弹硬编码地址, 0x90, 6);
VirtualProtectEx(GetCurrentProcess(), 子弹硬编码地址, 5, 子弹地址读写权限, 0);
}
else {
//还原硬编码
DWORD后坐力地址读写权限 = 0;
VirtualProtectEx(GetCurrentProcess(), 子弹硬编码地址, 5, PAGE_EXECUTE_READWRITE, &后坐力地址读写权限);
memcpy_s(子弹硬编码地址, 6, 旧的子弹硬编码, 6);
VirtualProtectEx(GetCurrentProcess(), 子弹硬编码地址, 5, 后坐力地址读写权限, 0);
}
Tip:代码段的修改要先修改内存属性,不然会报错游戏崩溃
本文章一共分为2期
第2期:子弹无后座、子弹连发实现、方框透视,敬请期待!