题目复现

XYCTF2024-ez unity

这个题是il2cpp打包的unity程序。但是打开后发现GameAssembly.dll加了UPX壳,这个无妨脱掉即可。

4764420e64dbe1d81a11b2dacaf9e1f3

但是吧,如果这样去使用Il2CppDumper.exe工具的话会报错,并不能成功dump。

ERROR: Metadata file not found or encrypted.

报这样的错,要么是没有找到文件,要么是文件被加密了。很大的可能就是global-metadata.dat文件被加密了

标准的global-metadata.dat文件有一个固定的文件头标识(Magic Bytes)AF 1B B1 FA (十六进制)。Il2CppDumper 在读取文件时,会首先检查这几个字节,如果不匹配就会直接抛出“被加密”的错误。

image-20260331155002347

这里很明显不是标准的文件头,说明可能被加密了

思路:

1.动态 Dump:游戏能正常运行,说明内存里的数据是解密后的,可以用CE附加,从内存里搜索AF 1B B1 FA。把解密后的 metadata 内存块给 Dump 出来保存为 .dat 文件。

也可以去写一个frida脚本,思路也是去内存里找AF 1B B1 FA这个魔术头来定位文件在内存里的起始地址,然后再解析文件头去计算文件的大小,最后dump。

2.静态分析解密算法:

libil2cpp.so里面有个il2cpp_init函数是加载函数调用链中的第一个函数,整个调用链是这样的

1
2
3
4
il2cpp_init
-> il2cpp::vm::Runtime::Init
-> il2cpp::vm::MetadataCache::Initialize
-> il2cpp::vm::MetadataLoader::LoadMetadataFile

用ida加载GameAssembly.dll,去搜索字符串global-metadata.dat,这样可以找到MetadataCache::InitializeMetadataLoader::LoadMetadataFile 函数,去分析加密算法,但是吧一般字符串global-metadata.dat会被加密,直接搜是搜不到的。

所以可以看这个调用链,去搜索il2cpp_init,然后接着去定位。

这里特别推荐阅读大佬的文章:https://cloud.tencent.com/developer/article/2216959

题目里提示到 il2cpp的源码是开源的,可以进行对照去看它的混淆

http://github.com/4ch12dy/il2cpp/blob/master/unity_2019_x/libil2cpp/il2cpp-api.cpp

打开 ida ,在Exports里面去搜索il2cpp_init

image-20260331162052131

image-20260331164014832

这个是对照源码的

1
2
3
4
5
6
7
int il2cpp_init(const char* domain_name)
{
// Use environment's default locale
setlocale(LC_ALL, "");

return Runtime::Init(domain_name);
}

il2cpp_init 是IL2CPP运行时的初始化入口函数,它会调用 Runtime::Init(domain_name) 去真正初始化 IL2CPP 运行,并把初始化结果返回出去。

sub_1801E8FD0函数就是Runtime::Init

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
// Hidden C++ exception states: #wind=3
__int64 __fastcall sub_1801E8FD0(il2cpp_baselib *a1)
{
__int64 CurrentThreadId; // rbx
int v3; // edx
int v4; // ecx
unsigned __int8 v5; // bl
__int64 v6; // rbx
__int64 v7; // rdi
__int64 v8; // rax
__int64 method; // rax
__int64 v10; // rax
__int64 method_1; // rbx
__int64 root_domain_0; // rsi
__int64 v13; // rax
__int64 v14; // rdi
__int64 v15; // rbx
__int64 field_from_name_0; // rbx
__int64 v17; // rax
void *MONO_REFLECTION_SERIALIZER_1; // rcx
void *no__1; // rcx
void *MONO_REFLECTION_SERIALIZER_2; // rcx
void *no__2; // rcx
__int64 v22; // rcx
_QWORD *v23; // rax
__int64 v24; // rcx
__int64 v25; // rcx
void *v26; // rcx
__int128 MONO_REFLECTION_SERIALIZER; // [rsp+20h] [rbp-60h] BYREF
__m128i v29; // [rsp+30h] [rbp-50h]
_QWORD v30[2]; // [rsp+40h] [rbp-40h] BYREF
__m128i v31; // [rsp+50h] [rbp-30h]
__int128 no_; // [rsp+60h] [rbp-20h] BYREF
__m128i si128; // [rsp+70h] [rbp-10h]
_QWORD *v34; // [rsp+B8h] [rbp+38h] BYREF
__int64 *v35; // [rsp+C0h] [rbp+40h]

v35 = &qword_181333970;
CurrentThreadId = il2cpp_baselib::Baselib_Thread_GetCurrentThreadId(a1);
if ( CurrentThreadId == ::CurrentThreadId )
{
v3 = dword_1813339C0 + 1;
}
else
{
if ( _InterlockedExchangeAdd(&dword_181333978, 0xFFFFFFFF) <= 0 )
il2cpp_baselib::Baselib_SystemSemaphore_Acquire(qword_181333970);
::CurrentThreadId = CurrentThreadId;
v3 = 1;
}
dword_1813339C0 = v3;
v4 = dword_181333908++;
if ( v4 <= 0 )
{
sub_180218C50();
sub_1801BC6A0();
sub_18022B2E0();
4_0 = (__int64)"4.0";
sub_1801BC610();
sub_1801A5E30();
sub_180181960();
psub_1800954F0();
if ( (unsigned __int8)sub_1801F5DE0() )
{
mono_thread_suspend_all_other_threads();
sub_18020F220();
sub_18018EC10();
sub_1801D1070(j_j__malloc_base, j_free);
memset(&::method, 0, 768u);
v6 = sub_18021C4B0("mscorlib.dll");
v7 = sub_18021C4B0("__Generated");
::method = jinfo_get_method(v6);
method_0 = jinfo_get_method(v7);
qword_1813335B0 = il2cpp_class_from_name_0(::method, "System", "Object");
qword_1813335C0 = il2cpp_class_from_name_0(::method, "System", "Void");
qword_1813335C8 = il2cpp_class_from_name_0(::method, "System", "Boolean");
qword_1813335B8 = il2cpp_class_from_name_0(::method, "System", "Byte");
qword_1813335D0 = il2cpp_class_from_name_0(::method, "System", "SByte");
qword_1813335D8 = il2cpp_class_from_name_0(::method, "System", "Int16");
qword_1813335E0 = il2cpp_class_from_name_0(::method, "System", "UInt16");
qword_1813335E8 = il2cpp_class_from_name_0(::method, "System", "Int32");
qword_1813335F0 = il2cpp_class_from_name_0(::method, "System", "UInt32");
qword_181333600 = il2cpp_class_from_name_0(::method, "System", "UIntPtr");
qword_1813335F8 = il2cpp_class_from_name_0(::method, "System", "IntPtr");
qword_181333608 = il2cpp_class_from_name_0(::method, "System", "Int64");
qword_181333610 = il2cpp_class_from_name_0(::method, "System", "UInt64");
qword_181333618 = il2cpp_class_from_name_0(::method, "System", "Single");
qword_181333620 = il2cpp_class_from_name_0(::method, "System", "Double");
qword_181333628 = il2cpp_class_from_name_0(::method, "System", "Char");
qword_181333630 = il2cpp_class_from_name_0(::method, "System", "String");
qword_181333638 = il2cpp_class_from_name_0(::method, "System", "Enum");
qword_181333640 = il2cpp_class_from_name_0(::method, "System", "Array");
qword_1813337F0 = il2cpp_class_from_name_0(::method, "System", "ValueType");
qword_181333648 = il2cpp_class_from_name_0(::method, "System", "Delegate");
qword_181333650 = il2cpp_class_from_name_0(::method, "System", "MulticastDelegate");
qword_181333658 = il2cpp_class_from_name_0(::method, "System.Runtime.Remoting.Messaging", "AsyncResult");
qword_181333788 = il2cpp_class_from_name_0(::method, "System", "MonoAsyncCall");
qword_181333660 = il2cpp_class_from_name_0(::method, "System.Threading", "ManualResetEvent");
qword_181333680 = il2cpp_class_from_name_0(::method, "System", "Type");
qword_181333688 = il2cpp_class_from_name_0(::method, "System", "MonoType");
qword_1813336A0 = il2cpp_class_from_name_0(::method, "System.Threading", "Thread");
qword_1813336A8 = il2cpp_class_from_name_0(::method, "System.Threading", "InternalThread");
qword_181333740 = il2cpp_class_from_name_0(::method, "System", "RuntimeType");
qword_1813336B0 = il2cpp_class_from_name_0(::method, "System", "AppDomain");
qword_1813336B8 = il2cpp_class_from_name_0(::method, "System", "AppDomainSetup");
qword_1813336C0 = il2cpp_class_from_name_0(::method, "System.Reflection", "MemberInfo");
qword_1813336C8 = il2cpp_class_from_name_0(::method, "System.Reflection", "FieldInfo");
qword_1813336D0 = il2cpp_class_from_name_0(::method, "System.Reflection", "MethodInfo");
qword_1813336D8 = il2cpp_class_from_name_0(::method, "System.Reflection", "PropertyInfo");
qword_1813336E0 = il2cpp_class_from_name_0(::method, "System.Reflection", "EventInfo");
qword_1813336E8 = il2cpp_class_from_name_0(::method, "System.Text", "StringBuilder");
qword_1813336F0 = il2cpp_class_from_name_0(::method, "System.Diagnostics", "StackFrame");
qword_1813336F8 = il2cpp_class_from_name_0(::method, "System.Diagnostics", "StackTrace");
qword_181333708 = il2cpp_class_from_name_0(::method, "System", "TypedReference");
qword_181333718 = il2cpp_class_from_name_0(::method, "System.Collections.Generic", "IList`1");
qword_181333720 = il2cpp_class_from_name_0(::method, "System.Collections.Generic", "ICollection`1");
qword_181333728 = il2cpp_class_from_name_0(::method, "System.Collections.Generic", "IEnumerable`1");
qword_181333730 = il2cpp_class_from_name_0(::method, "System.Collections.Generic", "IReadOnlyList`1");
qword_181333738 = il2cpp_class_from_name_0(::method, "System.Collections.Generic", "IReadOnlyCollection`1");
qword_181333748 = il2cpp_class_from_name_0(::method, "System", "Nullable`1");
qword_181333778 = il2cpp_class_from_name_0(::method, "System", "Version");
qword_181333780 = il2cpp_class_from_name_0(::method, "System.Globalization", "CultureInfo");
qword_181333790 = il2cpp_class_from_name_0(::method, "System.Reflection", "RuntimeAssembly");
qword_181333798 = il2cpp_class_from_name_0(::method, "System.Reflection", "AssemblyName");
qword_1813337A0 = il2cpp_class_from_name_0(::method, "System.Reflection", "RuntimeParameterInfo");
qword_1813337A8 = il2cpp_class_from_name_0(::method, "System.Reflection", "RuntimeModule");
qword_181333690 = il2cpp_class_from_name_0(::method, "System", "Exception");
qword_1813337B0 = il2cpp_class_from_name_0(::method, "System", "SystemException");
qword_1813337B8 = il2cpp_class_from_name_0(::method, "System", "ArgumentException");
qword_181333710 = il2cpp_class_from_name_0(::method, "System", "MarshalByRefObject");
qword_181333750 = il2cpp_class_from_name_0(method_0, "System", "__Il2CppComObject");
qword_1813337C8 = il2cpp_class_from_name_0(::method, "System.Runtime.InteropServices", "SafeHandle");
qword_1813337D0 = il2cpp_class_from_name_0(::method, "System.Globalization", "SortKey");
qword_1813337D8 = il2cpp_class_from_name_0(::method, "System", "DBNull");
qword_1813337E0 = il2cpp_class_from_name_0(::method, "System.Runtime.InteropServices", "ErrorWrapper");
qword_1813337E8 = il2cpp_class_from_name_0(::method, "System.Reflection", "Missing");
qword_181333758 = il2cpp_class_from_name_0(::method, "System", "Attribute");
qword_181333760 = il2cpp_class_from_name_0(::method, "System.Reflection", "CustomAttributeData");
qword_181333768 = il2cpp_class_from_name_0(::method, "System.Reflection", "CustomAttributeTypedArgument");
qword_181333770 = il2cpp_class_from_name_0(::method, "System.Reflection", "CustomAttributeNamedArgument");
qword_181333828 = il2cpp_class_from_name_0(::method, "System.Collections.Generic", "KeyValuePair`2");
qword_181333848 = il2cpp_class_from_name_0(::method, "System", "Guid");
qword_1813337F8 = il2cpp_class_from_name_0(::method, "System.Threading", "_ThreadPoolWaitCallback");
qword_181333808 = il2cpp_class_from_name_0(::method, "System.Runtime.Remoting.Messaging", "MonoMethodMessage");
method_from_name = il2cpp_class_get_method_from_name_0(qword_1813337F8, "PerformWaitCallback", 0);
qword_181333850 = il2cpp_class_from_name_0(::method, "System", "SByteEnum");
qword_181333858 = il2cpp_class_from_name_0(::method, "System", "Int16Enum");
qword_181333860 = il2cpp_class_from_name_0(::method, "System", "Int32Enum");
qword_181333868 = il2cpp_class_from_name_0(::method, "System", "Int64Enum");
qword_181333870 = il2cpp_class_from_name_0(::method, "System", "ByteEnum");
qword_181333878 = il2cpp_class_from_name_0(::method, "System", "UInt16Enum");
qword_181333880 = il2cpp_class_from_name_0(::method, "System", "UInt32Enum");
qword_181333888 = il2cpp_class_from_name_0(::method, "System", "UInt64Enum");
qword_181333890 = il2cpp_class_from_name_0(method_0, "Unity.IL2CPP.Metadata", "__Il2CppFullySharedGenericType");
qword_181333898 = il2cpp_class_from_name_0(
method_0,
"Unity.IL2CPP.Metadata",
"__Il2CppFullySharedGenericStructType");
mono_thread_suspend_all_other_threads();
sub_18021BE10();
sub_1801E1010(::method);
v8 = sub_18021C4B0("System");
if ( v8 )
{
method = jinfo_get_method(v8);
qword_181333840 = il2cpp_class_from_name_0(method, "System", "Uri");
}
v10 = sub_18021C4B0("WindowsRuntimeMetadata");
if ( v10 )
{
method_1 = jinfo_get_method(v10);
qword_181333810 = il2cpp_class_from_name_0(method_1, "Windows.Foundation", "IReference`1");
qword_181333818 = il2cpp_class_from_name_0(method_1, "Windows.Foundation", "IReferenceArray`1");
qword_181333820 = il2cpp_class_from_name_0(method_1, "Windows.Foundation.Collections", "IKeyValuePair`2");
qword_181333820 = il2cpp_class_from_name_0(method_1, "Windows.Foundation.Collections", "IKeyValuePair`2");
qword_181333830 = il2cpp_class_from_name_0(method_1, "Windows.Foundation", "Uri");
qword_181333838 = il2cpp_class_from_name_0(method_1, "Windows.Foundation", "IUriRuntimeClass");
}
sub_1801F57C0(qword_181333630);
sub_1801B2160();
root_domain_0 = mono_get_root_domain_0();
v13 = mono_thread_attach_0(root_domain_0);
sub_18018FE80(v13);
v14 = sub_1801E5B00(qword_1813336B8);
v15 = sub_1801E5B00(qword_1813336B0);
sub_18020EB10(v15 + 24, root_domain_0);
sub_18020EB10(root_domain_0, v15);
sub_18020EB10(root_domain_0 + 8, v14);
*(_DWORD *)(root_domain_0 + 40) = 1;
*(_QWORD *)(root_domain_0 + 32) = sub_1801EC000(a1);
sub_18018ED70();
sub_18020F2E0();
sub_1801F63E0();
sub_180211A10(qword_181333630);
sub_1801F57C0(qword_181333630);
field_from_name_0 = il2cpp_class_get_field_from_name_0(qword_181333630, "Empty");
v17 = sub_180211200();
il2cpp_field_static_set_value_0(field_from_name_0, v17);
byte_1813339C8 = 1;
no_ = 0;
si128 = _mm_load_si128((const __m128i *)&xmmword_181150060);
LODWORD(no_) = 7562617;
MONO_REFLECTION_SERIALIZER = 0;
*(_QWORD *)&MONO_REFLECTION_SERIALIZER = operator new(0x20u);
v29 = _mm_load_si128((const __m128i *)&xmmword_181150080);
strcpy((char *)MONO_REFLECTION_SERIALIZER, "MONO_REFLECTION_SERIALIZER");
sub_180181CE0(&MONO_REFLECTION_SERIALIZER, &no_);
if ( v29.m128i_i64[1] > 0xFuLL )
{
MONO_REFLECTION_SERIALIZER_1 = (void *)MONO_REFLECTION_SERIALIZER;
if ( (unsigned __int64)(v29.m128i_i64[1] + 1) >= 0x1000 )
{
if ( (unsigned __int64)(MONO_REFLECTION_SERIALIZER - *(_QWORD *)(MONO_REFLECTION_SERIALIZER - 8) - 8) > 0x1F )
invalid_parameter_noinfo_noreturn();
MONO_REFLECTION_SERIALIZER_1 = *(void **)(MONO_REFLECTION_SERIALIZER - 8);
}
j_j_free_0(MONO_REFLECTION_SERIALIZER_1);
}
v29 = _mm_load_si128(&xmmword_1810BCBF0);
LOBYTE(MONO_REFLECTION_SERIALIZER) = 0;
if ( si128.m128i_i64[1] > 0xFuLL )
{
no__1 = (void *)no_;
if ( (unsigned __int64)(si128.m128i_i64[1] + 1) >= 0x1000 )
{
if ( (unsigned __int64)(no_ - *(_QWORD *)(no_ - 8) - 8) > 0x1F )
invalid_parameter_noinfo_noreturn();
no__1 = *(void **)(no_ - 8);
}
j_j_free_0(no__1);
}
no_ = 0;
si128 = _mm_load_si128((const __m128i *)&xmmword_181150050);
strcpy((char *)&no_, "no");
MONO_REFLECTION_SERIALIZER = 0;
v29 = 0;
*(_QWORD *)&MONO_REFLECTION_SERIALIZER = operator new(0x20u);
v29 = _mm_load_si128((const __m128i *)&xmmword_181150070);
strcpy((char *)MONO_REFLECTION_SERIALIZER, "MONO_XMLSERIALIZER_THS");
sub_180181CE0(&MONO_REFLECTION_SERIALIZER, &no_);
if ( v29.m128i_i64[1] > 0xFuLL )
{
MONO_REFLECTION_SERIALIZER_2 = (void *)MONO_REFLECTION_SERIALIZER;
if ( (unsigned __int64)(v29.m128i_i64[1] + 1) >= 0x1000 )
{
if ( (unsigned __int64)(MONO_REFLECTION_SERIALIZER - *(_QWORD *)(MONO_REFLECTION_SERIALIZER - 8) - 8) > 0x1F )
invalid_parameter_noinfo_noreturn();
MONO_REFLECTION_SERIALIZER_2 = *(void **)(MONO_REFLECTION_SERIALIZER - 8);
}
j_j_free_0(MONO_REFLECTION_SERIALIZER_2);
}
v29 = _mm_load_si128(&xmmword_1810BCBF0);
LOBYTE(MONO_REFLECTION_SERIALIZER) = 0;
if ( si128.m128i_i64[1] > 0xFuLL )
{
no__2 = (void *)no_;
if ( (unsigned __int64)(si128.m128i_i64[1] + 1) >= 0x1000 )
{
if ( (unsigned __int64)(no_ - *(_QWORD *)(no_ - 8) - 8) > 0x1F )
invalid_parameter_noinfo_noreturn();
no__2 = *(void **)(no_ - 8);
}
j_j_free_0(no__2);
}
sub_18018BB40(root_domain_0);
sub_18018BB90(*(_QWORD *)(root_domain_0 + 16));
sub_1801BBFA0(v30);
sub_1801EAFA0(v30);
if ( !(unsigned int)sub_1801E7350() )
{
v23 = v30;
if ( v31.m128i_i64[1] > 0xFuLL )
v23 = (_QWORD *)v30[0];
v34 = v23;
sub_1801E7730(&v34, 1);
}
sub_1801F18A0(v22);
sub_1801F1930(v24);
sub_1801CA390(v25);
v5 = 1;
if ( v31.m128i_i64[1] > 0xFuLL )
{
v26 = (void *)v30[0];
if ( (unsigned __int64)(v31.m128i_i64[1] + 1) >= 0x1000 )
{
if ( (unsigned __int64)(v30[0] - *(_QWORD *)(v30[0] - 8LL) - 8LL) > 0x1F )
invalid_parameter_noinfo_noreturn();
v26 = *(void **)(v30[0] - 8LL);
}
j_j_free_0(v26);
}
v31 = _mm_load_si128(&xmmword_1810BCBF0);
LOBYTE(v30[0]) = 0;
}
else
{
--dword_181333908;
v5 = 0;
}
v3 = dword_1813339C0;
}
else
{
v5 = 1;
}
if ( v3 > 0 )
{
if ( v3 == 1 )
{
::CurrentThreadId = 0;
dword_1813339C0 = 0;
sub_1801A53E0(&qword_181333970);
}
else
{
dword_1813339C0 = v3 - 1;
}
}
return v5;
}

挨个去看函数,看到sub_1801F5DE0() 函数时,其实就是il2cpp::vm::MetadataCache::Initialize函数

image-20260331184746222

接着去看sub_18018E9C0函数

“fnlfdj*el~jhlzn>usg” 这个字符串就是被混淆的 global-metadata.dat

只是进行了简单的异或

1
2
3
4
5
6
7
str="fnlfdj*el~jhlzn>usg"
str1=''
for i in range(len(str)):
str1+=chr(ord(str[i])^i+1)
print(str1)

#global-metadata.dat

这个函数是将这个被混淆的字符串通过异或解出来,然后调用一个函数去加载这个global-metadata.dat。加载global-metadata.dat的函数就是il2cpp::vm::MetadataLoader::LoadMetadataFile。

下面是源码 vm/MetadataCache.cpp 里的 il2cpp::vm::MetadataCache::Initialize()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
bool il2cpp::vm::MetadataCache::Initialize()
{
s_GlobalMetadata = vm::MetadataLoader::LoadMetadataFile("global-metadata.dat");
if (!s_GlobalMetadata)
return false;

s_GlobalMetadataHeader = (const Il2CppGlobalMetadataHeader*)s_GlobalMetadata;
IL2CPP_ASSERT(s_GlobalMetadataHeader->sanity == 0xFAB11BAF);
IL2CPP_ASSERT(s_GlobalMetadataHeader->version == 24);

// Pre-allocate these arrays so we don't need to lock when reading later.
// These arrays hold the runtime metadata representation for metadata explicitly
// referenced during conversion. There is a corresponding table of same size
// in the converted metadata, giving a description of runtime metadata to construct.
s_TypeInfoTable = (Il2CppClass**)IL2CPP_CALLOC(s_Il2CppMetadataRegistration->typesCount, sizeof(Il2CppClass*));
s_TypeInfoDefinitionTable = (Il2CppClass**)IL2CPP_CALLOC(s_GlobalMetadataHeader->typeDefinitionsCount / sizeof(Il2CppTypeDefinition), sizeof(Il2CppClass*));
s_MethodInfoDefinitionTable = (const MethodInfo**)IL2CPP_CALLOC(s_GlobalMetadataHeader->methodsCount / sizeof(Il2CppMethodDefinition), sizeof(MethodInfo*));
s_GenericMethodTable = (const Il2CppGenericMethod**)IL2CPP_CALLOC(s_Il2CppMetadataRegistration->methodSpecsCount, sizeof(Il2CppGenericMethod*));
s_ImagesCount = s_GlobalMetadataHeader->imagesCount / sizeof(Il2CppImageDefinition);
s_ImagesTable = (Il2CppImage*)IL2CPP_CALLOC(s_ImagesCount, sizeof(Il2CppImage));
s_AssembliesCount = s_GlobalMetadataHeader->assembliesCount / sizeof(Il2CppAssemblyDefinition);
s_AssembliesTable = (Il2CppAssembly*)IL2CPP_CALLOC(s_AssembliesCount, sizeof(Il2CppAssembly));

// setup all the Il2CppImages. There are not many and it avoid locks later on
const Il2CppImageDefinition* imagesDefinitions = (const Il2CppImageDefinition*)((const char*)s_GlobalMetadata + s_GlobalMetadataHeader->imagesOffset);
for (int32_t imageIndex = 0; imageIndex < s_ImagesCount; imageIndex++)
{
const Il2CppImageDefinition* imageDefinition = imagesDefinitions + imageIndex;
Il2CppImage* image = s_ImagesTable + imageIndex;
image->name = GetStringFromIndex(imageDefinition->nameIndex);

std::string nameNoExt = il2cpp::utils::PathUtils::PathNoExtension(image->name);
image->nameNoExt = (char*)IL2CPP_CALLOC(nameNoExt.size() + 1, sizeof(char));
strcpy(const_cast<char*>(image->nameNoExt), nameNoExt.c_str());

image->assembly = const_cast<Il2CppAssembly*>(GetAssemblyFromIndex(imageDefinition->assemblyIndex));
image->typeStart = imageDefinition->typeStart;
image->typeCount = imageDefinition->typeCount;
image->exportedTypeStart = imageDefinition->exportedTypeStart;
image->exportedTypeCount = imageDefinition->exportedTypeCount;
image->entryPointIndex = imageDefinition->entryPointIndex;
image->token = imageDefinition->token;
image->customAttributeStart = imageDefinition->customAttributeStart;
image->customAttributeCount = imageDefinition->customAttributeCount;
for (uint32_t codeGenModuleIndex = 0; codeGenModuleIndex < s_Il2CppCodeRegistration->codeGenModulesCount; ++codeGenModuleIndex)
{
if (strcmp(image->name, s_Il2CppCodeRegistration->codeGenModules[codeGenModuleIndex]->moduleName) == 0)
image->codeGenModule = s_Il2CppCodeRegistration->codeGenModules[codeGenModuleIndex];
}
IL2CPP_ASSERT(image->codeGenModule);
image->dynamic = false;
}

// setup all the Il2CppAssemblies.
const Il2CppAssemblyDefinition* assemblyDefinitions = (const Il2CppAssemblyDefinition*)((const char*)s_GlobalMetadata + s_GlobalMetadataHeader->assembliesOffset);
for (int32_t assemblyIndex = 0; assemblyIndex < s_ImagesCount; assemblyIndex++)
{
const Il2CppAssemblyDefinition* assemblyDefinition = assemblyDefinitions + assemblyIndex;
Il2CppAssembly* assembly = s_AssembliesTable + assemblyIndex;

assembly->image = il2cpp::vm::MetadataCache::GetImageFromIndex(assemblyDefinition->imageIndex);
assembly->token = assemblyDefinition->token;
assembly->referencedAssemblyStart = assemblyDefinition->referencedAssemblyStart;
assembly->referencedAssemblyCount = assemblyDefinition->referencedAssemblyCount;

Il2CppAssemblyName* assemblyName = &assembly->aname;
const Il2CppAssemblyNameDefinition* assemblyNameDefinition = &assemblyDefinition->aname;

assemblyName->name = GetStringFromIndex(assemblyNameDefinition->nameIndex);
assemblyName->culture = GetStringFromIndex(assemblyNameDefinition->cultureIndex);
assemblyName->hash_value = GetStringFromIndex(assemblyNameDefinition->hashValueIndex);
assemblyName->public_key = GetStringFromIndex(assemblyNameDefinition->publicKeyIndex);
if (strcmp(assemblyName->public_key, "NULL") == 0)
assemblyName->public_key = NULL;
assemblyName->hash_alg = assemblyNameDefinition->hash_alg;
assemblyName->hash_len = assemblyNameDefinition->hash_len;
assemblyName->flags = assemblyNameDefinition->flags;
assemblyName->major = assemblyNameDefinition->major;
assemblyName->minor = assemblyNameDefinition->minor;
assemblyName->build = assemblyNameDefinition->build;
assemblyName->revision = assemblyNameDefinition->revision;
memcpy(assemblyName->public_key_token, assemblyNameDefinition->public_key_token, sizeof(assemblyNameDefinition->public_key_token));

il2cpp::vm::Assembly::Register(assembly);
}

InitializeUnresolvedSignatureTable();

这段源码的意思是调 LoadMetadataFile("global-metadata.dat") 去加载 metadata 文件,返回值放到 s_GlobalMetadata,接着就是校验metadata 文件是否合法,然后根据里面的内容,建立类型表、方法表、镜像表、程序集表,并把这些信息注册到IL2CPP运行里。

就是说如果遇到global-metadata.dat文件加密的话,最常见的情况就是MetadataCache::Initialize()调用到这条加载链里面,某一层会进行解密再返回,这个处理点经常在MetadataLoader::LoadMetadataFile()里面。

我们看到 v9 = sub_1801EA8A0(fnlfdj_el_jhlzn_usg_4);

跟进这个函数去看,发现这个函数其实就是我们要找的il2cpp::vm::MetadataLoader::LoadMetadataFile()。

这个看起来像“拼路径 + 打开文件 + 映射到内存”的通用 loader

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// Hidden C++ exception states: #wind=6
__int64 __fastcall sub_1801EA8A0(__int64 a1)
{
__int64 v2; // rdi
void *v3; // rcx
__int64 v4; // rax
__int64 v5; // rax
__int64 v6; // rsi
const char *v7; // rdx
__int64 v8; // rbx
void *v9; // rcx
void *v10; // rcx
_QWORD v12[2]; // [rsp+50h] [rbp-9h] BYREF
__m128i v13; // [rsp+60h] [rbp+7h]
void *Src; // [rsp+70h] [rbp+17h] BYREF
unsigned __int64 n0xF; // [rsp+88h] [rbp+2Fh]
void *v16; // [rsp+90h] [rbp+37h] BYREF
__m128i si128; // [rsp+A0h] [rbp+47h]
int v18; // [rsp+C8h] [rbp+6Fh] BYREF

v2 = 0;
sub_1801D1090(&v16);
sub_1801AFFC0(&Src);
if ( si128.m128i_i64[1] > 0xFuLL )
{
v3 = v16;
if ( (unsigned __int64)(si128.m128i_i64[1] + 1) >= 0x1000 )
{
if ( (unsigned __int64)v16 - *((_QWORD *)v16 - 1) - 8 > 0x1F )
invalid_parameter_noinfo_noreturn();
v3 = (void *)*((_QWORD *)v16 - 1);
}
j_j_free_0(v3);
}
si128 = _mm_load_si128(&xmmword_1810BCBF0);
LOBYTE(v16) = 0;
v4 = -1;
do
++v4;
while ( *(_BYTE *)(a1 + v4) );
sub_1801AFFC0(v12);
v18 = 0;
v5 = sub_1801B1CB0(v12, 3, 1);
v6 = v5;
if ( v18 )
{
v7 = (const char *)v12;
if ( v13.m128i_i64[1] > 0xFuLL )
v7 = (const char *)v12[0];
sub_1801DB530("ERROR: Could not open %s", v7);
}
else
{
v8 = sub_1801DAE10(v5);
sub_1801B19C0(v6, &v18);
if ( v18 )
sub_1801DB330(v8);
else
v2 = v8;
}
if ( v13.m128i_i64[1] > 0xFuLL )
{
v9 = (void *)v12[0];
if ( (unsigned __int64)(v13.m128i_i64[1] + 1) >= 0x1000 )
{
if ( (unsigned __int64)(v12[0] - *(_QWORD *)(v12[0] - 8LL) - 8LL) > 0x1F )
invalid_parameter_noinfo_noreturn();
v9 = *(void **)(v12[0] - 8LL);
}
j_j_free_0(v9);
}
v13 = _mm_load_si128(&xmmword_1810BCBF0);
LOBYTE(v12[0]) = 0;
if ( n0xF > 0xF )
{
v10 = Src;
if ( n0xF + 1 >= 0x1000 )
{
if ( (unsigned __int64)Src - *((_QWORD *)Src - 1) - 8 > 0x1F )
invalid_parameter_noinfo_noreturn();
v10 = (void *)*((_QWORD *)Src - 1);
}
j_j_free_0(v10);
}
return v2;
}

v5 = sub_1801B1CB0(v12, 3, 1); 这里根据路径 v12 打开了文件,返回的 v5 是一个文件句柄(File Handle)或内存映射对象。

然后走进了 else 分支: v8 = sub_1801DAE10(v5); 引擎把打开的文件句柄 v5 扔给了这个新函数去处理。

sub_1801B19C0(v6, &v18); 处理完之后,关闭文件句柄(v6 就是前面的 v5)。

v2 = v8; return v2; 最后把处理好的内存数据返回给上层。

然后继续去跟进sub_1801DAE10,里面是调用了sub_1801DAE20。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
// Hidden C++ exception states: #wind=6
char *__fastcall sub_1801DAE20(il2cpp_baselib *hFile, __int64 a2, __int64 a3, unsigned int n5)
{
__int64 CurrentThreadId; // rdi
int v8; // ebx
HANDLE hObject; // r15
char *v10; // rdi
char *v11; // rax
void *v12; // rsi
_QWORD *v13; // rax
_BYTE *v14; // rcx
_QWORD *v15; // rax
__int64 v16; // r14
void *v17; // rsi
_QWORD *v18; // rax
_BYTE *v19; // rcx
_QWORD *v20; // rax
signed __int32 i; // edx
signed __int32 i_1; // eax
__int64 v23; // rdx
__int64 a7; // [rsp+40h] [rbp-49h] BYREF
__int128 v26; // [rsp+48h] [rbp-41h]
__int64 v27; // [rsp+60h] [rbp-29h] BYREF
DWORD a4[2]; // [rsp+68h] [rbp-21h] BYREF
__int64 *v29; // [rsp+70h] [rbp-19h]
void **v30; // [rsp+80h] [rbp-9h]
__int64 v31; // [rsp+88h] [rbp-1h]
__int128 v32; // [rsp+90h] [rbp+7h] BYREF
void **v33; // [rsp+A0h] [rbp+17h]
__int64 v34; // [rsp+A8h] [rbp+1Fh]
__int128 v35; // [rsp+B0h] [rbp+27h] BYREF
__int64 v36; // [rsp+F8h] [rbp+6Fh] BYREF

v36 = a2;
v29 = &qword_181333410;
CurrentThreadId = il2cpp_baselib::Baselib_Thread_GetCurrentThreadId(hFile);
v8 = 1;
if ( CurrentThreadId == CurrentThreadId_1 )
{
++dword_181333460;
}
else
{
if ( _InterlockedExchangeAdd(&::i, 0xFFFFFFFF) <= 0 )
il2cpp_baselib::Baselib_SystemSemaphore_Acquire(qword_181333410);
CurrentThreadId_1 = CurrentThreadId;
dword_181333460 = 1;
}
*(_QWORD *)a4 = 0;
LODWORD(a7) = 0;
hObject = sub_1802183B0((__int64)hFile, 0, 0, a4, n5, 0, (int *)&a7);
if ( (_DWORD)a7 )
{
v10 = 0;
goto LABEL_30;
}
v27 = a3;
v11 = (char *)sub_180218D30(hObject, &v36, a3, n5, &v27, (int *)&a7);
v10 = v11;
if ( v11 )
{
v10 = &v11[a3 - v27];
if ( (unsigned __int8)sub_18006EE80(hObject) )
{
v12 = qword_181333478;
v13 = (_QWORD *)*((_QWORD *)qword_181333478 + 1);
*(_QWORD *)&v26 = v13;
DWORD2(v26) = 0;
v14 = qword_181333478;
while ( !*((_BYTE *)v13 + 25) )
{
*(_QWORD *)&v26 = v13;
if ( v13[4] >= (unsigned __int64)v10 )
{
DWORD2(v26) = 1;
v14 = v13;
v13 = (_QWORD *)*v13;
}
else
{
DWORD2(v26) = 0;
v13 = (_QWORD *)v13[2];
}
}
if ( v14[25] || (unsigned __int64)v10 < *((_QWORD *)v14 + 4) )
{
if ( qword_181333480 == 0x555555555555555LL )
goto LABEL_44;
v30 = &qword_181333478;
v31 = 0;
v15 = operator new(0x30u);
v15[4] = v10;
v15[5] = 0;
*v15 = v12;
v15[1] = v12;
v15[2] = v12;
*((_WORD *)v15 + 12) = 0;
v32 = v26;
v14 = (_BYTE *)sub_1801912E0(&qword_181333478, &v32, v15);
}
*((_QWORD *)v14 + 5) = hObject;
}
v16 = v36;
v17 = qword_181333468;
v18 = (_QWORD *)*((_QWORD *)qword_181333468 + 1);
*(_QWORD *)&v26 = v18;
DWORD2(v26) = 0;
v19 = qword_181333468;
while ( !*((_BYTE *)v18 + 25) )
{
*(_QWORD *)&v26 = v18;
if ( v18[4] >= (unsigned __int64)v10 )
{
DWORD2(v26) = 1;
v19 = v18;
v18 = (_QWORD *)*v18;
}
else
{
DWORD2(v26) = 0;
v18 = (_QWORD *)v18[2];
}
}
if ( !v19[25] && (unsigned __int64)v10 >= *((_QWORD *)v19 + 4) )
goto LABEL_29;
if ( qword_181333470 != 0x555555555555555LL )
{
v33 = &qword_181333468;
v34 = 0;
v20 = operator new(0x30u);
v20[4] = v10;
v20[5] = 0;
*v20 = v17;
v20[1] = v17;
v20[2] = v17;
*((_WORD *)v20 + 12) = 0;
v35 = v26;
v19 = (_BYTE *)sub_1801912E0(&qword_181333468, &v35, v20);
LABEL_29:
*((_QWORD *)v19 + 5) = v16;
goto LABEL_30;
}
LABEL_44:
std::vector<void *>::_Xlen();
}
LABEL_30:
if ( dword_181333460 > 0 )
{
if ( dword_181333460 == 1 )
{
CurrentThreadId_1 = 0;
dword_181333460 = 0;
for ( i = ::i; i != i_0; i = i_1 )
{
if ( i + v8 > i_0 )
v8 = i_0 - i;
i_1 = _InterlockedCompareExchange(&::i, i + v8, i);
if ( i == i_1 )
{
if ( i < 0 )
{
v23 = (unsigned int)-i;
if ( v8 < (int)v23 )
v23 = (unsigned int)v8;
il2cpp_baselib::Baselib_SystemSemaphore_Release(qword_181333410, v23);
}
return v10;
}
}
}
else
{
--dword_181333460;
}
}
return v10;
}

借助ai去分析,这个函数其实就是对 Windows 底层 MapViewOfFile(内存映射文件)的包装。它仅仅是把硬盘上的文件原封不动地搬到了内存里,并把内存指针返回给了上一层。这里面既没有解密逻辑,也没有文件头校验

那说明这个其实没有加密,没有文件头校验说明并不会检查文件头是不是AF 1B B1 FA,所以之前frida或者用CE在内存里搜索是找不到的。那么这个直接将文件头改成标准的AF 1B B1 FA就可以了吗?

其实经过实操之后发现依旧不行。那就再回去看看

下面是vm/MetadataLoader.cpp里面的il2cpp::vm::MetadataLoader::LoadMetadataFile()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void* il2cpp::vm::MetadataLoader::LoadMetadataFile(const char* fileName)
{
std::string resourcesDirectory = utils::PathUtils::Combine(utils::Runtime::GetDataDir(), utils::StringView<char>("Metadata"));

std::string resourceFilePath = utils::PathUtils::Combine(resourcesDirectory, utils::StringView<char>(fileName, strlen(fileName)));

int error = 0;
os::FileHandle* handle = os::File::Open(resourceFilePath, kFileModeOpen, kFileAccessRead, kFileShareRead, kFileOptionsNone, &error);
if (error != 0)
{
utils::Logging::Write("ERROR: Could not open %s", resourceFilePath.c_str());
return NULL;
}

void* fileBuffer = utils::MemoryMappedFile::Map(handle);

os::File::Close(handle, &error);
if (error != 0)
{
utils::MemoryMappedFile::Unmap(fileBuffer);
fileBuffer = NULL;
return NULL;
}

return fileBuffer;
}

void il2cpp::vm::MetadataLoader::UnloadMetadataFile(void* fileBuffer)
{
bool success = il2cpp::utils::MemoryMappedFile::Unmap(fileBuffer);
NO_UNUSED_WARNING(success);
IL2CPP_ASSERT(success);
}

这段源码里LoadMetadataFile 是负责把 metadata 文件打开并映射到内存里,然后返回这块内存地址。

UnloadMetadataFile 负责把这块映射取消。

在 C/C++ 程序里,去解析这个 global-metadata.dat 文件,是使用了结构体强转,引擎会把整个 .dat 文件原封不动地搬进内存,然后在它的“头部”套上一个 C++ 结构体(struct)。

官方的标准的结构体在vm/il2cpp-metadata.h文件里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
typedef struct Il2CppGlobalMetadataHeader
{
int32_t sanity; //偏移 0
int32_t version; //偏移 4
int32_t stringLiteralOffset; // string data for managed code 偏移 8
int32_t stringLiteralCount;
int32_t stringLiteralDataOffset;
int32_t stringLiteralDataCount;
int32_t stringOffset; // string data for metadata
int32_t stringCount;
int32_t eventsOffset; // Il2CppEventDefinition
int32_t eventsCount;
int32_t propertiesOffset; // Il2CppPropertyDefinition
int32_t propertiesCount;
int32_t methodsOffset; // Il2CppMethodDefinition
int32_t methodsCount;
int32_t parameterDefaultValuesOffset; // Il2CppParameterDefaultValue
int32_t parameterDefaultValuesCount;
int32_t fieldDefaultValuesOffset; // Il2CppFieldDefaultValue
int32_t fieldDefaultValuesCount;
int32_t fieldAndParameterDefaultValueDataOffset; // uint8_t
int32_t fieldAndParameterDefaultValueDataCount;
int32_t fieldMarshaledSizesOffset; // Il2CppFieldMarshaledSize
int32_t fieldMarshaledSizesCount;
int32_t parametersOffset; // Il2CppParameterDefinition
int32_t parametersCount;
int32_t fieldsOffset; // Il2CppFieldDefinition
int32_t fieldsCount;
int32_t genericParametersOffset; // Il2CppGenericParameter
int32_t genericParametersCount;
int32_t genericParameterConstraintsOffset; // TypeIndex
int32_t genericParameterConstraintsCount;
int32_t genericContainersOffset; // Il2CppGenericContainer
int32_t genericContainersCount;
int32_t nestedTypesOffset; // TypeDefinitionIndex
int32_t nestedTypesCount;
int32_t interfacesOffset; // TypeIndex
int32_t interfacesCount;
int32_t vtableMethodsOffset; // EncodedMethodIndex
int32_t vtableMethodsCount;
int32_t interfaceOffsetsOffset; // Il2CppInterfaceOffsetPair
int32_t interfaceOffsetsCount;
int32_t typeDefinitionsOffset; // Il2CppTypeDefinition
int32_t typeDefinitionsCount;

int32_t imagesOffset; // Il2CppImageDefinition 偏移 168 (0xA8)
int32_t imagesCount; //偏移 172 (0xAC)
int32_t assembliesOffset; // Il2CppAssemblyDefinition 偏移 176 (0xB0)
int32_t assembliesCount; //偏移 180 (0xB4)

int32_t metadataUsageListsOffset; // Il2CppMetadataUsageList
int32_t metadataUsageListsCount;
int32_t metadataUsagePairsOffset; // Il2CppMetadataUsagePair
int32_t metadataUsagePairsCount;
int32_t fieldRefsOffset; // Il2CppFieldRef
int32_t fieldRefsCount;
int32_t referencedAssembliesOffset; // int32_t
int32_t referencedAssembliesCount;
int32_t attributesInfoOffset; // Il2CppCustomAttributeTypeRange
int32_t attributesInfoCount;
int32_t attributeTypesOffset; // TypeIndex
int32_t attributeTypesCount;
int32_t unresolvedVirtualCallParameterTypesOffset; // TypeIndex
int32_t unresolvedVirtualCallParameterTypesCount;
int32_t unresolvedVirtualCallParameterRangesOffset; // Il2CppRange
int32_t unresolvedVirtualCallParameterRangesCount;
int32_t windowsRuntimeTypeNamesOffset; // Il2CppWindowsRuntimeTypeNamePair
int32_t windowsRuntimeTypeNamesSize;
int32_t exportedTypeDefinitionsOffset; // TypeDefinitionIndex
int32_t exportedTypeDefinitionsCount;
} Il2CppGlobalMetadataHeader;

imagesCount(Image 的总字节数)应该在偏移 172 的位置。

assembliesCount(Assembly 的总字节数)应该在偏移 180 的位置。

但是我们回归sub_18018E9C0函数,会看到引擎读取的都比标准的多4字节。

image-20260331212854502

这个是个很重要的发现,为什么偏移要比标准的多4字节呢?结合gm.dat文件的16进制去看,会发现这里还有4个奇怪的字母YURO,其实就是因为这4字节,才导致了后面的读取要比标准的多4字节。

另外还要说明的是在标准的 Unity 引擎源码里,在拿到内存指针 v9 之后,必定会有这样一行代码: if ( *(int *)v9 != 0xFAB11BAF ) return 0;去检查前四个字节的。这里没有就需要注意了,在你解密后应该还得看看是否魔数正确,不正确的话还得修改。

直接在010里面修改就行

image-20260331215752833

然后使用Il2CppDumper.exe工具去dump,这次就能成功了。

dc88d6677b16f4e6a1e36e473ead9bf3

接着就是用ida打开GameAssembly.dll

然后选择 File -> Script file 去应用 ida_with_struct_py3.py文件,依次导入Il2CppDumper.exe工具dump的script.json和il2cpp.h文件。

可以去用CE去定位关键的类和函数

image-20260331151924903

定位check类,可以看到check类下有很多关键方法名,AESEncryptCheckFlag 等。这样可以很快定位到ida里面的关键逻辑

image-20260331144919880

跟进Check__AESEncrypt函数里面看看,这个是标准的AES-CBC加密

第一个参数是我们的flag,第二个参数是key,第三个参数是iv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
System_String_o *Check__AESEncrypt(
System_String_o *text,
System_String_o *key,
System_String_o *iv,
const MethodInfo *method)
{
__int64 v7; // rdx
__int64 v8; // rdx
__int64 v9; // rdx
System_Security_Cryptography_RijndaelManaged_o *v10; // rbx
System_Text_Encoding_o *UTF8; // rax
__int64 sourceArray; // rsi
__int64 destinationArray; // rax
__int64 destinationArray_1; // rdi
int32_t length; // r8d
System_Text_Encoding_o *UTF8_1; // rax
__int64 v17; // rax
__int64 v18; // rbx
System_Text_Encoding_o *UTF8_2; // rax
__int64 v20; // rax
System_Byte_array *inArray; // rbx
__int64 v23; // [rsp+20h] [rbp-18h]

if ( !byte_18132CF22 )
{
sub_1801E5920((__int64)&byte___TypeInfo, (__int64)key);
sub_1801E5920((__int64)&System_Convert_TypeInfo, v7);
sub_1801E5920((__int64)&System_Security_Cryptography_ICryptoTransform_TypeInfo, v8);
sub_1801E5920((__int64)&System_Security_Cryptography_RijndaelManaged_TypeInfo, v9);
byte_18132CF22 = 1;
}
v10 = (System_Security_Cryptography_RijndaelManaged_o *)sub_1801E5B00(System_Security_Cryptography_RijndaelManaged_TypeInfo);
System_Security_Cryptography_RijndaelManaged___ctor(v10, 0);
if ( !v10 )
goto LABEL_16;
((void (__fastcall *)(System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v10->klass->vtable._13_set_Mode.methodPtr)(
v10,
1,
v10->klass->vtable._13_set_Mode.method);
((void (__fastcall *)(System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v10->klass->vtable._14_set_Padding.methodPtr)(
v10,
2,
v10->klass->vtable._14_set_Padding.method);
((void (__fastcall *)(System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v10->klass->vtable._12_set_KeySize.methodPtr)(
v10,
128,
v10->klass->vtable._12_set_KeySize.method);
((void (__fastcall *)(System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v10->klass->vtable._6_set_BlockSize.methodPtr)(
v10,
128,
v10->klass->vtable._6_set_BlockSize.method);
UTF8 = System_Text_Encoding__get_UTF8(0);
if ( !UTF8 )
goto LABEL_16;
sourceArray = ((__int64 (__fastcall *)(System_Text_Encoding_o *, System_String_o *, const MethodInfo *))UTF8->klass->vtable._16_GetBytes.methodPtr)(
UTF8,
key,
UTF8->klass->vtable._16_GetBytes.method);
destinationArray = sub_1801E4E50(byte___TypeInfo, 16);
destinationArray_1 = destinationArray;
if ( !sourceArray || !destinationArray )
goto LABEL_16;
length = *(_DWORD *)(sourceArray + 24);
if ( length > *(_DWORD *)(destinationArray + 24) )
length = *(_DWORD *)(destinationArray + 24);
System_Array__Copy_6451540736((System_Array_o *)sourceArray, (System_Array_o *)destinationArray, length, 0);
((void (__fastcall *)(System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v10->klass->vtable._10_set_Key.methodPtr)(
v10,
destinationArray_1,
v10->klass->vtable._10_set_Key.method);
UTF8_1 = System_Text_Encoding__get_UTF8(0);
if ( !UTF8_1
|| (v17 = ((__int64 (__fastcall *)(System_Text_Encoding_o *, System_String_o *, const MethodInfo *))UTF8_1->klass->vtable._16_GetBytes.methodPtr)(
UTF8_1,
iv,
UTF8_1->klass->vtable._16_GetBytes.method),
((void (__fastcall *)(System_Security_Cryptography_RijndaelManaged_o *, __int64, const MethodInfo *))v10->klass->vtable._8_set_IV.methodPtr)(
v10,
v17,
v10->klass->vtable._8_set_IV.method),
v18 = ((__int64 (__fastcall *)(System_Security_Cryptography_RijndaelManaged_o *, const MethodInfo *))v10->klass->vtable._15_CreateEncryptor.methodPtr)(
v10,
v10->klass->vtable._15_CreateEncryptor.method),
(UTF8_2 = System_Text_Encoding__get_UTF8(0)) == 0)
|| (v20 = ((__int64 (__fastcall *)(System_Text_Encoding_o *, System_String_o *, const MethodInfo *))UTF8_2->klass->vtable._16_GetBytes.methodPtr)(
UTF8_2,
text,
UTF8_2->klass->vtable._16_GetBytes.method)) == 0
|| !v18 )
{
LABEL_16:
sub_1801E5B50();
}
LODWORD(v23) = 0;
inArray = (System_Byte_array *)sub_180002940(
0,
System_Security_Cryptography_ICryptoTransform_TypeInfo,
v18,
v20,
v23,
*(_DWORD *)(v20 + 24));
if ( !System_Convert_TypeInfo->_2.cctor_finished )
il2cpp_runtime_class_init();
return System_Convert__ToBase64String(inArray, 0);
}

但是能够注意到的是最后返回的密文还经过了base64加密

下面就去找密文,key和iv的数据

image-20260331151803042

其实这些字符串都被放在了global-metadata.dat文件里面,引擎在运行游戏的时候是通过这些偏移量(offset)和数量(count)来找到它们的。

在脚本恢复符号的时候密文这些字符串就写在了注释里。

直接厨子一把唆

image-20260331142232797

后记

这道题还有其他的解法,看了出题人的文章才知道可以直接使用 frida-il2cpp-bridge

这个工具是可在运行时 dump、trace、hook IL2CPP 应用,而且不依赖 global-metadata.dat 文件

原理:frida把脚本注入到目标进程(此时明文信息都已经在运行时内存里面了),这个脚本就直接去调用IL2CPP runtime API(IL2CPP 运行时本身提供了一批 native API,用来取domain/assembly/image,枚举类,字段,方法,取字符串内容等),然后把这些native API 包成 JS对象模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
你的 JS 脚本

Frida 注入到目标进程

frida-il2cpp-bridge 等待 IL2CPP 初始化完成

把当前线程 attach 到 IL2CPP

调用 IL2CPP runtime API

把 native 层的 domain / assembly / class / method 封装成 JS 对象

你就能直接枚举、hook、改返回值、调方法

不过我在尝试的时候发现失败了,去翻了别的师傅的博客,里面也出现过一样的问题,可能是windows11的问题,那位师傅使用win10的虚拟机成功了,但是我没再尝试了,下次有时间再看看。

出题人的出题思路与题解:https://bbs.kanxue.com/thread-281560-1.htm