0x00 前言
文章中的文字可能存在语法错误以及标点错误,请谅解;
如果在文章中发现代码错误或其它问题请告知,感谢!
本例运行系统环境为windows 10,使用VS2010编译运行。
0x01实现原理
修改游戏中的数据就是要找到并修改其所使用的内存所保存的内容,通常情况下由于进程之间的地址空间不能够互相访问,所以要想修改游戏中的数据必须借助API函数才能够使得自己的修改程序访问该游戏的内存。
一般使用如下两个API函数进行对游戏内存的访问及修改操作:
BOOL ReadProcessMemory(HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
ReadProcessMemory 各参数含义如下:
HANDLE hProcess //待读进程句柄
LPVOID lpBaseAddress//目标进程中待读内存的起始地址
LPVOID lpBuffer //用来接受读取数据的缓冲区
DWORD nSize//要读取的字节数
LPDWORD lpNumberOfBytesRead//用来供函数返回实际读取的字节数
BOOL WriteProcessMemory(HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead);
WriteProcessMemory各参数含义同上。
另外,对游戏进程(目标进程)的内存搜索应该在整个用户空间进行,在32位windows中,可用的地址空间(虚拟地址空间)总计2^32 字节(4 GB,0x000000至0xFFFFFFFF),通常的 约2 GB (范围从0x00010000~0x7FFEFFFF)用于用户空间,另外 2 GB (范围 从0x80000000至0xFFFFFFFF)用于系统空间1。
由于不同版本的Windows用户空间范围不同,所以搜索空间的起止位置也不同,Win98系列操作系统搜索范围为4MB到2GB部分,Win2000系列搜索范围为64KB至2GB - 64KB。
需要注意的另外一点就是,Windows采用分页机制来管理内存,每页的大小为4KB(x86处理器上)2,应用程序的分配内存是按照4KB为单位进行的,这样可以提高搜索效率,在游戏内存修改的程序中我们也是按照页为单位来查找游戏内存地址的。
0x03 代码实现
该部分代码主要是源自《Windows程序设计(第3版)》。
首先需要编写一个测试程序作为游戏进程,这个程序在每按一次回车就会改变其值(自减1),程序代码如下:
#include "stdafx.h"
#include <stdio.h>
#include <wnidow.h>
// 全局变量测试
int g_nNum;
int main(int argc, char* argv[])
{
int i = 198; // 局部变量测试
g_nNum = 1003;
while(1)
{
// 输出个变量的值和地址
printf(" i = %d, addr = %08lX;g_nNum = %d, addr = %08lX \n",++i,&i, --g_nNum, &g_nNum);
getchar();
}
system("pause");
return 0;
}
然后是内存修改程序:
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include <iostream>
using namespace std;
BOOL FindFirst(DWORD dwValue); // 在目标进程空间进行第一次查找
BOOL FindNext(DWORD dwValue); // 在目标进程地址空间进行第2、3、4……次查找
DWORD g_arList[1024]; // 地址列表
int g_nListCnt; // 有效地址的个数
HANDLE g_hProcess; // 目标进程句柄
BOOL WriteMemory(DWORD dwAddr, DWORD dwValue);
void ShowList();
int main(int argc, char* argv[])
{
char szFileName[] = "E:\\下载\\01计算机学习\\windows程序设计学习\\windows程序设计光盘\\配书代码\\02Testor\\Debug\\02Testor.exe"; //目标进程的位置
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi;
::CreateProcess(NULL, szFileName, NULL, NULL, FALSE,
CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
// 关闭线程句柄,既然我们不使用它
::CloseHandle(pi.hThread);
g_hProcess = pi.hProcess;
// 输入要修改的值
int iVal;
printf(" Input val = ");
scanf("%d", &iVal);
// 进行第一次查找
FindFirst(iVal);
// 打印出搜索的结果
ShowList();
while(g_nListCnt > 1)
{
printf(" Input val = ");
scanf("%d", &iVal);
// 进行下次搜索
if(FALSE == FindNext(iVal))
{
printf("main:analysis erro\n");
::CloseHandle(g_hProcess);
system("pause");
return -1;
}
// 显示搜索结果
ShowList();
}
// 取得新值
printf(" New value = ");
scanf("%d", &iVal);
// 写入新值
if(WriteMemory(g_arList[0], iVal))
printf(" Write data success \n");
::CloseHandle(g_hProcess);
system("pause");
return 0;
}
BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
// 读取1页内存
BYTE arBytes[4096];
if(!::ReadProcessMemory(g_hProcess, (LPVOID)dwBaseAddr, arBytes, 4096, NULL))
return FALSE; // 此页不可读
// 在这1页内存中查找
DWORD* pdw;
for(int i=0; i<(int)4*1024-3; i++)
{
pdw = (DWORD*)&arBytes[i];
if(pdw[0] == dwValue) // 等于要查找的值?
{
if(g_nListCnt >= 1024)
return FALSE;
// 添加到全局变量中
g_arList[g_nListCnt++] = dwBaseAddr + i;
}
}
//printf("NUM is %d\n", g_nListCnt);
return TRUE;
}
BOOL FindFirst(DWORD dwValue)
{
const DWORD dwOneGB = 1024*1024*1024; // 1GB
const DWORD dwOnePage = 4*1024; // 4KB
if(g_hProcess == NULL)
return FALSE;
// 查看操作系统类型,以决定开始地址
DWORD dwBase;
OSVERSIONINFO vi = { sizeof(vi) };
::GetVersionEx(&vi);
if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
dwBase = 4*1024*1024; // Windows 98系列,4MB
else
dwBase = 640*1024; // Windows NT系列,64KB
// 在开始地址到2GB的地址空间进行查找
for(; dwBase < 2*dwOneGB - 64*1024; dwBase += dwOnePage)
{
// 比较1页大小的内存
CompareAPage(dwBase, dwValue);
}
return TRUE;
}
BOOL FindNext(DWORD dwValue)
{
// 保存m_arList数组中有效地址的个数,初始化新的m_nListCnt值
int nOrgCnt = g_nListCnt;
g_nListCnt = 0;
// 在m_arList数组记录的地址处查找
BOOL bRet = FALSE; // 假设失败
DWORD dwReadValue;
for(int i=0; i<nOrgCnt; i++)
{
if(::ReadProcessMemory(g_hProcess, (LPVOID)g_arList[i], &dwReadValue, sizeof(DWORD), NULL))
{
if(dwReadValue == dwValue)
{
g_arList[g_nListCnt++] = g_arList[i];
bRet = TRUE;
}
}
}
return bRet;
}
// 打印出搜索到的地址
void ShowList()
{
for(int i=0; i< g_nListCnt; i++)
{
printf("%08lX \n", g_arList[i]);
}
}
BOOL WriteMemory(DWORD dwAddr, DWORD dwValue)
{
return ::WriteProcessMemory(g_hProcess, (LPVOID)dwAddr, &dwValue, sizeof(DWORD), NULL);
}
运行结果:
在游戏内存修改程序中输入1002,然后回车:
在测试程序中输入回车,然后再在游戏内存修改程序中输入1001并回车:
找到了测试程序中正确的地址。
但是需要注意的是,这个游戏内存修改程序只能找出在一个固定内存地址上进行数值变换的游戏,若是想改变类似《植物大战僵尸》这类游戏的阳光值,则需要基址的知识了,可以参考我以前写的文章:
https://blog.csdn.net/wangqingchuan92/article/details/83036960
以上。
参考文档:
1.https://www.zhihu.com/question/29962475
2.https://docs.microsoft.com/zh-cn/windows-hardware/drivers/gettingstarted/virtual-address-spaces
3.https://blog.csdn.net/wang010366/article/details/52730052
4.https://blog.csdn.net/china_jeffery/article/details/79610915
5.张铮,孙宝山,周立天.Windows程序设计(第3版)[M].北京;人民邮电出版社,2018.7.
https://blog.csdn.net/china_jeffery/article/details/79610915 ↩︎
张铮,孙宝山,周立天.Windows程序设计(第3版)[M].北京;人民邮电出版社,2018.7. ↩︎