游戏修改器制作教程四:用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);
		}
	}
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章