zoukankan      html  css  js  c++  java
  • 浅谈逆向 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

  • 相关阅读:
    jquery.cookie.js 的使用
    2013年工作中遇到的20个问题:141-160
    提高生产力:文件和IO操作(ApacheCommonsIO-汉化分享)
    提高生产力:文件和IO操作(ApacheCommonsIO-汉化分享)
    我的网站恢复访问了,http://FansUnion.cn
    我的网站恢复访问了,http://FansUnion.cn
    噩梦遇地震,醒后忆岁月
    噩梦遇地震,醒后忆岁月
    2013年工作中遇到的20个问题:121-140
    2013年工作中遇到的20个问题:121-140
  • 原文地址:https://www.cnblogs.com/algonote/p/15596459.html
Copyright © 2011-2022 走看看