本章旨在講解外掛實現原理,未深入涉及至代碼層面。希望能與對這方面感興趣的朋友多多交流,畢竟理論是死的,套路是固定的,只有破解經驗是花大量時間和心血積累的。
- 對於單機游戲而言,游戲中絕大部分的參數(比如血、藍、能量亦或是金幣)都存儲在計算機的堆棧中,一些類似劇情進度的則加密后寫入本地的自定義配置文件中;
- 對於頁游、網游和手游,雖然服務器保存了大量的重要的參數,但由於客戶端不可避免的需要進行大量的計算和資源的加載,本地內存種必定存有部分的臨時變量,通過判斷這些變量的變化規律和函數的破密尋到利於自身的參數,比如傷害值一類,繼而尋找該變量的內存地址,根據指針偏移分析獲得內存基址,再提升權限利用Windows API把自定義數值寫入該內存塊,就完成了修改某項數值的操作,一般來說,只要破解了一項數值,利用規律繼而破解其他數值就更加容易了。
一般套路就是上述,一些防護性強大的游戲會在上述的每一步中都設置難題,等着我們去破解。
在此之前,我們來了解一些基礎知識:
- 數據類型:游戲中的血量、藍、生命值,我們將他們稱之為變量,變量包含了變量名稱和數據類型,那么它的名字就是"血量、生命值"等等,而它的數據類型決定了數值以何種方式存儲到計算機的內存中,想要找到自身需要的變量,如果可以確定這個變量的數據類型或者有根據的推測來縮小變量的數據類型范圍,那么對於快速定位該變量是具有幫助性的。以下是幾種常見的變量類型:
整數型:游戲中比如血量、法力可能用到這種類型。
字節型:根據不同的編輯器,1個整形占用N個字節(N>1),一般很早之前出產的GBA游戲為了節省開銷會用到這種類型。
浮點型:帶有小數點的數字,如果金幣或者傷害值帶有小數點,那很可能是這種數據類型保存的。
文本型:比如世界喊話,人物命名,一般采用文本型保存這類數據。
推薦閱讀《數據結構》相關書籍更好的熟悉數據結構,有編程經驗的就不必多說了。
- 進程:每一個應用程序/游戲啟動,都會產生至少一個進程Process,在任務管理器里可以看到進程名稱和進程PID。
- 句柄:英文HANDLE,一個整數值。數據的地址需要變動,變動以后就需要有人來記錄管理變動,就好像戶籍管理一樣,因此系統用句柄來記載數據地址的變更。肉眼看到的一個個文件夾,窗口,按鈕,圖標,應用程序能夠通過句柄訪問相應的對象的信息
推薦閱讀《計算機操作系統》相關書籍更多的了解計算機原理。
進入正題,根據目的反向推導下需要得到哪些信息,我們最終才能想要實現修改數值。
修改變量的數值→得先找到存儲變量的內存地址→得先找到游戲窗口的句柄→得先找到游戲的進程。
根據什么依據推導出的路線呢?
Windows系統庫的kernel32.dll庫文件中包含了內存操作的API,其中VirtualQueryEx用於查詢地址空間中內存地址的信息。
函數原型:
/// <summary> /// 查詢地址空間中內存地址存儲的信息 /// </summary> /// <param name="hProcess">句柄</param> /// <param name="lpAddress">內存地址</param> /// <param name="lpBuffer">結構體指針</param> /// <param name="dwLength">結構體大小</param> /// <returns>寫入字節數</returns> [DllImport("kernel32.dll")] public static extern int VirtualQueryEx( IntPtr hProcess, IntPtr lpAddress, out MEMORY_BASIC_INFORMATION lpBuffer, int dwLength);
我們下面逐個分析參數:
hProcess:顧名思義,進程句柄,也就是說想要查詢地址存放的信息,首先得獲得進程的句柄。
lpAddress:查詢的內存地址,輸入參數,需要主動提供地址。但我們並不知道我們需要的數值它被存放的地址,我們只能一個頁面一個頁面的猜測,直到掃描到某個頁面的某塊內存里面存放的信息正是與我們需要的信息一致或是存在一定的函數關系。其次,對於同一個數值也許出現在多個地方,比如 在某個時間區間人物的攻擊數值和防御數值等同都為1200,那么說明至少有兩個地址存放了這個數據。我們需要把這些地址全都篩選出來。
lpBuffer:結構體指針,用於存放內存信息。
dwLength:上述結構體的大小。
返回值:函數寫入lpBuffer的字節數,如果字節數等於結構體PMEMORY_BASIC_INFORMATION的大小,表示函數執行成功。
但此函數只負責獲取內存信息,而查詢內存信息中具體存放數值則用到另一函數ReadProcessMemory,來看一下函數原型:
/// <summary> /// 根據進程句柄讀入該進程的某個內存空間 /// </summary> /// <param name="lpProcess">進程句柄</param> /// <param name="lpBaseAddress">內存讀取的起始地址</param> /// <param name="lpBuffer">寫入地址</param> /// <param name="nSize">寫入字節數</param> /// <param name="BytesRead">實際傳遞的字節數</param> /// <returns>讀取結果</returns> [DllImportAttribute("kernel32.dll", EntryPoint = "ReadProcessMemory")] public static extern bool ReadProcessMemory ( IntPtr lpProcess, IntPtr lpBaseAddress, IntPtr lpBuffer, int nSize, IntPtr BytesRead );
此函數將根據句柄讀取該進程的某個內存空間,並將讀取到的字節數寫入到我們開辟的一塊空間中,而此空間存放的正是我們苦苦追尋的“有意義的數值”。此函數的部分參數依賴於上一個函數VirtualQueryEx產生的結果。
根據上面的API,先來看怎么獲取第一個參數:窗體句柄,同樣的kernel32.dll提供了名為OpenProcess的函數用來打開一個已存在的進程對象,並返回進程的句柄。
函數原型:
/// <summary> /// 打開一個已存在的進程對象,並返回進程的句柄 /// </summary> /// <param name="iAccess">渴望得到的訪問權限</param> /// <param name="Handle">是否繼承句柄</param> /// <param name="ProcessID">進程PID</param> /// <returns>進程的句柄</returns> [DllImportAttribute("kernel32.dll", EntryPoint = "OpenProcess")] public static extern IntPtr OpenProcess ( int iAccess, bool Handle, int ProcessID );
dwDesiredAccess :渴望得到的訪問權限,這里默認填寫PROCESS_ALL_ACCESS | 0x1F0FFF 給予所有可能允許的權限。
bInheritHandle :是否繼承句柄,FALSE即可。
dwProcessId :進程標示符PID。
對於進程PID各種編程語言有自己的獲取方式,以C#語言為例(針對非多開客戶端):
//根據進程名稱獲取PID public int GetPIDByPName(string ProcessName) { Process[] ArrayProcess = Process.GetProcessesByName(processName); foreach (Process pro in ArrayProcess) { return pro.Id; } return -1; }
我們獲取到進程的PID以后,就可以調用OpenProcess獲取窗體的句柄,然后利用函數VirtualQueryEx遍歷內存查找地址信息,根據地址利用ReadProcessMemory查找具體存放的值,最后利用WriteProcessMemory把修改后的值寫入該地址,這樣就完成了一次數據的修改。來看一下API的函數原型:
/// <summary> /// 寫入某一進程的內存區域 /// </summary> /// <param name="lpProcess">進程句柄</param> /// <param name="lpBaseAddress">寫入的內存首地址</param> /// <param name="lpBuffer">寫入數據的指針</param> /// <param name="nSize">寫入字節數</param> /// <param name="BytesWrite">實際寫入字節數的指針</param> /// <returns>大於0代表成功</returns> [DllImportAttribute("kernel32.dll", EntryPoint = "WriteProcessMemory")] public static extern bool WriteProcessMemory ( IntPtr lpProcess, IntPtr lpBaseAddress, int [] lpBuffer, int nSize, IntPtr BytesWrite );
參數就不再一一解釋了,注釋都有,最后一個參數默認填寫Null或者IntPtr.ZeroI即可。
到了這里,修改游戲數值的原理和套路已經很明白了,甚至脫離游戲來講,任何的應用如果沒有對緩存中的數據進行良好的加密,都是存在很大的風險隱患的,這一章節主要了解一些常用到的名詞和API的運用。具體如何利用代碼進行調用API,以及更詳細的剖析每一步的邏輯,將在下一章節講述。
PS:轉載請附帶原文路徑:http://www.cnblogs.com/lene-y/p/7096485.html 。