修改游戏中显示的数据就是要修改游戏所在进程的内存,因为这些数据都在内存中保留着。由于进程的地址空间是相互隔离的,所以必须有 API 函数的协助才能访问其他进程的内存。通常使用下面两个函数对其他进程的内存空间进行读写 *** 作。
BOOL ReadProcessMemory( HANDLE hProcess, //待读进程的句柄 LPCVOID lpbaseAddress, //目标进程中待读内存的起始地址 LPVOID lpBuffer, //用来接受读取数据的缓冲区 DWORD nSize, //要读取的字节数 LPDWORD lpNumberOfBytesRead //用来供函数返回实际读取的字节数 ); WriteProcessMemory(hProcess, lpbaseAddress, lpBuffer, nSize, lpNumberOfBytesRead); //参数含义同上
它们的作用一个是读指定进程的内存,另一个是写指定进程的内存,具体用法后面再介绍。
如何编程实现修改游戏里显示的生命力,金钱值等数据呢?首先应该在游戏进程中搜索在哪一个内存地址保存着这些数据,搜索到惟一的地址后调用 WriteProcessMemory 函数向这个地址中写入你期待的数据就行了。
这里面比较麻烦的一点就是搜索目标进程的内存。
应该在目标进程的整个用户地址空间进行搜索。在进程的整个 4GB 地址中,Windows 98系列的 *** 作系统为应用程序预留的是 4MB 到 2GB 部分,Windows 2000 系列的 *** 作系统预留的是 64KB 到 2GB 部分,所以在搜索前还要先判断 *** 作系统的类型,以决定搜索的范围。Windows 提供了 GetVersionEx 函数来返回当前 *** 作系统的版本信息,函数用法如下。
BOOL GetVersionEx(LPOSVERSIonINFO lpVersionInfo);
系统会将 *** 作系统的版本信息返回到参数lpVersionInfo指向的OSVERSIONINFO结构中。
typedef struct _OSVERSIONINFO { DWORD dwOSVersionInfoSize; //本结构的大小,必须在调用之前设置 DWORD dwMajorVersion; // *** 作系统的主版本号 DWORD dwMinorVersion; // *** 作系统的次版本号 DWORD dwBuildNumber; // *** 作系统的编译版本号 DWORD dwPlatformId; // *** 作系统平台。可以是VER_PLATFORM_WIN32_NT(2000 系列)等 TCHAR szCSDVersion[128]; //指定安装在系统上的最新服务包,例如“Service Pack 3”等 } OSVERSIONINFO;
这里只需要判断是 Windows 98 系列的系统还是 Windows 2000 系列的系统,所以使用下面的代码就足够了。
OSVERSIonINFO vi = { sizeof(vi) }; ::GetVersionEx(&vi); if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) ... //是 Windows 98 系列的 *** 作系统 else ... //是 Windows NT 系列的 *** 作系统
目标进程内存中很可能存在多个你要搜索的值,所以在进行第一次搜索的时候,要把搜索到的地址记录下来,然后让用户改变要搜索的值,再在记录的地址中搜索,直到搜索到的地址惟一为止。为此写两个辅助函数和 3 个全局变量。
BOOL FindFirst(DWORD dwValue); //在目标进程空间进行第一次查找 BOOL FindNext(DWORD dwValue); //在目标进程地址空间进行第2、3、4……次查找 DWORD g_arList[1024]; //地址列表 int g_nListCnt; //有效地址的个数 HANDLE g_hProcess; //目标进程句柄
上面这 5 行代码就组成了一个比较实用的搜索系统。比如游戏中显示的金钱值是 12345,首先将 12345 传给 FindFirst 函数进行第一次搜索,FindFirst 函数会将游戏进程内存中所有内容为 12345 的地址保存在 g_arList 全局数组中,将这样地址的个数记录在 g_nListCnt 变量中。
FindFirst 函数返回以后,检查 g_nListCnt 的值,如果大于 1 就说明搜索到的地址多于 1个。这时应该做一些事情改变游戏显示的金钱值。比如改变后金钱值变成了 13345,你要以13345 为参数调用 FindNext 函数。这个函数会在 g_arList 数组记录的地址中进行查找,并更新g_arList 数组的记录,将所有内容为 13345 的地址写到里面,将这样地址的个数写到 g_nListCnt变量中。
FindNext 函数返回后,检查 g_nListCnt 的值,如果不等于 1 还继续改变金钱值,调用函数 FindNext,直到最终 g_nListCnt 的值为 1 为止。这时,g_arList[0]的值就是目标进程中保存金钱值的地址。
编写测试程序为了进行实验编写一个测试程序作为目标进程(游戏进程)。先试着改变这个程序内存中的某个值就可以了。程序简单的实现代码如下。
StdAfx.h
// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #if !defined(AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_) #define AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers #include// TODO: reference additional headers your program requires here //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_)
StdAfx.cpp
// stdafx.cpp : source file that includes just the standard includes // 02ProcessList.pch will be the pre-compiled header // stdafx.obj will contain the pre-compiled type information #include "stdafx.h" // TODO: reference any additional headers you need in STDAFX.H // and not in this file
02Testor.cpp
#include "stdafx.h" #include// 全局变量测试 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(); } return 0; }
下面就来研究如何在另一个程序里改变这个程序中变量 g_nNum 和 i 的值,这一过程也就是改变游戏进程内存的过程。
接下来再创建一个工程 02MemRepair,用于编写内存修改器程序。为了实现搜索内存的功能,可以先将前面提到的 3 个全局变量的定义和两个函数的声明写到 main 函数前。
Windows 采用了分页机制来管理内存,每页的大小是 4KB(在 x86 处理器上)。也就是说Windows 是以 4KB 为单位来为应用程序分配内存的。所以可以按页来搜索目标内存,以提高搜索效率。下面的 CompareAPage 函数的功能就是比较目标进程内存中 1 页大小的内存。
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; } } return TRUE; }
FindFirst 函数工作时间最长了,因为它要在将近 2GB 大小的地址空间上搜索,下面是它的实现代码。
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; dwbase += dwOnePage) { // 比较1页大小的内存 CompareAPage(dwbase, dwValue); } return TRUE; }
FindFirst 函数将所有符合条件的内存地址都记录到了全局数组 g_arList 中。下面再编写一个辅助函数 ShowList 用来打印出搜索到的地址。
void ShowList() { for(int i=0; i< g_nListCnt; i++) { printf("%08lX n", g_arList[i]); } }
main 函数中的代码为。
int main(int argc, char* argv[]) { // 启动02testor进程 char szFileName[] = "C:\Users\Freddy\source\repostestor\Debugtestor.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_s("%d", &iVal); // 进行第一次查找 FindFirst(iVal); // 打印出搜索的结果 ShowList(); ::CloseHandle(g_hProcess); return 0; }
由于上面查找出来的地址不惟一,在 02testor 窗口中单击几次回车,改变变量的值后,还需要在 g_nListCnt 数组变量所列的地址中搜索,这就是 FindNext 函数的作用。
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在 main 函数中加上如下代码。
while(g_nListCnt > 1) { printf(" Input val = "); scanf_s("%d", &iVal); // 进行下次搜索 FindNext(iVal); // 显示搜索结果 ShowList(); }运行程序,当输出的地址不惟一时,就改变目标进程中变量的值,直到输出惟一的地址为止,搜索完毕。
写进程空间找到变量的地址后就可以改变它的值了,WriteMemory 函数用来实现这一功能。
BOOL WriteMemory(DWORD dwAddr, DWORD dwValue) { return ::WriteProcessMemory(g_hProcess, (LPVOID)dwAddr, &dwValue, sizeof(DWORD), NULL); }在 main 函数中加上如下代码。
// 取得新值 printf(" New value = "); scanf_s("%d", &iVal); // 写入新值 if(WriteMemory(g_arList[0], iVal)) printf(" Write data success n");现在基本功能都有了,启动程序。
(1)输入 1002,发现找出的地址不惟一。
(2)在 02testor 窗口敲两下回车,改变后再进行一次查找,这样循环直到找到的地址惟
一为止。
(3)输入期待的值,修改成功!
StdAfx.h// stdafx.h : include file for standard system include files, // or project specific include files that are used frequently, but // are changed infrequently // #if !defined(AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_) #define AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers #include// TODO: reference additional headers your program requires here //{{AFX_INSERT_LOCATION}} // Microsoft Visual C++ will insert additional declarations immediately before the previous line. #endif // !defined(AFX_STDAFX_H__9C88BEEE_5C7F_4140_B411_3B326A3986F1__INCLUDED_) StdAfx.cpp
// stdafx.cpp : source file that includes just the standard includes // 02ProcessList.pch will be the pre-compiled header // stdafx.obj will contain the pre-compiled type information #include "stdafx.h" // TODO: reference any additional headers you need in STDAFX.H // and not in this fileMemRepair.h
#ifndef __MEMFINDER_H__ #define __MEMFINDER_H__ #includeclass CMemFinder { public: CMemFinder(DWORD dwProcessId); virtual ~CMemFinder(); // 属性 public: BOOL IsFirst() const { return m_bFirst; } BOOL IsValid() const { return m_hProcess != NULL; } int GetListCount() const { return m_nListCnt; } DWORD operator [](int nIndex) { return m_arList[nIndex]; } // *** 作 virtual BOOL FindFirst(DWORD dwValue); virtual BOOL FindNext(DWORD dwValue); virtual BOOL WriteMemory(DWORD dwAddr, DWORD dwValue); // 实现 protected: virtual BOOL CompareAPage(DWORD dwbaseAddr, DWORD dwValue); DWORD m_arList[1024]; // 地址列表 int m_nListCnt; // 有效地址的个数 HANDLE m_hProcess; // 目标进程句柄 BOOL m_bFirst; // 是不是第一次搜索 }; CMemFinder::CMemFinder(DWORD dwProcessId) { m_nListCnt = 0; m_bFirst = TRUE; m_hProcess = ::OpenProcess(PROCESS_VM_READ|PROCESS_VM_WRITE|PROCESS_VM_OPERATION, FALSE, dwProcessId); } CMemFinder::~CMemFinder() { if(m_hProcess != NULL) ::CloseHandle(m_hProcess); } BOOL CMemFinder::FindFirst(DWORD dwValue) { const DWORD dwoneGB = 1024*1024*1024; // 1GB const DWORD dwonePage = 4*1024; // 4KB if(m_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; dwbase += dwOnePage) { // 比较1页大小的内存 CompareAPage(dwbase, dwValue); } m_bFirst = FALSE; return TRUE; } BOOL CMemFinder::CompareAPage(DWORD dwbaseAddr, DWORD dwValue) { // 读取1页内存 BYTE arBytes[4096]; if(!::ReadProcessMemory(m_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(m_nListCnt >= 1024) return FALSE; // 添加到全局变量中 m_arList[m_nListCnt++] = dwbaseAddr + i; } } return TRUE; } BOOL CMemFinder::FindNext(DWORD dwValue) { // 保存m_arList数组中有效地址的个数,初始化新的m_nListCnt值 int nOrgCnt = m_nListCnt; m_nListCnt = 0; // 在m_arList数组记录的地址处查找 BOOL bRet = FALSE; // 假设失败 DWORD dwReadValue; for(int i=0; i 02MemRepair.cpp
/// // 02MemRepair.cpp文件 #include "stdafx.h" #include "windows.h" #include "stdio.h" #includeBOOL 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[]) { // 启动02testor进程 char szFileName[] = "C:\Users\Freddy\source\repos\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_s("%d", &iVal); // 进行第一次查找 FindFirst(iVal); // 打印出搜索的结果 ShowList(); while(g_nListCnt > 1) { printf(" Input val = "); scanf_s("%d", &iVal); // 进行下次搜索 FindNext(iVal); // 显示搜索结果 ShowList(); } // 取得新值 printf(" New value = "); scanf_s("%d", &iVal); // 写入新值 if(WriteMemory(g_arList[0], iVal)) printf(" Write data success n"); ::CloseHandle(g_hProcess); 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; } } 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; 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
提炼接口
上面实现了修改游戏内存的核心功能。为了让读者在实际开发过程中能够直接使用本节的代码,本书将搜索内存的功能封装在一个 CMemFinder 类里面,下面是这个类的定义。class CMemFinder { public: CMemFinder(DWORD dwProcessId); virtual ~CMemFinder(); // 属性 public: BOOL IsFirst() const { return m_bFirst; } BOOL IsValid() const { return m_hProcess != NULL; } int GetListCount() const { return m_nListCnt; } DWORD operator [](int nIndex) { return m_arList[nIndex]; } // *** 作 virtual BOOL FindFirst(DWORD dwValue); virtual BOOL FindNext(DWORD dwValue); virtual BOOL WriteMemory(DWORD dwAddr, DWORD dwValue); // 实现 protected: virtual BOOL CompareAPage(DWORD dwbaseAddr, DWORD dwValue); DWORD m_arList[1024]; // 地址列表 int m_nListCnt; // 有效地址的个数 HANDLE m_hProcess; // 目标进程句柄 BOOL m_bFirst; // 是不是第一次搜索 };因为类的实现代码跟上面讲述的代码差不多,就不列在这里了。如果还有什么不清楚的地方,请查看本书的配套光盘(MemRepair 工程)。
这个类提供了友好的接口成员,也可以重载它,以实现特殊的功能。本书配套光盘的MemRepair 实例实现了修改游戏内存的大部分功能,而且通过重载 CMemFinder 类,消除了在长时间搜索内存的过程中遇到的线程的阻塞问题。如果当前开发的项目用到了搜索内存的功能,可以参考这个实例的源代码。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)