首先打开植物大战僵尸,进入游戏,初始阳光为50.
打开CE修改器,搜索50
进入游戏,种植向日葵,阳光变成0,再次搜索
再进入游戏,收取阳光,阳光变成25,搜索25
显然地址0x144344C8保存的就是阳光,现在退出游戏重新打开,重复上面步骤
现在地址变成0x1408EDC0了,说明阳光的地址并不是固定的。对这个地址进行检测,查看是哪条指令在修改
我刚刚进行了种植和收集阳光两个步骤,显然mov是减少阳光,add是增加阳光。查看详细信息
得到EAX = 0x14089860,这个0x5560其实就是二级偏移.然而EAX也是动态变化的,我们需要在内存中搜索EAX,来查找它到底保存在哪个地方
由于这个地址保存了阳光的地址,所以它应该是不变的,否则就找不到阳光的地址了,所以可以多次重复扫描,确保把会改变的量排除。
逐一查看哪个操作码访问了上面地址,发现地址0x028CA730很有趣
列表里清一色的都是0x768,证明[EAX + 0x028CA730]保存了结构体地址,查看EAX地址
这里不管是查看EAX还是ECX,结果肯定是一样的,因为它们都指向同一个地址,且偏移也相同。EAX = 0x028C9FC8,而0x768就是一级偏移。继续搜索EAX
列表里出现绿色的基址,查找结束。
开始写代码,C#无法直接修改内存,需要动态调用kernel32.dll
[DllImport("kernel32.dll", EntryPoint = "OpenProcess")]
public static extern IntPtr OpenProcess(int desiredAccess, bool heritHandle, int pocessID);
[DllImport("kernel32.dll", EntryPoint = "CloseHandle")]
public static extern void CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", EntryPoint = "ReadProcessMemory")]
public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr baseaddress, IntPtr buffer, int nsize, IntPtr bytesread);
[DllImport("kernel32.dll", EntryPoint = "WriteProcessMemory")]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr baseaddress, long[] buffer, int nSize, IntPtr byteswrite);
读写内存需要用到OpenProcess,官方文档里告诉我们第一个参数是访问权限,PROCESS_ALL_ACCESS指所有能获得的最高权限,但是PROCESS_ALL_ACCESS是在C++里定义的,C#里却没有,注意到这个值的类型是int,我们可以在C++里打印出这个值,然后直接写在C#里
所以我们只要输入0x1F0FFF就行了
private int ReadMemory(int pid,IntPtr toBase)
{
byte[] bytes = new byte[4];
IntPtr address = Marshal.UnsafeAddrOfPinnedArrayElement(bytes, 0);
IntPtr process = OpenProcess(0x1F0FFF, false, pid);
ReadProcessMemory(process, toBase, address, 4, IntPtr.Zero);
CloseHandle(process);
return Marshal.ReadInt32(address);
}
private void WriteMemory(int pid,IntPtr toBase,int num)
{
IntPtr process = OpenProcess(0x1F0FFF, false, pid);
WriteProcessMemory(process, toBase, new long[] { num }, 4, IntPtr.Zero);
CloseHandle(process);
}
获取进程PID
private int GetPid()
{
Process[] processes = Process.GetProcessesByName(ProcessName);
if(processes.Length == 0)
{
ShowDialog("没有检测到游戏进程.");
return -1;
}
if(processes.Length > 1)
{
ShowDialog("检测到多个进程,这可能是因为您开启了多个相同进程名的软件,请关闭多余软件.");
return -1;
}
return processes[0].Id;
}
定义全局变量
private const string ProcessName = "PlantsVsZombies";//进程名称
private const int sun = 9990;//每次修改的阳光数值
private int pid;//进程PID
private IntPtr intPtr;//阳光的地址
获取进程信息
private void GetInfo()
{
pid = GetPid();
if (pid == -1) return;
int num1 = ReadMemory(pid, (IntPtr)0x006A9EC0);
int num2 = ReadMemory(pid, (IntPtr)(num1 + 0x768));
intPtr = (IntPtr)(num2 + 0x5560);
}
添加两个按钮,第一个按钮用来读取进程信息,第二个按钮用来修改阳光
private void button1_Click(object sender, EventArgs e)
{
GetInfo();
}
private void button2_Click(object sender, EventArgs e)
{
WriteMemory(pid, intPtr, sun);
}
修改成功
关闭植物大战僵尸,重新打开,再次尝试。
修改成功,修改器制作完成。接下来是优化
MemoryIO.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace PVZ_Cheater
{
static class MemoryIO
{
[DllImport("kernel32.dll", EntryPoint = "OpenProcess")]
public static extern IntPtr OpenProcess(int desiredAccess, bool heritHandle, int pocessID);
[DllImport("kernel32.dll", EntryPoint = "CloseHandle")]
public static extern void CloseHandle(IntPtr hObject);
[DllImport("kernel32.dll", EntryPoint = "ReadProcessMemory")]
public static extern bool ReadProcessMemory(IntPtr hProcess, IntPtr baseaddress, IntPtr buffer, int nsize, IntPtr bytesread);
[DllImport("kernel32.dll", EntryPoint = "WriteProcessMemory")]
public static extern bool WriteProcessMemory(IntPtr hProcess, IntPtr baseaddress, long[] buffer, int nSize, IntPtr byteswrite);
public static int GetPid(string ProcessName)
{
Process[] processes = Process.GetProcessesByName(ProcessName);
if (processes.Length == 0)
{
throw new Exception("没有检测到游戏进程.");
}
if (processes.Length > 1)
{
throw new Exception("检测到多个进程,这可能是因为您开启了多个相同进程名的软件,请关闭多余软件.");
}
return processes[0].Id;
}
public static int ReadMemory(int pid, IntPtr toBase)
{
byte[] bytes = new byte[4];
IntPtr address = Marshal.UnsafeAddrOfPinnedArrayElement(bytes, 0);
IntPtr process = OpenProcess(0x1F0FFF, false, pid);
ReadProcessMemory(process, toBase, address, 4, IntPtr.Zero);
CloseHandle(process);
return Marshal.ReadInt32(address);
}
public static void WriteMemory(int pid, IntPtr toBase, int num)
{
IntPtr process = OpenProcess(0x1F0FFF, false, pid);
WriteProcessMemory(process, toBase, new long[] { num }, 4, IntPtr.Zero);
CloseHandle(process);
}
}
}
PVZInfo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace PVZ_Cheater
{
static class PVZInfo
{
public const string ProcessName = "PlantsVsZombies";
public static int Pid;
public static IntPtr Sun_Address;
public static int Sun_Value = 9990;
public static bool isReady = false;
public static void GetInfo()
{
isReady = false;
Pid = MemoryIO.GetPid(ProcessName);
int num1 = MemoryIO.ReadMemory(Pid, (IntPtr)0x006A9EC0);
int num2 = MemoryIO.ReadMemory(Pid, (IntPtr)(num1 + 0x768));
Sun_Address = (IntPtr)(num2 + 0x5560);
isReady = true;
}
public static void SetSun()
{
if(isReady) MemoryIO.WriteMemory(Pid, Sun_Address, Sun_Value);
}
}
}
增加一个Timer计时器,当选中"锁定阳光"选项时,每隔1秒将阳光赋值为9990,成功实现无限阳光