签,在“对象/库模块”中输入
Ws2_32.lib。
(6)、编译项目,产生wsock32.dll库文件。
(7)、将系统目录下原wsock32.dll库文件拷贝到被外挂程序的目录下,并将其改名
为wsock.001;再将上面产生的
wsock32.dll文件同样拷贝到被外挂程序的目录下。重新启动游戏程序,此时游戏程序
将先加载我们自己制作的
wsock32.dll文件,再通过该库文件间接调用原WinSock接口函数来实现访问网络。上面
我们仅仅介绍了挡载WinSock的
实现过程,至于如何加入外挂控制代码,还需要外挂开发人员对游戏数据包结构、内
容、加密算法等方面的仔细分析
(这个过程将是一个艰辛的过程),再生成外挂控制代码。关于数据包分析方法和技
巧,不是本文讲解的范围,如您
感兴趣可以到网上查查相关资料。
2.挡截API
挡截API技术与挡截WinSock技术在原理上很相似,但是前者比后者提供了更强大的
功能。挡截WinSock仅只能挡截
WinSock接口函数,而挡截API可以实现对应用程序调用的包括WinSock API函数在内的
所有API函数的挡截。如果您的
外挂程序仅打算对WinSock的函数进行挡截的话,您可以只选择使用上小节介绍的挡截
WinSock技术。随着大量外挂程
序在功能上的扩展,它们不仅仅只提供对数据包的挡截,而且还对游戏程序中使用的
Windows API或其它DLL库函数的
挡截,以使外挂的功能更加强大。例如,可以通过挡截相关API函数以实现对非中文游
戏的汉化功能,有了这个利器,
可以使您的外挂程序无所不能了。
挡截API技术的原理核心也是使用我们自己的函数来替换掉Windows或其它DLL库提
供的函数,有点同挡截WinSock
原理相似吧。但是,其实现过程却比挡截WinSock要复杂的多,如像实现挡截Winsock过
程一样,将应用程序调用的所
有的库文件都写一个模拟库有点不大可能,就只说Windows API就有上千个,还有很多
库提供的函数结构并未公开,所
以写一个模拟库代替的方式不大现实,故我们必须另谋良方。
挡截API的最终目标是使用自定义的函数代替原函数。那么,我们首先应该知道应
用程序何时、何地、用何种方式
调用原函数。接下来,需要将应用程序中调用该原函数的指令代码进行修改,使它将调
用函数的指针指向我们自己定
义的函数地址。这样,外挂程序才能完全控制应用程序调用的API函数,至于在其中如
何加入外挂代码,就应需求而异
了。最后还有一个重要的问题要解决,如何将我们自定义的用来代替原API函数的函数
代码注入被外挂游戏程序进行地
址空间中,因在Windows系统中应用程序仅只能访问到本进程地址空间内的代码和数
据。
综上所述,要实现挡截API函数,至少需要解决如下三个问题:
● 如何定位游戏程序中调用API函数指令代码?
● 如何修改游戏程序中调用API函数指令代码?
● 如何将外挂代码(自定义的替换函数代码)注入到游戏程序进程地址空间?
下面我们逐一介绍这几个问题的解决方法:
(1) 、定位调用API函数指令代码
我们知道,在汇编语言中使用CALL指令来调用函数或过程的,它是通过指令参数中
的函数地址而定位到相应的函
数代码的。那么,我们如果能寻找到程序代码中所有调用被挡截的API函数的CALL指令
的话,就可以将该指令中的函数
地址参数修改为替代函数的地址。虽然这是一个可行的方案,但是实现起来会很繁琐,
也不稳健。庆幸的是,Windows
系统中所使用的可执行文件(PE格式)采用了输入地址表机制,将所有在程序调用的
API函数的地址信息存放在输入地
址表中,而在程序代码CALL指令中使用的地址不是API函数的地址,而是输入地址表中
该API函数的地址项,如想使程
序代码中调用的API函数被代替掉,只用将输入地址表中该API函数的地址项内容修改即
可。具体理解输入地址表运行
机制,还需要了解一下PE格式文件结构,其中图三列出了PE格式文件的大致结构。
图三:PE格式大致结构图(003.jpg)
PE格式文件一开始是一段DOS程序,当你的程序在不支持Windows的环境中运行时,
它就会显示“This Program
cannot be run in DOS mode”这样的警告语句,接着这个DOS文件头,就开始真正的PE
文件内容了。首先是一段称为
“IMAGE_NT_HEADER”的数据,其中是许多关于整个PE文件的消息,在这段数据的尾端
是一个称为Data Directory的数
据表,通过它能快速定位一些PE文件中段(section)的地址。在这段数据之后,则是
一个“IMAGE_SECTION_HEADER”
的列表,其中的每一项都详细描述了后面一个段的相关信息。接着它就是PE文件中最主
要的段数据了,执行代码、数
据和资源等等信息就分别存放在这些段中。
在所有的这些段里,有一个被称为“.idata”的段(输入数据段)值得我们去注
意,该段中包含着一些被称为输
入地址表(IAT,Import Address Table)的数据列表。每个用隐式方式加载的API所在
的DLL都有一个IAT与之对应,
同时一个API的地址也与IAT中一项相对应。当一个应用程序加载到内存中后,针对每一
个API函数调用,相应的产生如
下的汇编指令:
JMP DWORD PTR [XXXXXXXX]
或
CALL DWORD PTR [XXXXXXXX]
其中,[XXXXXXXX]表示指向了输入地址表中一个项,其内容是一个DWORD,而正是
这个DWORD才是API函数在内存中
的真正地址。因此我们要想拦截一个API的调用,只要简单的把那个DWORD改为我们自己
的函数的地址。
(2) 、修改调用API函数代码
从上面对PE文件格式的分析可知,修改调用API函数代码其实是修改被调用API函数
在输入地址表中IAT项内容。由
于Windows系统对应用程序指令代码地址空间的严密保护机制,使得修改程序指令代码
非常困难,以至于许多高手为之
编写VxD进入Ring0。在这里,我为大家介绍一种较为方便的方法修改进程内存,它仅需
要调用几个Windows核心API函
数,下面我首先来学会一下这几个API函数:
DWORD VirtualQuery(
LPCVOID lpAddress, // address of region
PMEMORY_BASIC_INFORMATION lpBuffer, // information buffer
DWORD dwLength // size of buffer
);
该函数用于查询关于本进程内虚拟地址页的信息。其中,lpAddress表示被查询页
的区域地址;lpBuffer表示用于
保存查询页信息的缓冲;dwLength表示缓冲区大小。返回值为实际缓冲大小。
BOOL VirtualProtect(
LPVOID lpAddress, // region of committed pages
SIZE_T dwSize, // size of the region
DWORD flNewProtect, // desired access protection
PDWORD lpflOldProtect // old protection
);
该函数用于改变本进程内虚拟地址页的保护属性。其中,lpAddress表示被改变保
护属性页区域地址;dwSize表示
页区域大小;flNewProtect表示新的保护属性,可取值为PAGE_READONLY、
PAGE_READWRITE、PAGE_EXECUTE等;
lpflOldProtect表示用于保存改变前的保护属性。如果函数调用成功返回“T”,否则
返回“F”。
有了这两个API函数,我们就可以随心所欲的修改进程内存了。首先,调用
VirtualQuery()函数查询被修改内存的
页信息,再根据此信息调用VirtualProtect()函数改变这些页的保护属性为
PAGE_READWRITE,有了这个权限您就可以
任意修改进程内存数据了。下面一段代码演示了如何将进程虚拟地址为0x0040106c处的
字节清零。
BYTE* pData = 0x0040106c;
MEMORY_BASIC_INFORMATION mbi_thunk;
//查询页信息。
VirtualQuery(pData, &mbi_thunk, sizeof(MEMORY_BASIC_INFORMATION));
//改变页保护属性为读写。
VirtualProtect(mbi_thunk.BaseAddress,mbi_thunk.RegionSize,
PAGE_READWRITE, &mbi_thunk.Protect);
//清零。
*pData = 0x00;
//恢复页的原保护属性。
DWORD dwOldProtect;
VirtualProtect(mbi_thunk.BaseAddress,mbi_thunk.RegionSize,
mbi_thunk.Protect, &dwOldProtect);
(3)、注入外挂代码进入被挂游戏进程中
完成了定位和修改程序中调用API函数代码后,我们就可以随意设计自定义的API函
数的替代函数了。做完这一切
后,还需要将这些代码注入到被外挂游戏程序进程内存空间中,不然游戏进程根本不会
访问到替代函数代码。注入方
法有很多,如利用全局钩子注入、利用注册表注入挡截User32库中的API函数、利用
CreateRemoteThread注入(仅限于
NT/2000)、利用BHO注入等。因为我们在动作模拟技术一节已经接触过全局钩子,我相
信聪明的读者已经完全掌握了
全局钩子的制作过程,所以我们在后面的实例中,将继续利用这个全局钩子。至于其它
几种注入方法,如果感兴趣可
参阅MSDN有关内容。
有了以上理论基础,我们下面就开始制作一个挡截MessageBoxA和recv函数的实
例,在开发游戏外挂程序 时,可
以此实例为框架,加入相应的替代函数和处理代码即可。此实例的开发过程如下:
(1) 打开前面创建的ActiveKey项目。
(2) 在ActiveKey.h文件中加入HOOKAPI结构,此结构用来存储被挡截API函数名
称、原API函数地址和替代函数地
址。
typedef struct tag_HOOKAPI
{
LPCSTR szFunc;//被HOOK的API函数名称。
PROC pNewProc;//替代函数地址。
PROC pOldProc;//原API函数地址。
}HOOKAPI, *LPHOOKAPI;
(3) 打开ActiveKey.cpp文件,首先加入一个函数,用于定位输入库在输入数据段
中的IAT地址。代码如下:
extern "C" __declspec(dllexport)PIMAGE_IMPORT_DESCRIPTOR
LocationIAT(HMODULE hModule, LPCSTR szImportMod)
//其中,hModule为进程模块句柄;szImportMod为输入库名称。
{
//检查是否为DOS程序,如是返回NULL,因DOS程序没有IAT。
PIMAGE_DOS_HEADER pDOSHeader = (PIMAGE_DOS_HEADER) hModule;
if(pDOSHeader->e_magic != IMAGE_DOS_SIGNATURE) return NULL;
//检查是否为NT标志,否则返回NULL。
PIMAGE_NT_HEADERS pNTHeader = (PIMAGE_NT_HEADERS)((DWORD)pDOSHeader+
(DWORD)(pDOSHeader-
>e_lfanew));
if(pNTHeader->Signature != IMAGE_NT_SIGNATURE) return NULL;
//没有IAT表则返回NULL。
if(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Vir
tualAddress == 0)
return NULL;
//定位第一个IAT位置。
PIMAGE_IMPORT_DESCRIPTOR pImportDesc =
(PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDOSHeader + (DWORD)
(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].Virtu
alAddress));
//根据输入库名称循环检查所有的IAT,如匹配则返回该IAT地址,否则检测下
一个IAT。
while (pImportDesc->Name)
{
//获取该IAT描述的输入库名称。
PSTR szCurrMod = (PSTR)((DWORD)pDOSHeader +
(DWORD)(pImportDesc->Name));
if (stricmp(szCurrMod, szImportMod) == 0) break;
pImportDesc++;
}
if(pImportDesc->Name == NULL) return NULL;
return pImportDesc;
}
再加入一个函数,用来定位被挡截API函数的IAT项并修改其内容为替代函数地址。
代码如下:
extern "C" __declspec(dllexport)
HookAPIByName( HMODULE hModule, LPCSTR szImportMod, LPHOOKAPI
pHookApi)
//其中,hModule为进程模块句柄;szImportMod为输入库名称;pHookAPI为
HOOKAPI结构指针。
{
//定位szImportMod输入库在输入数据段中的IAT地址。
PIMAGE_IMPORT_DESCRIPTOR pImportDesc = LocationIAT(hModule,
szImportMod);
if (pImportDesc == NULL) return FALSE;
//第一个Thunk地址。
PIMAGE_THUNK_DATA pOrigThunk = (PIMAGE_THUNK_DATA)((DWORD)hModule +
(DWORD)(pImportDesc-
>OriginalFirstThunk));
//第一个IAT项的Thunk地址。
PIMAGE_THUNK_DATA pRealThunk = (PIMAGE_THUNK_DATA)((DWORD)hModule +
(DWORD)(pImportDesc-
>FirstThunk));
//循环查找被截API函数的IAT项,并使用替代函数地址修改其值。
while(pOrigThunk->u1.Function)
{
//检测此Thunk是否为IAT项。
if((pOrigThunk->u1.Ordinal & IMAGE_ORDINAL_FLAG) != IMAGE_ORDINAL_FLAG)
{
//获取此IAT项所描述的函数名称。
PIMAGE_IMPORT_BY_NAME pByName
=(PIMAGE_IMPORT_BY_NAME)((DWORD)hModule+(DWORD)(pOrigThunk-
>u1.AddressOfData));
if(pByName->Name[0] == /0) return FALSE;
//检测是否为挡截函数。
if(strcmpi(pHookApi->szFunc, (char*)pByName->Name) == 0)
{
MEMORY_BASIC_INFORMATION mbi_thunk;
//查询修改页的信息。
VirtualQuery(pRealThunk, &mbi_thunk,
sizeof(MEMORY_BASIC_INFORMATION));
//改变修改页保护属性为PAGE_READWRITE。
VirtualProtect(mbi_thunk.BaseAddress,mbi_thunk.RegionSize,
PAGE_READWRITE,
&mbi_thunk.Protect);
//保存原来的API函数地址。
if(pHookApi->pOldProc == NULL)
pHookApi->pOldProc = (PROC)pRealThunk->u1.Function;
//修改API函数IAT项内容为替代函数地址。
pRealThunk->u1.Function = (PDWORD)pHookApi->pNewProc;
//恢复修改页保护属性。
DWORD dwOldProtect;
VirtualProtect(mbi_thunk.BaseAddress, mbi_thunk.RegionSize,
mbi_thunk.Protect,
&dwOldProtect);
}
}
pOrigThunk++;
pRealThunk++;
}
SetLastError(ERROR_SUCCESS); //设置错误为ERROR_SUCCESS,表示成功。
return TRUE;
}
(4) 定义替代函数,此实例中只给MessageBoxA和recv两个API进行挡截。代码如下
:
static int WINAPI MessageBoxA1 (HWND hWnd , LPCTSTR lpText, LPCTSTR
lpCaption, UINT uType)
{
//过滤掉原MessageBoxA的正文和标题内容,只显示如下内容。
return MessageBox(hWnd, "Hook API OK!", "Hook API", uType);
}
static int WINAPI recv1(SOCKET s, char FAR *buf, int len, int flags )
{
//此处可以挡截游戏服务器发送来的网络数据包,可以加入分析和处理数据代
码。
return recv(s,buf,len,flags);
}
(5) 在KeyboardProc函数中加入激活挡截API代码,在if( wParam == 0X79 )语句
中后面加入如下else if语句:
......
//当激活F11键时,启动挡截API函数功能。
else if( wParam == 0x7A )
{
HOOKAPI api[2];
api[0].szFunc ="MessageBoxA";//设置被挡截函数的名称。
api[0].pNewProc = (PROC)MessageBoxA1;//设置替代函数的地址。
api[1].szFunc ="recv";//设置被挡截函数的名称。
api[1].pNewProc = (PROC)recv1; //设置替代函数的地址。
//设置挡截User32.dll库中的MessageBoxA函数。
HookAPIByName(GetModuleHandle(NULL),"User32.dll",&api[0]);
//设置挡截Wsock32.dll库中的recv函数。
HookAPIByName(GetModuleHandle(NULL),"Wsock32.dll",&api[1]);
}
......