Mono API 调用链#

┌──────────────────────┐
│ mono_get_root_domain │
│ 获取 Mono RootDomain  │
└──────────┬───────────┘
┌─────────────────────────┐
│ mono_thread_attach      │
│ 将当前线程附加到 Mono      │
└──────────┬──────────────┘
┌─────────────────────────┐
│ mono_domain_assembly_open│
│ 加载程序集 (.dll)         │
└──────────┬──────────────┘
┌─────────────────────────┐
│ mono_assembly_get_image │
│ 获取 MonoImage           │
│ (程序集元数据)            │
└──────────┬──────────────┘
┌─────────────────────────┐
│ mono_class_from_name    │
│ 获取类 MonoClass         │
│ (namespace + class)     │
└──────────┬──────────────┘
┌─────────────────────────┐
│ mono_class_get_method_  │
│ from_name               │
│ 获取方法 MonoMethod      │
└──────────┬──────────────┘
┌─────────────────────────┐
│ mono_runtime_invoke     │
│ 调用 C# 方法             │
└──────────┬──────────────┘
      C# Method 执行

即有这样的关系

Domain → Assembly → Image → Class → Method → Invoke

mono_get_root_domain()#

获取当前 Mono 运行时的根域 (Root Domain)

MonoDomain* domain = mono_get_root_domain();
mono_thread_attach(domain);

Mono 中有 Domain(应用域)的概念,类似 .NET 的 AppDomain

Root Domain
 ├─ Domain 1 (main app)
 ├─ Domain 2 (plugin / reload)
 └─ Domain 3 ...

根域是 Mono runtime 初始化时创建的主域

mono_get_root_domain() 最常见的用途是在 native 代码里操作 C#/Mono 程序

示例(Unity 注入常见流程):获取根域 -> 加载程序集 -> 获取类 -> 获取方法 -> 调用 C# 函数

MonoDomain* domain = mono_get_root_domain();
mono_thread_attach(domain);

MonoAssembly* assembly = mono_assembly_open("hack.dll", NULL);
MonoImage* image = mono_assembly_get_image(assembly);
MonoClass* klass = mono_class_from_name(image, "Namespace", "Class");
MonoMethod* method = mono_class_get_method_from_name(klass, "Start", 0);

mono_runtime_invoke(method, NULL, NULL, NULL);

mono_image_get_table_info()#

从 Image(即 Assembly 解析后的元数据结构)取出某一张元数据表(metadata table),用于遍历程序里的类型、方法、字段。

const MonoTableInfo* mono_image_get_table_info (MonoImage *image, int table_id);

比如可以拿 TypeDef 表的每一行,然后遍历拿到类

MonoImage* image = mono_assembly_get_image(assembly);

const MonoTableInfo* table = mono_image_get_table_info(image, MONO_TABLE_TYPEDEF);
int rows = mono_table_info_get_rows(table);

for (int i = 0; i < rows; ++i) {
    uint32_t token = MONO_TOKEN_TYPE_DEF | (i + 1);
    MonoClass* klass = mono_class_get(image, token);

    if (!klass)
        continue;

    // use klass
}

字段:field 与 token(FieldDef)#

assembly 中,类、字段、方法都有自己的元数据条目,IL 指令通过写 token(类似编号)来索引字段和方法。field 则像是一种运行时句柄,指向字段的元数据条目,mono_get_fields 拿到字段,mono_field_get_token 拿到句柄,mono_field_get_name 拿到字段名,这样 token 和字段就能对应起来

方法:MethodDef (def_token)、MemberRef (ref_oken)、MethodSpec (spec_oken)#

对于方法, mono_method_get_token 拿到的是定义 token (MethodDef),下面 hook dump 出来的 IL 调用方法有些是用 MemberRef

字段也有 MethodRef

高八位 Table 名 含义
0x04 FieldDef 字段
0x06 MethodDef 方法定义
0x0a MemberRef 成员引用
0x2b MethodSpec 泛型方法实例化(带上具体类型参数后)引用

RocketMouse#

Assembly-CSharp.dll 被加密了不用管,之后把 libmono.so hook 下来可以用 Mono API 拿所有信息(assembly -> image -> table -> fqcn -> methods -> signature)。

启动游戏报 hack detected,把 android_server 和 frida-server 改个名就过了,之后尝试注入 frida 又报了,换个监听端口

adb forward tcp:28080 tcp:28080
adb shell su -c '/data/local/tmp/fd-sv -l 127.0.0.1:28080' &

成功注入(后面发现弹窗后再 attach 就行了…)

恢复托管方法结构#

确认 libmono.so 导出表

nm -D resources/lib/armeabi-v7a/libmono.so | rg mono

资源非常充足,直接 hook libmono.so

frida -H 127.0.0.1:28080 -N com.personal.rocketmouse -l scripts/frida_noop.js
[MI 6::MOU ]-> [*] libmono.so loaded
[*] mono thread attached
[assembly] System
[assembly] System.Core
[assembly] UnityEngine.PlaymodeTestsRunner
[assembly] UnityEngine.Networking
[assembly] UnityEngine.UI
[assembly] Assembly-CSharp
[*] scanning Assembly-CSharp typedef rows=8
[class] LaserScript
  [method] .ctor ::
  [method] Start ::
  [method] FixedUpdate ::
[class] MouseController
  [method] .ctor ::
  [method] Start ::
  [method] FixedUpdate ::
  [method] UpdateGroundedStatus ::
  [method] AdjustJetpack :: bool
  [method] OnTriggerEnter2D :: UnityEngine.Collider2D
  [method] HitByLaser :: UnityEngine.Collider2D    # <- 这里
  [method] CollectCoin :: UnityEngine.Collider2D
  [method] AdjustFootstepsAndJetpackSound :: bool
  [method] RestartGame ::		# <- 这里
  [method] ExitToMenu ::
[class] UIManagerScript
  [method] CloseSettings ::
[*] target assembly scan complete
[assembly] UnityEngine
[assembly] mscorlib
[*] mono scan complete

去找 HitByLaser 和 RestartGame,依旧 frida

思路很简单:mono_get_root_domain 拿到根域 -> mono_assembly_get_image 拿到 Assembly-CSharp -> mono_class_from_name 拿到 MouseController -> mono_class_get_method_from_name 拿到两个方法 -> mono_method_get_header 拿到头 -> mono_method_header_get_code 拿到代码段地址 -> mono_disasm_code 拿到字节码 -> mono_compile_method JIT 成 native 入口(太方便了)

[Remote::com.personal.rocketmouse ]-> [*] libmono.so loaded
[*] mono thread attached
[*] located assembly image: Assembly-CSharp @ 0xdb3c20b0
[fields-begin] MouseController
0x04000014 jetpackForce
0x04000015 forwardMovementSpeed
0x04000016 groundCheckTransform
0x04000017 groundCheckLayerMask
0x04000018 jetpack
0x04000019 coinIconTexture
0x0400001a coinCollectSound
0x0400001b jetpackAudio
0x0400001c footstepsAudio
0x0400001d parallax
0x0400001e startButton
0x0400001f settingsButton
0x04000020 coinsLabel
0x04000021 restartDialog
0x04000022 animator
0x04000023 grounded
0x04000024 dead
0x04000025 coins
[fields-end] MouseController


=== MouseController.HitByLaser/1 ===
[meta] class=0xe930ab50 method=0xe930aeb8 header=0xe3c855c0
[il] code_size=64 max_stack=3 il_ptr=0xd0223e38
[il-bytes] 02 7b 24 00 00 04 3a 10 00 00 00 03 6f 32 00 00 0a 6f 06 00 00 2b 6f 35 00 00 0a 02 17 7d 24 00 00 04 02 7b 22 00 00 04 72 37 00 00 70 17 6f 2d 00 00 0a 02 7b 21 00 00 04 17 6f 22 00 00 0a 2a
[token-resolve-begin]
field 0x04000021
field 0x04000022
field 0x04000024
method 0x0a000032 -> UnityEngine.Component:get_gameObject () [def_token=0x06000305]
method 0x2b000006 -> UnityEngine.GameObject:GetComponent<UnityEngine.AudioSource> () [def_token=0x060003f3]
method 0x0a000035 -> UnityEngine.AudioSource:Play () [def_token=0x06001d12]
method 0x0a00002d -> UnityEngine.Animator:SetBool (string,bool) [def_token=0x06001ed0]
method 0x0a000022 -> UnityEngine.GameObject:SetActive (bool) [def_token=0x06000412]
[token-resolve-end]

[il-disasm-begin]
IL_0000: ldarg.0
IL_0001: ldfld     0x04000024
IL_0006: brtrue    IL_001b
IL_000b: ldarg.1
IL_000c: callvirt  0x0a000032
IL_0011: callvirt  0x2b000006
IL_0016: callvirt  0x0a000035
IL_001b: ldarg.0
IL_001c: ldc.i4.1
IL_001d: stfld     0x04000024
IL_0022: ldarg.0
IL_0023: ldfld     0x04000022
IL_0028: ldstr     "dead"
IL_002d: ldc.i4.1
IL_002e: callvirt  0x0a00002d
IL_0033: ldarg.0
IL_0034: ldfld     0x04000021
IL_0039: ldc.i4.1
IL_003a: callvirt  0x0a000022
IL_003f: ret

[il-disasm-end]
[native] compiled_ptr=0xd8af6160
[native-disasm-begin]
IL_0000: ldarg.0
IL_0001: ldfld     0x04000024
IL_0006: brtrue    IL_001b
IL_000b: ldarg.1
IL_000c: callvirt  0x0a000032
IL_0011: callvirt  0x2b000006
IL_0016: callvirt  0x0a000035
IL_001b: ldarg.0
IL_001c: ldc.i4.1
IL_001d: stfld     0x04000024
IL_0022: ldarg.0
IL_0023: ldfld     0x04000022
IL_0028: ldstr     "dead"
IL_002d: ldc.i4.1
IL_002e: callvirt  0x0a00002d
IL_0033: ldarg.0
IL_0034: ldfld     0x04000021
IL_0039: ldc.i4.1
IL_003a: callvirt  0x0a000022
IL_003f: ret
[il-disasm-end]

[native] compiled_ptr=0xd8af6160
[native-disasm-begin]
0xd8af6160  mov ip, sp
0xd8af6164  push {r6, r7, r8, fp, ip, lr}
0xd8af6168  sub sp, sp, #8
0xd8af616c  mov fp, sp
0xd8af6170  mov r6, r0
0xd8af6174  mov r7, r1
0xd8af6178  ldrb r0, [r6, #0x49]
0xd8af617c  cmp r0, #0
0xd8af6180  bne #0xd8af61ac
0xd8af6184  mov r0, r7
0xd8af6188  ldr lr, [r7]
0xd8af618c  bl #0xd8ae75f4
0xd8af6190  ldr lr, [r0]
0xd8af6194  movw r8, #0x8718
0xd8af6198  movt r8, #0xb267
0xd8af619c  bl #0xd8af6234
0xd8af61a0  mov r1, r0
0xd8af61a4  ldr lr, [r1]
0xd8af61a8  bl #0xd8af621c
0xd8af61ac  mov r0, #1
0xd8af61b0  strb r0, [r6, #0x49]
0xd8af61b4  ldr r3, [r6, #0x38]
0xd8af61b8  mov r0, r3
0xd8af61bc  movw r1, #0xc3a8
0xd8af61c0  movt r1, #0xe3b1
0xd8af61c4  mov r2, #1
0xd8af61c8  ldr lr, [r3]
0xd8af61cc  bl #0xd8af6204
0xd8af61d0  ldr r2, [r6, #0x34]
0xd8af61d4  mov r0, r2
0xd8af61d8  mov r1, #1
0xd8af61dc  ldr lr, [r2]
0xd8af61e0  bl #0xd8af61ec
0xd8af61e4  add sp, fp, #8
0xd8af61e8  ldm sp, {r6, r7, r8, fp, sp, pc}
0xd8af61ec  push {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, lr}
0xd8af61f0  ldr r1, [pc, #8]
0xd8af61f4  mov lr, pc
0xd8af61f8  bx r1
0xd8af61fc  ldcge p14, c4, [r2, #0x60]
0xd8af6200  ldr r7, [ip, #0]!
0xd8af6204  push {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, lr}
0xd8af6208  ldr r1, [pc, #8]
0xd8af620c  mov lr, pc
0xd8af6210  bx r1
<decode-error @ 0xd8af6214: Error: invalid instruction>
[native-disasm-end]


=== MouseController.RestartGame/0 ===
[meta] class=0xe930ab50 method=0xe930af18 header=0xb2ecbb68
[il] code_size=11 max_stack=8 il_ptr=0xd0223f3f
[il-bytes] 28 3a 00 00 0a 28 3b 00 00 0a 2a

[token-resolve-begin]
method 0x0a00003a -> UnityEngine.Application:get_loadedLevelName () [def_token=0x0600015b]
method 0x0a00003b -> UnityEngine.Application:LoadLevel (string) [def_token=0x0600015d]
[token-resolve-end]

[il-disasm-begin]
IL_0000: call      0x0a00003a
IL_0005: call      0x0a00003b
IL_000a: ret
[il-disasm-end]

[native] compiled_ptr=0xd8af6250
[native-disasm-begin]
0xd8af6250  mov ip, sp
0xd8af6254  push {r8, fp, ip, lr}
0xd8af6258  sub sp, sp, #8
0xd8af625c  mov fp, sp
0xd8af6260  str r0, [fp]
0xd8af6264  bl #0xd8af628c
0xd8af6268  bl #0xd8af6274
0xd8af626c  add sp, fp, #8
0xd8af6270  ldm sp, {r8, fp, sp, pc}
0xd8af6274  push {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, lr}
0xd8af6278  ldr r1, [pc, #8]
0xd8af627c  mov lr, pc
0xd8af6280  bx r1
0xd8af6284  blle #0xd984cb4c
0xd8af6288  ldr r7, [ip, #0]!
0xd8af628c  push {r0, r1, r2, r3, r4, r5, r6, r7, r8, sb, sl, fp, ip, lr}
0xd8af6290  ldr r1, [pc, #8]
0xd8af6294  mov lr, pc
0xd8af6298  bx r1
0xd8af629c  blle #0xd984ca64
0xd8af62a0  ldr r7, [ip, #0]!
0xd8af62a4  andeq r0, r0, r0
0xd8af62a8  andeq r0, r0, r0
0xd8af62ac  andeq r0, r0, r0
0xd8af62b0  andeq r0, r0, r0
0xd8af62b4  andeq r0, r0, r0
0xd8af62b8  andeq r0, r0, r0
0xd8af62bc  andeq r0, r0, r0
0xd8af62c0  andeq r0, r0, r0
0xd8af62c4  andeq r0, r0, r0
0xd8af62c8  andeq r0, r0, r0
0xd8af62cc  andeq r0, r0, r0
0xd8af62d0  andeq r0, r0, r0
0xd8af62d4  andeq r0, r0, r0
0xd8af62d8  andeq r0, r0, r0
0xd8af62dc  andeq r0, r0, r0
0xd8af62e0  andeq r0, r0, r0
0xd8af62e4  andeq r0, r0, r0
0xd8af62e8  andeq r0, r0, r0
0xd8af62ec  andeq r0, r0, r0
0xd8af62f0  andeq r0, r0, r0
0xd8af62f4  andeq r0, r0, r0
0xd8af62f8  andeq r0, r0, r0
0xd8af62fc  andeq r0, r0, r0
0xd8af6300  andeq r0, r0, r0
0xd8af6304  andeq r0, r0, r0
0xd8af6308  andeq r0, r0, r0
0xd8af630c  andeq r0, r0, r0
[native-disasm-end]
[*] key function dump complete

之后通过 frida 动态去 patch 就可以实现无敌了。要做成打包的破解版要去看加密程序集的逻辑,先咕咕