frida 调用 mono api 实现游戏破解
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 就可以实现无敌了。要做成打包的破解版要去看加密程序集的逻辑,先咕咕