什么是游戏外挂? 试想场景,在玩游戏时,没有得到良好的游戏体验,加之玩游戏的这位又是偏激之人,此时心生愤怒,但通过自己的游戏技术,又无法得到发泄。所以很无奈,只能打开一种游戏作弊程序,这种游戏作弊程序就叫做游戏外挂。
以下为本篇文章的最终效果
dPlayerOptions.push({"id":"7747e15fc5c6ec8c887e63c01b4bd83b","live":false,"autoplay":false,"theme":"#FADFA3","loop":false,"screenshot":false,"hotkey":true,"preload":"metadata","lang":"zh-cn","logo":null,"volume":0.7,"mutex":true,"video":{"url":"https:\/\/cdn.ttext.cn\/2019\/12\/20191219_135446.mp4","pic":"","type":"auto","thumbnails":""},"danmaku":null,"subtitle":null});
实际上外挂不止适用于游戏,比如就像去年2018年出现的滴滴打车计费外挂,明明车辆只从沿着长安街直路开到了王府井,滴滴APP上却显示你已经在三环兜了一个圈,外挂程序通过修改滴滴的程序参数,或者向滴滴的服务器提交假参数,达到计费作弊骗钱的目的。本篇文章只涉及“修改本地程序参数”的代码,而且我先声明,未经软件著作人允许或授权的,开挂属于违法行为,本篇内容只供学习交流,不负任何的责任。
为什么要用Java写外挂? 先了解上边所说的游戏参数指的是什么,游戏参数指的是比如说冷却时间、金币数量、血条、攻击力,而这些数据它必定是存在程序中变量里的,而变量是存在内存中的,所以要做的基本就是,先在内存中找到这个变量的内存地址,然后对这个变量进行读写值操作。那么说回来,为什么要用Java写外挂?,因为圈子里的很多人总说JAVA在这方面不行,而且网上关于JAVA写内存挂的文章也不多,所以本文将以植物大战僵尸这款游戏做演示,尝试修改其金币、冷却时间。
想要读写某个程序中的某个其他变量,就需要先在内存中找到这个变量的地址,这里用到一个工具 Cheat Engine,用它可以帮我们扫描出基址与偏移量,在这里就不去提供下载方式,也不说明使用的问题了,因为在百度上很容易搜到,而且与本文内容不符,所以就直接贴出植物大战僵尸的金币、冷却的基址与偏移量。
这里所说的基址是啥?基址为啥是静态的?偏移量是啥?这些问题要说清楚其实很难,必须要从代码编译至操作系统底层原理究其所以然才可能弄明白。
所以我就简单通俗的说一说,在我们印象中,一个变量的内存地址应该是应该是随机开辟的,但为什么会有静态的变量地址呢,比如0x006A9EC0,其实每个应用程序的源代码在被编译链接后,其中的全局变量地址就会被确定下来,所以每次运行时,它都是一样的地址,它在运行之前就已经被安排的明明白白了。
那么又会产生另一个问题,试想一个场景,我们把编译好的程序,运行两个,那进程A需要访问地址0x006A9EC0,进程B也需要访问地址0x006A9EC0,它们不会产生冲突吗?,其实在操作系统之上运行的每个进程,使用的都是虚拟地址空间,也就是说每个进程都有自己0x006A9EC0,进程操作的都是属于自己的0x006A9EC0,所以不会产生冲突,是相互隔离的,而且虚拟地址空间会由操作系统负责帮我们与物理地址空间映射转换。
了解了这些东西以后,我们就可以推测一下,所谓的基址0x006A9EC0可能是一个全局的结构体指针变量,在main方法运行后,这个指针指向了一个结构体,结构体中有N个成员,这些结构体成员的指针地址,会根据该结构体的首地址偏移,也就是说,结构体成员的指针地址是有规律的,结构体第一个成员的地址是0x7fff6e8e3d80,第二个成员的地址可能就会是0x7fff6e8e3d84,这俩成员的指针地址偏移量就是4,通常结构体的首地址与首成员地址相同,也就是说结构体首地址加偏移量4就可以找到第二个结构体成员,这个偏移量是由成员类型、成员的位置等决定。那么所谓的偏移量就可以理解为是这个结构体中的成员位置,甚至可以认为是数组的下标。至于一级偏移、二级偏移,为啥还分级,其实就是结构体中内嵌了其他结构体,当然也可以认为是多维数组。
我们需要使用几个Windows API来对内存进行读写操作:
这几个API在kernel32.dll里,我们使用JAN框架来调用DLL。引入以下jar包:
<dependency>
<groupId>net.java.dev.jna</groupId>
<artifactId>jna</artifactId>
<version>4.1.0</version>
</dependency>
代码如下:
MemoryManager
package cn.ttext.test.wg;
import com.sun.jna.Library;
import java.io.IOException;
public interface MemoryManager extends Library {
int OpenProcess(int processId);
int OpenProcess(String processName) throws IOException;
void CloseHandle(int processId);
int ReadIntProcessMemory(int processId,int address);
int ReadIntProcessMemory(int processId,int ... addresss);
void WriteIntProcessMemory(int processId,long value,int address);
void WriteIntProcessMemory(int processId,long value,int ... addresss);
}
MemoryManagerImpl
package cn.ttext.test.wg;
import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Pointer;
import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.Charset;
public class MemoryManagerImpl implements MemoryManager {
private interface Memory extends Library {
Memory INSTANCE = (Memory) Native.loadLibrary("kernel32", Memory.class);
int OpenProcess(int desiredAccess,boolean heritHandle,int pocessID);
void CloseHandle(int process);
boolean ReadProcessMemory(int process, int baseAddress, Pointer buffer, int size, int bytesread);
boolean WriteProcessMemory(int process,int baseAddress,long[] value,int size,int byteswrite);
}
public int OpenProcess(int processId) {
//0x1F0FFF获取最大权限
return Memory.INSTANCE.OpenProcess(0x1F0FFF, false, processId);
}
public int OpenProcess(String processName) throws IOException {
Process process = Runtime.getRuntime().exec("TASKLIST /FI \"IMAGENAME eq " + processName + "\"");
BufferedReader bufferedReader = new BufferedReader(
new InputStreamReader(
new BufferedInputStream(process.getInputStream()), Charset.forName("UTF-8")));
String str;
int pid = -1;
while ((str = bufferedReader.readLine()) != null){
if (str.contains(processName)){
pid = Integer.parseInt(str.substring(processName.length(),str.indexOf("Console")).trim());
}
}
if (pid != -1){
return this.OpenProcess(pid);
}else{
return -1;
}
}
public void CloseHandle(int processId) {
Memory.INSTANCE.CloseHandle(processId);
}
public int ReadIntProcessMemory(int processId, int address) {
Pointer buffer = new com.sun.jna.Memory(4);
Memory.INSTANCE.ReadProcessMemory(processId,address,buffer,4,0);
return buffer.getInt(0);
}
public int ReadIntProcessMemory(int processId, int... addresss) {
int address = 0;
for (int addr:addresss){
address = ReadIntProcessMemory(processId, addr + address);
}
return address;
}
public void WriteIntProcessMemory(int processId, long value, int address) {
Memory.INSTANCE.WriteProcessMemory(processId,address,new long[]{value},4,0);
}
public void WriteIntProcessMemory(int processId, long value, int... addresss) {
int[] t_a = new int[addresss.length - 1];
for (int i = 0; i < t_a.length; i++) {
t_a[i] = addresss[i];
}
WriteIntProcessMemory(processId,value,
this.ReadIntProcessMemory(processId, t_a) + addresss[addresss.length - 1]);
}
}
Main
package cn.ttext.test.wg;
import java.io.IOException;
public class Main {
public static void main(String[] args) throws Exception {
MemoryManager memoryManager = new MemoryManagerImpl();
//打开进程名:PlantsVsZombies.exe
int process = memoryManager.OpenProcess("PlantsVsZombies.exe");
System.out.println("启动成功。。。。。。。。。。");
//向阳光的地址写入数量
memoryManager.WriteIntProcessMemory(process,999999,0x006A9EC0, 0x768, 0x5560);
//清除冷却,每500毫秒清一次
new Thread(()->{
while (true){
for (int i = 0; i < 7; i++) {
memoryManager.WriteIntProcessMemory(process,1,0x006A9EC0, 0x768, 0x144,0x70 + 0x50 * i);
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//退出进程
//memoryManager.CloseHandle(process);
}
}
转发请注明原文链接!!!