:::info **免責聲明** 本教學文檔中提供的所有資訊僅供學術研究、技術交流和教育目的使用。文中所涉及的逆向工程技術旨在幫助讀者了解 Unity 應用程式的內部工作原理和安全機制。 嚴禁將本文中的技術用於任何商業或非法用途,包括但不限於破解、盜版、製作外掛或侵犯任何個人或實體的合法權益。 讀者應對自己的行為負全部責任。因使用本文技術而導致的任何法律糾紛或損失,作者概不負責。 ::: ## 什麼是逆向工程? 學過一點 Unity 之後,大家應該都可以理解製作一個遊戲需要什麼東西。 從最基本的遊戲素材(可能是圖片、影片或是音檔等等),到控制遊戲的腳本(在 Unity 中可能是 C#),以及更多遊戲 animator, 粒子特效的設定都是不可或缺的 但是這些東西在我們打包之後都跑到哪裡了? 通常遊戲素材可能會比較容易找出來,但是那些腳本和設定通常很難直接看出來。 只會多出來一個 `.exe` 執行檔和一堆 `.dll` 之類的檔案,為什麼用這些東西就可以成功執行遊戲? --- ### 步驟 要把遊戲打包出來的 package ,還原出所有資料通常會需要以下幾個步驟 ![image](https://hackmd.io/_uploads/SkcFkRDQge.png) --- #### 步驟 1:初步偵查(查檔案結構與去殼) * 查看遊戲是什麼格式,例如 `.apk`、`.exe`、`.unity3d` 等。 * 有些檔案實際上是壓縮包,可以解開查看內容。 * 若遊戲有加殼或加密,必須先解除,才能進一步分析。 #### 步驟 2:素材提取 * Unity 遊戲的素材會被打包成特定格式的檔案,例如圖片、音效、動畫、UI 等。 * 可以將這些資源提取出來,方便檢視或分析。 #### 步驟 3:反編譯 * Unity 遊戲的程式碼通常是用 C# 撰寫的。 * 如果是 Mono 模式,可以直接還原為接近原始碼的程式。 * 如果是 IL2CPP 模式,程式碼已經轉成 C++ 編譯,需要更多步驟才能看懂。 #### 步驟 4:反混淆 * 有些遊戲會把程式碼做混淆處理,讓程式名稱看不懂。 * 這會讓分析變得比較困難,需要花時間理解程式邏輯。 * 有時可以從執行流程或錯誤訊息中推測出原本的意圖。 接下來我們會依依介紹這些步驟要怎麼做。 --- ## 初步偵查 ### 工具 [DIE 工具下載](https://github.com/horsicq/DIE-engine/releases) **適用平台** : `Windows`, `MacOS`, `Linux` 我們這邊會使用 Detect it Easy (簡稱 DIE),來幫忙查殼,從上面的載點選擇適合自己電腦的版本下載下來 ### 選擇要反編譯的遊戲 接著我們選一款遊戲來做示範,這是一個之前蠻有名的手機遊戲 ["**元氣騎士**"](https://zh.wikipedia.org/zh-cn/%E5%85%83%E6%B0%94%E9%AA%91%E5%A3%AB),是用 Unity 開發的一款遊戲。 :::info 通常選擇較新較小的遊戲破解的成功率會比較大 ::: ![image](https://hackmd.io/_uploads/ryL7QDPXex.png) 首先,我們先上網查到這個遊戲的隨便一個載點(因為遊戲本身就是免費的,應該很好找) 假設我們下載到了 `soul-knight.apk` 這個檔案 (Andriod 程式檔案) 我們可以先打開 DIE 然後選取這個 apk 檔案進行解析(或是把 apk 拖曳過去 ) ![image](https://hackmd.io/_uploads/rkEB5hvXxl.png) 點選 `信息` > `保存`,就可以把訊息存成純文字檔案 :::spoiler 範例 ``` APK 操作系统: Android(15)[通用, Data, 包] 语言: Kotlin 库: Android Jetpack(1.15.0) 工具: Android SDK(API 35) 存档记录[classes.dex]: DEX 操作系统: Android(8.0)[Dalvik, 32 位, 主模块] 编译器: R8 工具: Android SDK(API 26) 保护器: DexGuard 存档记录[classes2.dex]: DEX 操作系统: Android(8.0)[Dalvik, 32 位, 主模块] 编译器: R8 工具: Android SDK(API 26) 存档记录[lib/arm64-v8a/libandroidx.graphics.path.so]: ELF64 操作系统: Android(5.0)[AARCH64, 64 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 21(Android 5.0)) 存档记录[lib/arm64-v8a/libdatastore_shared_counter.so]: ELF64 操作系统: Android(5.0)[AARCH64, 64 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 21(Android 5.0)) 存档记录[lib/armeabi-v7a/libandroidx.graphics.path.so]: ELF32 操作系统: Android(5.0)[ARM, 32 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 21(Android 5.0)) 存档记录[lib/armeabi-v7a/libdatastore_shared_counter.so]: ELF32 操作系统: Android(4.4-4.4.4)[ARM, 32 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 19(Android 4.4-4.4.4)) 存档记录[lib/x86/libandroidx.graphics.path.so]: ELF32 操作系统: Android(5.0)[386, 32 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 21(Android 5.0)) 存档记录[lib/x86/libdatastore_shared_counter.so]: ELF32 操作系统: Android(4.4-4.4.4)[386, 32 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 19(Android 4.4-4.4.4)) 存档记录[lib/x86_64/libandroidx.graphics.path.so]: ELF64 操作系统: Android(5.0)[AMD64, 64 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 21(Android 5.0)) 存档记录[lib/x86_64/libdatastore_shared_counter.so]: ELF64 操作系统: Android(5.0)[AMD64, 64 位, DYN] 链接程序: LDD(14.0.7) 编译器: Android clang(14.0.7) 语言: C/C++ 工具: Android NDK(r25c(9519653)) 工具: Android SDK(API 21(Android 5.0)) 存档记录[okhttp3/internal/publicsuffix/publicsuffixes.gz]: 存档 格式: GZIP ``` ::: 看起來有點複雜,但我們可以問問看 chatGPT 有什麼重要的訊息。 :::info #### DexGuard:高強度商業級保護 比 R8/ProGuard 更進一步,DexGuard 提供: 字串加密:無法從靜態分析看到明文 URL、錯誤訊息等 類別/方法加密:核心邏輯可能根本不在 classes.dex 反調試與反篡改:增加分析難度,可能會故意閃退 執行時解密載入程式碼:利用 Java 層的加載器載入記憶體中解密的真實邏輯 結論:這是一個被強殼保護的 App,靜態分析效果有限。 #### Unity 引擎痕跡缺失 未找到 libil2cpp.so 或 libunity.so,推測: 可能被加密藏起來(常見於商業遊戲) 被更名並儲存在 assets/ 等非標準位置 DIE 沒有辨識出(機率較低) ::: --- ### 結論 - 保護強度高:使用 DexGuard,靜態反編譯將極度困難 - Unity 元件被隱藏:必須進行更深層的資源搜尋與記憶體分析 - 傳統工具無效:Il2CppDumper 尚無用武之地 --- ### 下一步 這邊就可以發現,想要逆向一款熱門遊戲並不是這麼簡單XD,這款遊戲被許多工具給保護著 並且有時候一個遊戲的資源不會隨著 apk 一起被下載下來,要等到第一次打開時才會透過網路下載 當然我們可以選擇繼續去對他執行後面的步驟,但是大概率會遇到很多困難 所以我們先換個思路,不如我們去找看看 **舊版本** 的遊戲檔案,也許他那時候還沒有進行加密 --- ## 初步偵查 (Second Try) 在網路上搜索一番之後,我找到了稍微舊版一點的元氣騎士檔案 `Soul_knight_6.7.0.apk`。 一樣先對他進行分析 ![image](https://hackmd.io/_uploads/rkN-Rhvmll.png) ### 關鍵分析 #### Unity 與 IL2CPP 明確標示 IL2CPP / Unity 引擎:DIE 成功偵測到 Unity 引擎與 IL2CPP 技術棧,省去了推測流程。 libil2cpp.so 存在:直接位於 lib/arm64-v8a/ 中,確認 Unity 遊戲的核心 C# 邏輯被轉為原生程式庫,可進行靜態分析。 #### Java/Kotlin 層經過 ProGuard 混淆 使用了 ProGuard(或 R8),對 Java/Kotlin 程式碼進行混淆。 影響較小,因為我們的重點在原生的 Unity 程式碼 (libil2cpp.so)。 #### 未使用強加密殼 未偵測到 DexGuard、梆梆、愛加密等商業級加殼工具。 表示程式碼沒有經過高強度保護,可以直接開始逆向。 --- 重點就是他沒有被強殼保護著,所以我們可以破解他的機率大幅提升 所以我們就繼續使用這個檔案來進行後續的教學 ## 素材提取 首先有幾個觀念需要釐清,混淆通常是只針對 **程式碼**,讓人就算可以反編譯也看不懂他在做什麼 而對遊戲素材的保護方式通常是使用**壓縮**或是**加密**,或者是通常沒有特別去保護 所以我們開始嘗試直接使用 Asset Ripper 來進行素材的提取 ### 工具 [Asset Ripper 工具下載](https://assetripper.github.io/AssetRipper/articles/Downloads.html) **適用平台** : `Windows`, `MacOS`, `Linux` 這邊的工具就比較多樣了,比如說 AssetStudio 等等,但是我會優先選擇跨平台支援性比較好的來做介紹 ### 解壓縮 apk 其實大部分的應用程式,他們的本體都是一個資料夾,而 apk 就是一個壓縮過後的資料夾,而大部分的解壓縮工具都可以直接打開他 Windows 端可以直接使用一些工具像 `7-zip`, `bandizip` Mac / Linux 端可以去查看看怎麼使用 `unzip` 這個指令 解壓縮完成之後,我們會得到一個像這樣的資料夾 ![image](https://hackmd.io/_uploads/HkUqWpw7el.png) 其中最重要的就是 `asset` 和 `lib` 這兩個了,分別會用來提取素材和程式碼 ### 使用工具 接下來輪到使用工具的時間了,我們要做的事情很簡單,就是把上面提供的 Asset Ripper 下載下來 Windows 版本的話可以直接點擊 `.exe` 打開使用 Mac / Linux 的話可以參考這篇[教學](https://assetripper.github.io/AssetRipper/articles/RunningOnMac.html) 打開之後我們會看到這樣的介面 ![image](https://hackmd.io/_uploads/HJPRM6P7el.png) 點擊左上角 `File` > `Open Folder` 然後選取我們剛剛解壓縮完的 apk 資料夾中的 `asset` 資料夾,把它打開 等待一段時間讓他匯入之後 ![image](https://hackmd.io/_uploads/BJHYQaPXex.png) 中間按鈕會變成綠色,點進去就可以看到有哪些資料被找到了 ![image](https://hackmd.io/_uploads/rJLaXaD7ll.png) 這邊就會列出他找到的所有資源,如果列出來的資源很少,很可能就是在提取的時候遇到了問題,可以從終端機介面中查看有沒有出現 ERROR 可以從這個頁面先進行預覽,然後 export 你想要的出來 點擊 `Export` > `Export All Files` 然後 AssetRipper 就會把素材通通匯出到 AssetRipper 執行檔所在的資料中了 匯出的資料夾中會有兩個資料夾,Assets 和 Assemblies,素材會放在 Assets 中,大家就可以開始盡情地探索自己想要用看看的素材了 ![image](https://hackmd.io/_uploads/HJlcOpvQlx.png) 上面這個就是在 `Assets/skin/character/assassin/skin_0/assassin_0.png` 中找到的 sprite ## 你有一張 Sprite,卻不會讓角色動起來? ### Step 1:把 Sprite 丟進 Unity! 先把你準備好的 Spritesheet(建議是 `.png`)拖進 Unity 的 `Assets` 資料夾中。 這張圖應該長這樣(每一格是角色的一個動作): ![assassin_0](https://hackmd.io/_uploads/By5AK2F7xe.png) ### Step 2:切割sprite 1. 點選剛剛拖進來的圖片 3. 到右邊的 Inspector,把: - `Sprite Mode` 改成 `Multiple` - `Pixels Per Unit` 保持預設即可 ![截圖 2025-06-13 晚上11.36.12](https://hackmd.io/_uploads/HJOSu3K7eg.png) ![截圖 2025-06-13 晚上11.37.19](https://hackmd.io/_uploads/rkWvu2YXll.png) 4. 點「Open Sprite Editor」按鈕(第一次使用可能會跳出 Appky 視窗 → 點 Apply) ### 在 Sprite Editor 裡: - 點上方的「**Slice**」按鈕 - 把 `Type` 改成 **Automatic** - `Pivot` 可選擇 Center 或 Bottom(看你之後動畫要怎麼定位) - 按下 `Slice` → 再按右上角 `Apply` > Auto Slice 會自動根據圖像間的空隙偵測邊界,把每個角色動作自動切開,不需要你手動輸入尺寸~適合用在每格之間**有空隙**的 Spritesheet。 ![截圖 2025-06-13 晚上11.37.44](https://hackmd.io/_uploads/H1Nc_hK7xe.png) 如果你的圖片長這樣,就是切好拉~ ![截圖 2025-06-13 晚上11.38.05](https://hackmd.io/_uploads/B1nft3FXgl.png) ### Step 3:建立動畫檔,讓角色動起來! 1. 在 `Project` 面板中,展開剛剛的圖片(點下小箭頭) 2. 選取你要做成動畫的多張格子(按住 Shift 選取,例如走路的 6 張) ![截圖 2025-06-13 晚上11.49.04](https://hackmd.io/_uploads/HJJaqnt7lg.png) 4. **右鍵點擊其中一張 → 選擇 `Create > Animation`** ![截圖 2025-06-13 晚上11.49.34](https://hackmd.io/_uploads/rJzAcnt7lx.png) 6. Unity 會跳出儲存視窗,幫你建立 `.anim` 檔案 7. 點一下 `Create`,就完成囉! --- ### Step 4:讓動畫有地方住!建立角色物件 動畫做好了,但如果沒有角色 GameObject,動畫也沒地方可以「演 所以我們來自己手動生一個角色! ### 建立角色物件: 1. 在 `Hierarchy` 空白處右鍵 → `Create Empty`,幫它命名,例如 `Player` ![截圖 2025-06-14 凌晨12.02.42](https://hackmd.io/_uploads/BkpmAntmxx.png) 2. 選取 `Player` → 在右邊的 Inspector 按下 `Add Component` - 加一個 `Sprite Renderer`(這樣才看得到角色) - 一開始你可以先隨便給它一張靜態的 Sprite ![截圖 2025-06-14 凌晨12.03.43](https://hackmd.io/_uploads/H1UOA2FXlg.png) ![截圖 2025-06-14 凌晨12.04.00](https://hackmd.io/_uploads/S1ypRhYmxx.png) 3. **直接把step 3 創建的`.anim` 檔案拖曳到角色 GameObject 上** ![截圖 2025-06-14 凌晨12.04.42](https://hackmd.io/_uploads/S1TQ1TtQxx.png) Unity 會幫你: - 自動新增 `Animator` 元件 - 自動建立並綁定一個 Animator Controller - 自動設好動畫播放 #### 真的就是拖一下,全部搞定! ### Step 5:按下 Play!角色活起來啦! 點上方的 ▶️ 播放鍵,你會看到角色開始動作 ![螢幕錄影 2025-06-14 凌晨12.26.58](https://hackmd.io/_uploads/BJds7aFmlx.gif) 若沒看到動畫,請檢查: - `.anim` 有成功套用到 GameObject 上 - `Sprite Renderer` 有正常顯示(不是透明或沒圖片) --- :::warning 我發現我不會破解他的 `global-metadata.dat` 等我成功之後再來把教學補完 ::: ~~## 反編譯 有了素材雖然可以自己重新設定讓他動起來,但如果可以直接使用原本遊戲寫好的當然會更好 而反編譯顧名思義就是把原本運行遊戲的 Machine Code,想辦法還原成人能看懂的程式碼 當然,還原出來的程式碼不會和原本的一樣,像是註解不能被反編譯出來~~ ~~其實剛剛的 Asset Ripper 這個工具就可以幫忙做到反編譯 我們只需要在 Open Folder 時選擇整個 apk 資料夾即可 但是我們會遇到這個錯誤~~ ![image](https://hackmd.io/_uploads/By5N36P7gl.png) ~~簡單來說就是 `global-metadata.dat` 這個檔案被加密了,他是和 `libil2cpp.so` 一樣重要的檔案,如果被加密就沒辦法直接的進行反編譯~~ :::spoiler 額外資訊 簡單補充一下 Unity 後端,現在手機遊戲通常都是 IL2CPP :::info **Unity 編譯後端比較(Mono vs IL2CPP)** | 項目 | **Mono** | **IL2CPP** | | --------------------- | --------------------- | -------------------------------------- | | **編譯後端** | Mono(保留 .NET 結構) | IL2CPP(轉成 C++ 再編譯成原生程式) | | **遊戲內檔案** | `Assembly-CSharp.dll` | `libil2cpp.so` + `global-metadata.dat` | | **Asset Ripper 提取內容** | 原始 DLL 檔案(可直接反編譯) | 重建後的 C# 程式碼檔案(.cs) | | **混淆處理方式** | DLL 通常保留混淆,需手動閱讀與命名 | 還原出來的 .cs 仍保持混淆(如 a.a()、b.b()) | ::: ## 其他資源 ### 查殼 [Detect it Easy](https://github.com/horsicq/Detect-It-Easy) ### 反編譯 [dnspyEx](https://github.com/dnSpyEx/dnSpy/releases/tag/v6.5.1) ### 素材提取 [AssetRipper](https://assetripper.github.io/AssetRipper/) [AssetStudio 活躍分支](https://github.com/zhangjiequan/AssetStudio) [UABE](https://github.com/SeriousCache/UABE) ### 反混淆 [NETReactorSlayer](https://github.com/SychicBoy/NETReactorSlayer) 影片推薦:https://www.bilibili.com/video/BV1go4y1k7gr?spm_id_from=333.788.videopod.sections&vd_source=684e470d1f116a6ed25bbb1df06dce26