游戏修改器制作教程七:注入DLL的各种姿势

教程面向有C\C++基础的人,最好还要懂一些Windows编程知识
代码一律用Visual Studio 2013编译,如果你还在用VC6请趁早丢掉它...
写这个教程只是为了让玩家更好地体验所爱的单机游戏,顺便学到些逆向知识,我不会用网络游戏做示范,请自重

往其他进程注入代码大概分两种,一种是像CE注入代码那样在目标进程申请内存,然后把机器码写进去,另一种是用高级语言写一个DLL,然后注入目标进程(显然是用高级语言实现更方便!)
注入的代码就是目标进程的一部分了,可以直接用指针读写目标进程内存,还可以hook目标进程的函数

本章介绍几种常用的注入DLL的方法

远线程注入

远线程注入的原理是在目标进程调用LoadLibrary载入我们的DLL

首先写个DLL用来测试,实现禁止结束进程

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"

// hook代码略,参考上一章

BYTE terminateProcessOldCode[sizeof(JmpCode)];

BOOL WINAPI MyTerminateProcess(HANDLE hProcess, UINT uExitCode)
{
	return FALSE; // 禁止结束进程
}


// DLL被加载、卸载时调用
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
					 )
{
	TCHAR processPath[256];
	TCHAR msg[270];

	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		GetModuleFileName(GetModuleHandle(NULL), processPath, sizeof(processPath) / sizeof(processPath[0]));
		_stprintf_s(msg, _T("注入了进程 %s"), processPath);
		MessageBox(NULL, msg, _T(""), MB_OK);

		hook(GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "TerminateProcess"), MyTerminateProcess, terminateProcessOldCode);
		break;

	case DLL_PROCESS_DETACH:
		MessageBox(NULL, _T("DLL卸载中"), _T(""), MB_OK);

		// 卸载时记得unhook
		unhook(GetProcAddress(GetModuleHandle(_T("kernel32.dll")), "TerminateProcess"), terminateProcessOldCode);
		break;

	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
		break;
	}
	return TRUE;
}

然后写个EXE用来注入DLL

主要用到的API:

// 载入DLL
HMODULE WINAPI LoadLibrary(
  _In_ LPCTSTR lpFileName
);

// 在目标进程创建远线程,相当于调用目标进程的函数
HANDLE WINAPI CreateRemoteThread(
  _In_  HANDLE                 hProcess,
  _In_  LPSECURITY_ATTRIBUTES  lpThreadAttributes,
  _In_  SIZE_T                 dwStackSize,
  _In_  LPTHREAD_START_ROUTINE lpStartAddress,
  _In_  LPVOID                 lpParameter,
  _In_  DWORD                  dwCreationFlags,
  _Out_ LPDWORD                lpThreadId
);

// 在目标进程申请内存
LPVOID WINAPI VirtualAllocEx(
  _In_     HANDLE hProcess,
  _In_opt_ LPVOID lpAddress,
  _In_     SIZE_T dwSize,
  _In_     DWORD  flAllocationType,
  _In_     DWORD  flProtect
);

远线程注入DLL的实现有两个前提
1. kernel32比较特殊,这个模块的基址在每个进程是一样的(当然,32位和64位是不一样的),所以本进程中的LoadLibrary地址可以用在其他进程(只要本进程和其他进程位数一样)
2. LoadLibrary需要一个参数,而且CreateRemoteThread刚好能传入一个参数

EXE代码(建议下载GitHub上的整个项目看看):

// 注入DLL,返回模块句柄(64位程序只能返回低32位)
HMODULE InjectDll(HANDLE process, LPCTSTR dllPath)
{
	DWORD dllPathSize = ((DWORD)_tcslen(dllPath) + 1) * sizeof(TCHAR);

	// 申请内存用来存放DLL路径
	void* remoteMemory = VirtualAllocEx(process, NULL, dllPathSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (remoteMemory == NULL)
	{
		printf("申请内存失败,错误代码:%u\n", GetLastError());
		return 0;
	}

	// 写入DLL路径
	if (!WriteProcessMemory(process, remoteMemory, dllPath, dllPathSize, NULL))
	{
		printf("写入内存失败,错误代码:%u\n", GetLastError());
		return 0;
	}

	// 创建远线程调用LoadLibrary
	HANDLE remoteThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, remoteMemory, 0, NULL);
	if (remoteThread == NULL)
	{
		printf("创建远线程失败,错误代码:%u\n", GetLastError());
		return NULL;
	}
	// 等待远线程结束
	WaitForSingleObject(remoteThread, INFINITE);
	// 取DLL在目标进程的句柄
	DWORD remoteModule;
	GetExitCodeThread(remoteThread, &remoteModule);

	// 释放
	CloseHandle(remoteThread);
	VirtualFreeEx(process, remoteMemory, dllPathSize, MEM_DECOMMIT);

	return (HMODULE)remoteModule;
}

// 卸载DLL
BOOL FreeRemoteDll(HANDLE process, HMODULE remoteModule)
{
	// 创建远线程调用FreeLibrary
	HANDLE remoteThread = CreateRemoteThread(process, NULL, 0, (LPTHREAD_START_ROUTINE)FreeLibrary, (LPVOID)remoteModule, 0, NULL);
	if (remoteThread == NULL)
	{
		printf("创建远线程失败,错误代码:%u\n", GetLastError());
		return FALSE;
	}
	// 等待远线程结束
	WaitForSingleObject(remoteThread, INFINITE);
	// 取返回值
	DWORD result;
	GetExitCodeThread(remoteThread, &result);

	// 释放
	CloseHandle(remoteThread);
	return result != 0;
}

#ifdef _WIN64
#include 
HMODULE GetRemoteModuleHandle(DWORD pid, LPCTSTR moduleName)
{
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, pid);
	MODULEENTRY32 moduleentry;
	moduleentry.dwSize = sizeof(moduleentry);

	BOOL hasNext = Module32First(snapshot, &moduleentry);
	HMODULE handle = NULL;
	do
	{
		if (_tcsicmp(moduleentry.szModule, moduleName) == 0)
		{
			handle = moduleentry.hModule;
			break;
		}
		hasNext = Module32Next(snapshot, &moduleentry);
	} while (hasNext);

	CloseHandle(snapshot);
	return handle;
}
#endif

int _tmain(int argc, _TCHAR* argv[])
{
	// 提升权限,不提升貌似也可以,以管理员身份运行就行
	EnablePrivilege(TRUE);

	// 打开进程
	HWND hwnd = FindWindow(NULL, _T("任务管理器"));
	DWORD pid;
	GetWindowThreadProcessId(hwnd, &pid);
	HANDLE process = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
	if (process == NULL)
	{
		printf("打开进程失败,错误代码:%u\n", GetLastError());
		return 1;
	}
	
	// 要将RemoteThreadDll.dll放在本程序当前目录下
	TCHAR dllPath[MAX_PATH]; // 要用绝对路径
	GetCurrentDirectory(_countof(dllPath), dllPath);
	_tcscat_s(dllPath, _T("\\RemoteThreadDll.dll"));


	// 注入DLL
	HMODULE remoteModule = InjectDll(process, dllPath);
	if (remoteModule == NULL)
	{
		CloseHandle(process);
		return 2;
	}
#ifdef _WIN64
	remoteModule = GetRemoteModuleHandle(pid, _T("RemoteThreadDll.dll"));
	printf("模块句柄:0x%08X%08X\n", *((DWORD*)&remoteModule + 1), (DWORD)remoteModule);
#else
	printf("模块句柄:0x%08X\n", (DWORD)remoteModule);
#endif

	// 暂停
	printf("按回车卸载DLL\n");
	getchar();

	// 卸载DLL
	if (!FreeRemoteDll(process, remoteModule))
	{
		CloseHandle(process);
		return 3;
	}


	// 关闭进程
	CloseHandle(process);

	return 0;
}

结果

64位的win10:
游戏修改器制作教程七:注入DLL的各种姿势_第1张图片

然后任务管理器无法结束任务,按回车后
游戏修改器制作教程七:注入DLL的各种姿势_第2张图片

又可以结束任务了

32位的win7:
游戏修改器制作教程七:注入DLL的各种姿势_第3张图片

(同上)
游戏修改器制作教程七:注入DLL的各种姿势_第4张图片

在主线程运行前远线程注入

这个跟上面差不多,只是在目标进程的主线程运行前就可以运行我们DLL的代码,可以提前做些手脚

方法是用CreateProcess创建进程时指定CREATE_SUSPENDED标志,这样主线程就被挂起了,然后用远线程注入DLL,再恢复主线程

// 程序运行时注入DLL,返回模块句柄(64位程序只能返回低32位)
HMODULE InjectDll(LPTSTR commandLine, LPCTSTR dllPath, DWORD* pid, HANDLE* process)
{
	TCHAR* commandLineCopy = new TCHAR[32768]; // CreateProcess可能修改这个
	_tcscpy_s(commandLineCopy, 32768, commandLine);
	int cdSize = _tcsrchr(commandLine, _T('\\')) - commandLine + 1;
	TCHAR* cd = new TCHAR[cdSize];
	_tcsnccpy_s(cd, cdSize, commandLine, cdSize - 1);
	// 创建进程并暂停
	STARTUPINFO startInfo = {};
	PROCESS_INFORMATION processInfo = {};
	if (!CreateProcess(NULL, commandLineCopy, NULL, NULL, FALSE, CREATE_SUSPENDED, NULL, cd, &startInfo, &processInfo))
	{
		delete commandLineCopy;
		delete cd;
		return 0;
	}
	delete commandLineCopy;
	delete cd;

	*pid = processInfo.dwProcessId;
	*process = processInfo.hProcess;

	DWORD dllPathSize = ((DWORD)_tcslen(dllPath) + 1) * sizeof(TCHAR);

	// 申请内存用来存放DLL路径
	void* remoteMemory = VirtualAllocEx(processInfo.hProcess, NULL, dllPathSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
	if (remoteMemory == NULL)
	{
		printf("申请内存失败,错误代码:%u\n", GetLastError());
		return 0;
	}

	// 写入DLL路径
	if (!WriteProcessMemory(processInfo.hProcess, remoteMemory, dllPath, dllPathSize, NULL))
	{
		printf("写入内存失败,错误代码:%u\n", GetLastError());
		return 0;
	}

	// 创建远线程调用LoadLibrary
	HANDLE remoteThread = CreateRemoteThread(processInfo.hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibrary, remoteMemory, 0, NULL);
	if (remoteThread == NULL)
	{
		printf("创建远线程失败,错误代码:%u\n", GetLastError());
		return NULL;
	}
	// 等待远线程结束
	WaitForSingleObject(remoteThread, INFINITE);
	// 取DLL在目标进程的句柄
	DWORD remoteModule;
	GetExitCodeThread(remoteThread, &remoteModule);

	// 恢复线程
	ResumeThread(processInfo.hThread);

	// 释放
	CloseHandle(remoteThread);
	VirtualFreeEx(processInfo.hProcess, remoteMemory, dllPathSize, MEM_DECOMMIT);

	return (HMODULE)remoteModule;
}

消息钩子注入

还记得第二章的SetWindowsHookEx吗,只要把dwThreadId设置为其他进程的线程ID或0就可以注入指定进程或所有进程了!(当然,32位DLL只能注入32位程序,64位DLL只能注入64位程序)

当钩子过程被调用时,系统会检测钩子过程所在DLL是否已载入,如果没有就会载入
所以用SetWindowsHookEx注入DLL的前提是钩子过程会被调用(比如安装了键盘钩子,但是没有在目标进程按一下键盘,DLL就不会被注入)
而且用这种方法DLL必须实现并导出钩子过程(就算用不到)

测试用的DLL(dllmain.cpp不变,在另外一个文件导出钩子过程):

// MsgHookDll.cpp : 定义 DLL 应用程序的导出函数。
//

#include "stdafx.h"


extern "C" __declspec(dllexport) // 导出这个函数
LRESULT CALLBACK CallWndProc(int code, WPARAM wParam, LPARAM lParam)
{
	return CallNextHookEx(NULL, code, wParam, lParam);
}

为了让钩子过程被调用我选择了调用频率最高的WH_GETMESSAGE钩子(当然,没有消息循环的进程还是注入不了)

EXE代码(建议下载GitHub上的整个项目看看):

DWORD GetProcessThreadID(DWORD pid)
{
	HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, pid);
	THREADENTRY32 threadentry;
	threadentry.dwSize = sizeof(threadentry);

	BOOL hasNext = Thread32First(snapshot, &threadentry);
	DWORD threadID = 0;
	do
	{
		if (threadentry.th32OwnerProcessID == pid)
		{
			threadID = threadentry.th32ThreadID;
			break;
		}
		hasNext = Thread32Next(snapshot, &threadentry);
	} while (hasNext);

	CloseHandle(snapshot);
	return threadID;
}

// 注入DLL,返回钩子句柄,DLL必须导出CallWndProc钩子过程,pid = 0则安装全局钩子
HHOOK InjectDll(DWORD pid, LPCTSTR dllPath)
{
	// 载入DLL
	HMODULE module = LoadLibrary(dllPath);
	if (module == NULL)
	{
		printf("载入DLL失败,错误代码:%u\n", GetLastError());
		return NULL;
	}
	// 取钩子过程地址
	HOOKPROC proc = (HOOKPROC)GetProcAddress(module, "CallWndProc");
	if (proc == NULL)
	{
		printf("取钩子过程地址失败,错误代码:%u\n", GetLastError());
		return NULL;
	}

	// 取线程ID
	DWORD threadID = 0;
	if (pid != 0)
	{
		threadID = GetProcessThreadID(pid);
		if (threadID == 0)
		{
			printf("取线程ID失败\n");
			return NULL;
		}
	}

	// 安装钩子
	HHOOK hook = SetWindowsHookEx(WH_GETMESSAGE, proc, module, threadID);

	// 释放
	FreeLibrary(module);

	return hook;
}

int _tmain(int argc, _TCHAR* argv[])
{
	// 提升权限,不提升貌似也可以,以管理员身份运行就行
	EnablePrivilege(TRUE);

	// 取PID
	//DWORD pid = 0; // 全局钩子,少玩全局钩子不然会出问题...
	HWND hwnd = FindWindow(NULL, _T("任务管理器"));
	if (hwnd == NULL)
	{
		printf("寻找窗口失败,错误代码:%u\n", GetLastError());
		return 1;
	}
	DWORD pid;
	GetWindowThreadProcessId(hwnd, &pid);

	// 注入DLL
	// 要将MsgHookDll.dll放在本程序当前目录下
	HHOOK hook = InjectDll(pid, _T("MsgHookDll.dll"));
	if (hook == NULL)
		return 2;

	// 暂停
	printf("按回车卸载DLL\n");
	getchar();

	// 卸载DLL
	UnhookWindowsHookEx(hook);

	return 0;
}

安装全局钩子的结果

游戏修改器制作教程七:注入DLL的各种姿势_第5张图片

DLL劫持

这个是我最喜欢的注入方式,因为只需要DLL,连EXE都不用了,而且随着程序启动就自动注入,有些游戏补丁就用这个实现的

原理是系统加载DLL时会优先搜索程序当前目录下有没有这个DLL,没有就再去System32等目录搜索(除非在注册表的KnownDLLs里)
所以只要自己编个DLL,实现程序要用的函数(直接调用原DLL对应的函数就可以实现),再把它放到要注入的程序同目录下,程序启动时就会自动注入(当然,32位的程序会无视掉64位的DLL)


还是拿东方辉针城开刀,先看看它导入了什么函数
游戏修改器制作教程七:注入DLL的各种姿势_第6张图片

d3d9.dll只导入了一个函数,我们只用实现一个函数,就劫持它好了


DLL代码(完整源码):

// dllmain.cpp : 定义 DLL 应用程序的入口点。
#include "stdafx.h"
#include "DllHijact.h"


HMODULE g_d3d9Module = NULL;


// DLL被加载、卸载时调用
BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
	)
{
	TCHAR processPath[MAX_PATH];
	TCHAR msg[MAX_PATH + 20];

	switch (ul_reason_for_call)
	{
	case DLL_PROCESS_ATTACH:
		GetModuleFileName(GetModuleHandle(NULL), processPath, MAX_PATH);
		_tcscpy_s(msg, _T("注入了进程 "));
		_tcscat_s(msg, processPath);
		MessageBox(NULL, msg, _T(""), MB_OK);

		// 加载原DLL,获取真正的Direct3DCreate9地址
		g_d3d9Module = LoadLibrary(_T("C:\\Windows\\System32\\d3d9.dll"));
		RealDirect3DCreate9 = (Direct3DCreate9Type)GetProcAddress(g_d3d9Module, "Direct3DCreate9");
		if (RealDirect3DCreate9 == NULL)
		{
			MessageBox(NULL, _T("获取Direct3DCreate9地址失败"), _T(""), MB_OK);
			return FALSE;
		}

		break;

	case DLL_PROCESS_DETACH:
		MessageBox(NULL, _T("DLL卸载中"), _T(""), MB_OK);

		// 手动卸载原DLL
		FreeLibrary(g_d3d9Module);

		break;

	case DLL_THREAD_ATTACH:
	case DLL_THREAD_DETACH:
		break;
	}
	return TRUE;
}

// DllHijack.cpp : 定义 DLL 应用程序的导出函数。
//

#include "stdafx.h"
#include "DllHijact.h"


Direct3DCreate9Type RealDirect3DCreate9 = NULL;


// 把MyDirect3DCreate9导出为Direct3DCreate9,用__declspec(dllexport)的话实际上导出的是_MyDirect3DCreate9@4
#ifndef _WIN64
#pragma comment(linker, "/EXPORT:Direct3DCreate9=_MyDirect3DCreate9@4")
#else
#pragma comment(linker, "/EXPORT:Direct3DCreate9=MyDirect3DCreate9")
#endif
extern "C" void* WINAPI MyDirect3DCreate9(UINT SDKVersion)
{
	MessageBox(NULL, _T("调用了Direct3DCreate9"), _T(""), MB_OK);
	return RealDirect3DCreate9(SDKVersion);
}

编译后把DLL命名为d3d9.dll,放到目标程序同目录下,运行目标程序时就自动注入
游戏修改器制作教程七:注入DLL的各种姿势_第7张图片
游戏修改器制作教程七:注入DLL的各种姿势_第8张图片
游戏修改器制作教程七:注入DLL的各种姿势_第9张图片

这个也能用于其他用了D3D9的程序,比如东方神灵庙
游戏修改器制作教程七:注入DLL的各种姿势_第10张图片

你可能感兴趣的:(游戏修改器制作教程七:注入DLL的各种姿势)