遊戲修改器製作教程四:用API讀寫記憶體

炒雞嗨客協管徐發表於2017-04-01

本教程面向有C\C++基礎的人,最好還要懂一些Windows程式設計知識
程式碼一律用Visual Studio 2013編譯,如果你還在用VC6請趁早丟掉它...
寫這個教程只是為了讓玩家更好地體驗所愛的單機遊戲,順便學到些逆向知識,我不會用網路遊戲做示範,請自重

上一章講了用CE讀寫記憶體,本章講如何自己程式設計實現

用到的API:

// 讀記憶體
BOOL WINAPI ReadProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPCVOID lpBaseAddress,
  _Out_ LPVOID  lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesRead
);
// 寫記憶體
BOOL WINAPI WriteProcessMemory(
  _In_  HANDLE  hProcess,
  _In_  LPVOID  lpBaseAddress,
  _In_  LPCVOID lpBuffer,
  _In_  SIZE_T  nSize,
  _Out_ SIZE_T  *lpNumberOfBytesWritten
);
// 開啟程式
HANDLE WINAPI OpenProcess(
  _In_ DWORD dwDesiredAccess,
  _In_ BOOL  bInheritHandle,
  _In_ DWORD dwProcessId
);

本章開始最好學習彙編知識了,也不用太深,能做逆向工程就行了
逆向工程(一):彙編、逆向工程基礎篇 這篇文章講得不錯

另外VS2013(我就不告訴你VC6也有)除錯時在選單-除錯-視窗-反彙編可以看到C/C++程式碼的對應彙編程式碼,多看看就熟悉了

本章以製作東方輝針城修改器的實戰講解讀寫記憶體

東方輝針城下載地址

分析

首先分析一下目標程式的記憶體

用CE搜尋一下HP地址,找到0x004F5864,然後用分析資料/結構分析一下它附近的記憶體(其實這些變數並不在一個struct或class內,但看看附近的記憶體總會有驚喜)

這是一個指向資源資訊的指標

然後是關於遊戲資料的

然後提取出有用的資料

取程式ID

要讀寫一個程式的記憶體首先要開啟程式,開啟程式需要程式ID(PID)

一般有兩種方式獲取PID,第一種通過視窗控制程式碼:

HWND hwnd = FindWindow(_T("BASE"), NULL);
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);

第二種通過程式名:

#include <tlhelp32.h>
DWORD GetPid(LPCTSTR name)
{
	DWORD pid = 0;
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);

	PROCESSENTRY32 processEntry;
	processEntry.dwSize = sizeof(PROCESSENTRY32);
	// 列舉程式
	BOOL hasNext = Process32First(snapshot, &processEntry);
	while (hasNext)
	{
		// 比較程式名
		if (_tcsicmp(processEntry.szExeFile, name) == 0)
		{
			pid = processEntry.th32ProcessID;
			break;
		}
		hasNext = Process32Next(snapshot, &processEntry);
	}

	CloseHandle(snapshot);
	return pid;
}

DWORD pid = GetPid(_T("th14.exe"));

開啟程式

沒什麼好講的,讀記憶體需要PROCESS_VM_READ許可權,寫記憶體需要PROCESS_VM_WRITE和PROCESS_VM_OPERATION許可權

HANDLE process = OpenProcess(/*PROCESS_ALL_ACCESS*/ PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);

寫記憶體

需要注意的是靜態地址也不是不變的,準確來說應該用模組基址+偏移量來表示,因為模組基址可能會變,如果模組基址會變的話還要取模組基址(其實就是模組控制程式碼)
不過大部分exe模組的基址是不變的(32位預設0x00400000,64位預設0x100000000)

DWORD buffer;
WriteProcessMemory(m_process, (LPVOID)0x004F5864, &(buffer = 8), sizeof(DWORD), NULL);

東方輝針城修改器

然後我們就可以實現這個修改器了,依然用到了MFC(為了少寫UI程式碼)

完整原始碼見GitHub

// 處理定時器
void CTH14CheatDlg::OnTimer(UINT_PTR nIDEvent)
{
	HWND hwnd = ::FindWindow(_T("BASE"), NULL);
	if (hwnd == NULL) // 程式已關閉
	{
		if (m_process != NULL)
		{
			// 釋放控制程式碼
			CloseHandle(m_process);
			m_process = NULL;
		}
	}
	else
	{
		// 開啟程式
		if (m_process == NULL)
		{
			DWORD pid;
			GetWindowThreadProcessId(hwnd, &pid);
			m_process = OpenProcess(/*PROCESS_ALL_ACCESS*/ PROCESS_VM_WRITE | PROCESS_VM_OPERATION, FALSE, pid);
			if (m_process == NULL)
			{
				CString msg;
				msg.Format(_T("開啟程式失敗,錯誤程式碼:%u"), GetLastError());
				MessageBox(msg, NULL, MB_ICONERROR);
				CDialogEx::OnTimer(nIDEvent);
				return;
			}
		}

		// 寫記憶體
		DWORD buffer;
		if (m_lockHp)
		{
			WriteProcessMemory(m_process, (LPVOID)0x004F5864, &(buffer = 8), sizeof(DWORD), NULL);
		}
		if (m_lockBomb)
		{
			WriteProcessMemory(m_process, (LPVOID)0x004F5870, &(buffer = 8), sizeof(DWORD), NULL);
		}
		if (m_lockPower)
		{
			WriteProcessMemory(m_process, (LPVOID)0x004F5858, &(buffer = 400), sizeof(DWORD), NULL);
		}
	}

	CDialogEx::OnTimer(nIDEvent);
}

東方輝針城修改器V2

每秒鐘寫記憶體的方法看上去太蠢了,而且會影響效能,一勞永逸的方法就是修改程式碼

首先找出減少殘機數的指令

地址是0x0044F618,機器碼A3 64 58 4F 00,把它全部改成90(nop指令)

減少bomb的指令

地址0x0041218A,機器碼A3 70 58 4F 00,改成nop

然後是判斷bomb夠不夠用的指令

要修改的是下面的jle指令(小於或等於時跳轉),把它改成nop
地址0x0044DD68,機器碼7E 0E

然後是遊戲剛開始時賦值靈力的

直接改這條指令長度會變長,改上面的mov eax吧,改成賦值400

地址0x00435DAF,原機器碼A3 58 58 4F 00,修改成B8 90 01 00 00

死亡後賦值靈力的

這堆程式碼的意思是把靈力讀到ecx暫存器,減少後寫回記憶體,改成賦值400吧

地址0x0044DDB8,原機器碼03 C8 3B CE 0F 4C CE,修改成B9 90 01 00 00 90 90

實現程式碼(完整原始碼地址同上):

// 修改關於殘機的程式碼
void CTH14CheatDlg::modifyHpCode()
{
	static const BYTE originalCode[] = { 0xA3, 0x64, 0x58, 0x4F, 0x00 };
	static const BYTE modifiedCode[] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
	if (m_process != NULL)
	{
		WriteProcessMemory(m_process, (LPVOID)0x0044F618, m_lockHp ? modifiedCode : originalCode, sizeof(originalCode), NULL);
	}
}

// 修改關於炸彈的程式碼
void CTH14CheatDlg::modifyBombCode()
{
	static const BYTE originalCode1[] = { 0xA3, 0x70, 0x58, 0x4F, 0x00 };
	static const BYTE modifiedCode1[] = { 0x90, 0x90, 0x90, 0x90, 0x90 };
	static const BYTE originalCode2[] = { 0x7E, 0x0E };
	static const BYTE modifiedCode2[] = { 0x90, 0x90 };
	if (m_process != NULL)
	{
		WriteProcessMemory(m_process, (LPVOID)0x0041218A, m_lockBomb ? modifiedCode1 : originalCode1, sizeof(originalCode1), NULL);
		WriteProcessMemory(m_process, (LPVOID)0x0044DD68, m_lockBomb ? modifiedCode2 : originalCode2, sizeof(originalCode2), NULL);
	}
}

// 修改關於靈力的程式碼
void CTH14CheatDlg::modifyPowerCode()
{
	static const BYTE originalCode1[] = { 0xA3, 0x58, 0x58, 0x4F, 0x00 };
	static const BYTE modifiedCode1[] = { 0xB8, 0x90, 0x01, 0x00, 0x00 };
	static const BYTE originalCode2[] = { 0x03, 0xC8, 0x3B, 0xCE, 0x0F, 0x4C, 0xCE };
	static const BYTE modifiedCode2[] = { 0xB9, 0x90, 0x01, 0x00, 0x00, 0x90, 0x90 };
	if (m_process != NULL)
	{
		WriteProcessMemory(m_process, (LPVOID)0x00435DAF, m_lockPower ? modifiedCode1 : originalCode1, sizeof(originalCode1), NULL);
		WriteProcessMemory(m_process, (LPVOID)0x0044DDB8, m_lockPower ? modifiedCode2 : originalCode2, sizeof(originalCode2), NULL);
		if (m_lockPower)
		{
			DWORD buffer;
			WriteProcessMemory(m_process, (LPVOID)0x004F5858, &(buffer = 400), sizeof(DWORD), NULL);
		}
	}
}

相關文章