[轉貼] 遊戲外掛設計技術探討

轉貼至小新a家
遊戲外掛設計技術探討(上)


  一、 前言

  所謂遊戲外掛,其實是一種遊戲 外輔程序,它可以協助玩家自動產生遊戲動作、修改遊戲網路封包以及修改遊戲記憶體資料等,以實現玩家用最少的時間和金錢去完成功力升級和過關斬將。雖然, 現在對遊戲外掛程序的「合法」身份眾說紛紜,在這裡我不想對此發表任何個人意見,讓時間去說明一切吧。

  不管遊戲外掛程序是不是「合法」身份,但是它卻是具有一定的技術含量的,在這些小小程序中使用了許多高端技術,如攔截Sock技術、攔截API技術、模擬鍵盤與滑鼠技術、直接修改程序記憶體技術等等。本文將對常見的遊戲外掛中使用的技術進行全面剖析。

  二、認識外掛

  遊戲外掛的歷史可以追溯到單機版遊戲時代,只不過當時它使用了另一個更通俗易懂的名字??遊戲修改器。它可以在遊戲中追蹤鎖定遊戲主人公的各項能力數值。這樣玩家在遊戲中可以達到主角不掉血、不耗費魔法、不消耗金錢等目的。這樣降低了遊戲的難度,使得玩家更容易通關。

   隨著網路遊戲的時代的來臨,遊戲外掛在原有的功能之上進行了新的發展,它變得更加多種多樣,功能更加強大,操作更加簡單,以至有些遊戲的外掛已經成為一 個體系,比如《石器時代》,外掛品種達到了幾十種,自動戰鬥、自動行走、自動練級、自動補血、加速、不遇敵、原地遇敵、快速增加經驗值、按鍵精靈……幾乎 無所不包。

  遊戲外掛的設計主要是針對於某個遊戲開發的,我們可以根據它針對的遊戲的類型可大致可將外掛分為兩種大類。

   一類是將遊戲中大量繁瑣和無聊的攻擊動作使用外掛自動完成,以幫助玩家輕鬆搞定攻擊對象並可以快速的增加玩家的經驗值。比如在《龍族》中有一種工作的設 定,玩家的工作等級越高,就可以駕馭越好的裝備。但是增加工作等級卻不是一件有趣的事情,毋寧說是重複枯燥的機械勞動。如果你想做法師用的杖,首先需要做 基本工作--?砍樹。砍樹的方法很簡單,在一棵大樹前不停的點滑鼠就可以了,每10000的經驗升一級。這就意味著玩家要在大樹前不停的點擊滑鼠,這種無 聊的事情通過"按鍵精靈"就可以解決。外掛的"按鍵精靈"功能可以讓玩家擺脫無趣的點擊滑鼠的工作。

  另一類是由外掛程序產生欺騙性的 網路遊戲封包,並將這些封包發送到網路遊戲服務器,利用這些虛假信息欺騙服務器進行遊戲數值的修改,達到修改角色能力數值的目的。這類外掛程序針對性很 強,一般在設計時都是針對某個遊戲某個版本來做的,因為每個網路遊戲服務器與客戶端交流的封包各不相同,外掛程序必須要對欺騙的網路遊戲服務器的封包進行 分析,才能產生服務器識別的封包。這類外掛程序也是當前最流利的一類遊戲外掛程序。

  另外,現在很多外掛程序功能強大,不僅實現了自動 動作代理和封包功能,而且還提供了對網路遊戲的客戶端程序的資料進行修改,以達到欺騙網路遊戲服務器的目的。我相信,隨著網路遊戲商家的反外掛技術的進 展,遊戲外掛將會產生更多更優秀的技術,讓我們期待著看場技術大戰吧......

  三、外掛技術綜述

  可以將開發遊戲外掛程序的過程大體上劃分為兩個部分:

   前期部分工作是對外掛的主體遊戲進行分析,不同類型的外掛分析主體遊戲的內容也不相同。如外掛為上述談到的外掛類型中的第一類時,其分析過程常是針對遊 戲的場景中的攻擊對象的位置和分佈情況進行分析,以實現外掛自動進行攻擊以及位置移動。如外掛為外掛類型中的第二類時,其分析過程常是針對遊戲服務器與客 戶端之間通訊包資料的結構、內容以及加密算法的分析。因網路遊戲公司一般都不會公佈其遊戲產品的通訊包資料的結構、內容和加密算法的信息,所以對於開發第 二類外掛成功的關鍵在於是否能正確分析遊戲包資料的結構、內容以及加密算法,雖然可以使用一些工具輔助分析,但是這還是一種堅苦而複雜的工作。

   後期部分工作主要是根據前期對遊戲的分析結果,使用大量的程序開發技術編寫外掛程序以實現對遊戲的控制或修改。如外掛程序為第一類外掛時,通常會使用到 滑鼠模擬技術來實現遊戲角色的自動位置移動,使用鍵盤模擬技術來實現遊戲角色的自動攻擊。如外掛程序為第二類外掛時,通常會使用到擋截Sock和擋截 API函數技術,以擋截遊戲服務器傳來的網路封包並將封包修改後封包後傳給遊戲服務器。另外,還有許多外掛使用對遊戲客戶端程序記憶體資料修改技術以及遊 戲加速技術。

  本文主要是針對開發遊戲外掛程序後期使用的程序開發技術進行探討,重點介紹的如下幾種在遊戲外掛中常使用的程序開發技術:

  ● 動作模擬技術:主要包括鍵盤模擬技術和滑鼠模擬技術。

  ● 封包技術:主要包括擋截Sock技術和擋截API技術。


四、動作模擬技術

   我們在前面介紹過,幾乎所有的遊戲都有大量繁瑣和無聊的攻擊動作以增加玩家的功力,還有那些數不完的迷宮,這些好像已經成為了角色遊戲的代名詞。現在, 外掛可以幫助玩家從這些繁瑣而無聊的工作中擺脫出來,專注於遊戲情節的進展。外掛程序為了實現自動角色位置移動和自動攻擊等功能,需要使用到鍵盤模擬技術 和滑鼠模擬技術。下面我們將重點介紹這些技術並編寫一個簡單的實例幫助讀者理解動作模擬技術的實現過程。

  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檔案中設置共享資料段屬性,代碼如下:

   SETCTIONS
   shareddata 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 "windows.h"
   #include "stdio.h"
   #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

   ......其它輸出函數聲明代碼略。

  (5)、從「工程」選單中選擇「設置」,彈出Project Setting對話框,選擇Link標籤,在「對像/庫模塊」中輸入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格式大致結構圖


   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].VirtualAddress == 0) return NULL;
    //定位第一個IAT位置。
     PIMAGE_IMPORT_DESCRIPTOR pImportDesc = (PIMAGE_IMPORT_DESCRIPTOR)((DWORD)pDOSHeader + (DWORD)(pNTHeader->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT].VirtualAddress));
    //根據輸入庫名稱循環檢查所有的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]);
   }
   ......

  (6) 在ActiveKey.cpp中加入頭檔案聲明 "#include "wsock32.h"。 從「工程」選單中選擇「設置」,彈出Project Setting對話框,選擇Link標籤,在「對像/庫模塊」中輸入Ws2_32..lib。

   (7) 重新編譯ActiveKey項目,產生ActiveKey.dll檔案,將其拷貝到Simulate.exe目錄下。運行Simulate.exe並啟動 全局鉤子。激活任意應用程序,按F11鍵後,運行此程序中可能調用MessageBoxA函數的操作,看看信息框是不是有所變化。同樣,如此程序正在接收 網路封包,就可以實現封包功能了。

  六、結束語

  除了以上介紹的幾種遊戲外掛程序常用的技術以外,在一些外掛程序中還使用了遊戲資料修改技術、遊戲加速技術等。在這篇文章裡,就不逐一介紹了。

留言