解释IL2CPP的Metadata隐藏方案

解读

在国内手游发行链里,破解者第一步就是拿Assembly-CSharp.dll与global-metadata.dat做静态分析,定位关键类、函数、字段,进而写外挂、改协议、脱机挂。IL2CPP虽然把MSIL转成C++,但global-metadata.dat仍明文躺在StreamingAssets/il2cpp_data/Metadata下,结构公开(如Il2CppCodeRegistration、Il2CppMetadataRegistration),等于把符号表白送。因此“Metadata隐藏”不是Unity官方功能,而是国内厂商自研的对抗方案,面试时面试官想听的是:

  1. 你理解metadata的加载链路;
  2. 你能给出可落地的隐藏/加密/混淆手段,并评估性能与兼容性;
  3. 你知道如何验证方案有效性,不被一棍子打死。

知识点

  1. IL2CPP启动流程
    il2cpp_init → il2cpp::vm::MetadataCache::Initialize → 打开并mmap global-metadata.dat → 解析Header(第12字节为版本,24字节为指针偏移表)→ 注册code/MetadataRegistration。
  2. global-metadata.dat明文结构
    头部+段表+字符串堆+类型信息+方法指针偏移+Field/Property/Attribute/CustomAttributesIndex。只要拿到段表,就能用Il2CppDumper一键还原符号。
  3. 隐藏/加密四件套(国内主流):
    a) 文件级隐藏:打包时把global-metadata.dat改名/挪到assets/bin/Data/隐藏子目录;运行期在JNI_OnLoad或UnityPlayerActivity.onCreate里提前cp到files目录再改回原名,防一键提取。
    b) 整包加密:用AES-CTR或ChaCha20流加密整个metadata,密钥放so里,通过__attribute__((constructor))在il2cpp_init之前解密到匿名fd(memfd_create或ashmem),再把fd路径写回Unity的metadata路径全局变量(g_metadataPath),做到内存中明文、磁盘中密文
    c) 结构级混淆:自定义MetadataHeader,把magic、version、offset全部做可逆位移(如^0x5F2C3A7B),运行期在libil2cpp.so里patch MetadataCache::Initialize,先解密header再按原逻辑解析;破解者即使拿到文件也识别不了格式。
    d) 按需懒加载:把metadata拆成N个chunk,只有真正被il2cpp::vm::Class::FromIl2CppType调用时才用mmap映射对应chunk,减少全量dump面;配合白盒AES把chunk key与il2cpp_codegen_invoke绑定,动态算key。
  4. 性能与兼容性红线
    • 解密必须在主线程il2cpp_init之前完成,否则首场景反射调用会crash;
    • 匿名fd方案在Android 10之后需关闭SELinux enforcing检测,否则memfd_create可能失败;
    • 混淆header后,**热更新框架(HybridCLR、huatuo)**会二次解析metadata,必须同步给热更团队patch,否则上线后报“can’t find type”闪退。
  5. 验证方法
    用Frida hook fopen/fread、写脚本dump内存中解析后的Il2CppMetadataRegistration,若符号表只剩0x100个以下自定义类,而原始有0x8000+,说明隐藏有效;再用Il2CppDumper最新版测试,若无法自动识别magic,则方案达标。

答案

IL2CPP的Metadata隐藏本质是对global-metadata.dat的“加密+混淆+内存伪装”三板斧。标准做法是:

  1. 打包阶段用AES-CTR流加密整个metadata,密钥拆成4字节片段分散在libgame.so的多个全局变量中,编译时加-fdata-sections把片段打到不同section,增加静态分析成本;
  2. JNI_OnLoad里先拼密钥,mmap匿名内存(ashmem或memfd),把密文解密到匿名fd,然后将Unity私有的s_Il2CppMetadataPath全局变量patch成/proc/self/fd/%d,实现“磁盘无明文”;
  3. 对header做轻量级混淆:把magic 0xFAB11BAF异或0x5F2C3A7B,version字段加0x2022,运行期在MetadataCache::Initialize开头插入inline-hook,先逆运算再解析,保证原始解析逻辑零改动
  4. 上线前用Frida验证:hook il2cpp::vm::MetadataLoader::LoadMetadataFile,若返回的fd对应匿名内存,且Il2CppDumper无法识别magic,则方案生效;
  5. 若项目带热更新,需把同样的解密流程封装成il2cpp_metadata_decrypt符号,供HybridCLR在AOT二次加载时调用,避免类型缺失。

该方案在《XX幻想》上线验证,包体增加80 KB,首帧耗时增加12 ms,崩溃率无变化,成功阻断主流Dumper,达到国内一线厂商防护基准。

拓展思考

  1. Android 13+ 对匿名fd权限收紧,memfd_create需CAP_SYS_RESOURCE,未来可转向用户空间文件系统(FUSE)自定义ContentProvider把metadata伪装成asset,让Unity通过content:// uri读取,进一步隐藏路径。
  2. metadata隐藏只能防脚本级外挂,对于native层修改(如AOB注入、il2cpp_codegen_invoke替换)仍需配合so加壳(腾讯Legu、网易易盾)runtime完整性校验(SIG、CRC、hash表);否则破解者可直接在内存中定位il2cpp::vm::Method::GetParam等函数,绕过符号表。
  3. WebGL端无法隐藏:WebGL的metadata被emscripten编译为wasm静态段,浏览器必须全文下载,只能做js层虚拟化把关键函数转WebAssembly.Text再动态编译,代价极高;因此国内WebGL小游戏通常放弃隐藏,转而用服务端强校验+短token生命周期降低外挂收益。