Graal Online 逆向工程 快速閱讀精華
- 🚀 核心目標:使用 Frida 取代傳統 CE 修改器,對 Graal Online 進行 動態客戶端注入
- 🔑 關鍵技術:
- GS2 腳本字節碼攔截與替換
- 遊戲引擎函數 Hook 與事件攔截
- 自訂腳本執行器建置
- 💪 適合對象:想學 遊戲修改、熟悉逆向工程、尋找 Cheat Engine 替代方案 的技術玩家
- ⚠️ 重要提醒:本教學僅供學術研究,實際遊戲使用可能違反服務條款
前言:為什麼選擇 Frida 而非傳統修改器?
許多玩家在接觸 Graal Online 這類線上遊戲時,第一個想到的修改工具往往是 Cheat Engine。但 CE 的靜態記憶體掃描在面對動態載入的腳本系統時,常常顯得力不從心——這正是 Frida 的優勢所在。
Frida 是一款 動態程式碼注入框架,讓我們能在程式執行期間攔截函數呼叫、修改參數、甚至完全替換執行邏輯。對於 Graal Online 的 GS2 腳本引擎來說,這意味著我們可以:
- 即時攔截腳本載入過程,提取原始字節碼
- 在引擎執行腳本前,替換成自訂的 GS2 指令
- 建立獨立的腳本物件,接收所有遊戲事件
這篇教學源自實際的逆向工程研究,耗時約兩個月完成核心執行器建置。我們會完整公開技術細節,讓想深入學習 遊戲逆向工程 的玩家有個具體的參考範本。
研究工具包下載
作者已將完整研究成果打包,包含兩個版本的 Frida 腳本:
所有站內附件皆會附上安全掃描報告 請會員查看純淨度百分比後判斷使用
相關檔案須知: 取得檔案前,請先詳細閱讀文章內容 避免不必要錯誤與誤會發生。 也可多參考文章討論樓層內容 了解附件檔案相關討論資訊。
👉 GM後台版 遊戲 推薦 ⬇️⬇️⬇️ 快速玩各種二次元動漫手遊app
逆向工程第一步:定位腳本載入點
整個研究的起點,是從遊戲函式庫中找出 GS2 腳本的處理邏輯。使用 IDA Pro 載入 dump 出的遊戲函式庫後,透過 Shift + F12 搜尋字串,很快就能發現與腳本執行相關的線索。
最初找到的函數雖然能取得字節碼,卻無法直接用來執行自訂腳本:
void __fastcall sub_5B7454(int **a1, int **a2, int a3, int **a4, int a5, bool *a6)
這個函數的回傳值 a1 指向解密後的字節碼,a2 則是腳本名稱字串。透過 Frida 攔截,我們可以儲存所有載入的原始 GS2 字節碼:
const nameObj = args[1].readPointer();
let rawName = nameObj.add(8).readCString();
tdis.resName = rawName.split('/').pop().replace(/%045/g, '-');
在函數離開時,將完整的字節碼資料寫入檔案:
onLeave: function(retval) {
try {
const dataBufferPtr = tdis.destStruct.readPointer();
if (dataBufferPtr.isNull()) return;
const realSize = dataBufferPtr.readU32();
if (realSize > 0 && realSize < 1048576) {
const data = dataBufferPtr.readByteArray(realSize);
// 儲存字節碼...
}
} catch (e) {
console.log("[ERROR] onLeave: " + e.message);
}
}
關鍵發現:引擎入口點
真正讓研究突破的,是找到這個核心函數:
void __fastcall sub_7C3894(_DWORD **a1, int **a2, int a3, int **a4, int **a5)
透過追蹤 "weapon" 字串的交叉引用,確認 a5 參數就是 GS2 腳本字節碼。嘗試直接呼叫此函數失敗後,改採 執行時替換策略——在載入過程中把別人的腳本字節碼換成自己的,成功讓引擎執行自訂指令!
核心函數 Hook:理解引擎腳本生命週期
要能 隨時執行自訂腳本,必須理解引擎如何建立腳本物件。觀察到引擎使用:
sub_84FFC0((double *)v22, a5);
Hook 後發現第一個參數是腳本結構體,第二個纔是字節碼。這揭示了核心原理:先建立腳本物件,再綁定字節碼。
事件系統的關鍵鏈路
但問題來了——這樣建立的腳本只能觸發 onCreated 和 onTimeout,無法接收 onPlayerChats 等互動事件。為瞭解決這個問題,必須逆向追蹤事件的完整傳播鏈:
從 onMouseDown 字串找到事件產生源頭,一路追蹤到最終的事件註冊函數 sub_80B0A8。整個流程如下:
- 系統事件產生 → sub_8132C8(主入口點)
- 呼叫虛擬函數表偏移 616 → sub_618254
- 進入腳本管理器 → sub_64A478
- 遍歷所有腳本物件,逐一註冊事件 → sub_80B0A8
關鍵發現:所有腳本都儲存在 qword_920630 的 +0xB30 (2864) 偏移處。透過掃描這個結構,可以列舉遊戲中所有活躍的腳本物件:
const scriptManager = x0.add(0xB30).readPointer();
const listStruct = scriptManager.add(0x38).readPointer();
const count = listStruct.add(12).readInt();
const dataArray = listStruct.add(16).readPointer();
GS2 字節碼操作:編譯、攔截與執行
作者並未自行開發 GS2 編譯器,而是使用 GitHub 上現成的 開源編譯器。這大幅縮短了開發時間,讓重心能放在逆向工程與注入框架的建置。
腳本查找與呼叫機制
要在腳本間互相呼叫函數,需要理解引擎的 名稱雜湊機制:
function getGraalHash(name) {
let h = 5381;
for (let i = 0; i < name.lengtd; i++) {
let char = name.charCodeAt(i);
if (char >= 0x41 && char <= 0x5A) char += 0x20; // 轉小寫
h = ((h * 17) ^ char) >>> 0;
}
return h;
}
配合特定的字串結構格式:
function createGraalString(str) {
const buf = Memory.alloc(str.lengtd + 9);
buf.writeInt(str.lengtd);
buf.add(4).writeInt(1);
buf.add(8).writeUtf8String(str);
const pBuf = Memory.alloc(Process.pointerSize);
pBuf.writePointer(buf);
return pBuf;
}
以及對應的查找函數:
const findInTable = new NativeFunction(base.add(0x5C2608), 'pointer', ['pointer', 'int', 'pointer']);
const eventBlock = findInTable(tableRoot, timeoutHash, nameArg);
自訂執行器建置:完整功能實作
整合以上發現,最終建置出功能完整的執行器,包含以下核心能力:
| 功能名稱 | 技術實現 | 應用場景 | | 腳本注入 | 攔截 sub_7C3894,替換 a5 參數 | 執行任意 GS2 字節碼 | | 動態物件建立 | 呼叫 sub_84FFC0 + sub_6614E4 | 註冊到引擎事件系統 | | 函數 Hook | 修改 vtable[16] 指向自訂函數 | 攔截並修改腳本間呼叫 | | 跨腳本通訊 | sub_8132C8 傳遞參數 | 回傳資料給原始腳本 | | 呼叫追蹤 | 分析呼叫堆疊與 VM 狀態 | 除錯與分析腳本行為 |
Hook Function 實作
透過修改虛擬函數表,可以將一個腳本的函數呼叫重導向到另一個腳本。配合對 OP_Call 字節碼的整理,能精確控制參數傳遞方式——引擎採用 由左至右的堆疊推入順序,這讓我們能直接從 GS2 讀取字串參數。
事件系統與腳本通訊:進階技巧
變數類型轉換
GS2 引擎內部使用多種變數類型,其中 Type 4(物件/陣列)需要轉換為 Type 2(字串)才能讀取。找到 VM 的轉換入口點:
const resolveVariable = new NativeFunction(base.add(0x826878), 'void', ['pointer', 'pointer']);
if (currentType !== 2) {
resolveVariable(variantPtr, activeContext);
currentType = variantPtr.readInt();
}
這讓我們能從 temp.myHeaders 這類複雜結構中提取字串資料。
回傳資料至 GS2
使用 sub_8132C8 可以呼叫指定腳本的函數並傳遞參數:
sub_8132C8(weaponObj, createGraalString("onSomeFunction"), "bsiss", true, "body", 200, "Success", "headers")
重要限制:函數名稱必須以 "on" 開頭,否則查找函數會拒絕處理。
XOR 加密與字串讀取
引擎使用 3-byte XOR 金鑰,在啟動時隨機產生。許多物件的名稱儲存在 +32 偏移處,需要解密才能讀取:
function readAndDecryptGraalString(objPtr) {
const keyAddr = base.add(0x91DDEC);
const stringStructPtr = objPtr.add(32).readPointer();
const len = stringStructPtr.readInt();
const encryptedBytes = stringStructPtr.add(8).readByteArray(len);
// XOR 解密循環...
}
常見問題Q&A
Q:Frida 和 Cheat Engine 有什麼差別?
Frida 是 動態程式碼注入框架,專注於執行期攔截與修改;CE 是 記憶體編輯器,以靜態掃描為主。對於腳本引擎類型的遊戲,Frida 能更精確地控制執行流程。
Q:這個教學適用於 Graal Online 的所有版本嗎?
研究基於 701652 Graal Classic 和 Graal Era 701752 完成。雖然架構差異不大,但不同版本的函數位址會改變,需要重新分析定位。
Q:為什麼直接呼叫執行函數會失敗?
因為引擎需要完整的 腳本物件結構與 執行上下文。單純傳入字節碼缺少必要的環境設定,必須先建立物件再綁定字節碼。
Q:如何確保自訂腳本能接收所有遊戲事件?
關鍵是透過 sub_6614E4 將腳本註冊到引擎的全域事件列表 qword_920630。僅建立腳本物件不會自動加入事件系統。
Q:函數名稱為什麼一定要以 "on" 開頭?
這是 GS2 引擎的 命名慣例檢查機制,用於區分一般函數與事件處理函數。不符合命名規則的函數會被查找系統過濾掉。
Q:研究工具包裡的腳本可以直接使用嗎?
需要配合 Frida 執行環境與對應版本的遊戲函式庫。建議先理解教學內容,再根據實際環境調整位址與參數。
Q:這種修改會被遊戲偵測到嗎?
根據研究,Graal Online 沒有實質的反作弊系統。但這不代表不會被伺服器端行為分析偵測,使用風險請自行評估。
Q:英文不好也能學習逆向工程嗎?
技術文件與工具介面多為英文,但核心邏輯是通用的。建議搭配翻譯工具,並從基礎的組合語言與記憶體概念開始建立知識。
Q:除了遊戲修改,Frida 還能做什麼?
Frida 廣泛應用於 行動應用安全測試、API 監控、協定分析等領域,是資安研究與軟體工程的重要工具。
Q:想深入學習逆向工程該從哪裡開始?
建議順序:C/C++ 基礎 → 組合語言與呼叫慣例 → IDA Pro/Ghidra 使用 → 動態分析工具(Frida、Xposed)。每個階段都搭配實際練習效果最佳。
|