实现原理
修改游戏中显示的数据就是要修改游戏所在进程的内存,因为这些数据都在内存中保留着。由于进程的地址空间是相互隔离的,所以必须有 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 <stdio.h>
// 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 <stdio.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();
}
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\\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();
::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<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;
}
在 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 <stdio.h>
// 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
MemRepair.h
#ifndef __MEMFINDER_H__
#define __MEMFINDER_H__
#include <windows.h>
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; // 是不是第一次搜索
};
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<nOrgCnt; i++)
{
if(::ReadProcessMemory(m_hProcess, (LPVOID)m_arList[i], &dwReadValue, sizeof(DWORD), NULL))
{
if(dwReadValue == dwValue)
{
m_arList[m_nListCnt++] = m_arList[i];
bRet = TRUE;
}
}
}
return bRet;
}
BOOL CMemFinder::WriteMemory(DWORD dwAddr, DWORD dwValue)
{
return ::WriteProcessMemory(m_hProcess, (LPVOID)dwAddr, &dwValue, sizeof(DWORD), NULL);
}
#endif // __MEMFINDER_H__
02MemRepair.cpp
///
// 02MemRepair.cpp文件
#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include <iostream>
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[])
{
// 启动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<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);
}
提炼接口
上面实现了修改游戏内存的核心功能。为了让读者在实际开发过程中能够直接使用本节的代码,本书将搜索内存的功能封装在一个 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 类,消除了在长时间搜索内存的过程中遇到的线程的阻塞问题。如果当前开发的项目用到了搜索内存的功能,可以参考这个实例的源代码。