搜尋

Graal OnlineGraalOnline遊戲修改器

返回清單
切換到指定樓層
通知這文章過時或找檔案 發表主題

[電玩遊戲] 《Graal Online》Frida 客戶端修改器教學 GS2腳本注入、記憶體修改、動態掛載實戰

[複製連結]
1
A7533984132 ( Lv.30 大天使 ) 發表於 4 小時前 | 只看該作者 回覆獎勵 |降序瀏覽 |閱讀模式

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 後發現第一個參數是腳本結構體,第二個纔是字節碼。這揭示了核心原理:先建立腳本物件,再綁定字節碼

事件系統的關鍵鏈路



但問題來了——這樣建立的腳本只能觸發 onCreatedonTimeout,無法接收 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 ClassicGraal 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)。每個階段都搭配實際練習效果最佳。





大家正在看啥


收藏收藏 分享文章到FB上分享
回覆 使用道具 檢舉
複製專屬你的推廣連結:發至FB與各論壇宣傳:累積點數換GP商品 & 藍鑽
每五點閱率就可以兌換藍鑽積分或遊戲點卡 夢遊推廣文章換GP商品

你需要登入後才可以回覆 登入 | 加入會員

本版積分規則

Copyright (C) 2010-2020 夢遊電玩論壇

廣告合作:請直接聯繫我們,並附上您預刊登位置的預算。  

快速回覆 返回頂端 返回清單