上文终于找到了行棋的起始位置和结束位置,这样就可以动态扫描的方式进行处理。这种方式是一种拉的方式,和使用HTTP协议非常相似。也就是说,你不知道游戏内存的数据什么时候,变成了什么。与之相似的处理方式,比如不停扫描数据库,看看是否记录增加等。因为对方不会通知,所以,需要主动去读,而缺点也显现出来了,与其它主动扫描一样有很多毛病。这里就不详细讲这个了,很多游戏外挂也是用的这样的思路,比如自动喝血喝蓝外挂,一般都是主动扫描内存,初级一点呢就是模拟键盘,高级一点就是使用CALL。实际上任何语言都可以使用CALL,包括C#、VB之类。原理就是向内存中写入2进制代码,计算机执行代码本质上就是2进制数字。
现在来说说HOOK API的方式化主动为被动,与IOC注入思想很相似,我提供一个函数,让游戏主动调用,而不是我主动去查询游戏的状态。
要用API注入,那就要寻找一个好的注入点,为了方便测试,这里以吃子为例。当玩家A的棋子吃掉了玩家B的棋子,我们现在就要在这个地方做处理。
HOOK API有多种处理方式,抛开C#之类的非标准PE文件格式,下面讲C++的实现。C++也有两种实现方式,一个是钩子注入,一个是DLL注入。钩子注入和DLL注入有一个本质区别,钩子注入一般是热键注入,比如,可以让游戏使用F12键,调出我们需要执行的代码,本文也是讲的这种方式。而DLL注入需要在游戏进程分配一个空间,然后把DLL放到该空间,这样DLL和游戏在同一个起始地址下,可以使用偏移直接操作。他们的本质区别就是DLL加载到的进程不一样。而我觉得钩子注入相对来说方便一些。
首先,创建两个项目,一个是对话框应用程序,一个是DLL,方便起见都用MFC库。应用程序相对简单,上面有两个按钮,两个按钮对应的事件就是加载和释放钩子的过程。
//dll句柄 HINSTANCE hmod; DWORD pid; //开启记牌功能 void CJunqiRpgPlugDlg::OnBnClickedOk() { // TODO: 在此添加控件通知处理程序代码 HWND hWnd = ::FindWindow(NULL,L"四国军棋角色版"); if(!hWnd){ AfxMessageBox(L"请先启动游戏。"); return; } DWORD tid = GetWindowThreadProcessId(hWnd,&pid); typedef BOOL (WINAPI * InstallHook)(DWORD dwThreadID); hmod = LoadLibrary(L"JunqiRpgExt.dll"); if(hmod==NULL){ AfxMessageBox(L"DLL加载失败。"); return; } InstallHook enbleHook; enbleHook = (InstallHook)GetProcAddress(hmod,"_InstallHook@4"); if(enbleHook){ (*enbleHook)(tid); }else{ AfxMessageBox(L"调用方法失败"); } } //退出记牌器 void CJunqiRpgPlugDlg::OnBnClickedCancel() { // TODO: 在此添加控件通知处理程序代码 typedef BOOL (WINAPI * UninstallHook)(); UninstallHook uHook; uHook = (UninstallHook)GetProcAddress(hmod,"_UninstallHook@0"); if(uHook != NULL){ (*uHook)(); OnCancel(); }else{ AfxMessageBox(L"无法找到卸载函数",MB_OK,NULL); OnCancel(); } }
而DLL才是最重要的东西。这里选择注入点为播放吃子音频的那个地方:
004119ED |. /75 0F JNZ SHORT JunQiRpg.004119FE
004119EF |. |68 ACCE4500 PUSH JunQiRpg.0045CEAC ; res\eat.wav
004119F4 |> |B9 00BC4900 MOV ECX,JunQiRpg.0049BC00
004119F9 |. |E8 C22B0100 CALL JunQiRpg.004245C0
004119FE |> \F605 C59A4900>TEST BYTE PTR DS:[499AC5],40
选择004119EF |. |68 ACCE4500 PUSH JunQiRpg.0045CEAC ; res\eat.wav这句话作为注入口。
注入的方法一般选用call或jmp,这两个语句加上后面的数据都是5个字节,而四国军旗的这个地方占用5个字节的好几句话,这样操作起来更加方便。如果这附近没有5个自己的代码,那自己写的注入的汇编代码就要复杂一些。
首先,DLL需要实现应用程序调用的InstallHook和UninstallHook这两个导出函数。
在.h文件定义
#ifndef JunqiRpgAPI
#define JunqiRpgAPI extern “C” __declspec(dllimport)
#endif
JunqiRpgAPI BOOL WINAPI InstallHook(DWORD dwProcessID);
JunqiRpgAPI BOOL WINAPI UninstallHook();
在.cpp文件定义
#define JunqiRpgAPI extern “C” __declspec(dllexport) //这句话一定要在文件头之前
#include “JunqiRpgExt.h”
//定义变量和回调函数
HHOOK g_hhook;
HANDLE hProcess;
bool enable = false;
//键盘事件回调(这就是钩子)
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
// CJunqiRpgExtApp 初始化
BOOL CJunqiRpgExtApp::InitInstance()
{
CWinApp::InitInstance();
DWORD pid = GetCurrentProcessId();
hProcess = ::OpenProcess(PROCESS_ALL_ACCESS,FALSE,pid);
return TRUE;
}
//导出函数入口点
JunqiRpgAPI BOOL WINAPI InstallHook(DWORD dwProcessID){
if (g_hhook == NULL) {
g_hhook = ::SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, theApp.m_hInstance, dwProcessID);
if (g_hhook != NULL)
return TRUE;
}
return FALSE;
}
//键盘事件,按F12键打开或关闭记牌器
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
LPARAM bKeyUp = lParam & (1 << 31);
if (bKeyUp && wParam == VK_F12 && nCode == HC_ACTION) {
if(!enable){
enable = true;
AfxMessageBox(L”记牌器被打开!”,MB_OK,NULL);
JunqiRpgCall call;
call.RegisterCall();
}else{
AfxMessageBox(L”记牌器关闭!”,MB_OK,NULL);
JunqiRpgCall call;
call.UnRegisterCall();
}
}
return ::CallNextHookEx(g_hhook, nCode, wParam ,lParam);
}
//导出函数入口点
JunqiRpgAPI BOOL WINAPI UninstallHook(){
return ::UnhookWindowsHookEx(g_hhook);
}
现在就要实现JunqiRpgCall 这个类了,这个类的RegisterCall负责注入HOOK API而UnRegisterCall负责还原。下面是吃子部分的实现。
//吃子发生时,我们要做的事情
void __stdcall InsideEatAction(){
AfxMessageBox(L”吃子!”,MB_OK,NULL);
}
//设定地址访问权限
BOOL JunqiRpgCall::InsideTo(LPCVOID lpAddress)
{
MEMORY_BASIC_INFORMATION mInfo;
MEMORY_BASIC_INFORMATION *lpmInfo = &mInfo;
VirtualQuery(lpAddress, lpmInfo, sizeof(mInfo));
if(lpmInfo->Protect == PAGE_EXECUTE_READWRITE)
{
return TRUE;
}
else
{
DWORD dwOldProtect;
if( VirtualProtect(lpmInfo->BaseAddress, lpmInfo->RegionSize, PAGE_EXECUTE_READWRITE, &dwOldProtect) )
{
return TRUE;
}
else
{
return FALSE;
}
}
}
//游戏吃子注入点
/*
004119EF |. 68 ACCE4500 PUSH JunQiRpg.0045CEAC ; res\eat.wav //注入点
004119F4 |> B9 00BC4900 MOV ECX,JunQiRpg.0049BC00
004119F9 |. E8 C22B0100 CALL JunQiRpg.004245C0
*/
__declspec(naked) BYTE* __stdcall InsideEat(){
__asm{
pushad
call InsideEatAction //调用我们自己的方法
popad
PUSH 0x0045CEAC //还原堆栈
MOV ECX,0x004119F4
jmp ECX; //跳转到下一步执行
}
}
/**********************************************************
注入吃子CALL
**********************************************************/
void JunqiRpgCall::InsideEatCall(void)
{
if(InsideTo((LPCVOID)0x004119F4)){ //这个参数的地址不重要,重要的是这个地址的数组是5字节的
*(BYTE*)0x004119EF = 0xE9; // E9为jmp长跳的机器码
*(DWORD*)0x004119F0 = (DWORD)InsideEat – 0x004119EF – 5; //计算InsideEat的便宜地址
}
}
/**********************************************************
还原数据
**********************************************************/
void JunqiRpgCall::UnInsideEatCall(void)
{
if(InsideTo((LPCVOID)0x004119EF)){
*(BYTE*)0x004119EF = 0x68; // 68为push的机器码
*(DWORD*)0x004119F0 = 0x45CEAC; //0x45CEAC就是原先的值,被压入堆栈后会变成ACCE4500
}
}
这里的还原数据段一定要有,要不然,当游戏没退出,而外挂退出了,那么就会报错。原因就是游戏代码被更改了,会跳转到DLL的某个函数指针上,而现在DLL退出,这个函数也就没了,就会报内存读写错误。
这样以后,测试(一定要按F12启动,这是钩子注入的毛病),发现在有吃子动作时,就会弹出吃子对话框,而这个对话框出现次数太多,对游戏有很大干扰,并且子也没了。这表明,这地方不能用对话框来中断掉,有一个状态没有更改造成了这个问题。解决方法一般就是在附近找到只调用一次的地方再次注入代码就行了。