遊戲修改器製作教程四:用API讀寫內存

本教程面向有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);
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章