淺談逆向 Unity WebGL Il2Cpp 中 WebAssembly 函數的方法


簡介

在使用 Il2CppDumper 解包 Unity WebGL 程序后可以得到包含 C# 方法信息的 dump.cs 文件。

但是 dump.cs 文件中給出的函數偏移 offset 既不是 wasm 文件中的偏移,也不是 WebAssembly.Table 表項的偏移,這給我們逆向 Unity 的過程帶來了許多不便。

本文將探討如何通過 dump.cs 文件中給出的 offset 來定位 wasm 文件中對應的函數實現。

方法

檢查導出表,可以看到 wasm 文件中導出了許多形如 dynCall_iiii 的函數:

這些函數是 Binaryen 生成用來幫助動態調用 wasm 內部 C# 方法的輔助函數。

接下來以 dynCall_iiii 函數為例進行說明輔助函數的工作方式。

通過 dynCall_iiii 函數可以調用接受三個整型參數的內部 C# 方法,函數名稱 iiii 中的第一個 i 代表目標 C# 方法的返回類型為 int,后面三個 i 代表目標 C# 方法接受三個 int 類型作為參數。

dynCall_iiii 函數接收四個參數,其中第一個參數用來指示目標 C# 方法在 iiii 子表的編號,即 dump.cs 文件中給出的 offset,后面的參數會依次傳遞給指定的 C# 方法。

dynCall_iiii 函數實現如下:

undefined4 export::dynCall_iiii(uint param1,undefined4 param2,undefined4 param3,undefined4 param4)
{
  undefined4 uVar1;
  
  uVar1 = (**(code **)((longlong)(int)((param1 & 0xfff) + 0x2b70) * 8))(param2,param3,param4);
  return uVar1;
}

輔助函數通過基址加偏移的方式在 WebAssembly.Table 表項中定位目標 C# 方法的地址,其中基址 0x2b70 對應 iiii 子表在 WebAssembly.Table 中的起始地址,而偏移量則對應目標 C# 方法在 iiii 子表的編號。

還有許多形如 dynCall_vijfd 的函數,具體的含義以及命名方法可以參考下面的表格:

字符 C++ 類型 C# 類型
v void void
i int int
j long long long
f float float
d double double

實例

下面以 N1CTF 2021 中的題目 Nu1L Hotel Checkin 為例說明如何定位 C# 方法在 wasm 中的函數實現。

通過 Il2CppDumper 解包 global-metadata.datwasm 文件得到的 dump.cs 內容如下:

// Namespace: 
public class N1CTFChecker : MonoBehaviour // TypeDefIndex: 2380
{
	// Methods

	// RVA: 0x90F Offset: 0x90F VA: 0x90F
	private void Start() { }

	// RVA: 0x910 Offset: 0x910 VA: 0x910
	private void Update() { }

	// RVA: 0x635 Offset: 0x635 VA: 0x635
	public bool check(string flag) { }

	// RVA: 0x911 Offset: 0x911 VA: 0x911
	public void OnClick() { }

	// RVA: 0x912 Offset: 0x912 VA: 0x912
	public void .ctor() { }
}

現在我們要定位 N1CTFChecker::check 方法在 wasm 文件中的位置。

首先在 Il2CppDumper 解包得到的 script.json 中找到 N1CTFChecker::check 方法在經過 Il2Cpp 轉換后的函數聲明:

    {
      "Address": 1589,
      "Name": "N1CTFChecker$$check",
      "Signature": "bool N1CTFChecker__check (N1CTFChecker_o* __this, System_String_o* flag, const MethodInfo* method);"
    },

根據返回類型以及參數類型可以判斷對應的輔助函數為 dynCall_iiii

接着使用 Ghidraghidra-wasm-plugin 插件對 wasm 文件進行反編譯,查看 dynCall_iiii 函數實現:

undefined4 export::dynCall_iiii(uint param1,undefined4 param2,undefined4 param3,undefined4 param4)
{
  undefined4 uVar1;
  
  uVar1 = (**(code **)((longlong)(int)((param1 & 0xfff) + 0x2b70) * 8))(param2,param3,param4);
  return uVar1;
}

得到對應的基址為 0x2b70,加上 N1CTFChecker::check 方法的偏移量 0x635 得到索引 0x2b70 + 0x635 = 0x31a5

然后在控制台中使用 js 索引 WebAssembly.Table 的第 0x31a5 項即可得到目標 C# 方法在 wasm 文件中的函數編號 30814

table = UnityLoader.Blobs["blob:https://n1ctf-hotel-checkin.misty.workers.dev/9de00924-0574-43d8-a65f-a7992b7d289e"].Module.asmLibraryArg.table
table.get(0x31a5)

或者在 wasm2wat 解析得到的 wat 文件中搜索 (elem,也可以查看 WebAssembly.Table 的內容:

(elem (;0;) (global.get 0) func 33703 16150 16157 33703 33704 16142 ...)

最后在 Ghidra 中定位到 unnamed_function_30814 即可找到 N1CTFChecker::check 方法在 wasm 中的函數實現:

undefined4 unnamed_function_30814(undefined4 param1,undefined4 flag,undefined4 param3)

{
  int index;
  int index2;
  int iVar1;
  undefined4 uVar2;
  int index3;
  int table_;
  int target_;
  int buffer;
  int *flag__;
  int length;
  undefined4 uStack00000000;
  undefined4 uStack00000004;
  undefined4 uStack00000008;
  
  if (cRam0028289f == '\0') {
    unnamed_function_31565(PTR_DAT_ram_00001536_ram_00065510);
    cRam0028289f = '\x01';
  }
  uVar2 = unnamed_function_27012(0);
  index3 = unnamed_function_13755(0xf,uVar2,flag);
                      /* 54*54 */
  table_ = malloc_int(_DAT_ram_001fbaec,&DAT_ram_00000b64);
  uStack00000004 = _DAT_ram_00201408;
  uStack00000008 = _DAT_ram_00201408;
  unnamed_function_14607(table_,&stack0x00000008,0);
                      /* 54 */
  target_ = malloc_int(_DAT_ram_001fbaec,0x36);
  uStack00000000 = _DAT_ram_00201478;
  uStack00000008 = _DAT_ram_00201478;
  unnamed_function_14607(target_,&stack0x00000008,0);
  flag__ = (int *)(index3 + 0xc);
                      /* cmp length
                         *(flag_ + 0xc)==*(target + 0xc) */
  if (*flag__ == *(int *)(target_ + 0xc)) {
    buffer = malloc_int(_DAT_ram_001fbaec,*flag__);
    for (index = 0; length = *flag__, index < length; index = index + 1) {
                      /* transform */
      iVar1 = 0;
      for (index2 = 0; index2 < length; index2 = index2 + 1) {
         iVar1 = *(int *)(table_ + 0x10 + (index2 + *flag__ * index) * 4) *
                  (uint)*(byte *)(index3 + 0x10 + index2) + iVar1;
         length = *flag__;
                      /* :transform_inner
                         int table[54*54]
                         char flag[54]
                         var1 += table[index2+length*index] * flag[index2] */
      }
      *(int *)(buffer + 0x10 + index * 4) = iVar1;
    }
    for (index3 = 0; index3 < length; index3 = index3 + 1) {
                      /* :compare
                         int code[54]
                         buffer[index3] == code[index3] */
      if (*(int *)(buffer + 0x10 + index3 * 4) != *(int *)(target_ + 0x10 + index3 * 4)) {
         return 0;
      }
      length = *flag__;
    }
                      /* right */
    uVar2 = 1;
  }
  else {
                      /* wrong */
    uVar2 = 0;
  }
  return uVar2;
}

至此,我們成功在 Il2Cpp 轉換后的 wasm 中找到了 N1CTFChecker::check 方法的位置。

后記

Wasm 動態調用

由於 wasm 設計中的安全限制,wasm 內部無法直接通過變量中的函數地址進行跳轉,所以只能通過輔助函數調用 call_indirect 指令結合 WebAssembly.Table 來實現函數的動態調用。

接下來以 VirtFuncInvoker 函數為例進行說明動態調用在 x86-64wasm 中的區別。

VirtFuncInvokerx86-64 下,先從 klass->vtable 中獲取函數地址,再通過函數指針進行調用:

graph LR VirtFuncInvoker-->VTable VTable-->call call-->VirtFunc

VirtFuncInvokerwasm 下,先從 klass->vtable 中獲取函數編號,加上一個與目標 C# 方法的返回類型以及參數類型有關的基址后,再通過 call_indirect 指令結合 WebAssembly.Table 進行調用:

graph LR VirtFuncInvoker-->VTable Signature-->WebAssembly.Table VTable-->WebAssembly.Table WebAssembly.Table-->call_indirect call_indirect-->VirtFunc

Unity Il2Cpp 中相關代碼:

FORCE_INLINE const VirtualInvokeData& il2cpp_codegen_get_virtual_invoke_data(Il2CppMethodSlot slot, const RuntimeObject* obj)
{
    Assert(slot != kInvalidIl2CppMethodSlot && "il2cpp_codegen_get_virtual_invoke_data got called on a non-virtual method");
    return obj->klass->vtable[slot];
}

關於 ghidra-wasm-plugin

Unity WebGLElement 段的加載位置依賴於外部變量 tableBase

(import "env" "table" (table (;0;) 51716 51716 funcref))
(import "env" "tableBase" (global (;0;) i32))
...
(elem (;0;) (global.get 0) func 33703 16150 16157 33703 33704 16142 16143 ...)

在這種情況下,插件不會自動分析 Table 段的內容。

UnityLoader 中可以找到 tableBase 的值為 0

if (!env["tableBase"]) {
    env["tableBase"] = 0
}

根據 tableBase 的值將 Element #0 段映射到 Table #0 段的 0 號偏移處:

from wasm import WasmLoader
from wasm.analysis import WasmAnalysis
from ghidra.util.task import ConsoleTaskMonitor
monitor = ConsoleTaskMonitor()
WasmLoader.loadElementsToTable(currentProgram, WasmAnalysis.getState(currentProgram).module, 0, 0, 0, monitor)

Data 段的加載位置是立即數,插件可以自動分析,所以不需要手動指定偏移:

(data (;14128;) (i32.const 1567959) "\5c")

然后執行 analyze_dyncalls.py 分析程序中的 dynCall

該腳本可以自動提取所有 dynCall 函數中的基址,並將 WebAssembly.Table 中的函數重命名為 func_[signature]_[offset] 的格式。

執行完成后搜索 func_iiii_1589 就可以定位到 N1CTFChecker::check 的位置,省去了手動計算的過程。

關於 Il2CppDumper

其實到這里可以看出來,使用腳本自動恢復符號需要做的工作很簡單,把上面的幾個步驟連起來就可以了,於是就有了這個 Pull request

添加 ghidra_wasm.py 腳本,根據 script.json 的內容恢復 wasm 中的符號信息。

似乎有少量函數名稱沒有正確恢復出來,看了一下是 Il2CppDumper 生成 C 函數簽名的邏輯有點問題,但是應該問題不大(

使用腳本恢復 wasm 符號后的結果:

和帶調試符號的 wasm 進行對比:

和帶調試符號的 x86-64 進行對比:

可以看出來除了像 IsInstInterfaceFuncInvokerVirtFuncInvoker 這樣不需要導出的符號以外,其他符號的恢復效果都很可觀。

StructGenerator.cs這里 不知道為什么作者沒有用標准的 C 語言格式輸出 struct,導致 Ghidra 不能正常解析,還有一些類型聲明問題 #287 似乎並沒有很好的解決,在作者博客中也有提及到 這點,不過這個功能好像不是很重要,就先不修了(

參考

Il2CppDumperhttps://github.com/Perfare/Il2CppDumper

ghidra-wasm-pluginhttps://github.com/nneonneo/ghidra-wasm-plugin/

WebAssemblyhttps://developer.mozilla.org/en-US/docs/WebAssembly

WebAssembly.Tablehttps://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/Table

WebAssembly 文本格式:https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format

WebAssembly 工具集:https://github.com/WebAssembly/wabt

Emscriptenhttps://github.com/emscripten-core/emscripten

Binaryenhttps://github.com/WebAssembly/binaryen

GenerateDynCallshttps://github.com/WebAssembly/binaryen/blob/main/src/passes/GenerateDynCalls.cpp

Unity WebGL 分析:https://qiita.com/hikipuro/items/d7cbc4294dd6b58d0ffb


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM