实现原理

修改游戏中显示的数据就是要修改游戏所在进程的内存,因为这些数据都在内存中保留着。由于进程的地址空间是相互隔离的,所以必须有 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;
}

Memory Analyzer Tool的使用方法_搜索


下面就来研究如何在另一个程序里改变这个程序中变量 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;
}

Memory Analyzer Tool的使用方法_搜索_02


由于上面查找出来的地址不惟一,在 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);
}

Memory Analyzer Tool的使用方法_搜索_03


提炼接口

上面实现了修改游戏内存的核心功能。为了让读者在实际开发过程中能够直接使用本节的代码,本书将搜索内存的功能封装在一个 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 类,消除了在长时间搜索内存的过程中遇到的线程的阻塞问题。如果当前开发的项目用到了搜索内存的功能,可以参考这个实例的源代码。