网络游戏外挂编写原理(二)

四、动作模拟技术

  我们在前面介绍过,几乎所有的游戏都有大量繁琐和无聊的攻击动作以增加玩家的
功力,还有那些数不完的迷宫

,这些好像已经成为了角色游戏的代名词。现在,外挂可以帮助玩家从这些繁琐而无聊
的工作中摆脱出来,专注于游

戏情节的进展。外挂程序为了实现自动角色位置移动和自动攻击等功能,需要使用到键
盘模拟技术和鼠标模拟技术。

下面我们将重点介绍这些技术并编写一个简单的实例帮助读者理解动作模拟技术的实现
过程。

  1. 鼠标模拟技术
  
  几乎所有的游戏中都使用了鼠标来改变角色的位置和方向,玩家仅用一个小小的鼠
标,就可以使角色畅游天下。

那么,我们如何实现在没有玩家的参与下角色也可以自动行走呢。其实实现这个并不
难,仅仅几个Windows API函数就

可以搞定,让我们先来认识认识这些API函数。

  (1) 模拟鼠标动作API函数mouse_event,它可以实现模拟鼠标按下和放开等动作。

    VOID mouse_event(
      DWORD dwFlags, // 鼠标动作标识。
      DWORD dx, // 鼠标水平方向位置。
      DWORD dy, // 鼠标垂直方向位置。
      DWORD dwData, // 鼠标轮子转动的数量。
      DWORD dwExtraInfo // 一个关联鼠标动作辅加信息。
    );

  其中,dwFlags表示了各种各样的鼠标动作和点击活动,它的常用取值如下:

   MOUSEEVENTF_MOVE 表示模拟鼠标移动事件。

   MOUSEEVENTF_LEFTDOWN 表示模拟按下鼠标左键。

   MOUSEEVENTF_LEFTUP 表示模拟放开鼠标左键。

   MOUSEEVENTF_RIGHTDOWN 表示模拟按下鼠标右键。

   MOUSEEVENTF_RIGHTUP 表示模拟放开鼠标右键。

   MOUSEEVENTF_MIDDLEDOWN 表示模拟按下鼠标中键。

   MOUSEEVENTF_MIDDLEUP 表示模拟放开鼠标中键。

  (2)、设置和获取当前鼠标位置的API函数。获取当前鼠标位置使用GetCursorPos()
函数,设置当前鼠标位置使用

SetCursorPos()函数。

    BOOL GetCursorPos(
     LPPOINT lpPoint // 返回鼠标的当前位置。
    );
    BOOL SetCursorPos(
    int X, // 鼠标的水平方向位置。
      int Y //鼠标的垂直方向位置。
    );

  通常游戏角色的行走都是通过鼠标移动至目的地,然后按一下鼠标的按钮就搞定
了。下面我们使用上面介绍的API

函数来模拟角色行走过程。

   CPoint oldPoint,newPoint;
   GetCursorPos(&oldPoint); //保存当前鼠标位置。
   newPoint.x = oldPoint.x+40;
   newPoint.y = oldPoint.y+10;
   SetCursorPos(newPoint.x,newPoint.y); //设置目的地位置。
   mouse_event(MOUSEEVENTF_RIGHTDOWN,0,0,0,0);//模拟按下鼠标右键。
   mouse_event(MOUSEEVENTF_RIGHTUP,0,0,0,0);//模拟放开鼠标右键。

  2. 键盘模拟技术

  在很多游戏中,不仅提供了鼠标的操作,而且还提供了键盘的操作,在对攻击对象
进行攻击时还可以使用快捷键

。为了使这些攻击过程能够自动进行,外挂程序需要使用键盘模拟技术。像鼠标模拟技
术一样,Windows API也提供了

一系列API函数来完成对键盘动作的模拟。

  模拟键盘动作API函数keydb_event,它可以模拟对键盘上的某个或某些键进行按下
或放开的动作。

   VOID keybd_event(
     BYTE bVk, // 虚拟键值。
     BYTE bScan, // 硬件扫描码。
     DWORD dwFlags, // 动作标识。
     DWORD dwExtraInfo // 与键盘动作关联的辅加信息。
   );

  其中,bVk表示虚拟键值,其实它是一个BYTE类型值的宏,其取值范围为1-254。有
关虚拟键值表请在MSDN上使用

关键字“Virtual-Key Codes”查找相关资料。bScan表示当键盘上某键被按下和放开
时,键盘系统硬件产生的扫描码

,我们可以MapVirtualKey()函数在虚拟键值与扫描码之间进行转换。dwFlags表示各种
各样的键盘动作,它有两种取

值:KEYEVENTF_EXTENDEDKEY和KEYEVENTF_KEYUP。

  下面我们使用一段代码实现在游戏中按下Shift+R快捷键对攻击对象进行攻击。

   keybd_event(VK_CONTROL,MapVirtualKey(VK_CONTROL,0),0,0); //按下CTRL
键。
   keybd_event(0x52,MapVirtualKey(0x52,0),0,0);//键下R键。
   keybd_event(0x52,MapVirtualKey(0x52,0), KEYEVENTF_KEYUP,0);//放开R键。
   keybd_event(VK_CONTROL,MapVirtualKey(VK_CONTROL,0),
   KEYEVENTF_KEYUP,0);//放开CTRL键。

  3. 激活外挂

  上面介绍的鼠标和键盘模拟技术实现了对游戏角色的动作部分的模拟,但要想外挂
能工作于游戏之上,还需要将

其与游戏的场景窗口联系起来或者使用一个激活键,就象按键精灵的那个激活键一样。
我们可以用GetWindow函数来枚

举窗口,也可以用Findwindow函数来查找特定的窗口。另外还有一个FindWindowEx函数
可以找到窗口的子窗口,当游

戏切换场景的时候我们可以用FindWindowEx来确定一些当前窗口的特征,从而判断是否
还在这个场景,方法很多了,

比如可以GetWindowInfo来确定一些东西,比如当查找不到某个按钮的时候就说明游戏
场景已经切换了等等办法。当使

用激活键进行关联,需要使用Hook技术开发一个全局键盘钩子,在这里就不具体介绍全
局钩子的开发过程了,在后面

的实例中我们将会使用到全局钩子,到时将学习到全局钩子的相关知识。

4. 实例实现

  通过上面的学习,我们已经基本具备了编写动作式游戏外挂的能力了。下面我们将
创建一个画笔程序外挂,它实

现自动移动画笔字光标的位置并写下一个红色的“R”字。以这个实例为基础,加入相
应的游戏动作规则,就可以实现

一个完整的游戏外挂。这里作者不想使用某个游戏作为例子来开发外挂(因没有游戏商
家的授权啊!),如读者感兴

趣的话可以找一个游戏试试,最好仅做测试技术用。

  首先,我们需要编写一个全局钩子,使用它来激活外挂,激活键为F10。创建全局
钩子步骤如下:

  (1).选择MFC AppWizard(DLL)创建项目ActiveKey,并选择MFC Extension DLL
(共享MFC拷贝)类型。

  (2).插入新文件ActiveKey.h,在其中输入如下代码:

   #ifndef _KEYDLL_H
   #define _KEYDLL_H

   class AFX_EXT_CLASS CKeyHook:public CObject
   {
    public:
 CKeyHook();
 ~CKeyHook();
 HHOOK Start(); //安装钩子
 BOOL Stop(); //卸载钩子
   };
   #endif

  (3).在ActiveKey.cpp文件中加入声明"#include ActiveKey.h"。

  (4).在ActiveKey.cpp文件中加入共享数据段,代码如下:

   //Shared data section
   #pragma data_seg("sharedata")
   HHOOK glhHook=NULL; //钩子句柄。
   HINSTANCE glhInstance=NULL; //DLL实例句柄。
   #pragma data_seg()

  (5).在ActiveKey.def文件中设置共享数据段属性,代码如下:

   SECTIONS
   sharedata READ WRITE SHARED

  (6).在ActiveKey.cpp文件中加入CkeyHook类的实现代码和钩子函数代码:

   //键盘钩子处理函数。
   extern "C" LRESULT WINAPI KeyboardProc(int nCode,WPARAM wParam,LPARAM
lParam)
   {
   if( nCode >= 0 )
   {
   if( wParam == 0X79 )//当按下F10键时,激活外挂。
 {
  //外挂实现代码。
CPoint newPoint,oldPoint;
   GetCursorPos(&oldPoint);
   newPoint.x = oldPoint.x+40;
   newPoint.y = oldPoint.y+10;
   SetCursorPos(newPoint.x,newPoint.y);
   mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);//模拟按下鼠标左键。
  mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);//模拟放开鼠标左键。
  keybd_event(VK_SHIFT,MapVirtualKey(VK_SHIFT,0),0,0); //按下SHIFT键。
  keybd_event(0x52,MapVirtualKey(0x52,0),0,0);//按下R键。
  keybd_event(0x52,MapVirtualKey(0x52,0),KEYEVENTF_KEYUP,0);//放开R键。
  keybd_event(VK_SHIFT,MapVirtualKey(VK_SHIFT,0),KEYEVENTF_KEYUP,0);//放开SHIFT键。
  SetCursorPos(oldPoint.x,oldPoint.y);
 }
   }
   return CallNextHookEx(glhHook,nCode,wParam,lParam);
   }

   CKeyHook::CKeyHook(){}
   CKeyHook::~CKeyHook()
   { 
   if( glhHook )
Stop();
   }
   //安装全局钩子。
   HHOOK CKeyHook::Start()
   {
glhHook = SetWindowsHookEx(WH_KEYBOARD,KeyboardProc,glhInstance,0);//设置键盘钩子。
return glhHook;
}
   //卸载全局钩子。
   BOOL CKeyHook::Stop()
   {
   BOOL bResult = TRUE;
 if( glhHook )
   bResult = UnhookWindowsHookEx(glhHook);//卸载键盘钩子。
   return bResult;
   }

  (7).修改DllMain函数,代码如下:

   extern "C" int APIENTRY
   DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
   {
//如果使用lpReserved参数则删除下面这行
UNREFERENCED_PARAMETER(lpReserved);

if (dwReason == DLL_PROCESS_ATTACH)
{
  TRACE0("NOtePadHOOK.DLL Initializing!/n");
   //扩展DLL仅初始化一次
  if (!AfxInitExtensionModule(ActiveKeyDLL, hInstance))
return 0;
  new CDynLinkLibrary(ActiveKeyDLL);
      //把DLL加入动态MFC类库中
  glhInstance = hInstance;
  //插入保存DLL实例句柄
}
else if (dwReason == DLL_PROCESS_DETACH)
{
  TRACE0("NotePadHOOK.DLL Terminating!/n");
  //终止这个链接库前调用它
  AfxTermExtensionModule(ActiveKeyDLL);
}
return 1;
   }

  (8).编译项目ActiveKey,生成ActiveKey.DLL和ActiveKey.lib。

  接着,我们还需要创建一个外壳程序将全局钩子安装了Windows系统中,这个外壳
程序编写步骤如下:

  (1).创建一个对话框模式的应用程序,项目名为Simulate。

  (2).在主对话框中加入一个按钮,使用ClassWizard为其创建CLICK事件。

  (3).将ActiveKey项目Debug目录下的ActiveKey.DLL和ActiveKey.lib拷贝到
Simulate项目目录下。

  (4).从“工程”菜单中选择“设置”,弹出Project Setting对话框,选择Link标
签,在“对象/库模块”中输入 ActiveKey.lib。

      (5).将ActiveKey项目中的ActiveKey.h头文件加入到Simulate项目中,并在Stdafx.h中加入

            #include  ActiveKey.h。

       (6).在按钮单击事件函数输入如下代码:

            void CSimulateDlg::OnButton1()
            {
                // TODO: Add your control notification handler code here
                if( !bSetup )
                 {
                      m_hook.Start();//激活全局钩子。
                  }
                else
                 {
                      m_hook.Stop();//撤消全局钩子。
                  }
                bSetup = !bSetup;

       }

  (7).编译项目,并运行程序,单击按钮激活外挂。

  (8).启动画笔程序,选择文本工具并将笔的颜色设置为红色,将鼠标放在任意位置
后,按F10键,画笔程序自动移

动鼠标并写下一个红色的大写R。图一展示了按F10键前的画笔程序的状态,图二展示了
按F10键后的画笔程序的状态。


图一:按F10前状态(001.jpg)


图二:按F10后状态(002.jpg)


五、封包技术

  通过对动作模拟技术的介绍,我们对游戏外挂有了一定程度上的认识,也学会了使
用动作模拟技术来实现简单的

动作模拟型游戏外挂的制作。这种动作模拟型游戏外挂有一定的局限性,它仅仅只能解
决使用计算机代替人力完成那

么有规律、繁琐而无聊的游戏动作。但是,随着网络游戏的盛行和复杂度的增加,很多
游戏要求将客户端动作信息及

时反馈回服务器,通过服务器对这些动作信息进行有效认证后,再向客户端发送下一步
游戏动作信息,这样动作模拟

技术将失去原有的效应。为了更好地“外挂”这些游戏,游戏外挂程序也进行了升级换
代,它们将以前针对游戏用户

界面层的模拟推进到数据通讯层,通过封包技术在客户端挡截游戏服务器发送来的游戏
控制数据包,分析数据包并修

改数据包;同时还需按照游戏数据包结构创建数据包,再模拟客户端发送给游戏服务
器,这个过程其实就是一个封包

的过程。

  封包的技术是实现第二类游戏外挂的最核心的技术。封包技术涉及的知识很广泛,
实现方法也很多,如挡截

WinSock、挡截API函数、挡截消息、VxD驱动程序等。在此我们也不可能在此文中将所
有的封包技术都进行详细介绍,

故选择两种在游戏外挂程序中最常用的两种方法:挡截WinSock和挡截API函数。

  1. 挡截WinSock

  众所周知,Winsock是Windows网络编程接口,它工作于Windows应用层,它提供与
底层传输协议无关的高层数据传

输编程接口。在Windows系统中,使用WinSock接口为应用程序提供基于TCP/IP协议的网
络访问服务,这些服务是由

Wsock32.DLL动态链接库提供的函数库来完成的。

  由上说明可知,任何Windows基于TCP/IP的应用程序都必须通过WinSock接口访问网
络,当然网络游戏程序也不例

外。由此我们可以想象一下,如果我们可以控制WinSock接口的话,那么控制游戏客户
端程序与服务器之间的数据包也

将易如反掌。按着这个思路,下面的工作就是如何完成控制WinSock接口了。由上面的
介绍可知,WinSock接口其实是

由一个动态链接库提供的一系列函数,由这些函数实现对网络的访问。有了这层的认
识,问题就好办多了,我们可以

制作一个类似的动态链接库来代替原WinSock接口库,在其中实现WinSock32.dll中实现
的所有函数,并保证所有函数

的参数个数和顺序、返回值类型都应与原库相同。在这个自制作的动态库中,可以对我
们感兴趣的函数(如发送、接

收等函数)进行挡截,放入外挂控制代码,最后还继续调用原WinSock库中提供的相应
功能函数,这样就可以实现对网

络数据包的挡截、修改和发送等封包功能。

  下面重点介绍创建挡截WinSock外挂程序的基本步骤:

  (1) 创建DLL项目,选择Win32 Dynamic-Link Library,再选择An empty DLL
project。

  (2) 新建文件wsock32.h,按如下步骤输入代码:

  ① 加入相关变量声明:

   HMODULE hModule=NULL; //模块句柄
   char buffer[1000]; //缓冲区
   FARPROC proc; //函数入口指针

  ② 定义指向原WinSock库中的所有函数地址的指针变量,因WinSock库共提供70多
个函数,限于篇幅,在此就只选

择几个常用的函数列出,有关这些库函数的说明可参考MSDN相关内容。

   //定义指向原WinSock库函数地址的指针变量。
   SOCKET (__stdcall *socket1)(int ,int,int);//创建Sock函数。
   int (__stdcall *WSAStartup1)(WORD,LPWSADATA);//初始化WinSock库函数。
   int (__stdcall *WSACleanup1)();//清除WinSock库函数。
   int (__stdcall *recv1)(SOCKET ,char FAR * ,int ,int );//接收数据函数。
   int (__stdcall *send1)(SOCKET ,const char * ,int ,int);//发送数据函
数。
   int (__stdcall *connect1)(SOCKET,const struct sockaddr *,int);//创建连
接函数。
   int (__stdcall *bind1)(SOCKET ,const struct sockaddr *,int );//绑定函
数。
   ......其它函数地址指针的定义略。

  (3) 新建wsock32.cpp文件,按如下步骤输入代码:

  ① 加入相关头文件声明:

   #include
   #include
   #include "wsock32.h"

  ② 添加DllMain函数,在此函数中首先需要加载原WinSock库,并获取此库中所有
函数的地址。代码如下:

   BOOL WINAPI DllMain (HANDLE hInst,ULONG ul_reason_for_call,LPVOID
lpReserved)
   {
    if(hModule==NULL){
     //加载原WinSock库,原WinSock库已复制为wsock32.001。
   hModule=LoadLibrary("wsock32.001");
  }
    else return 1;
//获取原WinSock库中的所有函数的地址并保存,下面仅列出部分代码。
if(hModule!=NULL){
     //获取原WinSock库初始化函数的地址,并保存到WSAStartup1中。
proc=GetProcAddress(hModule,"WSAStartup");
   WSAStartup1=(int (_stdcall *)(WORD,LPWSADATA))proc;
     //获取原WinSock库消除函数的地址,并保存到WSACleanup1中。
    proc=GetProcAddress(hModule i,"WSACleanup");
    WSACleanup1=(int (_stdcall *)())proc;
     //获取原创建Sock函数的地址,并保存到socket1中。
    proc=GetProcAddress(hModule,"socket");
     socket1=(SOCKET (_stdcall *)(int ,int,int))proc;
     //获取原创建连接函数的地址,并保存到connect1中。
     proc=GetProcAddress(hModule,"connect");
     connect1=(int (_stdcall *)(SOCKET ,const struct sockaddr
*,int ))proc;
     //获取原发送函数的地址,并保存到send1中。
     proc=GetProcAddress(hModule,"send");
     send1=(int (_stdcall *)(SOCKET ,const char * ,int ,int ))proc;
     //获取原接收函数的地址,并保存到recv1中。
     proc=GetProcAddress(hModule,"recv");
     recv1=(int (_stdcall *)(SOCKET ,char FAR * ,int ,int ))proc;
     ......其它获取函数地址代码略。
   }
   else return 0;
   return 1;
}

  ③ 定义库输出函数,在此可以对我们感兴趣的函数中添加外挂控制代码,在所有
的输出函数的最后一步都调用原

WinSock库的同名函数。部分输出函数定义代码如下:

//库输出函数定义。
//WinSock初始化函数。
    int PASCAL FAR WSAStartup(WORD wVersionRequired, LPWSADATA
lpWSAData)
    {
     //调用原WinSock库初始化函数
     return WSAStartup1(wVersionRequired,lpWSAData);
    }
    //WinSock结束清除函数。
    int PASCAL FAR WSACleanup(void)
    {
     return WSACleanup1(); //调用原WinSock库结束清除函数。
    }
    //创建Socket函数。
    SOCKET PASCAL FAR socket (int af, int type, int protocol)
    {
     //调用原WinSock库创建Socket函数。
     return socket1(af,type,protocol);
    }
    //发送数据包函数
    int PASCAL FAR send(SOCKET s,const char * buf,int len,int flags)
    {
   //在此可以对发送的缓冲buf的内容进行修改,以实现欺骗服务器。
   外挂代码......
   //调用原WinSock库发送数据包函数。
     return send1(s,buf,len,flags);
    }
//接收数据包函数。
    int PASCAL FAR recv(SOCKET s, char FAR * buf, int len, int flags)
    {
   //在此可以挡截到服务器端发送到客户端的数据包,先将其保存到buffer中。
   strcpy(buffer,buf);
   //对buffer数据包数据进行分析后,对其按照玩家的指令进行相关修改。
   外挂代码......
   //最后调用原WinSock中的接收数据包函数。
     return recv1(s, buffer, len, flags);
     }
    .......其它函数定义代码略。

  (4)、新建wsock32.def配置文件,在其中加入所有库输出函数的声明,部分声明代
码如下:

   LIBRARY "wsock32"
   EXPORTS
    WSAStartup @1
   WSACleanup @2
    recv @3
    send @4
    socket @5
   bind @6
   closesocket @7
   connect @8

   ......其它输出函数声明代码略。


本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/zhoubaocun/archive/2009/03/04/3957216.aspx

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值