Game memory modifier

Implementation principle

Modifying the data displayed in the game is to modify the memory of the process where the game is located, because these data are retained in memory. Because the address spaces of processes are isolated from each other, API functions must be used to access the memory of other processes. The following two functions are usually used to read and write the memory space of other processes.

BOOL ReadProcessMemory( 
   HANDLE hProcess, //Handle to the process to be read
   LPCVOID lpBaseAddress, //The starting address of the memory to be read in the target process
   LPVOID lpBuffer, //A buffer used to accept read data
   DWORD nSize, //Number of bytes to read
   LPDWORD lpNumberOfBytesRead //Used by the function to return the number of bytes actually read
); 
WriteProcessMemory(hProcess, lpBaseAddress, lpBuffer, nSize, lpNumberOfBytesRead); //The meaning of parameters is the same as above

One of their functions is to read the memory of the specified process, and the other is to write the memory of the specified process. The specific usage will be introduced later.

How to program and modify the vitality, money value and other data displayed in the game? First of all, in the game process, which memory address should be searched to store these data, search the unique address, and then call the WriteProcessMemory function to write the data you expect to this address.

The trouble is to search the memory of the target process.

You should search the entire user address space of the target process. In the whole 4GB address of the process, the operating system of Windows 98 series reserves 4MB to 2GB for the application, and the operating system of Windows 2000 series reserves 64KB to 2GB. Therefore, before searching, it is necessary to judge the type of operating system to determine the search scope. Windows provides the GetVersionEx function to return the version information of the current operating system. The function usage is as follows.

BOOL GetVersionEx(LPOSVERSIONINFO lpVersionInfo);

The system will return the version information of the operating system to the OSVERSIONINFO structure pointed to by the parameter lpVersionInfo.

typedef struct _OSVERSIONINFO { 
      DWORD dwOSVersionInfoSize; //The size of this structure must be set before calling
      DWORD dwMajorVersion; //Major version number of the operating system
      DWORD dwMinorVersion; //Minor version number of the operating system
      DWORD dwBuildNumber; //The compiled version number of the operating system
      DWORD dwPlatformId; //Operating system platform. Can be VER_PLATFORM_WIN32_NT (2000 Series), etc
     TCHAR szCSDVersion[128]; //Specify the latest service pack installed on the system, such as "Service Pack 3"
} OSVERSIONINFO;

Here, you only need to judge whether it is a Windows 98 series system or a Windows 2000 series system, so the following code is sufficient.

OSVERSIONINFO vi = { sizeof(vi) }; 
::GetVersionEx(&vi); 
if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS) 
... //Windows 98 series operating system
else 
... //Windows NT Series operating system

There are probably multiple values you want to search in the memory of the target process, so when searching for the first time, record the searched address, then let the user change the value to be searched, and then search in the recorded address until the searched address is unique. To do this, write two auxiliary functions and three global variables.

BOOL FindFirst(DWORD dwValue); //Make the first lookup in the target process space
BOOL FindNext(DWORD dwValue); //Perform the 2nd, 3rd, 4th... Lookup in the address space of the target process
DWORD g_arList[1024]; //Address list
int g_nListCnt; //Number of valid addresses
HANDLE g_hProcess; //Target process handle

The above five lines of code constitute a more practical search system. For example, the money value displayed in the game is 12345. First, pass 12345 to the FindFirst function for the first search. The FindFirst function will save all the addresses with 12345 in the memory of the game process in G_ In the arlist global array, record the number of such addresses in g_nListCnt variable.

After the FindFirst function returns, check G_ If the value of nlistcnt is greater than 1, it means that more than 1 addresses have been searched. At this time, you should do something to change the money value displayed in the game. For example, after changing the money value to 13345, you need to call the FindNext function with 13345 as the parameter. This function will be in g_arList array to find the address of the record and update G_ For the record of arlist array, write all the addresses with 13345 contents to it, and write the number of such addresses to g_nListCnt variable.

After the FindNext function returns, check G_ If the value of nlistcnt is not equal to 1, continue to change the money value and call the function FindNext until the final g_nListCnt has a value of 1. At this time, G_ The value of arlist [0] is the address where the money value is saved in the target process.

Write test program

In order to carry out the experiment, write a test program as the target process (game process). Try to change a value in the program memory first. The simple implementation code of the program is as follows.
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>

// Global variable test
int g_nNum;	
int main(int argc, char* argv[])
{
	int i = 198;	// Local variable test
	g_nNum = 1003;

	while(1)
	{
		// Output the value and address of a variable
		printf(" i = %d, addr = %08lX;   g_nNum = %d, addr = %08lX \n",
								++i, &i, --g_nNum, &g_nNum);
		getchar();
	}

	return 0;
}


Now let's study how to change the variable g in this program in another program_ The values of nnum and i, which is the process of changing the memory of the game process.

Search memory

Next, create a project 02MemRepair to write the memory modifier program. In order to realize the function of searching memory, you can first write the definitions of the three global variables and the declarations of the two functions mentioned above before the main function.

Windows uses paging mechanism to manage memory. The size of each page is 4KB (on x86 processor). In other words, windows allocates memory for applications in 4KB units. Therefore, you can search the target memory by page to improve the search efficiency. The function of the CompareAPage function below is to compare the memory of 1 page in the memory of the target process.

BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
	// Read 1 page of memory
	BYTE arBytes[4096];
	if(!::ReadProcessMemory(g_hProcess, (LPVOID)dwBaseAddr, arBytes, 4096, NULL))
		return FALSE;	// This page is unreadable

	// Find in this 1 page of memory
	DWORD* pdw;
	for(int i=0; i<(int)4*1024-3; i++)
	{
		pdw = (DWORD*)&arBytes[i];
		if(pdw[0] == dwValue)	// Equal to the value to find?
		{
			if(g_nListCnt >= 1024)
				return FALSE;
			// Add to global variable
			g_arList[g_nListCnt++] = dwBaseAddr + i;
		}
	}

	return TRUE;
}

The FindFirst function takes the longest time because it needs to search in an address space of nearly 2GB. Here is its implementation code.

BOOL FindFirst(DWORD dwValue)
{
	const DWORD dwOneGB = 1024*1024*1024;	// 1GB
	const DWORD dwOnePage = 4*1024;		// 4KB

	if(g_hProcess == NULL)
		return FALSE;
	
	// View the operating system type to determine the start address
	DWORD dwBase;
	OSVERSIONINFO vi = { sizeof(vi) };
	::GetVersionEx(&vi);
	if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
		dwBase = 4*1024*1024;		// Windows 98 series, 4MB	
	else
		dwBase = 640*1024;		// Windows NT Series, 64KB

	// Search in the address space from the start address to 2GB
	for(; dwBase < 2*dwOneGB; dwBase += dwOnePage)
	{
		// Compare 1 page of memory
		CompareAPage(dwBase, dwValue);
	}

	return TRUE;
}

The FindFirst function records all qualified memory addresses into the global array G_ In arlist. Next, write an auxiliary function ShowList to print out the searched address.

void ShowList()
{
	for(int i=0; i< g_nListCnt; i++)
	{
		printf("%08lX \n", g_arList[i]);
	}
}

The code in the main function is.

int main(int argc, char* argv[])
{
	// Start the 02tester process
	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);
	// Close the thread handle, since we don't use it
	::CloseHandle(pi.hThread);
	g_hProcess = pi.hProcess;

	// Enter the value to modify
	int	iVal;
	printf(" Input val = ");
	scanf_s("%d", &iVal);

	// Make the first find
	FindFirst(iVal);
	
	// Print out search results
	ShowList();
	::CloseHandle(g_hProcess);
	return 0;
}


Since the address found above is not unique, click enter several times in the 02 tester window. After changing the value of the variable, you need to click g_nListCnt searches the addresses listed in the array variable, which is the function of FindNext.

BOOL FindNext(DWORD dwValue)
{
	// Save M_ The number of valid addresses in the arlist array, initializing a new m_nListCnt value
	int nOrgCnt = g_nListCnt;
	g_nListCnt = 0;	

	// In M_ The arlist array looks at the address of the record
	BOOL bRet = FALSE;	// Hypothetical failure	
	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;
}

Add the following code to the main function.

while(g_nListCnt > 1)
{
	printf(" Input val = ");
	scanf_s("%d", &iVal);

	// Next search
	FindNext(iVal);

	// Display search results
	ShowList();
}

Run the program. When the output address is not unique, change the value of the variable in the target process until the unique address is output, and the search is completed.

Write process space

After finding the address of the variable, you can change its value. The WriteMemory function is used to achieve this function.

BOOL WriteMemory(DWORD dwAddr, DWORD dwValue)
{
	return ::WriteProcessMemory(g_hProcess, (LPVOID)dwAddr, &dwValue, sizeof(DWORD), NULL);
}

Add the following code to the main function.

// Get new value
printf(" New value = ");
scanf_s("%d", &iVal);	     
// Write new value
if(WriteMemory(g_arList[0], iVal))
	printf(" Write data success \n");

Now that you have all the basic functions, start the program.
(1) Enter 1002 and the found address is not unique.
(2) Click enter twice in the 02tester window, and then search again after the change. This cycle will continue until the address found is unique
One.
(3) Enter the expected value and modify it successfully!
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();

// attribute
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]; }

// operation
	virtual BOOL FindFirst(DWORD dwValue);
	virtual BOOL FindNext(DWORD dwValue);
	virtual BOOL WriteMemory(DWORD dwAddr, DWORD dwValue);

// realization
protected:
	virtual BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue);

	DWORD m_arList[1024];	// Address list
	int m_nListCnt;		// Number of valid addresses
	HANDLE m_hProcess;	// Target process handle
	BOOL m_bFirst;		// Is this your first search
};

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;
	
	// View the operating system type to determine the start address
	DWORD dwBase;
	OSVERSIONINFO vi = { sizeof(vi) };
	::GetVersionEx(&vi);
	if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
		dwBase = 4*1024*1024;		// Windows 98 series, 4MB	
	else
		dwBase = 640*1024;		// Windows NT Series, 64KB

	// Search in the address space from the start address to 2GB
	for(; dwBase < 2*dwOneGB; dwBase += dwOnePage)
	{
		// Compare 1 page of memory
		CompareAPage(dwBase, dwValue);
	}

	m_bFirst = FALSE;

	return TRUE;
}

BOOL CMemFinder::CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
	// Read 1 page of memory
	BYTE arBytes[4096];
	if(!::ReadProcessMemory(m_hProcess, (LPVOID)dwBaseAddr, arBytes, 4096, NULL))
		return FALSE;	// This page is unreadable

	// Find in this 1 page of memory
	DWORD* pdw;
	for(int i=0; i<(int)4*1024-3; i++)
	{
		pdw = (DWORD*)&arBytes[i];
		if(pdw[0] == dwValue)	// Equal to the value to find?
		{
			if(m_nListCnt >= 1024)
				return FALSE;
			// Add to global variable
			m_arList[m_nListCnt++] = dwBaseAddr + i;
		}
	}

	return TRUE;
}

BOOL CMemFinder::FindNext(DWORD dwValue)
{
	// Save M_ The number of valid addresses in the arlist array, initializing a new m_nListCnt value
	int nOrgCnt = m_nListCnt;
	m_nListCnt = 0;	

	// In M_ The arlist array looks at the address of the record
	BOOL bRet = FALSE;	// Hypothetical failure	
	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 file

#include "stdafx.h"
#include "windows.h"
#include "stdio.h"
#include <iostream>


BOOL FindFirst(DWORD dwValue);	// Make the first lookup in the target process space
BOOL FindNext(DWORD dwValue);	// Perform the 2nd, 3rd, 4th... Lookup in the address space of the target process

DWORD g_arList[1024];		// Address list
int g_nListCnt;			// Number of valid addresses
HANDLE g_hProcess;		// Target process handle


//

BOOL WriteMemory(DWORD dwAddr, DWORD dwValue);
void ShowList();


int main(int argc, char* argv[])
{
	// Start the 02tester process
	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);
	// Close the thread handle, since we don't use it
	::CloseHandle(pi.hThread);
	g_hProcess = pi.hProcess;

	// Enter the value to modify
	int	iVal;
	printf(" Input val = ");
	scanf_s("%d", &iVal);

	// Make the first find
	FindFirst(iVal);
	
	// Print out search results
	ShowList();

	
	while(g_nListCnt > 1)
	{
		printf(" Input val = ");
		scanf_s("%d", &iVal);

		// Next search
		FindNext(iVal);

		// Display search results
		ShowList();
	}

	
	// Get new value
	printf(" New value = ");
	scanf_s("%d", &iVal);	     

	// Write new value
	if(WriteMemory(g_arList[0], iVal))
		printf(" Write data success \n");	

    
	::CloseHandle(g_hProcess);
	return 0;
}

BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue)
{
	// Read 1 page of memory
	BYTE arBytes[4096];
	if(!::ReadProcessMemory(g_hProcess, (LPVOID)dwBaseAddr, arBytes, 4096, NULL))
		return FALSE;	// This page is unreadable

	// Find in this 1 page of memory
	DWORD* pdw;
	for(int i=0; i<(int)4*1024-3; i++)
	{
		pdw = (DWORD*)&arBytes[i];
		if(pdw[0] == dwValue)	// Equal to the value to find?
		{
			if(g_nListCnt >= 1024)
				return FALSE;
			// Add to global variable
			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;
	
	// View the operating system type to determine the start address
	DWORD dwBase;
	OSVERSIONINFO vi = { sizeof(vi) };
	::GetVersionEx(&vi);
	if (vi.dwPlatformId == VER_PLATFORM_WIN32_WINDOWS)
		dwBase = 4*1024*1024;		// Windows 98 series, 4MB	
	else
		dwBase = 640*1024;		// Windows NT Series, 64KB

	// Search in the address space from the start address to 2GB
	for(; dwBase < 2*dwOneGB; dwBase += dwOnePage)
	{
		// Compare 1 page of memory
		CompareAPage(dwBase, dwValue);
	}

	return TRUE;
}

BOOL FindNext(DWORD dwValue)
{
	// Save M_ The number of valid addresses in the arlist array, initializing a new m_nListCnt value
	int nOrgCnt = g_nListCnt;
	g_nListCnt = 0;	

	// In M_ The arlist array looks at the address of the record
	BOOL bRet = FALSE;	// Hypothetical failure	
	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;
}

// Print out the searched address
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);
}


extract interface
The above implements the core function of modifying the game memory. In order to enable readers to directly use the code in this section in the actual development process, this book encapsulates the function of searching memory in a CMemFinder class. The following is the definition of this class.

class CMemFinder
{
public:
	CMemFinder(DWORD dwProcessId);
	virtual ~CMemFinder();

// attribute
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]; }

// operation
	virtual BOOL FindFirst(DWORD dwValue);
	virtual BOOL FindNext(DWORD dwValue);
	virtual BOOL WriteMemory(DWORD dwAddr, DWORD dwValue);

// realization
protected:
	virtual BOOL CompareAPage(DWORD dwBaseAddr, DWORD dwValue);

	DWORD m_arList[1024];	// Address list
	int m_nListCnt;		// Number of valid addresses
	HANDLE m_hProcess;	// Target process handle
	BOOL m_bFirst;		// Is this your first search
};

Because the implementation code of the class is similar to the code described above, it is not listed here. If there is anything unclear, please check the supporting CD of this book (MemRepair project).

This class provides friendly interface members, which can also be overloaded to achieve special functions. The MemRepair instance of the CD-ROM in this book realizes most of the functions of modifying the game memory, and eliminates the thread blocking problem encountered in the process of searching the memory for a long time by overloading the CMemFinder class. If the current development project uses the function of searching memory, you can refer to the source code of this example.

Keywords: Windows

Added by KrisCons on Thu, 11 Nov 2021 12:36:59 +0200