電子遊戲不僅吸引玩家,還吸引開發者製作各種工具,如資料監視器、資料修改器和外觀修改器
這些工具能夠讓遊戲即使失去官方支援後,仍然保持玩家的熱情
然而,由於許多電子遊戲,尤其是電腦遊戲,主要在 Windows 上運行
因此,普遍工具通常只能在 Windows 平台運作,其他平台往往缺乏相應的工具
故此,在下打算了解這些工具的運作原理,並嘗試自行製作類似的工具能在 Linux 上運作
使用 Game Conqueror
Game Conqueror 是能在 Linux 上運作的 記憶體資料修改工具
界面與能在 Windows 及 Mac OS 上運作的 Cheat Engine 相似
不過功能較少,但操作仍然簡單
在下使用 Retroarch 載入 Bio Hazard 進行測試
在 Game Conqueror 中選擇目標程式
將 Data Type 設定為 int,並將 Search Scope 設定為 Full
一般情況下,遊戲顯示的資料都是整數,但如果不確定資料長度,通常會使用 int
如果確認資料的長度,例如 8位元 長度,可以指定使用 int8,這樣搜尋速度會更快且更準確
在 Value 輸入需要搜尋的數值
(例如遊戲中,手槍有 15發子彈,輸入 15)
Game Conqueror 會根據搜尋類型、數值及範圍,顯示搜尋結果
通常第一次搜尋不會過濾到目標地址,例如第一次搜尋可能會找到 超過20萬筆相關資料
由於搜尋結果太多,不可能測試 超過20萬筆資料
因此通常都需要搜尋多次,將搜尋結果的資料減少
而要將搜尋結果的數量減少,是 將遊戲中相關的資料改變再搜尋來追蹤
直到相關資料的搜尋結果能夠減少到可接受的測試數量
選取需要測試的項目,然後加入後,更改 Value 的數值
如果遊戲的資料被修改,則修改的項目應對應到遊戲的內容
使用 ScanMem
Game Conqueror 背後實際上是使用 ScanMem,可以將 ScanMem 視為指令操作的 Game Conqueror
而且 ScanMem 的效能更高,開啟 Terminal 並輸入:
1 | sudo scanmem -p <PID> |
便會開始偵測指定程式的資料
與 Game Conqueror 相同,輸入需要搜尋的數值
而且 ScanMem 還會顯示搜尋地址的範圍
當搜尋結果的數量達到可接受的數量時,便可以輸入:
1 | list |
列出過濾後的地址資料
確認需要更改資料的地址後,輸入:
1 | write < type > <address> <value> |
可以將資料寫入到指定地址
- type 為資料類型,可以使用:
- int8 為 8位元整數,範圍 0 至 255
- int16 為 16位元整數,範圍 0 至 65535
- int32 為 32位元整數,範圍 0 至 16777216
- int64 為 64位元整數,範圍 0 至 4294967296
- float32 為 32位元浮點數,範圍約 1.4E−45 至 3.4E+38
- float64 為 64位元浮點數,範圍約 5.0E−324 至 1.8E+308
- bytearray 為 位元組陣列,範圍為十六進制 00 至 FF
- string 為 字串
- address 為 十六進制地址
- value 為 修改數值;當使用 type 為 bytearray 時,value 必須為 2位十六進制數值,並使用空格分隔下一組數值
同樣可以將資料修改
使用 Bash
讀取資料
1 2 3 4 5 6 7 8 9 10 11 12 13 | pid= "target-process-id" size "1" address= "address-from-process" sudo dd if = "/proc/${pid}/mem" bs= "1" count= "${size}" skip=$(($address)) 2> "/dev/null" \ | xxd -p \ | sed -r 's/([0-9a-f]{2})/\1 /g' \ | awk -F ' ' '{ sum = 0; for (i = 1; i <= NF; i++) { sum += strtonum( "0x" $i) * (256 ^ (i - 1)); } print sum ; }' |
- pid 為 設定要讀取的目標程序的 ID
- size 為 設定要讀取的數量
- address 為 指定要從程序記憶體中讀取的地址
修改為閣下需要的 pid 、 size 、 address 來讀取程序記憶體中的地址中的資料
寫入資料
1 2 3 4 | pid= "target-process-id" address= "address-from-process" value= "" printf "${value}" | sudo dd of= "/proc/${pid}/mem" bs= "1" seek=$(($address)) conv=notrunc |
- pid 為 設定要讀取的目標程序的 ID
- address 為 指定要從程序記憶體中十寫入的地址
- value 為 設定要寫入的數值
修改為閣下需要的 pid 、 address 、 value 來寫入資料到程序記憶體中的地址中
(胡亂修改程序記憶體資料,有可能導致程序錯誤)
雖然沒有規定,但 value 的數值最好使用 十六進制 寫法 ,即是 \xXX
另外數值長度同樣與能保持 8位元 、 16位元 、 32位元 、 64位元 的長度,避免有不確定問題出現
使用 Bash 修改程序記憶體的資料
ScamMem 能夠手動搜尋指定地址及需要獲取的資料的長度,輸入:
1 | dump <address> <length> |
手動搜尋指定地址的資料
- address 為 十六進制地址
- length 為 需要搜尋的資料的長度
手動搜尋的結果會顯示類似 xxd 的格式,因此建議搜尋的資料長度為 16的倍數,這樣會比較容易觀察
使用 Python
讀取資料
1 2 3 4 5 6 7 8 9 | import sys pid = # Process ID size = 1 address = # Address must an integer with open ( "/proc/{}/mem" . format (pid), "rb" ) as mem: mem.seek(address) data = mem.read(size) value = int .from_bytes(data, byteorder = sys.byteorder, signed = True ) print (value) |
基本上與 Bash 相同,但 size 和 address 必須是 int 類別
而且 Python 能夠使用內建函式 int.from_bytes 將 bytearray 轉換成 int
寫入資料
1 2 3 4 5 6 7 8 | import sys pid = # Process ID size = 1 address = # Address must an integer value = update - value with open ( "/proc/{}/mem" . format (pid), "r+b" ) as mem: mem.seek(address) mem.write(value.to_bytes(size, byteorder = sys.byteorder)) |
基本上與 Bash 相同,同樣 size、address 及 value 必須是 int 類別
Python 亦有內建函式 int.to_bytes 將 int 轉換成 bytearray
使用 Python 修改程序記憶體的資料,更加直觀及簡單
地址 、 偏移 及 基礎地址
不過在下發現每次重新開啟程序,對應資料的 地址 (Address) 都會變化,因此無法指定 地址 來顯示資料
不過,偏移 (Offset) 則必定相同,因此相比地址,偏移的數值更重要
配合 地址 及 偏移,便可以計算 基礎地址 (Base Address)
計算 基礎地址 其實很簡單
1 | 基礎地址 = 地址 - 偏移 |
找到 基礎地址 後,可以到 /proc/<pid>/maps 尋找相關地址位置
經過測試重新開啟程序多次後,發現雖然不是在相同行數中找到 基礎地址
但根據在下在 Retroarch 載入 PSX核心 運行 Bio Hazard 的測試中
基礎地址 每次都是 /usr/lib/x86_64-linux-gnu/libretro/mednafen_psx_libretro.so 最後項目的結束地址
因此在下可以借助 /usr/lib/x86_64-linux-gnu/libretro/mednafen_psx_libretro.so 這個關鍵內容來尋找 基礎地址
而不需要每次重新啟動程序都要手動計算
1 2 3 4 5 6 7 8 9 10 11 | pid = # Process ID offset = # Offset must an integer lines = [] with open ( "/proc/{}/maps" . format (pid), "r" ) as file : for line in file : line = line.replace( "\r" , " ").replace(" \n ", " ") if "/usr/lib/x86_64-linux-gnu/libretro/mednafen_psx_libretro.so" in line: lines.append( int (line.split( " " )[ 0 ].split( "-" )[ 1 ], 16 )) base_address = lines[ - 1 ] print ( "{:x}" . format (base_address)) print ( "{:x}" . format (base_address - offset)) |
編寫 Python 來測試能否獲取正確的地址
注意:十六進制數值必須以 0x 前綴
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 | from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse import sys, json class MyRequestHandler(BaseHTTPRequestHandler): __encode = "UTF-8" __pid = sys.argv[ 1 ] __lines = [] with open ( "/proc/{}/maps" . format (__pid), "r" ) as maps: for line in maps: line = line.replace( "\n" , " ").replace(" \r ", " ") if "/usr/lib/x86_64-linux-gnu/libretro/mednafen_psx_libretro.so" in line: __lines.append( int (line.split( " " )[ 0 ].split( "-" )[ 1 ], 16 )) __base_address = __lines[ - 1 ] # debug print ( "PID - {}" . format (__pid)) print ( "Base Address - {}" . format (__base_address)) __items = { 0 : { "name" : " " , "image" : " " }, 1 : { "name" : "Knife" , "image" : "001-knife.png" }, 2 : { "name" : "Handgun" , "image" : "002-handgun.png" }, 3 : { "name" : "Shotgun" , "image" : "003-shotgun.png" }, 4 : { "name" : "Magnum (Prototype)" , "image" : "004-magnum-prototype.png" }, 5 : { "name" : "Magnum" , "image" : "005-magnum.png" }, 6 : { "name" : "Flamethrower" , "image" : "006-flamethrower.png" }, 7 : { "name" : "Grenade Launcher (Grenade)" , "image" : "007-grenade-launcher-grenade.png" }, 8 : { "name" : "Grenade Launcher (Acid)" , "image" : "008-grenade-launcher-acid.png" }, 9 : { "name" : "Grenade Launcher (Flame)" , "image" : "009-grenade-launcher-flame.png" }, 10 : { "name" : "Rocket Launcher" , "image" : "010-rocket-launcher.png" }, 11 : { "name" : "Handgun Ammo" , "image" : "011-handgun-ammo.png" }, 12 : { "name" : "Shotgun Ammo" , "image" : "012-shotgun-ammo.png" }, 13 : { "name" : "Magnum Ammo (Prototype)" , "image" : "013-magnum-ammo-prototype.png" }, 14 : { "name" : "Magnum Ammo" , "image" : "014-magnum-ammo.png" }, 15 : { "name" : "Flamethrower Ammo" , "image" : "015-flamethrower-ammo.png" }, 16 : { "name" : "Grenade Ammo" , "image" : "016-grenade-ammo.png" }, 17 : { "name" : "Acid Ammo" , "image" : "017-acid-ammo.png" }, 18 : { "name" : "Flame Ammo" , "image" : "018-flame-ammo.png" }, 19 : { "name" : "Empty Bottle" , "image" : "019-empty-bottle.png" }, 20 : { "name" : "Water" , "image" : "020-water.png" }, 21 : { "name" : "UMB No.2" , "image" : "021-umb-no-2.png" }, 22 : { "name" : "UMB No.4" , "image" : "022-umb-no-4.png" }, 23 : { "name" : "UMB No.7" , "image" : "023-umb-no-7.png" }, 24 : { "name" : "UMB No.13" , "image" : "024-umb-no-13.png" }, 25 : { "name" : "Yellow-6" , "image" : "025-yellow-6.png" }, 26 : { "name" : "NP-003" , "image" : "026-np-003.png" }, 27 : { "name" : "V-Jolt" , "image" : "027-v-jolt.png" }, 28 : { "name" : "Broken Shotgun" , "image" : "028-broken-shotgun.png" }, 29 : { "name" : "Square Crank" , "image" : "029-square-crank.png" }, 30 : { "name" : "Hexagonal Crank" , "image" : "030-hexagonal-crank.png" }, 31 : { "name" : "Emblem" , "image" : "031-emblem.png" }, 32 : { "name" : "Gold Emblem" , "image" : "032-gold-emblem.png" }, 33 : { "name" : "Blue Jewel" , "image" : "033-blue-jewel.png" }, 34 : { "name" : "Red Jewel" , "image" : "034-red-jewel.png" }, 35 : { "name" : "Music Note" , "image" : "035-music-note.png" }, 36 : { "name" : "Wolf Medal" , "image" : "036-wolf-medal.png" }, 37 : { "name" : "Eagle Medal" , "image" : "037-eagle-medal.png" }, 38 : { "name" : "Herbicide" , "image" : "038-herbicide.png" }, 39 : { "name" : "Battery" , "image" : "039-battery.png" }, 40 : { "name" : "MO Disc" , "image" : "040-mo-disc.png" }, 41 : { "name" : "Wind Crest" , "image" : "041-wind-crest.png" }, 42 : { "name" : "Flare" , "image" : "042-flare.png" }, 43 : { "name" : "Slides" , "image" : "043-slides.png" }, 44 : { "name" : "Moon Crest" , "image" : "044-moon-crest.png" }, 45 : { "name" : "Star Crest" , "image" : "045-star-crest.png" }, 46 : { "name" : "Sun Crest" , "image" : "046-sun-crest.png" }, 47 : { "name" : "Ink Ribbon" , "image" : "047-ink-ribbon.png" }, 48 : { "name" : "Lighter" , "image" : "048-lighter.png" }, 49 : { "name" : "Lock Pick" , "image" : "049-lock-pick.png" }, 50 : { "name" : "Oil" , "image" : "050-oil.png" }, 51 : { "name" : "Mansion Sword Key" , "image" : "051-mansion-sword-key.png" }, 52 : { "name" : "Mansion Armor Key" , "image" : "052-mansion-armor-key.png" }, 53 : { "name" : "Mansion Shield Key" , "image" : "053-mansion-shield-key.png" }, 54 : { "name" : "Mansion Helmet Key" , "image" : "054-mansion-helmet-key.png" }, 55 : { "name" : "Laboratory Master Key" , "image" : "055-laboratory-master-key.png" }, 56 : { "name" : "Mansion Closet Key" , "image" : "056-mansion-closet-key.png" }, 57 : { "name" : "Guardhouse Room 002 Key" , "image" : "057-guardhouse-room-002-key.png" }, 58 : { "name" : "Guardhouse Room 003 Key" , "image" : "058-guardhouse-room-003-key.png" }, 59 : { "name" : "Guardhouse Control Room Key" , "image" : "059-guardhouse-control-room-key.png" }, 60 : { "name" : "Laboratory Power ROom Key" , "image" : "060-laboratory-power-room-key.png" }, 61 : { "name" : "Small Key" , "image" : "061-small-key.png" }, 62 : { "name" : "Red Book" , "image" : "062-red-book.png" }, 63 : { "name" : "Doom Book 2" , "image" : "063-doom-book-2.png" }, 64 : { "name" : "Doom Book 1" , "image" : "064-doom-book-1.png" }, 65 : { "name" : "First Aid Spary" , "image" : "065-first-aid-spray.png" }, 66 : { "name" : "Serum" , "image" : "066-serum.png" }, 67 : { "name" : "Red Herb" , "image" : "067-red-herb.png" }, 68 : { "name" : "Green Herb" , "image" : "068-green-herb.png" }, 69 : { "name" : "Blue Herb" , "image" : "069-blue-herb.png" }, 70 : { "name" : "Mixed Herb (GB)" , "image" : "072-mixed-herb-gb.png" }, 71 : { "name" : "Mixed Herb (GG)" , "image" : "071-mixed-herb-gg.png" }, 72 : { "name" : "Mixed Herb (RG)" , "image" : "070-mixed-herb-rg.png" }, 73 : { "name" : "Mixed Herb (RGB)" , "image" : "073-mixed-herb-rgb.png" }, 74 : { "name" : "Mixed Herb (GGG)" , "image" : "074-mixed-herb-ggg.png" }, 75 : { "name" : "Mixed Herb (GGB)" , "image" : "075-mixed-herb-ggb.png" }, 76 : { "name" : "Hoe" , "image" : "076-hoe.png" }, 77 : { "name" : "Radio" , "image" : "077-radio.png" }, } __character_hp_offset = 0xC97CC __character_hp_length = 2 __enemy_hp_offset = 0xC994C __enemy_hp_length = 2 __character_slot_offset = 0xCCDA4 __character_slot_length = 16 def __send_response( self , code = 200 , headers = { "Content-Type" : "text/html" }, message = "OK" ): self .send_response(code) for key, value in headers.items(): self .send_header(key, value) self .end_headers() self .wfile.write(message.encode(MyRequestHandler.__encode)) def __get_memory_data(memory, offset, length): memory.seek(MyRequestHandler.__base_address + offset) return memory.read(length) def do_GET( self ): try : url = urlparse( self .path) if url.path = = "/" : self .__send_response() elif url.path = = "/bio-hazard.json" : result = {} result[ "item" ] = MyRequestHandler.__items with open ( "/proc/{}/mem" . format (MyRequestHandler.__pid), "rb" ) as memory: data = MyRequestHandler.__get_memory_data(memory, MyRequestHandler.__character_hp_offset, MyRequestHandler.__character_hp_length) result[ "character-hp" ] = int .from_bytes(data, byteorder = sys.byteorder, signed = True ) data = MyRequestHandler.__get_memory_data(memory, MyRequestHandler.__enemy_hp_offset, MyRequestHandler.__enemy_hp_length) result[ "enemy-hp" ] = int .from_bytes(data, byteorder = sys.byteorder, signed = True ) data = MyRequestHandler.__get_memory_data(memory, MyRequestHandler.__character_slot_offset, MyRequestHandler.__character_slot_length) result[ "character-slot" ] = [] for i in range ( int (MyRequestHandler.__character_slot_length / 2 )): result[ "character-slot" ].append({ "type" : data[i * 2 ], "value" : data[i * 2 + 1 ]}) self .__send_response( 200 , { "Content-Type" : "application/json; charset={}" . format (MyRequestHandler.__encode)}, json.dumps(result)) elif url.path = = "/bio-hazard.html" : message = """<!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <title>PSX Bio Hazard @ Retroarch Monitor</title> <style> table { border-collapse: collapse; font-family: "Courier New"; font-size: 12pt; } img { margin: 0px; padding: 0px; } table.character-slot img { width: 160px; height: 120px; } </style> <script> window.addEventListener("load", function(loadEvent) { window.setInterval(function() { fetch("/bio-hazard.json").then(function(response) { if (response.status == 200) { return response.json() } else { throw new Error("status code is not 200"); } }).then(function(data) { { var element = document.getElementById("character-hp"); if (element.innerHTML != data["character-hp"]) { element.innerHTML = data["character-hp"]; } } { var element = document.getElementById("enemy-hp"); if (element.innerHTML != data["enemy-hp"]) { element.innerHTML = data["enemy-hp"]; } } { var tsection = document.getElementById("character-slot"); while (tsection.hasChildNodes()) { tsection.removeChild(tsection.firstChild); } var tr = null; for (var i in data["character-slot"]) { if (i % 2 == 0) { tr = tsection.insertRow(-1); } var td = tr.insertCell(-1); td.setAttribute("align", "center"); td.innerHTML = '<img src="' + data["item"][data["character-slot"][i]["type"]]["image"] + ' alt="' + data["item"][data["character-slot"][i]["type"]]["name"] + ': ' + data["character-slot"][i]["value"] + '"/><b' + 'r/>' + data["character-slot"][i]["value"]; } } }).catch(function(error) { console.log(error); }); }, 200); }); </script> </head> <body> <table> <tr valign="top"> <td> <table border="1" cellpadding="5" cellspacing="5"> <tbody> <tr> <td>Character HP:</td> <td align="right" id="character-hp"></td> </tr> <tr> <td>Enemy HP 0:</td> <td align="right" id="enemy-hp"></td> </tr> </tbody> </table> </td> <td> <table class="character-slot" border="1" cellpadding="5" cellspacing="5"> <colgroup> <col width="1"/> <col width="1"/> </colgroup> <thead> <tr> <th colspan="2">Character Slot:</th> </tr> </thead> <tbody id="character-slot"> </tbody> </table> </td> </tr> </table> </body> </html>""" self .__send_response( 200 , { "Content-Type" : "text/html; charset={}" . format (MyRequestHandler.__encode)}, message) else : self .__send_response( 404 , { "Content-Type" : "text/plain; charset={}" . format (MyRequestHandler.__encode)}, "404 Not Found" ) except Exception as exception: self .__send_response( 500 , { "Content-Type" : "text/plain; charset={}" . format (MyRequestHandler.__encode)}, exception.__str__()) if __name__ = = "__main__" : port = 8080 httpd = HTTPServer(("", port), MyRequestHandler) httpd.serve_forever() |
尋找對應資料的偏移後,在下使用 Python 作為編寫網頁伺服器功能,讓資料能夠在網頁瀏覽器上顯示
測試 Python 網頁伺服器
(頁面顯示的內容沒有限制,在下只是隨意顯示一些內容)
將資料以 JSON格式 顯示
使用 Fetch API 傳回 JSON 資料,再將資料格式化顯示
由於顯示頁面每 200毫秒 獲取資訊,因此 Terminal 顯示資料頁面不斷被存取
(頁面更新速度不應少於 100毫秒,否則會影響效能)
與遊戲同步顯示
補充資料
資料判斷
不論 Game Conqueror 、 ScanMem ,除了使用絕對數值搜尋目標數值,還能夠以範圍方式搜尋
- N 當前數值 等於 N
- M..N 當前數值 介乎 M 至 N 之間 (不要空格)
- ? 不確定 當前數值
- < 或 - 當前數值 小於 上次數值
- > 或 + 當前數值 大於 上次數值
- = 當前數值 等於 上次數值
- != 當前數值 不等於 上次數值
- < N 當前數值 小於 N (必須有空格)
- > N 當前數值 大於 N (必須有空格)
- - N 當前數值 減少 N (必須有空格)
- + N 當前數值 增加 N (必須有空格)
- != N 當前數值 不等於 N (必須有空格)
端序計算
由於記憶體資料是一連串位元組,資料的表示方式受 大端序 (Big Endian) 及 小端序 (Little Endian) 的影響
不同的 端序 計算方式不同,例如:
當數值為 0x1234 時, 大端序 會將 12 34 的次序儲存,而 小端序 則會以 34 12 次序儲存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | def data_to_list(data, endian = "little" , length = 0 ): byte_array = [] while data > 0 : byte_array.append(data & 0xFF ) data >> = 8 while len (byte_array) < length: byte_array.append( 0x00 ) if endian ! = "little" : byte_array.reverse() return byte_array data = 0x1234 print (data_to_list(data)) # [52, 18] # [0x34, 0x12] print (data_to_list(data, "big" )) # [18, 52] # [0x12, 0x34] print (data_to_list(data, "little" , 4 )) # [18, 52, 0, 0] # [0x12, 0x34, 0x00, 0x00] print (data_to_list(data, "big" , 4 )) # [0, 0, 18, 52] # [0x00, 0x00, 0x12, 0x34] |
將端序資料轉換回數值時,端序 的計算方法:
1 2 3 4 5 6 7 8 9 10 | def list_to_data(byte_array, endian = "little" ): data = 0 if endian ! = "little" : byte_array.reverse() for i, byte in enumerate (byte_array): data | = byte << ( 8 * i) return data data = [ 0x12 , 0x34 ] print ( "0x{:x}" . format (list_to_data(data))) # 0x3412 print ( "0x{:x}" . format (list_to_data(data, "big" ))) # 0x1234 |
小端序不需要將資料排序即可計算
兩種端序各有其優缺點
一般來說,x86、x86-64 和 Arm 架構使用小端序,因此 大部分Linux 、 Windows、Mac OS、Android 及 iOS 系統都採用小端序
而某些特定架構,如 SPARC、AIX 及 嵌入式系統,則通常使用 大端序
maps 結構
之前提及過的 /proc/<pid>/maps 是用來標記 記憶體的地址位置
除了地址位置,還記錄著相對地址的其他資訊,例如:
1 | 7f3d0295b000-7f3d02970000 rw-p 001af000 08:02 21372837 /usr/lib/x86_64-linux-gnu/libretro/mednafen_psx_libretro .so |
每條條目總共有六部分
- 地址範圍;十六進制數值;- (減號) 分隔
- 第1項目為 開始地址
- 第2項目為 結束地址
- 權限
- 第1項目為 可讀取權限, r 為可以讀取, - 為不能讀取
- 第2項目為 可寫入權限, w 為可以寫入, - 為不能寫入
- 第3項目為 可執行權限, x 為可以執行, - 為不能執行
- 第4項目為 可分享權限, s 為可以分享, p 為私有使用
- 偏移;十六進制數值;映射檔案的 起始偏移,如果沒有則為 0
- 裝置類別;十六進制數值
- 第1項目為 主要裝置類別
- 第2項目為 次要裝置類別
- inode編號;映射檔案的 inode編號,如果沒有則為 0
- 記憶體映射區域;映射檔案的 絕對路徑,否則為 空白 或 其他特殊區域
詳細資料還是需要到 https://www.kernel.org/ 了解
在下嘗試製作類似 ScanMem 的掃描器,發現速度非常慢,首次掃描幾乎需要 10分鐘 才能完成
然而,Game Conqueror 和 ScanMem 的首次掃描僅需約 20秒 完成
(以 Retroarch 載入 Bio Hazard 進行測試)
在下將 ScanMem 顯示的地址範圍與 /proc/<pid>/maps 進行比較後,發現 ScanMem 掃描的地址範圍的權限必定是 rw-p
此外,掃描的第一組地址範圍的記憶體映射區域必定是 /proc/<pid>/maps 的 來源路徑
(例如:555d4ef65000-555d4ef6b000)
其餘都是:
- 偏移 為 00000000
- 裝置類別 為 00:00
- inode編號 為 0
因此,可以將 /proc/<pid>/maps 中的項目過濾,僅保留符合上述條件的地址範圍
過濾後的項目數量大約僅佔原來的 20分之1 ,這樣可以顯著減少掃描時間,從 10分鐘 縮短至 約30秒
雖然這個速度仍不及 ScanMem ,但已經算是可接受的時間了
自製掃描器及修改器
了解運作原理後,在下開始嘗試自行編寫一個功能類似的掃描器和修改器,並且支持通過網絡進行存取
這樣便能夠由另一個裝置協助進行掃描和修改操作
以 API方式 修改資料
以 網頁介面 修改資料
在下的電腦已經使用多年,硬件效能相對較舊
雖然錄影遊戲的過程並未顯著影響效能,但當同時進行遊戲、錄影和掃描時,效能開始受到影響
導致遊戲的 FPS 稍微不穩定,掃描則需要大約 6分鐘 才能完成
為了解決這個問題,在下實踐了剛才的想法,將遊戲和掃描分開到兩部電腦執行;不過,掃描時間仍需接近 4分鐘
因此,在下唯有將掃描時的錄影縮減,以避免影片過於沉悶
總結
在下曾經使用 Game Conqueror 作為資料監視器,對於顯示數值資料沒有問題,但顯示非數值資料如文字或圖像則相當困難
其實在下多年來一直想製作這個專案,但當時找到的資料非常有限(可能是在下搜尋關鍵字不當),因此花了很多時間仍未能完成
不過,這次借助一些 AI平台;雖然 AI 無法提供完全正確的範例,但提供的資訊幫助在下尋找更精確的資料
在下覺得能使用 Bash 直接修改到遊戲資料是非常方便的操作
而使用 Python 建立伺服器再經網頁顯示資料同樣方便,可以根據需要變更顯示的資料及展示效果
亦可以使用純文字的方法讓 Curl 等工具表達內容
還可以讓其他裝置連接到來顯示,不需要完全依賴同一部電腦的資源
在下使用類似 MVC 的方式,將需要顯示的資料格式化為 JSON , 並在 HTML頁面 取得 JSON 的資料
最初在下使用 XMLHttpRequest 來取得 JSON 資料,不過繼續查找資料後發現 Fetch API ,能夠更簡單取得另一個頁面的資料
而且最重要 Fetch API 能直接傳回 JSON物件 , 令顯示格式化資料的頁面更方便
另外,當需要修改為跨平台使用時,只需要將 資料頁面 配合對應平台修改即可
(Linux 及 Windows 的存取記憶體的方法不同)
在下已經原始碼分享到 https://bitbucket.org/hkgoldenmra/remote-memory-scanner
歡迎閣下使用及修改
由於 Unix 的 一切皆檔案 (Everything is a file) 設計理念,Linux 也繼承了這一概念,並且實現得更為徹底
例如這個專案經常訪問 /proc 中的檔案屬於 ProcFS,是一種虛擬檔案系統
此外 /proc 中的所有檔案不佔用磁碟空間,而只使用記憶體空間
除了 ProcFS ,還有:
- 裝置檔案系統 DevFS
- 系統檔案系統 SysFS
- 暫存資料檔案系統 TmpFS
這種統一的操作介面使得系統資料可以直接存取,而無需特殊操作或學習特定的 API
舉例來說,在存取 /proc/<pid>>mem 的資料時,使用 Bash 或 Python 的操作邏輯完全相同,只是語法不同
首先封存他在香港的所有brocer 特他不能夠對外通訊 然後如果真係查到他家有無器 問法庭拎手查令 入屋進入地煎式的搜索 再扣查事主 暫時咁多先 有咩再報告
回覆刪除