遊戲外掛基本原理及實現

遊戲外掛已經深深地影響着衆多網絡遊戲玩家,今天在網上看到了一些關於遊戲外掛編寫的技術,於是轉載上供大家參考 

原文出處:http://blog.csdn.net/weiqubo/article/details/7044206


  1、遊戲外掛的原理 

  外掛現在分爲好多種,比如模擬鍵盤的,鼠標的,修改數據包的,還有修改本地內存的,但好像沒有修改服務器內存的哦,呵呵。其實修改服務器也是有辦法的,只是技術太高一般人沒有辦法入手而已。(比如請GM去夜總會、送禮、收黑錢等等辦法都可以修改服務器數據,哈哈) 

  修改遊戲無非是修改一下本地內存的數據,或者截獲API函數等等。這裏我把所能想到的方法都作一個介紹,希望大家能做出很好的外掛來使遊戲廠商更好的完善自己的技術。我見到一篇文章是講魔力寶貝的理論分析,寫得不錯,大概是那個樣子。下來我就講解一下技術方面的東西,以作引玉之用。 

  2 技術分析部分 

  2.1 模擬鍵盤或鼠標的響應 

  我們一般使用: 

  UINT SendInput( 
    UINT nInputs,   // count of input events 
    LPINPUT pInputs, // array of input events 
    int cbSize    // size of structure 
  ); 

  API函數。第一個參數是說明第二個參數的矩陣的維數的,第二個參數包含了響應事件,這個自己填充就可以,最後是這個結構的大小,非常簡單,這是最簡單的方法模擬鍵盤鼠標了,呵呵。注意,這個函數還有個替代函數: 

  VOID keybd_event( 
    BYTE bVk,       // 虛擬鍵碼 
    BYTE bScan,      // 掃描碼 
    DWORD dwFlags, 
    ULONG_PTR dwExtraInfo // 附加鍵狀態 
  ); 

  與 

  VOID mouse_event( 
    DWORD dwFlags,      // motion and click options 
    DWORD dx,         // horizontal position or change 
    DWORD dy,        // vertical position or change 
    DWORD dwData,      // wheel movement 
    ULONG_PTR dwExtraInfo  // application-defined information 
  ); 

  這兩個函數非常簡單了,我想那些按鍵精靈就是用的這個吧。上面的是模擬鍵盤,下面的是模擬鼠標的。這個僅僅是模擬部分,要和遊戲聯繫起來我們還需要找到遊戲的窗口才行,或者包含快捷鍵,就象按鍵精靈的那個激活鍵一樣,我們可以用GetWindow函數來枚舉窗口,也可以用Findwindow函數來查找制定的窗口(注意,還有一個FindWindowEx),FindwindowEx可以找到窗口的子窗口,比如按鈕,等什麼東西。當遊戲切換場景的時候我們可以用FindWindowEx來確定一些當前窗口的特徵,從而判斷是否還在這個場景,方法很多了,比如可以GetWindowInfo來確定一些東西,比如當查找不到某個按鈕的時候就說明遊戲場景已經切換了,等等辦法。有的遊戲沒有控件在裏面,這是對圖像做座標變換的話,這種方法就要受到限制了。這就需要我們用別的辦法來輔助分析了。 

  至於快捷鍵我們要用動態連接庫實現了,裏面要用到hook技術了,這個也非常簡單。大家可能都會了,其實就是一個全局的hook對象然後SetWindowHook就可以了,回調函數都是現成的,而且現在網上的例子多如牛毛。這個實現在外掛中已經很普遍了。如果還有誰不明白,那就去看看MSDN查找SetWindowHook就可以了。 

  不要低估了這個動態連接庫的作用,它可以切入所有的進程空間,也就是可以加載到所有的遊戲裏面哦,只要用對,你會發現很有用途的。這個需要你複習一下Win32編程的基礎知識了。呵呵,趕快去看書吧。 

  2.2 截獲消息 

  有些遊戲的響應機制比較簡單,是基於消息的,或者用什麼定時器的東西。這個時候你就可以用攔截消息來實現一些有趣的功能了。 

  我們攔截消息使用的也是hook技術,裏面包括了鍵盤消息,鼠標消息,系統消息,日誌等,別的對我們沒有什麼大的用處,我們只用攔截消息的回調函數就可以了,這個不會讓我寫例子吧。其實這個和上面的一樣,都是用SetWindowHook來寫的,看看就明白了很簡單的。 

  至於攔截了以後做什麼就是你的事情了,比如在每個定時器消息裏面處理一些我們的數據判斷,或者在定時器裏面在模擬一次定時器,那麼有些數據就會處理兩次,呵呵。後果嘛,不一定是好事情哦,呵呵,不過如果數據計算放在客戶端的遊戲就可以真的改變數據了,呵呵,試試看吧。用途還有很多,自己想也可以想出來的,呵呵。 

  2.3 攔截Socket包 

  這個技術難度要比原來的高很多。 

  首先我們要替換WinSock.DLL或者WinSock32.DLL,我們寫的替換函數要和原來的函數一致才行,就是說它的函數輸出什麼樣的,我們也要輸出什麼樣子的函數,而且參數,參數順序都要一樣才行,然後在我們的函數裏面調用真正的WinSock32.DLL裏面的函數就可以了。 

  首先:我們可以替換動態庫到系統路徑。 

  其次:我們應用程序啓動的時候可以加載原有的動態庫,用這個函數LoadLibary然後定位函數入口用GetProcAddress函數獲得每個真正Socket函數的入口地址。 

  當遊戲進行的時候它會調用我們的動態庫,然後從我們的動態庫中處理完畢後才跳轉到真正動態庫的函數地址,這樣我們就可以在裏面處理自己的數據了,應該是一切數據。呵呵,興奮吧,攔截了數據包我們還要分析之後才能進行正確的應答,不要以爲這樣工作就完成了,還早呢。等分析完畢以後我們還要仿真應答機制來和服務器通信,一個不小心就會被封號。 

  分析數據纔是工作量的來源呢,遊戲每次升級有可能加密方式會有所改變,因此我們寫外掛的人都是亡命之徒啊,被人愚弄了還不知道。 

  2.4 截獲API 

  上面的技術如果可以靈活運用的話我們就不用截獲API函數了,其實這種技術是一種補充技術。比如我們需要截獲Socket以外的函數作爲我們的用途,我們就要用這個技術了,其實我們也可以用它直接攔截在Socket中的函數,這樣更直接。 

  現在攔截API的教程到處都是,我就不列舉了,我用的比較習慣的方法是根據輸入節進行攔截的,這個方法可以用到任何一種操作系統上,比如Windows 98/2000等,有些方法不是跨平臺的,我不建議使用。這個技術大家可以參考《Windows核心編程》裏面的545頁開始的內容來學習,如果是Win98系統可以用“Windows系統奧祕”那個最後一章來學習。 


網絡遊戲外掛編寫基礎①

要想在修改遊戲中做到百戰百勝,是需要相當豐富的計算機知識的。有很多計算機高手就是從玩遊戲,修改遊戲中,逐步對計算機產生濃厚的興趣,逐步成長起來的。不要在羨慕別人能夠做到的,因爲別人能夠做的你也能夠!我相信你們看了本教程後,會對遊戲有一個全新的認識,呵呵,因爲我是個好老師!(別拿雞蛋砸我呀,救命啊!#¥%……*)   不過要想從修改遊戲中學到知識,增加自己的計算機水平,可不能只是靠修改遊戲呀! 要知道,修改遊戲只是一個驗證你對你所瞭解的某些計算機知識的理解程度的場所,只能給你一些發現問題、解決問題的機會,只能起到幫助你提高學習計算機的興趣的作用,而決不是學習計算機的捷徑。 

  一:什麼叫外掛? 

  現在的網絡遊戲多是基於Internet上客戶/服務器模式,服務端程序運行在遊戲服務器上,遊戲的設計者在其中創造一個龐大的遊戲空間,各地的玩家可以通過運行客戶端程序同時登錄到遊戲中。簡單地說,網絡遊戲實際上就是由遊戲開發商提供一個遊戲環境,而玩家們就是在這個環境中相對自由和開放地進行遊戲操作。那麼既然在網絡遊戲中有了服務器這個概念,我們以前傳統的修改遊戲方法就顯得無能爲力了。記得我們在單機版的遊戲中,隨心所欲地通過內存搜索來修改角色的各種屬性,這在網絡遊戲中就沒有任何用處了。因爲我們在網絡遊戲中所扮演角色的各種屬性及各種重要資料都存放在服務器上,在我們自己機器上(客戶端)只是顯示角色的狀態,所以通過修改客戶端內存裏有關角色的各種屬性是不切實際的。那麼是否我們就沒有辦法在網絡遊戲中達到我們修改的目的?回答是"否"。

  我們知道Internet客戶/服務器模式的通訊一般採用TCP/IP通信協議,數據交換是通過IP數據包的傳輸來實現的,一般來說我們客戶端向服務器發出某些請求,比如移動、戰鬥等指令都是通過封包的形式和服務器交換數據。那麼我們把本地發出消息稱爲SEND,意思就是發送數據,服務器收到我們SEND的消息後,會按照既定的程序把有關的信息反饋給客戶端,比如,移動的座標,戰鬥的類型。那麼我們把客戶端收到服務器發來的有關消息稱爲RECV。知道了這個道理,接下來我們要做的工作就是分析客戶端和服務器之間往來的數據(也就是封包),這樣我們就可以提取到對我們有用的數據進行修改,然後模擬服務器發給客戶端,或者模擬客戶端發送給服務器,這樣就可以實現我們修改遊戲的目的了。

  目前除了修改遊戲封包來實現修改遊戲的目的,我們也可以修改客戶端的有關程序來達到我們的要求。我們知道目前各個服務器的運算能力是有限的,特別在遊戲中,遊戲服務器要計算遊戲中所有玩家的狀況幾乎是不可能的,所以有一些運算還是要依靠我們客戶端來完成,這樣又給了我們修改遊戲提供了一些便利。比如我們可以通過將客戶端程序脫殼來發現一些程序的判斷分支,通過跟蹤調試我們可以把一些對我們不利的判斷去掉,以此來滿足我們修改遊戲的需求。 在下幾個章節中,我們將給大家講述封包的概念,和修改跟蹤客戶端的有關知識。大家準備好了嗎? 

  遊戲數據格式和存儲: 

  在進行我們的工作之前,我們需要掌握一些關於計算機中儲存數據方式的知識和遊戲中儲存數據的特點。本章節是提供給菜鳥級的玩家看的,如果你是高手就可以跳過了,如果,你想成爲無堅不摧的劍客,那麼,這些東西就會花掉你一些時間;如果,你只想作個江湖的遊客的話,那麼這些東西,瞭解與否無關緊要。是作劍客,還是作遊客,你選擇吧! 

  現在我們開始!首先,你要知道遊戲中儲存數據的幾種格式,這幾種格式是:字節(BYTE)、字(WORD)和雙字(DOUBLE WORD),或者說是8位、16位和32位儲存方式。字節也就是8位方式能儲存0~255的數字;字或說是16位儲存方式能儲存0~65535的數;雙字即32位方式能儲存0~4294967295的數。 

  爲何要了解這些知識呢?在遊戲中各種參數的最大值是不同的,有些可能100左右就夠了,比如,金庸羣俠傳中的角色的等級、隨機遇敵個數等等。而有些卻需要大於255甚至大於65535,象金庸羣俠傳中角色的金錢值可達到數百萬。所以,在遊戲中各種不同的數據的類型是不一樣的。在我們修改遊戲時需要尋找準備修改的數據的封包,在這種時候,正確判斷數據的類型是迅速找到正確地址的重要條件。 

  在計算機中數據以字節爲基本的儲存單位,每個字節被賦予一個編號,以確定各自的位置。這個編號我們就稱爲地址。 

  在需要用到字或雙字時,計算機用連續的兩個字節來組成一個字,連續的兩個字組成一個雙字。而一個字或雙字的地址就是它們的低位字節的地址。 現在我們常用的Windows 9x操作系統中,地址是用一個32位的二進制數表示的。而在平時我們用到內存地址時,總是用一個8位的16進制數來表示它。 

  二進制和十六進制又是怎樣一回事呢? 

  簡單說來,二進制數就是一種只有0和1兩個數碼,每滿2則進一位的計數進位法。同樣,16進制就是每滿十六就進一位的計數進位法。16進制有0--F十六個數字,它爲表示十到十五的數字採用了A、B、C、D、E、F六個數字,它們和十進制的對應關係是:A對應於10,B對應於11,C對應於12,D對應於13,E對應於14,F對應於15。而且,16進制數和二進制數間有一個簡單的對應關係,那就是;四位二進制數相當於一位16進制數。比如,一個四位的二進制數1111就相當於16進制的F,1010就相當於A。 

  瞭解這些基礎知識對修改遊戲有着很大的幫助,下面我就要談到這個問題。由於在計算機中數據是以二進制的方式儲存的,同時16進制數和二進制間的轉換關係十分簡單,所以大部分的修改工具在顯示計算機中的數據時會顯示16進制的代碼,而且在你修改時也需要輸入16進制的數字。你清楚了吧? 

  在遊戲中看到的數據可都是十進制的,在要尋找並修改參數的值時,可以使用Windows提供的計算器來進行十進制和16進制的換算,我們可以在開始菜單裏的程序組中的附件中找到它。 

  現在要了解的知識也差不多了!不過,有個問題在遊戲修改中是需要注意的。在計算機中數據的儲存方式一般是低位數儲存在低位字節,高位數儲存在高位字節。比如,十進制數41715轉換爲16進制的數爲A2F3,但在計算機中這個數被存爲F3A2。 

  看了以上內容大家對數據的存貯和數據的對應關係都瞭解了嗎? 好了,接下來我們要告訴大家在遊戲中,封包到底是怎麼一回事了,來!大家把袖口捲起來,讓我們來幹活吧! 
  二:什麼是封包? 

  怎麼截獲一個遊戲的封包?怎麼去檢查遊戲服務器的ip地址和端口號? Internet用戶使用的各種信息服務,其通訊的信息最終均可以歸結爲以IP包爲單位的信息傳送,IP包除了包括要傳送的數據信息外,還包含有信息要發送到的目的IP地址、信息發送的源IP地址、以及一些相關的控制信息。當一臺路由器收到一個IP數據包時,它將根據數據包中的目的IP地址項查找路由表,根據查找的結果將此IP數據包送往對應端口。下一臺IP路由器收到此數據包後繼續轉發,直至發到目的地。路由器之間可以通過路由協議來進行路由信息的交換,從而更新路由表。 

  那麼我們所關心的內容只是IP包中的數據信息,我們可以使用許多監聽網絡的工具來截獲客戶端與服務器之間的交換數據,下面就向你介紹其中的一種工具:WPE。 

  WPE使用方法:執行WPE會有下列幾項功能可選擇: 

  SELECT GAME選擇目前在記憶體中您想攔截的程式,您只需雙擊該程式名稱即可。 

  TRACE追蹤功能。用來追蹤擷取程式送收的封包。WPE必須先完成點選欲追蹤的程式名稱,纔可以使用此項目。 按下Play鍵開始擷取程式收送的封包。您可以隨時按下 | | 暫停追蹤,想繼續時請再按下 | | 。按下正方形可以停止擷取封包並且顯示所有已擷取封包內容。若您沒按下正方形停止鍵,追蹤的動作將依照OPTION裏的設定值自動停止。如果您沒有擷取到資料,試試將OPTION裏調整爲Winsock Version 2。WPE 及 Trainers 是設定在顯示至少16 bits 顏色下才可執行。 

  FILTER過濾功能。用來分析所擷取到的封包,並且予以修改。 

  SEND PACKET送出封包功能。能夠讓您送出假造的封包。 

  TRAINER MAKER製作修改器。 

  OPTIONS設定功能。讓您調整WPE的一些設定值。 

  FILTER的詳細教學 

  - 當FILTER在啓動狀態時 ,ON的按鈕會呈現紅色。- 當您啓動FILTER時,您隨時可以關閉這個視窗。FILTER將會保留在原來的狀態,直到您再按一次 on / off 鈕。- 只有FILTER啓用鈕在OFF的狀態下,纔可以勾選Filter前的方框來編輯修改。- 當您想編輯某個Filter,只要雙擊該Filter的名字即可。 

  NORMAL MODE: 

  範例: 

  當您在 Street Fighter Online ﹝快打旋風線上版?#123;遊戲中,您使用了兩次火球而且擊中了對方,這時您會擷取到以下的封包:SEND-> 0000 08 14 21 06 01 04 SEND-> 0000 02 09 87 00 67 FF A4 AA 11 22 00 00 00 00 SEND-> 0000 03 84 11 09 11 09 SEND-> 0000 0A 09 C1 10 00 00 FF 52 44 SEND-> 0000 0A 09 C1 10 00 00 66 52 44 

  您的第一個火球讓對方減了16滴﹝16 = 10h?#123;的生命值,而您觀察到第4跟第5個封包的位置4有10h的值出現,應該就是這裏了。 

  您觀察10h前的0A 09 C1在兩個封包中都沒改變,可見得這3個數值是發出火球的關鍵。 

  因此您將0A 09 C1 10填在搜尋列﹝SEARCH?#123;,然後在修改列﹝MODIFY?#123;的位置4填上FF。如此一來,當您再度發出火球時,FF會取代之前的10,也就是攻擊力爲255的火球了! 

  ADVANCED MODE: 

  範例: 當您在一個遊戲中,您不想要用真實姓名,您想用修改過的假名傳送給對方。在您使用TRACE後,您會發現有些封包裏面有您的名字出現。假設您的名字是Shadow,換算成16進位則是﹝53 68 61 64 6F 77?#123;;而您打算用moon﹝6D 6F 6F 6E 20 20?#123;來取代他。1) SEND-> 0000 08 14 21 06 01 042) SEND-> 0000 01 06 99 53 68 61 64 6F 77 00 01 05 3) SEND-> 0000 03 84 11 09 11 094) SEND-> 0000 0A 09 C1 10 00 53 68 61 64 6F 77 00 11 5) SEND-> 0000 0A 09 C1 10 00 00 66 52 44 

  但是您仔細看,您的名字在每個封包中並不是出現在相同的位置上 

  - 在第2個封包裏,名字是出現在第4個位置上- 在第4個封包裏,名字是出現在第6個位置上 

  在這種情況下,您就需要使用ADVANCED MODE- 您在搜尋些zSEARCH?#123;填上:53 68 61 64 6F 77 ﹝請務必從位置1開始填?#123;- 您想要從原來名字Shadow的第一個字母開始置換新名字,因此您要選擇從數值被發現的位置開始替代連續數值﹝from the position of the chain found?#123;。- 現在,在修改列﹝MODIFY?#123;000的位置填上:6D 6F 6F 6E 20 20 ﹝此爲相對應位置,也就是從原來搜尋欄的+001位置開始遞換?#123;- 如果您想從封包的第一個位置就修改數值,請選擇﹝from the beginning of the packet?#123; 

  瞭解一點TCP/IP協議常識的人都知道,互聯網是將信息數據打包之後再傳送出去的。每個數據包分爲頭部信息和數據信息兩部分。頭部信息包括數據包的發送地址和到達地址等。數據信息包括我們在遊戲中相關操作的各項信息。那麼在做截獲封包的過程之前我們先要知道遊戲服務器的IP地址和端口號等各種信息,實際上最簡單的是看看我們遊戲目錄下,是否有一個SERVER.INI的配置文件,這個文件裏你可以查看到個遊戲服務器的IP地址,比如金庸羣俠傳就是如此,那麼除了這個我們還可以在DOS下使用NETSTAT這個命令, 

  NETSTAT命令的功能是顯示網絡連接、路由表和網絡接口信息,可以讓用戶得知目前都有哪些網絡連接正在運作。或者你可以使用木馬客星等工具來查看網絡連接。工具是很多的,看你喜歡用哪一種了。 

  NETSTAT命令的一般格式爲:NETSTAT [選項] 

  命令中各選項的含義如下:-a 顯示所有socket,包括正在監聽的。-c 每隔1秒就重新顯示一遍,直到用戶中斷它。-i 顯示所有網絡接口的信息。-n 以網絡IP地址代替名稱,顯示出網絡連接情形。-r 顯示核心路由表,格式同"route -e"。-t 顯示TCP協議的連接情況。-u 顯示UDP協議的連接情況。-v 顯示正在進行的工作。 




網絡遊戲外掛編寫基礎②

三:怎麼來分析我們截獲的封包? 

  首先我們將WPE截獲的封包保存爲文本文件,然後打開它,這時會看到如下的數據(這裏我們以金庸羣俠傳裏PK店小二客戶端發送的數據爲例來講解): 

  第一個文件:SEND-> 0000 E6 56 0D 22 7E 6B E4 17 13 13 12 13 12 13 67 1BSEND-> 0010 17 12 DD 34 12 12 12 12 17 12 0E 12 12 12 9BSEND-> 0000 E6 56 1E F1 29 06 17 12 3B 0E 17 1ASEND-> 0000 E6 56 1B C0 68 12 12 12 5ASEND-> 0000 E6 56 02 C8 13 C9 7E 6B E4 17 10 35 27 13 12 12SEND-> 0000 E6 56 17 C9 12 

  第二個文件:SEND-> 0000 83 33 68 47 1B 0E 81 72 76 76 77 76 77 76 02 7ESEND-> 0010 72 77 07 1C 77 77 77 77 72 77 72 77 77 77 6DSEND-> 0000 83 33 7B 94 4C 63 72 77 5E 6B 72 F3SEND-> 0000 83 33 7E A5 21 77 77 77 3FSEND-> 0000 83 33 67 AD 76 CF 1B 0E 81 72 75 50 42 76 77 77SEND-> 0000 83 33 72 AC 77 

  我們發現兩次PK店小二的數據格式一樣,但是內容卻不相同,我們是PK的同一個NPC,爲什麼會不同呢? 原來金庸羣俠傳的封包是經過了加密運算纔在網路上傳輸的,那麼我們面臨的問題就是如何將密文解密成明文再分析了。 

  因爲一般的數據包加密都是異或運算,所以這裏先講一下什麼是異或。 簡單的說,異或就是"相同爲0,不同爲1"(這是針對二進制按位來講的),舉個例子,0001和0010異或,我們按位對比,得到異或結果是0011,計算的方法是:0001的第4位爲0,0010的第4位爲0,它們相同,則異或結果的第4位按照"相同爲0,不同爲1"的原則得到0,0001的第3位爲0,0010的第3位爲0,則異或結果的第3位得到0,0001的第2位爲0,0010的第2位爲1,則異或結果的第2位得到1,0001的第1位爲1,0010的第1位爲0,則異或結果的第1位得到1,組合起來就是0011。異或運算今後會遇到很多,大家可以先熟悉熟悉,熟練了對分析很有幫助的。 

  下面我們繼續看看上面的兩個文件,按照常理,數據包的數據不會全部都有值的,遊戲開發時會預留一些字節空間來便於日後的擴充,也就是說數據包裏會存在一些"00"的字節,觀察上面的文件,我們會發現文件一里很多"12",文件二里很多"77",那麼這是不是代表我們說的"00"呢?推理到這裏,我們就開始行動吧! 

  我們把文件一與"12"異或,文件二與"77"異或,當然用手算很費事,我們使用"M2M 1.0 加密封包分析工具"來計算就方便多了。得到下面的結果: 

  第一個文件:1 SEND-> 0000 F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09SEND-> 0010 05 00 CF 26 00 00 00 00 05 00 1C 00 00 00 892 SEND-> 0000 F4 44 0C E3 3B 13 05 00 29 1C 05 083 SEND-> 0000 F4 44 09 D2 7A 00 00 00 484 SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 005 SEND-> 0000 F4 44 05 DB 00 

  第二個文件:1 SEND-> 0000 F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09SEND-> 0010 05 00 70 6B 00 00 00 00 05 00 05 00 00 00 1A2 SEND-> 0000 F4 44 0C E3 3B 13 05 00 29 1C 05 843 SEND-> 0000 F4 44 09 D2 56 00 00 00 484 SEND-> 0000 F4 44 10 DA 01 B8 6C 79 F6 05 02 27 35 01 00 005 SEND-> 0000 F4 44 05 DB 00 

  哈,這一下兩個文件大部分都一樣啦,說明我們的推理是正確的,上面就是我們需要的明文! 

  接下來就是搞清楚一些關鍵的字節所代表的含義,這就需要截獲大量的數據來分析。 

  首先我們會發現每個數據包都是"F4 44"開頭,第3個字節是變化的,但是變化很有規律。我們來看看各個包的長度,發現什麼沒有?對了,第3個字節就是包的長度! 通過截獲大量的數據包,我們判斷第4個字節代表指令,也就是說客戶端告訴服務器進行的是什麼操作。例如向服務器請求戰鬥指令爲"30",戰鬥中移動指令爲"D4"等。 接下來,我們就需要分析一下上面第一個包"F4 44 1F 30 6C 79 F6 05 01 01 00 01 00 01 75 09 05 00 CF 26 00 00 00 00 05 00 1C 00 00 00 89",在這個包裏包含什麼信息呢?應該有通知服務器你PK的哪個NPC吧,我們就先來找找這個店小二的代碼在什麼地方。 我們再PK一個小嘍羅(就是大理客棧外的那個咯):SEND-> 0000 F4 44 1F 30 D4 75 F6 05 01 01 00 01 00 01 75 09SEND-> 0010 05 00 8A 19 00 00 00 00 11 00 02 00 00 00 C0 我們根據常理分析,遊戲裏的NPC種類雖然不會超過65535(FFFF),但開發時不會把自己限制在字的範圍,那樣不利於遊戲的擴充,所以我們在雙字裏看看。通過"店小二"和"小嘍羅"兩個包的對比,我們把目標放在"6C 79 F6 05"和"CF 26 00 00"上。(對比一下很容易的,但你不能太遲鈍咯,呵呵)我們再看看後面的包,在後面的包裏應該還會出現NPC的代碼,比如移動的包,遊戲允許觀戰,服務器必然需要知道NPC的移動座標,再廣播給觀戰的其他玩家。在後面第4個包"SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 00"裏我們又看到了"6C 79 F6 05",初步斷定店小二的代碼就是它了!(這分析裏邊包含了很多工作的,大家可以用WPE截下數據來自己分析分析) 

  第一個包的分析暫時就到這裏(裏面還有的信息我們暫時不需要完全清楚了) 

  我們看看第4個包"SEND-> 0000 F4 44 10 DA 01 DB 6C 79 F6 05 02 27 35 01 00 00",再截獲PK黃狗的包,(狗會出來2只哦)看看包的格式:SEND-> 0000 F4 44 1A DA 02 0B 4B 7D F6 05 02 27 35 01 00 00SEND-> 0010 EB 03 F8 05 02 27 36 01 00 00 

  根據上面的分析,黃狗的代碼爲"4B 7D F6 05"(100040011),不過兩隻黃狗服務器怎樣分辨呢?看看"EB 03 F8 05"(100140011),是上一個代碼加上100000,呵呵,這樣服務器就可以認出兩隻黃狗了。我們再通過野外遇敵截獲的數據包來證實,果然如此。 

  那麼,這個包的格式應該比較清楚了:第3個字節爲包的長度,"DA"爲指令,第5個字節爲NPC個數,從第7個字節開始的10個字節代表一個NPC的信息,多一個NPC就多10個字節來表示。 

  大家如果玩過網金,必然知道隨機遇敵有時會出現增援,我們就利用遊戲這個增援來讓每次戰鬥都會出現增援的NPC吧。 

  通過在戰鬥中出現增援截獲的數據包,我們會發現服務器端發送了這樣一個包:F4 44 12 E9 EB 03 F8 05 02 00 00 03 00 00 00 00 00 00 第5-第8個字節爲增援NPC的代碼(這裏我們就簡單的以黃狗的代碼來舉例)。 那麼,我們就利用單機代理技術來同時欺騙客戶端和服務器吧! 

  好了,呼叫NPC的工作到這裏算是完成了一小半,接下來的事情,怎樣修改封包和發送封包,我們下節繼續講解吧。 
  四:怎麼冒充"客戶端"向"服務器"發我們需要的封包? 

  這裏我們需要使用一個工具,它位於客戶端和服務器端之間,它的工作就是進行數據包的接收和轉發,這個工具我們稱爲代理。如果代理的工作單純就是接收和轉發的話,這就毫無意義了,但是請注意:所有的數據包都要通過它來傳輸,這裏的意義就重大了。我們可以分析接收到的數據包,或者直接轉發,或者修改後轉發,或者壓住不轉發,甚至僞造我們需要的封包來發送。 

  下面我們繼續講怎樣來同時欺騙服務器和客戶端,也就是修改封包和僞造封包。 通過我們上節的分析,我們已經知道了打多個NPC的封包格式,那麼我們就動手吧! 

  首先我們要查找客戶端發送的包,找到戰鬥的特徵,就是請求戰鬥的第1個包,我們找"F4 44 1F 30"這個特徵,這是不會改變的,當然是要解密後來查找哦。 找到後,表示客戶端在向服務器請求戰鬥,我們不動這個包,轉發。 繼續向下查找,這時需要查找的特徵碼不太好辦,我們先查找"DA",這是客戶端發送NPC信息的數據包的指令,那麼可能其他包也有"DA",沒關係,我們看前3個字節有沒有"F4 44"就行了。找到後,我們的工作就開始了! 

  我們確定要打的NPC數量。這個數量不能很大,原因在於網金的封包長度用一個字節表示,那麼一個包可以有255個字節,我們上面分析過,增加一個NPC要增加10個字節,所以大家算算就知道,打20個NPC比較合適。 

  然後我們要把客戶端原來的NPC代碼分析計算出來,因爲增加的NPC代碼要加上100000哦。再把我們增加的NPC代碼計算出來,並且組合成新的封包,注意代表包長度的字節要修改啊,然後轉發到服務器,這一步在編寫程序的時候要注意算法,不要造成較大延遲。 

  上面我們欺騙服務器端完成了,欺騙客戶端就簡單了。

  發送了上面的封包後,我們根據新增NPC代碼構造封包馬上發給客戶端,格式就是"F4 44 12 E9 NPC代碼 02 00 00 03 00 00 00 00 00 00",把每個新增的NPC都構造這樣一個包,按順序連在一起發送給客戶端,客戶端也就被我們騙過了,很簡單吧。 

  以後戰鬥中其他的事我們就不管了,盡情地開打吧。 



網絡遊戲通訊模型初探①
序言

  網絡遊戲,作爲遊戲與網絡有機結合的產物,把玩家帶入了新的娛樂領域。網絡遊戲在中國開始發展至今也僅有3,4年的歷史,跟已經擁有幾十年開發歷史的單機遊戲相比,網絡遊戲還是非常年輕的。當然,它的形成也是根據歷史變化而產生的可以說沒有互聯網的興起,也就沒有網絡遊戲的誕生。作爲新興產物,網絡遊戲的開發對廣大開發者來說更加神祕,對於一個未知領域,開發者可能更需要了解的是網絡遊戲與普通單機遊戲有何區別,網絡遊戲如何將玩家們連接起來,以及如何爲玩家提供一個互動的娛樂環境。本文就將圍繞這三個主題來給大家講述一下網絡遊戲的網絡互連實現方法。 
 網絡遊戲與單機遊戲

  說到網絡遊戲,不得不讓人聯想到單機遊戲,實際上網絡遊戲的實質脫離不了單機遊戲的製作思想,網絡遊戲和單機遊戲的差別大家可以很直接的想到:不就是可以多人連線嗎?沒錯,但如何實現這些功能,如何把網絡連線合理的融合進單機遊戲,就是我們下面要討論的內容。在瞭解網絡互連具體實現之前,我們先來了解一下單機與網絡遊戲它們各自的運行流程,只有瞭解這些,你才能深入網絡遊戲開發的核心。


現在先讓我們來看一下普通單機遊戲的簡化執行流程:

Initialize() // 初始化模塊
{
 初始化遊戲數據;
}
Game() // 遊戲循環部分
{
 繪製遊戲場景、人物以及其它元素;
 獲取用戶操作輸入;
 switch( 用戶輸入數據)
 {
  case 移動:
  {
   處理人物移動;
  }
  break;
  case 攻擊:
  {
   處理攻擊邏輯:
  }
  break;
  ...
  其它處理響應;
  ...
  default:
   break;
 }
 遊戲的NPC等邏輯AI處理;
}
Exit() // 遊戲結束
{
 釋放遊戲數據;
 離開遊戲;



  我們來說明一下上面單機遊戲的流程。首先,不管是遊戲軟件還是其他應用軟件,初始化部分必不可少,這裏需要對遊戲的數據進行初始化,包括圖像、聲音以及一些必備的數據。接下來,我們的遊戲對場景、人物以及其他元素進行循環繪製,把遊戲世界展現給玩家,同時接收玩家的輸入操作,並根據操作來做出響應,此外,遊戲還需要對NPC以及一些邏輯AI進行處理。最後,遊戲數據被釋放,遊戲結束。
  網絡遊戲與單機遊戲有一個很顯著的差別,就是網絡遊戲除了一個供操作遊戲的用戶界面平臺(如單機遊戲)外,還需要一個用於連接所有用戶,併爲所有用戶提供數據服務的服務器,從某些角度來看,遊戲服務器就像一個大型的數據庫,提供數據以及數據邏輯交互的功能。讓我們來看看一個簡單的網絡遊戲模型執行流程:



 客戶機:

Login()// 登入模塊
{
 初始化遊戲數據;
 獲取用戶輸入的用戶和密碼;
 與服務器創建網絡連接;
 發送至服務器進行用戶驗證;
 ...
 等待服務器確認消息;
 ...
 獲得服務器反饋的登入消息;
 if( 成立 )
  進入遊戲;
 else
  提示用戶登入錯誤並重新接受用戶登入;
}
Game()// 遊戲循環部分
{
 繪製遊戲場景、人物以及其它元素;
 獲取用戶操作輸入;
 將用戶的操作發送至服務器;
 ...
 等待服務器的消息;
 ...
 接收服務器的反饋信息;
 switch( 服務器反饋的消息數據 )
 {
  case 本地玩家移動的消息:
  {
   if( 允許本地玩家移動 )
    客戶機處理人物移動;
   else
    客戶機保持原有狀態;
  }
   break;
  case 其他玩家/NPC的移動消息:
  {
   根據服務器的反饋信息進行其他玩家或者NPC的移動處理;
  }
  break;
  case 新玩家加入遊戲:
  {
   在客戶機中添加顯示此玩家;
  }
   break;
  case 玩家離開遊戲:
  {
   在客戶機中銷燬此玩家數據;
  }
   break;
  ...
  其它消息類型處理;
  ... 
  default:
   break;
 }
}
Exit()// 遊戲結束
{
 發送離開消息給服務器;
 ...
 等待服務器確認;
 ...
 得到服務器確認消息;
 與服務器斷開連接;
 釋放遊戲數據;
 離開遊戲;



  服務器: 

Listen()  // 遊戲服務器等待玩家連接模塊
{
 ...
 等待用戶的登入信息;
 ...
 接收到用戶登入信息;
 分析用戶名和密碼是否符合;
 if( 符合 )
 {
  發送確認允許進入遊戲消息給客戶機; 
  把此玩家進入遊戲的消息發佈給場景中所有玩家;
  把此玩家添加到服務器場景中;
 }
 else
 {
  斷開與客戶機的連接; 
 }
}
Game() // 遊戲服務器循環部分
{
 ...
 等待場景中玩家的操作輸入;
 ...
 接收到某玩家的移動輸入或NPC的移動邏輯輸入; 
 // 此處只以移動爲例
 進行此玩家/NPC在地圖場景是否可移動的邏輯判斷;

 if( 可移動 )
 {
  對此玩家/NPC進行服務器移動處理;
  發送移動消息給客戶機;
  發送此玩家的移動消息給場景上所有玩家;
 }
 else
  發送不可移動消息給客戶機; 
}
Exit()  // 遊戲服務=器結束
{
 接收到玩家離開消息;
 將此消息發送給場景中所有玩家;
 發送允許離開的信息;
 將玩家數據存入數據庫;
 註銷此玩家在服務器內存中的數據;
}



  讓我們來說明一下上面簡單網絡遊戲模型的運行機制。先來講講服務器端,這裏服務器端分爲三個部分(實際上一個完整的網絡遊戲遠不止這些):登入模塊、遊戲模塊和登出模塊。登入模塊用於監聽網絡遊戲客戶端發送過來的網絡連接消息,並且驗證其合法性,然後在服務器中創建這個玩家並且把玩家帶領到遊戲模塊中; 遊戲模塊則提供給玩家用戶實際的應用服務,我們在後面會詳細介紹這個部分; 在得到玩家要離開遊戲的消息後,登出模塊則會把玩家從服務器中刪除,並且把玩家的屬性數據保存到服務器數據庫中,如: 經驗值、等級、生命值等。

  接下來讓我們看看網絡遊戲的客戶端。這時候,客戶端不再像單機遊戲一樣,初始化數據後直接進入遊戲,而是在與服務器創建連接,並且獲得許可的前提下才進入遊戲。除此之外,網絡遊戲的客戶端遊戲進程需要不斷與服務器進行通訊,通過與服務器交換數據來確定當前遊戲的狀態,例如其他玩家的位置變化、物品掉落情況。同樣,在離開遊戲時,客戶端會向服務器告知此玩家用戶離開,以便於服務器做出相應處理。


以上用簡單的僞代碼給大家闡述了單機遊戲與網絡遊戲的執行流程,大家應該可以清楚看出兩者的差別,以及兩者間相互的關係。我們可以換個角度考慮,網絡遊戲就是把單機遊戲的邏輯運算部分搬移到遊戲服務器中進行處理,然後把處理結果(包括其他玩家數據)通過遊戲服務器返回給連接的玩家。
  網絡互連

  在瞭解了網絡遊戲基本形態之後,讓我們進入真正的實際應用部分。首先,作爲網絡遊戲,除了常規的單機遊戲所必需的東西之外,我們還需要增加一個網絡通訊模塊,當然,這也是網絡遊戲較爲主要的部分,我們來討論一下如何實現網絡的通訊模塊。

  一個完善的網絡通訊模塊涉及面相當廣,本文僅對較爲基本的處理方式進行討論。網絡遊戲是由客戶端和服務器組成,相應也需要兩種不同的網絡通訊處理方式,不過也有相同之處,我們先就它們的共同點來進行介紹。我們這裏以Microsoft Windows 2000 [2000 Server]作爲開發平臺,並且使用Winsock作爲網絡接口(可能一些朋友會考慮使用DirectPlay來進行網絡通訊,不過對於當前在線遊戲,DirectPlay並不適合,具體原因這裏就不做討論了)。



  確定好平臺與接口後,我們開始進行網絡連接創建之前的一些必要的初始化工作,這部分無論是客戶端或者服務器都需要進行。讓我們看看下面的代碼片段: 

WORD wVersionRequested; 
WSADATAwsaData; 
wVersionRequested MAKEWORD(1, 1); 
if( WSAStartup( wVersionRequested, &wsaData ) !0 )
{
 Failed( WinSock Version Error!" );



  上面通過調用Windows的socket API函數來初始化網絡設備,接下來進行網絡Socket的創建,代碼片段如下:

SOCKET sSocket socket( AF_INET, m_lProtocol, 0 ); 
if( sSocket == INVALID_SOCKET ) 
{
 Failed( "WinSocket Create Error!" );



  這裏需要說明,客戶端和服務端所需要的Socket連接數量是不同的,客戶端只需要一個Socket連接足以滿足遊戲的需要,而服務端必須爲每個玩家用戶創建一個用於通訊的Socket連接。當然,並不是說如果服務器上沒有玩家那就不需要創建Socket連接,服務器端在啓動之時會生成一個特殊的Socket用來對玩家創建與服務器連接的請求進行響應,等介紹網絡監聽部分後會有更詳細說明。



  有初始化與創建必然就有釋放與刪除,讓我們看看下面的釋放部分:

if( sSocket != INVALID_SOCKET ) 
{
 closesocket( sSocket );
}
if( WSACleanup() != 0 )
{
 Warning( "Can't release Winsocket" ); 





  這裏兩個步驟分別對前面所作的創建初始化進行了相應釋放。

  接下來看看服務器端的一個網絡執行處理,這裏我們假設服務器端已經創建好一個Socket供使用,我們要做的就是讓這個Socket變成監聽網絡連接請求的專用接口,看看下面代碼片段: 

SOCKADDR_IN addr;
memset( &addr, 0, sizeof(addr) );
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl( INADDR_ANY );
addr.sin_port = htons( Port );  // Port爲要監聽的端口號
// 綁定socket
if( bind( sSocket, (SOCKADDR*)&addr, sizeof(addr) ) == SOCKET_ERROR )
{
 Failed( "WinSocket Bind Error!");
}
// 進行監聽
if( listen( sSocket, SOMAXCONN ) == SOCKET_ERROR )
{
 Failed( "WinSocket Listen Error!");



  這裏使用的是阻塞式通訊處理,此時程序將處於等待玩家用戶連接的狀態,倘若這時候有客戶端連接進來,則通過accept()來創建針對此玩家用戶的Socket連接,代碼片段如下: 

sockaddraddrServer; 
int nLen sizeof( addrServer ); 
SOCKET sPlayerSocket accept( sSocket, &addrServer, &nLen ); 
if( sPlayerSocket == INVALID_SOCKET )

 Failed( "WinSocket Accept Error!"); 



  這裏我們創建了sPlayerSocket連接,此後遊戲服務器與這個玩家用戶的通訊全部通過此Socket進行,到這裏爲止,我們服務器已經有了接受玩家用戶連接的功能,現在讓我們來看看遊戲客戶端是如何連接到遊戲服務器上,代碼片段如下: 

SOCKADDR_IN addr;
memset( &addr, 0, sizeof(addr) );
addr.sin_family = AF_INET;// 要連接的遊戲服務器端口號
addr.sin_addr.s_addr = inet_addr( IP );// 要連接的遊戲服務器IP地址,
addr.sin_port = htons( Port );//到此,客戶端和服務器已經有了通訊的橋樑,
//接下來就是進行數據的發送和接收:
connect( sSocket, (SOCKADDR*)&addr, sizeof(addr) );
if( send( sSocket, pBuffer, lLength, 0 ) == SOCKET_ERROR )
{
 Failed( "WinSocket Send Error!");



  這裏的pBuffer爲要發送的數據緩衝指針,lLength爲需要發送的數據長度,通過這支Socket API函數,我們無論在客戶端或者服務端都可以進行數據的發送工作,同時,我們可以通過recv()這支Socket API函數來進行數據接收: 

if( recv( sSocket, pBuffer, lLength, 0 ) == SOCKET_ERROR )
{
 Failed( "WinSocket Recv Error!"); 



  其中pBuffer用來存儲獲取的網絡數據緩衝,lLength則爲需要獲取的數據長度。

  現在,我們已經瞭解了一些網絡互連的基本知識,但作爲網絡遊戲,如此簡單的連接方式是無法滿足網絡遊戲中百人千人同時在線的,我們需要更合理容錯性更強的網絡通訊處理方式,當然,我們需要先了解一下網絡遊戲對網絡通訊的需求是怎樣的。

rning收集整理(請*勿刪除)


網絡遊戲通訊模型初探②

大家知道,遊戲需要不斷循環處理遊戲中的邏輯並進行遊戲世界的繪製,上面所介紹的Winsock處理方式均是以阻塞方式進行,這樣就違背了遊戲的執行本質,可以想象,在客戶端連接到服務器的過程中,你的遊戲不能得到控制,這時如果玩家想取消連接或者做其他處理,甚至顯示一個最基本的動態連接提示都不行。

  所以我們需要用其他方式來處理網絡通訊,使其不會與遊戲主線相沖突,可能大家都會想到: 創建一個網絡線程來處理不就可以了?沒錯,我們可以創建一個專門用於網絡通訊的子線程來解決這個問題。當然,我們遊戲中多了一個線程,我們就需要做更多的考慮,讓我們來看看如何創建網絡通訊線程。

  在Windows系統中,我們可以通過CreateThread()函數來進行線程的創建,看看下面的代碼片段: 

DWORD dwThreadID;
HANDLE hThread = CreateThread( NULL, 0, NetThread/*網絡線程函式*/, sSocket, 0, &dwThreadID );
if( hThread == NULL )
{
 Failed( "WinSocket Thread Create Error!");
}


  這裏我們創建了一個線程,同時將我們的Socket傳入線程函數: 

DWORD WINAPINetThread(LPVOID lParam) 



 SOCKET sSocket (SOCKET)lParam; 
 ... 
 return 0; 




  NetThread就是我們將來用於處理網絡通訊的網絡線程。那麼,我們又如何把Socket的處理引入線程中?

  看看下面的代碼片段: 

HANDLE hEvent;
hEvent = CreateEvent(NULL,0,0,0);
// 設置異步通訊
if( WSAEventSelect( sSocket, hEvent,
FD_ACCEPT|FD_CONNECT|FD_READ|FD_WRITE|FD_CLOSE ) ==SOCKET_ERROR )
{
 Failed( "WinSocket EventSelect Error!");



  通過上面的設置之後,WinSock API函數均會以非阻塞方式運行,也就是函數執行後會立即返回,這時網絡通訊會以事件方式存儲於hEvent,而不會停頓整支程式。

  完成了上面的步驟之後,我們需要對事件進行響應與處理,讓我們看看如何在網絡線程中獲得網絡通訊所產生的事件消息: 

WSAEnumNetworkEvents( sSocket, hEvent, &SocketEvents );
if( SocketEvents.lNetworkEvents != 0 )

 switch( SocketEvents.lNetworkEvents ) 
 { 
  case FD_ACCEPT:
   WSANETWORKEVENTS SocketEvents;
   break; 
  case FD_CONNECT: 
  { 
   if( SocketEvents.iErrorCode[FD_CONNECT_BIT] == 0) 
   // 連接成功  
   {
   // 連接成功後通知主線程(遊戲線程)進行處理 
   } 
  } 
   break; 
  case FD_READ: 
  // 獲取網絡數據 
  {
   if( recv( sSocket, pBuffer, lLength, 0) == SOCKET_ERROR ) 
   { 
    Failed( "WinSocket Recv Error!"); 
   } 
  } 
   break; 
  case FD_WRITE: 
   break; 
  case FD_CLOSE:
   // 通知主線程(遊戲線程), 網絡已經斷開 
   break; 
  default: 
   break; 
 }



  這裏僅對網絡連接(FD_CONNECT) 和讀取數據(FD_READ) 進行了簡單模擬操作,但實際中網絡線程接收到事件消息後,會對數據進行組織整理,然後再將數據回傳給我們的遊戲主線程使用,遊戲主線程再將處理過的數據發送出去,這樣一個往返就構成了我們網絡遊戲中的數據通訊,是讓網絡遊戲動起來的最基本要素。

  最後,我們來談談關於網絡數據包(數據封包)的組織,網絡遊戲的數據包是遊戲數據通訊的最基本單位,網絡遊戲一般不會用字節流的方式來進行數據傳輸,一個數據封包也可以看作是一條消息指令,在遊戲進行中,服務器和客戶端會不停的發送和接收這些消息包,然後將消息包解析轉換爲真正所要表達的指令意義並執行。
 互動與管理

  說到互動,對於玩家來說是與其他玩家的交流,但對於計算機而言,實現互動也就是實現數據消息的相互傳遞。前面我們已經瞭解過網絡通訊的基本概念,它構成了互動的最基本條件,接下來我們需要在這個網絡層面上進行數據的通訊。遺憾的是,計算機並不懂得如何表達玩家之間的交流,因此我們需要提供一套可讓計算機瞭解的指令組織和解析機制,也就是對我們上面簡單提到的網絡數據包(數據封包)的處理機制。


爲了能夠更簡單的給大家闡述網絡數據包的組織形式,我們以一個聊天處理模塊來進行討論,看看下面的代碼結構: 

struct tagMessage{ 
 long lType; 
 long lPlayerID; 
}; 
// 消息指令
// 指令相關的玩家標識 
char strTalk[256]; // 消息內容 


  上面是抽象出來的一個極爲簡單的消息包結構,我們先來談談其各個數據域的用途:

  首先,lType 是消息指令的類型,這是最爲基本的消息標識,這個標識用來告訴服務器或客戶端這條指令的具體用途,以便於服務器或客戶端做出相應處理。lPlayerID 被作爲玩家的標識。大家知道,一個玩家在機器內部實際上也就是一堆數據,特別是在遊戲服務器中,可能有成千上萬個玩家,這時候我們需要一個標記來區分玩家,這樣就可以迅速找到特定玩家,並將通訊數據應用於其上。

  strTalk 是我們要傳遞的聊天數據,這部分纔是真正的數據實體,前面的參數只是數據實體應用範圍的限定。

  在組織完數據之後,緊接着就是把這個結構體數據通過Socket 連接發送出去和接收進來。這裏我們要了解,網絡在進行數據傳輸過程中,它並不關心數據採用的數據結構,這就需要我們把數據結構轉換爲二進制數據碼進行發送,在接收方,我們再將這些二進制數據碼轉換回程序使用的相應數據結構。讓我們來看看如何實現: 

tagMessageMsg; 
Msg.lTypeMSG_CHAT; 
Msg.lPlayerID 1000; 
strcpy( &Msg.strTalk, "聊天信息" ); 


  首先,我們假設已經組織好一個數據包,這裏MSG_CHAT 是我們自行定義的標識符,當然,這個標識符在服務器和客戶端要統一。玩家的ID 則根據遊戲需要來進行設置,這裏1000 只作爲假設,現在繼續: 

char* p = (char*)&Msg;
long lLength = sizeof( tagMessage ); 
send( sSocket, p, lLength );
// 獲取數據結構的長度 


  我們通過強行轉換把結構體轉變爲char 類型的數據指針,這樣就可以通過這個指針來進行流式數據處理,這裏通過sizeof() 獲得結構體長度,然後用WinSock 的Send() 函數將數據發送出去。

  接下來看看如何接收數據: 

long lLength = sizeof( tagMessage );
char* Buffer = new char[lLength];
recv( sSocket, Buffer, lLength );
tagMessage* p = (tagMessage*)Buffer;
// 獲取數據 


  在通過WinSock 的recv() 函數獲取網絡數據之後,我們同樣通過強行轉換把獲取出來的緩衝數據轉換爲相應結構體,這樣就可以方便地對數據進行訪問。(注:強行轉換僅僅作爲數據轉換的一種手段,實際應用中有更多可選方式,這裏只爲簡潔地說明邏輯)談到此處,不得不提到服務器/ 客戶端如何去篩選處理各種消息以及如何對通訊數據包進行管理。無論是服務器還是客戶端,在收到網絡消息的時候,通過上面的數據解析之後,還必須對消息類型進行一次篩選和派分,簡單來說就是類似Windows 的消息循環,不同消息進行不同處理。這可以通過一個switch 語句(熟悉Windows 消息循環的朋友相信已經明白此意),基於消
息封包裏的lType 信息,對消息進行區分處理,考慮如下代碼片段: 

switch( p->lType ) // 這裏的p->lType爲我們解析出來的消息類型標識
{
 case MSG_CHAT: // 聊天消息
  break;
 case MSG_MOVE: // 玩家移動消息
  break;
 case MSG_EXIT: // 玩家離開消息
  break;
 default:
  break;



  上面片段中的MSG_MOVE 和MSG_EXIT 都是我們虛擬的消息標識(一個真實遊戲中的標識可能會有上百個,這就需要考慮優化和優先消息處理問題)。此外,一個網絡遊戲服務器面對的是成百上千的連接用戶,我們還需要一些合理的數據組織管理方式來進行相關處理。普通的單體遊戲服務器,可能會因爲當機或者用戶過多而導致整個遊戲網絡癱瘓,而這也就引入分組服務器機制,我們把服務器分開進行數據的分佈式處理。

  我們把每個模塊提取出來,做成專用的服務器系統,然後建立一個連接所有服務器的數據中心來進行數據交互,這裏每個模塊均與數據中心創建了連接,保證了每個模塊的相關性,同時玩家轉變爲與當前提供服務的服務器進行連接通訊,這樣就可以緩解單獨一臺服務器所承受的負擔,把壓力分散到多臺服務器上,同時保證了數據的統一,而且就算某臺服務器因爲異常而當機也不會影響其他模塊的遊戲玩家,從而提高了整體穩定性。

  分組式服務器緩解了服務器的壓力,但也帶來了服務器調度問題,分組式服務器需要對服務器跳轉進行處理,就以一個玩家進行遊戲場景跳轉作爲討論基礎:假設有一玩家處於遊戲場景A,他想從場景A 跳轉到場景B,在遊戲中,我們稱之場景切換,這時玩家就會觸發跳轉需求,比如走到了場景中的切換點,這樣服務器就把玩家數據從"遊戲場景A 服務器"刪除,同時在"遊戲場景B 服務器"中把玩家建立起來。 

  這裏描述了場景切換的簡單模型,當中處理還有很多步驟,不過通過這樣的思考相信大家可以派生出很多應用技巧。不過需要注意的是,在場景切換或者說模塊間切換的時候,需要切實考慮好數據的傳輸安全以及邏輯合理性,否則切換很可能會成爲將來玩家複製物品的橋樑。 



  總結

  本篇講述的都是通過一些簡單的過程來進行網絡遊戲通訊,提供了一個製作的思路,雖然具體實現起來還有許多要做,但只要順着這個思路去擴展、去完善,相信大家很快就能夠編寫出自己的網絡通訊模塊。由於時間倉促,本文在很多細節方面都有省略,文中若有錯誤之處也望大家見諒。


go*odmorning收集整理(請勿刪除)

遊戲外掛設計技術探討①

一、 前言 

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

  不管遊戲外掛程序是不是“合法”身份,但是它卻是具有一定的技術含量的,在這些小小程序中使用了許多高端技術,如攔截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的實現過程,至於如何加入外掛控制代碼,還需要外掛開發人員對遊戲數據包結構、內容、加密算法等方面的仔細分析(這個過程將是一個艱辛的過程),再生成外掛控制代碼。關於數據包分析方法和技巧,不是本文講解的範圍,如您感興趣可以到網上查查相關資料。

g*oodmorning收集整理(請勿刪除)

遊戲外掛設計技術探討③

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].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函數的操作,看看信息框是不是有所變化。同樣,如此程序正在接收網絡數據包,就可以實現封包功能了。

  六、結束語

  除了以上介紹的幾種遊戲外掛程序常用的技術以外,在一些外掛程序中還使用了遊戲數據修改技術、遊戲加速技術等。在這篇文章裏,就不逐一介紹了。 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章