本章旨在講解外掛實現原理,未深入涉及至代碼層面。希望能與對這方面感興趣的朋友多多交流,畢竟理論是死的,套路是固定的,只有破解經驗是花大量時間和心血積累的。 對於單機游戲而言,游戲中絕大部分的參數(比如血、藍、能量亦或是金幣)都存儲在電腦的堆棧中,一些類似劇情進度的則加密後寫入本地的自定義配置文件中 ...
本章旨在講解外掛實現原理,未深入涉及至代碼層面。希望能與對這方面感興趣的朋友多多交流,畢竟理論是死的,套路是固定的,只有破解經驗是花大量時間和心血積累的。
- 對於單機游戲而言,游戲中絕大部分的參數(比如血、藍、能量亦或是金幣)都存儲在電腦的堆棧中,一些類似劇情進度的則加密後寫入本地的自定義配置文件中;
- 對於頁游、網游和手游,雖然伺服器保存了大量的重要的參數,但由於客戶端不可避免的需要進行大量的計算和資源的載入,本地記憶體種必定存有部分的臨時變數,通過判斷這些變數的變化規律和函數的破密尋到利於自身的參數,比如傷害值一類,繼而尋找該變數的記憶體地址,根據指針偏移分析獲得記憶體基址,再提升許可權利用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 ,我已委托“維權騎士”為我的文章進行維權行動。
歡迎關註微信公眾號[游戲外掛原理解析與製作],對本文有不理解的地方或者不同的觀點可以給我留言,一定回覆。