目前,网络游戏的外挂从程序角度主要分为辅助型的动作外挂和内核型的协议型外挂。动作外挂主要帮助玩家进行一些重复性的劳动量,网络上有许多介绍这方面的程序,按键精灵就是一个很好的例子。协议型外挂则给人一个很神秘的感觉,这方面在网络游戏上最多的恐怕是传奇的脱机外挂,这也是因为传奇最受欢迎而已。
在具体的讲解前,我很想扯点别的事情,毕竟学院不是我的风格,我更喜欢自由一点的。既然我们主要是针对协议型外挂的制作教程,就先扯扯网络游戏的流程和一些乱其八糟的事情,如果有冒犯你们的地方本人则不胜感激。如果你感到烦的话可以直接跳到你感兴趣的地方。
我玩网游的历史很短,刚开始玩得是魔力宝贝,总是在免费的时候玩,在一个服务器里面呆的最长的就是在四川卧龙了,现在服务器大概早就该名或者并组了吧,在里面我的名字叫Bluerose,名字前面带的是我的职业,小号一大堆,不过名字都是一样的。用这个名字是因为朋友说我很忧郁,但却总抱着希望,所以就用这个名字了。后来玩的是大话西游II,是朋友拉着玩的,现在虽然上了班,但偶尔还玩,里面的名字叫星泪,用的是女性玩家,因为这个名字太女性化了。在刚开始玩得时候,我觉得自己好孤独(那时候在大学虽然旁边都是同学朋友,但内心仍然孤独),朋友拉我玩大话的时候,那天晚上星光倒不错,洒下的枪馊梦蚁肫鹆搜劾岬母芯酰??跃徒卸拦滦抢崃耍?还?芯跽飧雒?痔?懵叮?徒?拦露?秩サ袅恕?
由于玩大话西游II的历史比较长,而且对大话西游的游戏也比较熟悉,所以这次的教程就用大话西游II做为目标了,但教程尽可能考虑通用性。不过提前声明,本人对网络游戏并不熟悉,因此请勿和本人谈论网络游戏的前因后果和发展以及网游外挂对网络游戏的冲击等乱其八糟的事情。我对网络编程也是一知半解,因此我尽可能的避免含有网络的代码,同时,由于协议型的外挂需要发送数据,但可以通过其它的办法来进行(比如通过网游客户端来代理)。
先说说服务器和客户端的通讯,由于服务器和本地的客户端不在同一个地理位置,相距比较遥远,因此数据的传输就需要一定的时间,这就决定了在网游中数据只能进行采样的收集处理而不能进行真正实时的数据处理。举例来说,局域网C-S(反恐精英)的数据处理,当你移动的时候,必须告知别的电脑玩家自己的移动,这个数据的传输由于在局域网内部,数据的传输比较快,量也比较小,电脑可以进行快速的采样和数据处理来进行判断是否打中或者移动是否违反规则(比如凌空徐步)等等,但在网络上进行玩的时候,对数据的采样就不像本地局域网那么快了,以大话西游II(以后简称大话吧,少打几个字)为例,在移动的时候,并不是将每一步的数据一个一个传送给服务器,而是将本次移动规则打成数据包提交给服务器,让后客户端开始播放动画,当服务器处理完数据之后,就会将位置回传给客户端,客户端以这个位置数据为基点,进行人物的下次移动,这个数据的采集需要隔一段时间来能进行一次采集,相对于CS来说,这个采集密度要比CS采集密度小。
当服务器和客户端进行通讯的时候,数据包是至关重要的。数据包中数据的规则则是协议型外挂最重要的基础之一。由于网络数据可以进行拦截,为了防止数据被修改,数据包中的数据都是加密进行,至于如何加密,这由服务器和客户端通过一定的算法来执行。因此,服务器和客户端的通讯大概就是下面这个样子:
客户端进行数据的采集===〉数据打包==〉数据加密==〉发送数据到服务器==〉服务器进行数据解密====〉服务器处理数据包==(处理完毕回传数据)==〉回传数据打包==〉回传数据加密==〉数据回传==〉客户端接收数据==〉客户端解密数据==〉客户端数据处理
大概的流程就是上面的,本来想用图的,由于我前两天玩内存不小心弄坏了一根内存条(大概内存条拔插的时候颗粒随坏了吧),现在电脑128MB的内存,运行一个Word 2003都不是非常的快了,因此就放弃了在Photoshop中画图了,抱歉抱歉。
我们的目标就是数据包,即拦截游戏通讯间的数据来进行相应的修改或者进行发送伪数据包。我大概定了一个计划,不过这一节肯定不能全部讲完,我只能在下班之后写上一点,时间有限,能写多少就写多少吧。
目标程序:大话西游II客户端。(你手上有服务器端吗?有的话我也想要^_^)
目标:数据包
目的:数据包拦截,修改,伪发送
编程软件:这个无所谓吧,不过我这里用的是C++ Builder 6,前段时间做CB 6相关的项目,而且CB 对于程序界面的编写是最方便不过的了,就是编译的有点慢和生成的程序有点大。(旁白:又不是做手机项目,担心容量吗?)我做受限程序做惯了,养成了不良习惯,没办法了。
思路:我们的程序要干扰别的程序的运行,最好的办法是使用debug的办法,不过,我并没有打算使用debug的办法,我对程序的debug并不太熟悉,而且讨厌编写没用的代码。我准备采用线程注入的办法,至于线程注入,和为什么要线程注入才能干预,这方面的知识最好自己看看《Windows 核心编程》里面讲的,否则这个教程要没完没了了。当我们的线程注入到目标程序之后就方便多了,就可以为所欲为了。因此我们的第一目的是将线程注入到目标程序中。
预备活动:
线程注入最简单的莫过于hook了,如果连这都不知道的话,最好赶快到网上查查或者翻翻《Windows核心编程》。为了防止游戏内部存在反hook的存在和外挂的检测,我将用自己的程序来启动目标程序。由于网游的不定期更新,因此在启动程序的时候最好将升级跳过去,至少在大话这样的程序中我是这样做的,因为频繁升级和版本检测总让我等的时间太长。
下面来进行具体的做法,我尽可能的弄出详细的步骤,如果你用的是VC或者其它的话,只要注意核心的代码就可以了。
新建一个工程,在窗体上添加两个按钮(TButton或者其它类型的按钮),一个将标题改为启动游戏,另一个标题改为启动外挂。再添加一个TOpenDialog。对于默认的窗体那么大的界面有点浪费,因此将窗体弄得小点,别大大的怪吓人的。
双点启动游戏的按钮就可以进行编写该按钮的事件了,默认的是OnClick事件。下面就是事件的代码:
if(FileExists(ExtractFileDir(Application->ExeName)+"//path.ini")==FALSE)
{ /*我将目标程序的路径保存到了当前程序目录中的path.ini文件中,但如果当前程序第一次运行的话,是不存在这个文件的,所以就可以用TOpdnDialog来打开了,做这点只是为了方便,不用每次都得点目标程序*/
if(OpenFile->Execute())
{
AnsiString AppPath="path="+ExtractFilePath(OpenFile->FileName);
WritePrivateProfileSection("XY2PATH",AppPath.c_str(),(ExtractFileDir(Application->ExeName)+"//path.ini").c_str());//蒋目标程序的路径存到path.ini文件中。
}else
{
return;
}
}
//下面的代码开始启动目标程序
PROCESS_INFORMATION pi;
STARTUPINFO si;
si.cb=sizeof(si);
si.lpReserved=NULL;
si.lpDesktop=NULL;
si.lpTitle=NULL;
si.cbReserved2=0;
si.lpReserved2=NULL;
si.dwFlags=STARTF_USEPOSITION;
si.dwX=0;
si.dwY=0;
char Appname[300];
GetPrivateProfileString("XY2PATH","path","",Appname,250,(ExtractFileDir(Application->ExeName)+"//path.ini").c_str());
strcat(Appname,"//xy2.exe");
/*以上都在构建目标程序的环境设置,下面调用CreateProcess来启动目标程序,注意将倒数第3个参数要填为目标程序的路径,第6个参数为CREATE_SUSPENDED是为了将程序加载到内存中之后可以进行一些修改,以更好的配合外挂程序的运行*/
if(CreateProcess(Appname,NULL,NULL,NULL,FALSE,CREATE_SUSPENDED,NULL,ExtractFileDir(Appname).c_str(),&si,&pi)==0)
{
//启动目标程序失败
ShowMessage("error open exe file");
return;
}
gamehandle=pi.hProcess;
/*在本节中要执行程序的话,最好将这个条件注释掉,我将在以后的教程中进行讲解,这里大概说一下功能,第一个Write是为了跳过Update,第二个是为了退出的时候不打开网页,我的电脑要是退出大话的时候打开网页的话,中间的时间可以抽上几根烟了,所以将程序改了*/
if(WriteProcessMemory(gamehandle,(void*)0x0042BC13,No_Update,1,NULL)==false
||WriteProcessMemory(gamehandle,(void*)0x00430a80,No_HTML,2,NULL)==false
)
return;
threadhand=pi.hThread;
gamethreadid=pi.dwThreadId;
//恢复程序,让程序执行
ResumeThread(pi.hThread);
/*下面的代码也是本节中不需要的,我将物品的有关信息存到了当前目录(外挂启动程序目录)中的item.ini文件中,但目标程序中并不知道外挂启动程序的路径,因此我在目标程序文件夹中建立了一个名字叫path.ini文件,里面包含了item.ini的路径*/
String inipath= "path="+ExtractFileDir(Application->ExeName)+"//item.ini";
WritePrivateProfileSection("ITEM",inipath.c_str(),(ExtractFileDir(OpenFile->FileName)+"//path.ini").c_str());
启动程序中将启动属性设置为CREATE_SUSPENDED属性是为了考虑到程序的通用性和稳定性,在该函数之后,如果目标程序中存在有必要修改的代码的话,可以在这里进行修改,也可以对目标程序进行反反外挂的处理。其实,debug形式的外挂就可以在这里进行debug环境的建立,以及在目标程序中插入Int 3指令来进行拦截处理了(我怎么越来越感觉到自己在写调试器的教程??)。
这节就讲到这里,如果再晚的话,我就没办法赶上公交车了,然后还得走回家,天哪,这么冷的天~~~~赶紧上传回家吧。CSDN上的Blog不知道怎么贴附件,我还得考虑附件放到什么地方,因为有些文件没办法贴出来了。不过gameres上可以贴上300K的附件,希望能够用的。
上一次我们说了目标程序的启动,以及对目标程序的预处理。这一节中争取可以将外挂的窗口显出来,具体能不能说这么多,只能看着办了。
因为我决定采用最俗的办法Hook来注入线程(有时候我都觉得自己是否有必要这么做,因为Debug的办法也不错),为了程序的更普遍性和更快的移植,以及简单一点,我决定还是采用Hook。这里提前说一下,如果不懂汇编和程序调试的话,最好先补一下课,这在以后要用的。
我们先来编写Hook.dll部分,程序的启动部分暂时不用理会了(我以后就将那部分叫做wg.exe吧),昨天着急忘了说一声了,我让屏蔽的WriteProcessMemory中的数据地址是大话9.16更新之前的最后一个版本,在9.16更新之后的版本中需要先将程序脱壳,这部分我在以后会说的,所以让大家屏蔽掉那两个写内存的操作。
对于Hook.dll来说,我们准备使用F12键来激活外挂,在CB中编写dll非常简单,建立一个dll项目工程,然后就可以添加代码了。建立工程的时候一点记得选上使用C++,使用VCL,Multi Thread这几个选项,理由:
1、使用C++是为了让我省点口水(我将APIHOOK封装到了一个类里面)。
2、使用VCL是因为我太懒惰,不想编写界面代码。
3、使用多线程是因为程序必须。
由于外挂主窗体在dll中,因此生成的dll就会比一般的dll大。窗体其实可以放到任何地方的,只是放到dll中比较方便而以,而且在说的时候可以更好的分开。
以下是Hook.cpp的代码:
//---------------------------------------------------------------------------
#include <vcl.h>
#include <windows.h>
#include "hookapi.h"
#include "hookform.h"
#pragma hdrstop
#pragma argsused
HHOOK g_hHook = NULL;//Hook的句柄
HINSTANCE DllHinst = NULL; //Dll的句柄
HWND gamehWnd; //游戏句柄
HANDLE hThread = NULL; //线程句柄
HWND wghandle = NULL; //外挂窗口句柄
HANDLE gamehandle; //游戏窗口句柄,忘了有没有用
HINSTANCE gameInstance; //游戏的,也不知道用了没用
DWORD ThreadID; //线程ID
LRESULT CALLBACK KeyBoardHook(int nCode, WPARAM wParam, LPARAM lParam);//键盘Hook
extern "C" __declspec(dllexport)bool EnableHook(DWORD dwThreadId);//启动Hook的函数
extern "C" __declspec(dllexport)bool DisableHook();//卸载Hook的函数,和上面的函数一样都是为了外部可以控制
DWORD WINAPI Thread1(PVOID param);//线程函数,在该函数中,将启动外挂窗口
int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved)
{
DllHinst = hinst;//载入Dll
return 1;
}
extern "C" __declspec(dllexport)bool EnableHook(DWORD dwThreadId)
// 导出函数EnableHook()
{
if (g_hHook == NULL)
// 安装新钩子
{
g_hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyBoardHook, DllHinst,
dwThreadId);/*记得CreateProcess中的参数吗?我们传进的参数是目标程序的主线程ID,表示我们启动的是线程Hook,而不是全局Hook,这样不会对其他程序产生任何影响*/
}
if (g_hHook)
{
return true;
}
return false;
}
extern "C" __declspec(dllexport)bool DisableHook() // 导出函数DisableHook()
{
/*卸载Hook,现在暂时先这样了,其实在真实的情况下如果要做的完美的话,需要做许多事情,如果直接关闭客户端的话,这样就足够了,这个函数其实并没有任何的用处,这里仅仅是为了说明外部可以主动控制外挂的启动和关闭而已*/
if (g_hHook != NULL)
{
UnhookWindowsHookEx(g_hHook);
g_hHook = NULL; // 卸掉新钩子
return true;
}
return false;
}
LRESULT CALLBACK KeyBoardHook(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
if (wParam == 123)//123为F12的键码,可以查看MSDN或者Windows API参考方面的书找到,自己写个小程序测试也可以
{
if (hThread == NULL)//这里确保线程启动一次,而不是多次,每一次的启动都回引入一个外挂窗口
{
hThread = CreateThread(NULL, 0, Thread1, NULL, NULL, &ThreadID);//启动线程,该线程很快执行完毕
}
}
}
return (CallNextHookEx(g_hHook, nCode, wParam, lParam));//剩下的让目标程序去处理
}
Dll中的函数是外挂程序的核心。在线程启动成功之后,就可以卸载Hook了,这里只是为了简便,所以将Hook仍然保留。
在CB中编程的时候,最好将程序的文件名保存成你想要的名字,别用默认的名字,默认的名字是Unit+数字组成,而不是类名之类的,这是我不喜欢CB的一个原因,另外一个原因是没有全屏幕专家界面,编写代码的时候其他的太碍事,第三个原因是可以在任意的地方写代码,我的代码又没有具体的风格,经常造成莫名其妙的错误。喜欢他的原因是因为用它开发东西太快了,而且方便,比在VC中默写代码方便多了。
废话说完,可以添加外挂窗口的视图,在New菜单中选择New Form,如果你想New其他的话我不反对,能不能得到正确的结果我就不知道了。
网上有不少人问怎么在游戏中弹出外挂窗口,我已经回答的有点不耐烦了。在CB中做是最方便的事情了,但得设置好控件的属性,因为我对使用VC来编写界面不熟悉,更多的时候我都是直接使用API来编写的(我没有学习MFC的打算),所以对于喜欢VC的朋友们只能说抱歉了。
对于新窗体的属性设置是最重要的,要不然莫名其妙的错误和结果让人变得神经。下面是我对dll中窗体属性的一些总结,如果你有其他不懂的地方,可以给我E-Mail或者MSN或者QQ。
1、 Visible属性一定要为false,否则窗体没办法移动
2、 FormStyle属性最好为fsNormal,但一定不要是fsMDIChild或者fsMDIForm,这两个会引起莫名其妙的错误。
3、 如果刚开始弹不出来的话,将BorderStyle属性改为bsDialog。我在后面的教程中由于要重载TForm的函数,因此这里是不是必须为bsDialog不太清楚了,毕竟是很早以前的代码了。
其他的注意点好像没有了。下面是CreateThread调用中的Thread1函数实现:
DWORD WINAPI Thread1(PVOID param)
{
TwgHookForm* wgHookForm;
wgHookForm = new TwgHookForm(NULL);
wghandle = wgHookForm->Handle;
/*暂时将下面的发送消息屏蔽掉,我在窗体创建之后需要窗体做一部分必要动作,所以采用发送消息的机制来了,其实并不是必需这么做的,由许刚开始编写的时候,APIHOOK中的内容都是通过Message的方式来做的,这里为了方便就通过Message来弄了,算是点历史原因吧*/
SendMessage(wghandle, WM_USER + 2, NULL, NULL);
wgHookForm->ShowModal();
delete wgHookForm;
return 1;
}
到现在为止应该可以在游戏中弹出外挂窗口了,我们的第一步也算完成了,外挂程序的平台到现在为止搭建完了,剩下的就是工具的制作和必要代码的编写。在下一节中我准备说一下APIHOOK的方法。
上一节中我们说了外挂平台的搭建,我们做完了Hook.dll和wg.exe,但如何让wg.exe调用Hook.dll中的函数,可以自己查看CB的教程或者到网上查查。在这里我是在wg.exe的Hook按钮事件中添加了下面的语句来实现:
if(gamethreadid)
{
if(EnableHook(gamethreadid)==false)
ShowMessage("error");
}
你可以点击Hook按钮在游戏界面出来之后,然后在游戏界面中按F12键调出外挂的窗口。
昨天的尾巴完事之后,开始今天的教程。今天我想说说APIHOOK。虽然APIHOOK在大话游戏的外挂制作中不是必须的,但为了按照一般的制作流程顺序,就先将这部分加入到里面去了。
使用APIHOOK的原因也很简单,游戏肯定要调用某些系统函数,使用APIHOOK可以简单的查看一些关键的信息并进行修改(就这么简单的理由?是的,我们一向再用杀牛的刀宰鸡的。。。)。
Jeffrey Richter用了大量的篇幅来讲如何插入DLL和挂接API,如果你不知道Jeffrey Richter是谁的话,总该知道《Windows核心编程》的作者吧,如果不知道,我倒,系统抛出例外,你是外星人吧。我们的程序运行在用户层上,J。R提出了两种办法,一种是改写代码,我刚开始也试图用这种办法,后来发现这种办法确实存在的漏洞多多,和J。R说的一样。最后还是采用操作模块的输入节了。
在查看资料的过程中,我发现J。R的代码在中文Windows 2000上并不能运行(难道是外国人用的系统和中国的不一样?),后来只好J。R的思路,重新安排了一下函数,但大部分函数都一样的。为了方便,我没有在类中捕获LoadLibraryA、LoadLibraryW、LoadLibraryExA和LoadLibraryExW,也是因为我们的外挂程序运行的时候游戏的窗口已经出来了,该加载的一般都加载了。
下面是我的APIHOOK类的源代码,该源代码是根据J.R的思路重新整理他的源代码来的:
/*HookAPI.h*/
#include "windows.h"
class CAPIHOOK
{
public:
CAPIHOOK(PSTR pszCalleeModName,PSTR pszFuncName,PROC pfnHook,HANDLE prochandle,HMODULE hmod);
~CAPIHOOK();
operator PROC(){return (m_pfnOrig);};
public:
static PVOID sm_pvMaxAppAddr;
static CAPIHOOK* sm_pHead;
CAPIHOOK* m_pNext;
PCSTR m_pszCalleeModName;
PCSTR m_pszFuncName;
PROC m_pfnOrig;
PROC m_pfnHook;
BOOL m_fExcludeAPIHookMod;
HMODULE m_module;
HANDLE m_handle;
private:
pfnOrig,PROC pfnHook,BOOL fExcludeAPIHookMod);
void WINAPI ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,PROC pfnOrig,PROC pfnHook,HMODULE hmodcaller,HANDLE handle);
void WINAPI FixupNewlyLoadedModule(HMODULE hmod,DWORD dwFlags);
FARPROC WINAPI GetProcAddress(HMODULE hmod,PCSTR pszProcName);
};
/*HookApi.cpp*/
#include "hookapi.h"
#include <assert.h>
#include "imagehlp.h"
PVOID CAPIHOOK::sm_pvMaxAppAddr = NULL;
const BYTE cPushOpCode = 0x68;
CAPIHOOK *CAPIHOOK::sm_pHead = NULL;
CAPIHOOK::CAPIHOOK(PSTR pszCalleeModName, PSTR pszFuncName, PROC pfnHook,
HANDLE prochandle, HMODULE hmod)
{
m_handle = prochandle;
if (sm_pvMaxAppAddr == NULL)
{
SYSTEM_INFO si;
GetSystemInfo(&si);
sm_pvMaxAppAddr = si.lpMaximumApplicationAddress;
}
m_pNext = sm_pHead;
sm_pHead = this;
m_pszCalleeModName = pszCalleeModName;
m_pszFuncName = pszFuncName;
m_pfnHook = pfnHook;
m_pfnOrig = ::GetProcAddress(GetModuleHandleA(pszCalleeModName),
m_pszFuncName);
assert(m_pfnOrig != NULL);
if (m_pfnOrig == NULL)
{
return;
}
if (m_pfnOrig > sm_pvMaxAppAddr)
{
PBYTE pb = (PBYTE)m_pfnOrig;
if (pb[0] == cPushOpCode)
{
PVOID pv = *(PVOID*) &pb[1];
m_pfnOrig = (PROC)pv;
}
}
m_module = GetModuleHandle(pszCalleeModName);
ReplaceIATEntryInOneMod(m_pszCalleeModName, m_pfnOrig, m_pfnHook, m_module,
prochandle);
}
CAPIHOOK::~CAPIHOOK()
{
ReplaceIATEntryInOneMod(m_pszCalleeModName, m_pfnHook, m_pfnOrig, m_module,
m_handle);
CAPIHOOK *p = sm_pHead;
if (p == this)
{
sm_pHead = p->m_pNext;
}
else
{
BOOL fFound = FALSE;
for (; !fFound && (p->m_pNext != NULL); p = p->m_pNext)
{
if (p->m_pNext == this)
{
p->m_pNext = p->m_pNext->m_pNext;
break;
}
}
assert(fFound);
}
}
void WINAPI CAPIHOOK::FixupNewlyLoadedModule(HMODULE hmod, DWORD dwFlags)
{
if ((hmod != NULL) && ((dwFlags &LOAD_LIBRARY_AS_DATAFILE) == 0))
{
for (CAPIHOOK *p = sm_pHead; p != NULL; p = p->m_pNext)
{
ReplaceIATEntryInOneMod(p->m_pszCalleeModName, p->m_pfnOrig, p->m_pfnHook,
hmod, m_handle);
}
}
}
FARPROC WINAPI CAPIHOOK::GetProcAddress(HMODULE hmod, PCSTR pszProcName)
{
FARPROC pfn = ::GetProcAddress(hmod, pszProcName);
CAPIHOOK *p = sm_pHead;
for (; (pfn != NULL) && (p != NULL); p = p->m_pNext)
{
if (pfn == p->m_pfnOrig)
{
pfn = p->m_pfnHook;
break;
}
}
return (pfn);
}
void WINAPI CAPIHOOK::ReplaceIATEntryInOneMod(PCSTR pszCalleeModName, PROC
pfnCurrent, PROC pfnHook, HMODULE hmodcaller, HANDLE handle)
{
ULONG ulSize;
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)
ImageDirectoryEntryToData(hmodcaller, TRUE, IMAGE_DIRECTORY_ENTRY_IMPORT,
&ulSize);
if (pImportDesc == NULL)
{
return ;
}
for (; pImportDesc->Name; pImportDesc++)
{
PSTR pszModName = (PSTR)((PBYTE)hmodcaller + pImportDesc->Name);
if (lstrcmpiA(pszModName, pszCalleeModName) == 0)
{
break;
}
}
if (pImportDesc->Name == 0)
{
return ;
}
PIMAGE_THUNK_DATA pThunk = (PIMAGE_THUNK_DATA)((PBYTE)hmodcaller +
pImportDesc->FirstThunk);
for (; pThunk->u1.Function; pThunk++)
{
PROC *ppfn = (PROC*) &pThunk->u1.Function;
BOOL fFound = (*ppfn == pfnCurrent);
if (!fFound && (*ppfn > sm_pvMaxAppAddr))
{
PBYTE pbInFunc = (PBYTE) *ppfn;
if (pbInFunc[0] == cPushOpCode)
{
ppfn = (PROC*) &pbInFunc[1];
fFound = (*ppfn == pfnCurrent);
}
}
if (fFound)
{
HANDLE handle1 = OpenProcess(PROCESS_ALL_ACCESS, FALSE,
GetCurrentProcessId());
DWORD dwIdOld;
VirtualProtectEx(handle1, ppfn, sizeof(pfnHook), PAGE_READWRITE, &dwIdOld)
;
if (WriteProcessMemory(handle1, ppfn, &pfnHook, sizeof(pfnHook), NULL) ==
false)
{
return ;
}
else
{
VirtualProtectEx(handle1, ppfn, sizeof(pfnHook), dwIdOld, &dwIdOld);
return ;
}
}
}
}
上面是APIHOOK的完整代码。下面是使用的例子(拦截WString2ID函数):
typedef unsigned long(__stdcall *WString2ID)(char const*);
unsigned long __stdcall myWString2ID(char const*);
CAPIHOOK *My_WString2ID;
My_WString2ID = new CAPIHOOK("windsoul.dll", "?WString2ID@@YGKPBD@Z",
(PROC)myWString2ID, gamehandle, gameInstance);
自己的myWString2ID的实现:
unsigned long __stdcall myWString2ID(char const *a)
{
// SendMessage(wghandle,WM_USER+1,(WPARAM)a,NULL);
return (((WString2ID)My_WString2ID->m_pfnOrig)(a));
}
下面是用来拦截游戏的WndProc函数的,当时写的时候为了全面,至于如何去用,随便自己了,反正我没有用。
gamehWnd = GetActiveWindow();
gamehandle =GetCurrentProcess();
gameInstance = (HINSTANCE)GetWindowLong(gamehWnd, GWL_HINSTANCE);
gameproc = (WNDPROC)SetWindowLong(gamehWnd, GWL_WNDPROC, (LONG)
MyMsgProc);
自己用来替换游戏的WndProc函数:
LRESULT APIENTRY MyMsgProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM
lParam)
{
/*在这里做自己想做的事情,剩下的让游戏的WndProc来处理*/
return CallWindowProc(gameproc, hwnd, message, wParam, lParam);
}
这一节到这里就结束了,下一节开始游戏程序的研究。最好准备大话客户端9.16更新之前的最后一个版本,不使用最新的版本有下面的原因:
1、 如果对现在客户端作过多的透漏的话,将会发现做盗号类的程序比做外挂要简单,这不是我所希望看到的。
2、 新版本采用的加密办法(双精度浮点数加密)在讲解上非常的麻烦,不是一般人容易入门的,但解决的办法和9.16之前的版本一样,只是繁琐而以。
3、 脱壳后的程序有更多的需要人工识别的部分,这会造成不必要的麻烦,免得误导大家。
上一次我们说了外挂中APIHOOK和基本平台的搭建,但仅仅只用APIHOOK在大多数的时候并不能达到目的,没有哪个公司会将网络打包加密解密解包的程序放在dll中以函数的形式就可以查看。所以这节来讲解为目标程序打内存补丁来修改程序的执行路径,通过这种办法让目标程序成为我们的奴隶。
前两天由于项目的关系,耽搁了两天教程,对不起大家。今天写教程时才发现,我当时研究程序文件用的是ida 4.17的版本,前段时间刚刚换成了4.7版本。虽然现在的版本比以前更好用,但版本的不兼容却让我头痛,最后没办法又找了个4.17的版本来用。这节中还需要使用的另外一个工具是W32dasm,W32dasm的快捷和为程序打补丁的方便是我最喜欢他的原因,ida 4.7是否支持和W32dasm那样的功能我还不知道,毕竟我刚刚装上,还没怎么用过呢。
废话就不说了,为程序打内存补丁和做游戏修改器差的不太多,只是游戏修改器修改的是数据,而内存补丁修改的是指令,在做之前,请一定要做好祈祷,虽然我会尽可能的将我所知道的注意点说出来,但仍然不敢保证你是否会出现非法指令、内存越界等等,如果出现蓝屏一定要买彩票阿,反正我是没有遇到过。
关于如何做内存补丁,在看雪出的那本《加密与解密》上有详细的讨论,这里简单说一下注意点:
1、 尽可能的调用程序本身的函数,除非没有办法再自己加载函数。
2、 在调用函数之前,一定要将相关函数的寄存器内容保存,在调用结束后,要将其内容恢复。
3、 注意堆栈平衡。
4、 由于我们要给源程序添加代码,因此尽可能的找一块空余的不会被修改内存(内存页4K对齐,这样的内存应该不难找),如果运气差的话,只能自己来申请内存了。
对于xy2.exe的程序来说,程序里面使用了SendMessage函数,这个函数使用方便,居有很大的广泛性就不用说了。因此我们准备使用该函数来将相关内容发送给外挂程序。不过在做这部分之前,我准备先给程序打补丁来防止目标程序升级来练练手。
作外挂最主要的工作是对目标程序的分析程度,分析程序主要用的是汇编的知识,有时候我想将汇编说成是调试程序更确切些,因为我大部分时间都是在用汇编调试,只有很少一部分时间来用汇编写程序。
将xy2.exe的程序用ida反编译,这里用的xy2.exe的版本是9.16之前的版本,从8.12之后到9.16之前xy2.exe的程序都没有做任何改动,因此这段时间的任意一个都是可以的。
移到地址.text:0042BB80处,这里就不给出代码了,代码太长了,足足有8个页面。这里大概就是WinMain函数,我不知道Ida4.7会将这里作为那个函数名,我用的是4.17,给的函数名是:sub_0_42BB80 proc near。通过研究该段代码,我们可以发现这段代码是函数的主程序,功能有下面的几点:
1、 检查更新,启动更新程序。
2、 查看是否播放录像文件,并将程序设置为相应的状态。
3、 设置程序运行的速度。
程序整理后的C代码我就不写了,根据ida给出的程序流程图很容易写出来的。
研究程序可以发现在地址.text:0042BC13处经过判断之后,就会检查大话程序的5个主版本的文件,通过调用下面的函数:
.text:0042BC15 0B8 call sub_0_4480E0
.text:0042BC1A 0B8 call sub_0_4481C0
.text:0042BC1F 0B8 call sub_0_4482A0
.text:0042BC24 0B8 call sub_0_448380
.text:0042BC29 0B8 call sub_0_448460
.text:0042BC2E 0B8 push ebx
.text:0042BC2F 0BC call sub_0_4488E0
.text:0042BC34 0BC add esp, 4
对于之前的这个判断,我想可能是播放录像用的。在接下来的5个call之后,调用sub_0_4488e0来调用升级用的对话框。要修改程序避免升级就简单多了,只用将这个跳转跳过去就行了。这里不用考虑堆栈的平衡,从ida给出的堆栈指针来看,跳转前后的堆栈是一样的。
因此在我们调用CreateProcess之后,就可以通过WriteProcessMemory来进行修改了,将跳转指令的前一个字节改为0xEB,后面的那个字节不用动了,后面的那个字节是用来控制跳转的距离的。这样原来的代码就由:
.text:0042BBE9 0B8 jz short loc_0_42BBF1
变为
.text:0042BBE9 0B8 jmp short loc_0_42BBF1
了。
如果你不愿意通过程序来调用,也可以直接修改xy2.exe文件来做到,不过建议将xy2.exe文件备份一个,方便以后升级用。
对于避免弹出主页,也可以通过相同的办法,但修改的不是一个跳转指令了。这里仅仅给出一点点提示,可以自己去尝试:
1、 xy2.exe通过调用ShellExecute函数来弹出主页,可以根据引用段来快速找到调用的办法。
2、 在修改的时候,注意堆栈的平衡点。
上面的都是对于9.16之前的版本,之前的版本没有给程序加壳,可以这么来做。但9.16之后的程序都是被加了壳的,这样做就不可以了。不过可以先将程序脱壳。大话的注程序使用的是PECompact的壳,脱这个壳有个最快速的办法,这也是我在郁闷了几小时之后才发现的。刚开始的时候我用peid版本太低,竟然不认识这个壳,老实的我只好手动脱壳。脱完之后在看雪的站上转转,看有什么新的点子没有,发现peid的V.92版本,比我的新多了,就下载下来看看,发现是PECompact2.X的壳,更重要的发现是这个版本的peid可以脱比较简单的壳了,就在插件里面躺着,随便一试,发现竟然可以直接脱掉,这更过程不用1分钟。脱壳之后就可以按照之前的办法弄了,不过目标程序改为了脱壳后的文件,最新版本的xy2.exe程序(10.30之前)的免升级跳转点在
.text:0042BA7B jnz short loc_42BAA8
。免弹出主页修改地址在:
.text:00444840 mov eax, [esp+8+var_8]
今天这节就讲解这些,并没有用到w32dasm,下一节主要讲解用于拦截数据的内存补丁,将会频繁使用w32dasm。
非常感谢热心网友alan将之前的教程整理成为VC的版本,如果对此版本有什么疑问的话,可以给他发E-mail:tyr_alan@hotmal.com。alan整理出来的源文件我会随着教程和我拿到的版本粘贴在www.gameres.com上,源文件的版本可能和教程的内容不符合^_^。
Alan的文件地址:http://blog.gameres.com/upload/sf_20041030104834.rar
第一节到第三节我们说了基本工具的准备,第四节使用免升级和免弹出主页来做了一下基本的练习。第四节中和普通的游戏修改器没有太大的区别,只是一个修改的是数据,一个修改的是代码。这一节中我们将通过在dll中修改大话的代码来进行外挂的制作。其实在dll中动态修改代码和上一节用的方法一样,只是要改写的东西更多了而已。
原理和上一节中的函数一样,都是调用WriteProcessMemory。
这一节的任务是拦截接收到的数据,关于发送的数据可以进行相类似的处理。一般在分析网络游戏的时候,都是先分析接收到的数据。对于大话的程序,中间如何去分析的过程就不说了,这要看自己的调适能力了。不过对于9.16之前的大话客户端程序,里面含有大量的调试信息(也可能是脚本信息),大致分析程序可以发现,程序总是在打印调试信息之后,然后做实际的工作。其中对于"rx_decode”这个字段很感兴趣,看看调用的地方:
.text:00449D4F 154 push offset aRx_decode ; "rx_decode"
在前方不久的地方就是网络函数recv,因此可以这样来理解,程序接收到数据之后,打印出调试信息,然后跳转到:
.text:00449D95 154 push ebp
的地方继续执行,通过不断的跟踪发现,大部分时候程序都执行到地址:
.text:00449DED 154 mov [eax], edi
而且,[edi]中的内容在相同的时刻几乎是相似的,通过在游戏中随机的打开中断,将[edi]中的内容dump出来,然后组成ASCII码便可以发现,里面的内容相对来说是不变的,如果你运气好刚好可以拦截到聊天数据的话,就会发现里面的内容就是聊天的内容。这有点像碰运气。不过,如果采用下面的方法的话,就可以不用碰运气了。首先,我们发现edi是一个数据的地址,ebp中是我们接收到的数据的长度。当对其中的内容感到怀疑的时候,我们就想将该语句执行的时候[edi]中的内容dump出来,dump的长度就是ebp中的值。因此我们通过w32dasm来制作内存补丁,使用W32dasm反编译程序之后,使用快捷键Ctrl+L可以将程序加载到内存中,不让程序执行,快捷键Ctrl+F12跳转代码窗口的地址到00449DED 一行,方便恢复代码的时候用。在调试窗口中按Ctrl+F12将当前代码位置跳转到00449DED。
我们将在这里进行内存补丁的编写。点击Patch Code按钮就可以直接写内存代码了。在00449DED的位置的补丁如下:
:00449DED E90ECA0400 jmp 00496800
00496800地址的内容是一段空闲得内存。在ida中可以看到程序中没有任何地方使用这块内存,我们将在这里进行程序的修改。当程序执行到00449DED的时候,就会跳转到00496800接着执行,因此,我们还必须修改00496800处的代码,使用Ctrl+F12跳转到00496800处,开始打补丁:
:00496800 50 push eax;保存各寄存器的值
:00496801 53 push ebx
:00496802 51 push ecx
:00496803 52 push edx
:00496804 55 push ebp;ebp为这次接收到的数据长度
:00496805 57 push edi;edi为数据地址
:00496806 6804040000 push 00000404;向外挂程序发送拦截消息ID
:0049680B A1D0664900 mov eax, dword ptr [004966D0];[004966d0]中包含的是外挂窗口的窗口句柄
:00496810 50 push eax
:00496811 3EFF1574924700 call dword ptr ds:[00479274];ds:[00479274]为SendMessage的函数地址,调用SendMessage函数向外挂发送命令
:00496818 5A pop edx;恢复各寄存器
:00496819 59 pop ecx
:0049681A 5B pop ebx
:0049681B 58 pop eax
:0049681C 8938 mov dword ptr [eax], edi;调用原来的操作,因为我们打补丁的时候跳过了部分操作,因此在这里进行原来的操作。
:0049681E 5F pop edi
:0049681F 5E pop esi
:00496820 5D pop ebp
:00496821 E9CC35FBFF jmp 00449DF2;跳回原来的地址之后接着执行
以上就是拦截补丁的完整代码。在使用的时候,必须先将大话程序的[004966d0]中填充上外挂的窗口句柄,要不然是没办法弄得。
使用W32dasm做补丁的时候好处在于我们看到的就是程序执行时用的虚拟地址,并且,W32dasm在给出汇编代码的同时给出了代码的16进制表示。补丁做完之后,剩下的就是如何将补丁程序放入到目标程序中了。
当外挂窗口创建之后,我们通过向外挂窗口发送WM_USER+2来命令外挂窗口执行修改大话程序的操作。
下面是具体的修改操作:
void TwgHookForm::ModifyXy2(TMessage Message)
{
DWORD dwIdOld1, dwIdOld2, dwIdOld3, dwIdOld4, dwIdOld5;
DWORD id=GetCurrentProcessId();
HANDLE handle1 = OpenProcess(PROCESS_ALL_ACCESS, FALSE,id);
if (wghandle)
{
Byte getrecv1[] =
{
0xE9, 0x0E, 0xCa, 0x04, 0x00
}; //5
Byte getrecv2[] =
{
0x50, 0x53, 0x51, 0x52, 0x55, 0x57, 0x68, 0x04, 0x04, 0x00,
0x00, 0xA1, 0xd0, 0x66, 0x49, 0x00, 0x50, 0x3E, 0xFF, 0x15,
0x74, 0x92, 0x47, 0x00, 0x5A, 0x59, 0x5B, 0x58, 0x89, 0x38,
0x5F, 0x5E, 0x5D, 0xE9, 0xcc, 0x35, 0xFB, 0xFF
}; //38
VirtualProtectEx(handle1, (void*)(0x004966d0), 4, PAGE_READWRITE,
&dwIdOld1);
if ((WriteProcessMemory(handle1, (void*)(0x004966d0), &wghandle, 4,
NULL)) == false)
{
ShowMessage("写句柄错误!");
return ;
}
VirtualProtectEx(handle1, (void*)(0x004966d0), 4, dwIdOld1, &dwIdOld1);
VirtualProtectEx(handle1, (void*)(0x00449DED), 5, PAGE_READWRITE,
&dwIdOld4);
if ((WriteProcessMemory(handle1, (void*)(0x00449DED), getrecv1, 5, NULL)
) == false)
{
ShowMessage("拦截接收修正补丁错误!");
}
VirtualProtectEx(handle1, (void*)(0x00449DED), 5, dwIdOld4, &dwIdOld4);
VirtualProtectEx(handle1, (void*)(0x00496800), 38, PAGE_READWRITE,
&dwIdOld5);
if ((WriteProcessMemory(handle1, (void*)(0x00496800), getrecv2, 38,
NULL)) == false)
{
ShowMessage("拦截接收补丁错误!");
}
VirtualProtectEx(handle1, (void*)(0x00496800), 38, dwIdOld5, &dwIdOld5);
}
}
在CB中使用自定义消息,需要在头文件中加入:
#define WM_USER_MODIF (WM_USER+2) //修改大话程序的消息
#define WM_USER_GETSEND (WM_USER+1) //拦截到发送数据接收到的消息
#define WM_USER_APIHOOK (WM_USER+5) //APIHOOK拦截到接收到的消息
#define WM_USER_GETRECV (WM_USER+4) //修改后的程序会向外挂窗口发送该消息。
在外挂窗口类里面(我这里是TwgHookForm),添加函数void ModifyXy2(TMessage Message);
void GetRecv(TMessage Message);
在protected:关键字下面添加消息映射声明:
BEGIN_MESSAGE_MAP
VCL_MESSAGE_HANDLER(WM_USER_MODIF, TMessage, ModifyXy2)
VCL_MESSAGE_HANDLER(WM_USER_APIHOOK, TMessage, HOOKAPITest)
VCL_MESSAGE_HANDLER(WM_USER_GETSEND, TMessage, GetSend)
VCL_MESSAGE_HANDLER(WM_USER_GETRECV, TMessage, GetRecv)
END_MESSAGE_MAP(TForm)
以上就是拦截接收数据的全部了。结合以前的代码,运行程序,可以发现,拦截到的数据为所有的已经解密了的游戏数据,至于数据的解析,就看自己的了,这里给出一个解析的代码框架:
#define CMDVOID(a) /
void CMDSAY##a(int cmd, int length, char* date)
#define RECVCASE(cmd,length,data) /
case cmd:/
CMDSAY##cmd(cmd,length,data);/
break
class中的声明:
CMDVOID(10);
CMDVOID(11);
CMDVOID(21);
在接收到的函数GetRecv中:
void TwgHookForm::GetRecv(TMessage Message)
{
if (Message.WParam == 1)
{
cmdrecvstate = RECVCMD;//如果接收到的是1个字符,则接收到的是命令
}
else if (Message.WParam == 2)
{
cmdrecvstate = RECVLENGTH;//如果接收到长度是2个字符,则为将要接收到的数据长度
}
else
{
cmdrecvstate = RECVDATA;//否则接收到的就是数据
}
static int cmd,datalength;
switch (cmdrecvstate)
{
case RECVCMD:
cmd = *((int*)(Message.LParam));
return;
case RECVLENGTH:
datalength = *((int*)(Message.LParam));
return;
case RECVDATA:
if (datalength == Message.WParam)
{
Cmdsay(cmd, datalength, (char*)Message.LParam);//如果数据包没有被拆分,则进行命令解释
}
else
{//否则,打印这个包现在内容
Memo1->Lines->Add("注意:这个数据不完整:");
AnsiString astemp1 = "[接受][";
astemp1.cat_sprintf("%x][", cmd);
astemp1.cat_sprintf("应收长度:%d实际长度:%d][",datalength,Message.WParam);
BYTE *temp = (BYTE*)Message.LParam;
for (int i = 0; i < Message.WParam; i++)
{
astemp1.cat_sprintf("%0.2x ", temp[i]);
}
astemp1.cat_sprintf("][");
for (int i = 0; i < Message.WParam; i++)
{
if(temp[i]>=0x20)
astemp1.cat_sprintf("%c", temp[i]);
else
astemp1.cat_sprintf(".");
}
astemp1.cat_sprintf("]");
Memo1->Lines->Add(astemp1);
}
return;
}
程序仅仅很简单的进行数据包的拦截和解析,对于被拆分的包不作处理,在真实应用中应该将这些包合并。
上面的接收规则对于9.16之前的大话程序有效,大话程序的一组数据会分成3次发送,首先接收到的是名字字,这个为1字节长度,接着是要接收的数据的长度,这个为2个字节,接下来就是数据了。如果真实接收到的数据和应该接收的数据长度不一样的话,表示这个包被拆分了。
对于9.16之后的程序,大话不一定按照这样的规则来进行,因此上面的只适应于9.16之前的程序。
下面是Cmdsay的函数实现:
void TwgHookForm::Cmdsay(int cmd, int length, char* date)
{
switch (cmd)
{
RECVCASE (10,length,date);
RECVCASE (11,length,date);
RECVCASE (21,length,date);
default://未被解析的命令
AnsiString astemp1 = "[接受][";
astemp1.cat_sprintf("%x][", cmd);
astemp1.cat_sprintf("%d][", length);
BYTE *temp = (BYTE*)date;
for (int i = 0; i < length; i++)
{
astemp1.cat_sprintf("%0.2x ", temp[i]);
}
astemp1.cat_sprintf("][");
for(int i=0;i<length;i++)
if(temp[i]>=0x20)
astemp1.cat_sprintf("%c",temp[i]);
else
astemp1.cat_sprintf(".");
astemp1.cat_sprintf("]");
Memo1->Lines->Add(astemp1);
break;
}
}
上面就是全部了,对于发送的命令拦截,和接收的类似,可以自己分析。在前几节教程中我已经将打补丁的注意点说了一遍,这里就不再说了。对于9.16之后的程序,我只做了很少的研究,主要是因为没有大量的时间和心情来做这些事情,这里仅仅给一点初步的提示:
1、 对于发送的数据,有些部分可能会采用二次加密的办法。
2、 对于接收的数据,程序为了增加调试的难度,将数据转换成浮点数之后不断的进行地址的变换和数据的转移。
3、 程序将接收和发送放入了一个线程中(这个可能,因为9.16之后比9.16之前多了一个线程)。
4、 数据采用浮点数存储,加密过程中有可能转换成浮点数,但加密完之后又会转换成浮点数,直到最后的时候才会转换成整数发送。
由于大话程序内部进行了非常大量的浮点和整数的转换,因此现在的大话程序是非常的耗费CPU资源的。以下面的代码来说:
Int I;
Float k=(float)I;//利用浮点数寄存器,这个用的时间很短
I=(int)k;//在编译器编译的时候,会用自己的浮点整型转换,而不是数学协处理器进行转换,这个转换大概比使用浮点寄存器转换慢12—60倍左右或者更多。使用ida4.7可以看到反编译之后调用了库函数_ftol。慢的原因是因为库函数是为数学计算进行的,而不是为了游戏中的效率而设计的。这是9.16之后大话变得卡的主要原因。
对于9.16之后的程序,也可以进行相同的处理,只是在调试的时侯对于代码地址的查找比较麻烦。如果不喜欢自己来写代码数组的话,可以自己根据PE文件来写个代码补丁工具。这个我就不讲了,事实上,我自己也没有写,时间不足是一方面,人懒是没办法的。
在前面APIHOOK一节(教程三)中,我的API类有点错误,这里更正一下:
在APIHOOK.h中
private:
pfnOrig,PROC pfnHook,BOOL fExcludeAPIHookMod);
void WINAPI ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,PROC pfnOrig,PROC pfnHook,HMODULE hmodcaller,HANDLE handle);
void WINAPI FixupNewlyLoadedModule(HMODULE hmod,DWORD dwFlags);
应该改为:
private:
//pfnOrig,PROC pfnHook,BOOL fExcludeAPIHookMod);这个是没有删除干净的注释
void WINAPI ReplaceIATEntryInOneMod(PCSTR pszCalleeModName,PROC pfnOrig,PROC pfnHook,HMODULE hmodcaller,HANDLE handle);
void WINAPI FixupNewlyLoadedModule(HMODULE hmod,DWORD dwFlags);
APIHOOK.cpp中:
m_module = GetModuleHandle(pszCalleeModName);
ReplaceIATEntryInOneMod(m_pszCalleeModName, m_pfnOrig, m_pfnHook, m_module,
prochandle);
应该修改为:
m_module = hmod;
ReplaceIATEntryInOneMod(m_pszCalleeModName, m_pfnOrig, m_pfnHook, m_module,
prochandle);
上次alan给我的源代码我上传之后才看了一下他的代码,发现他仅仅做了教程一里的。我将源代码整理了一下并给了他一份,应该不久之后就可以看到了。关于alan的联系方式:
E-mail:tyr_alan@hotmal.com如果对他的代码有疑问的话,可以给他发邮件。
主要内容终于讲完了,比我想象得要少,下一节主要讲解一下善后的处理,以便让外挂看起来更专业。由于下一节不是主要的内容,因此可能会拖后一点。
协议型外挂制作
最新推荐文章于 2023-03-13 15:27:20 发布