遊戲外掛基本原理及實現

遊戲外掛基本原理及實現html

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

  一、遊戲外掛的原理算法

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

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

  2 技術分析部分小程序

  2.1 模擬鍵盤或鼠標的響應windows

  咱們通常使用:api

  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收集整理(請勿刪除)

遊戲外掛設計技術探討①

1、 前言

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

  無論遊戲外掛程序是否是「合法」身份,可是它倒是具備必定的技術含量的,在這些小小程序中使用了許多高端技術,如攔截Sock技術、攔截API技術、模擬鍵盤與鼠標技術、直接修改程序內存技術等等。本文將對常見的遊戲外掛中使用的技術進行全面剖析。

  2、認識外掛

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

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

  遊戲外掛的設計主要是針對於某個遊戲開發的,咱們能夠根據它針對的遊戲的類型可大體可將外掛分爲兩種大類。

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

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

  另外,如今不少外掛程序功能強大,不只實現了自動動做代理和封包功能,並且還提供了對網絡遊戲的客戶端程序的數據進行修改,以達到欺騙網絡遊戲服務器的目的。我相信,隨着網絡遊戲商家的反外掛技術的進展,遊戲外掛將會產生更多更優秀的技術,讓咱們期待着看場技術大戰吧......

  3、外掛技術綜述

  能夠將開發遊戲外掛程序的過程大致上劃分爲兩個部分:

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

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

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

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

  ● 封包技術:主要包括擋截Sock技術和擋截API技術。
4、動做模擬技術

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

  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)
  5、封包技術

  經過對動做模擬技術的介紹,咱們對遊戲外掛有了必定程度上的認識,也學會了使用動做模擬技術來實現簡單的動做模擬型遊戲外掛的製做。這種動做模擬型遊戲外掛有必定的侷限性,它僅僅只能解決使用計算機代替人力完成那麼有規律、繁瑣而無聊的遊戲動做。可是,隨着網絡遊戲的盛行和複雜度的增長,不少遊戲要求將客戶端動做信息及時反饋回服務器,經過服務器對這些動做信息進行有效認證後,再向客戶端發送下一步遊戲動做信息,這樣動做模擬技術將失去原有的效應。爲了更好地「外掛」這些遊戲,遊戲外掛程序也進行了升級換代,它們將之前針對遊戲用戶界面層的模擬推動到數據通信層,經過封包技術在客戶端擋截遊戲服務器發送來的遊戲控制數據包,分析數據包並修改數據包;同時還需按照遊戲數據包結構建立數據包,再模擬客戶端發送給遊戲服務器,這個過程其實就是一個封包的過程。

  封包的技術是實現第二類遊戲外掛的最核心的技術。封包技術涉及的知識很普遍,實現方法也不少,如擋截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函數的操做,看看信息框是否是有所變化。一樣,如此程序正在接收網絡數據包,就能夠實現封包功能了。

  6、結束語

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

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/wgm001/archive/2007/11/25/1901372.aspx

再分享一下我老師大神的人工智能教程吧。零基礎!通俗易懂!風趣幽默!還帶黃段子!但願你也加入到咱們人工智能的隊伍中來!https://blog.csdn.net/jiangjunshow

相關文章
相關標籤/搜索