IL2CPP 代码保护分析小记:native 化藏住了什么,又漏了什么
前段时间一个团队拿包来问:用了 IL2CPP,C# 全编成 native,dnSpy 拖进去一片空白,代码保护这块是不是就到位了。我把包解开看了一圈,结论没那么乐观。这里把当时的分析过程记一下——IL2CPP 到底挡住了什么,又漏在哪。
先说 IL2CPP 确实挡住的
IL2CPP 把 C# 先编成 IL、再转 C++、最后编成 native,产物是 libil2cpp.so。Mono 时代那条最舒服的逆向路——Assembly-CSharp.dll 里是 IL,dnSpy 一拖近乎源码——到这里确实断了。想读逻辑,得直接面对 ARM 汇编,分析门槛实打实抬上去了。
问题是,"编成 native"不等于"读不出来"。
漏在 global-metadata.dat
native 这层丢掉的不只是可读性,还有一整套"关于代码的信息":类名、方法签名、字段在对象里的偏移。可 .NET 靠反射运行,这些信息运行时必须拿得到。IL2CPP 的处理方式,是把它们从 native 里剥出来,单独存成一个文件带在包里,就是 global-metadata.dat,默认不加密,路径在 assets/bin/Data/Managed/Metadata/ 下。
文件头四个字节是 sanity 值 AF 1B B1 FA,Il2CppDumper 正是靠它识别文件。往后是成对的 offset/size,把类型定义、方法签名、字段偏移、字符串表一段段划开,类名方法名就明文存在字符串表里。
于是逆向根本不用碰 native 那层的算法:拿 libil2cpp.so 配上 global-metadata.dat 喂给 Il2CppDumper,它把二进制里的地址和 metadata 里的名字对起来,IDA 里满屏的 sub_XXXXXX 就还原成带类名的可读方法。
// Il2CppDumper 产出的 dump.cs
public void AddGold(int amount) { } // 对应 IDA 里的 PlayerController$$AddGold
那个团队原以为下沉 native 就藏住的接口地址、开关字段,strings 扫 libil2cpp.so 确实扫不出来,但它们大多明文躺在 metadata 的字符串表里,直接读文件就有。native 抹掉的符号,metadata 又原样交了回来——这是只上默认 IL2CPP 最致命的一点。
加密 metadata 挡得住吗
顺着往下,第一反应是把 global-metadata.dat 加密。这一步有用,但天花板很明确:游戏运行时必须用到 metadata,它就一定会在内存里被解成明文。攻击者不去碰你的加密算法,等进程把它解开,直接从内存里 dump——Zygisk-Il2CppDumper 这类工具走的就是这条路,加密对它作用不大。
加密真正买到的,是把攻击成本从"解包、跑一遍 Il2CppDumper、导出符号",抬到"上 Frida、定位解密时机、绕过反调试"。能挡掉相当一批人,但挡不死。
要让 dump 出来的东西也读不懂,光加密容器不够,得在 IL2CPP 编译之前就把 C# 的类名方法名混淆掉,让写进字符串表的本来就是无意义符号。这里有个必须留意的约束:名字不能全抹。反射按字符串查找的类型、序列化用到的字段、GetComponent 按名字取的组件,运行时都要靠原名对上号,一并混淆会直接崩,得在混淆规则里显式排除。
它管不到的那几层
分析到最后,最该跟那个团队讲清的是 IL2CPP 的作用边界:它只负责"读代码"这一段。他们最初的期望,是拿它一个把"防破解"全兜住,但下面几件事 IL2CPP 一概不接——包被改后重签名分发,归签名校验和完整性校验;进程跑起来被 hook 改返回值、改内存数值,归反调试反 hook 和运行时检测;请求被原样重放,归协议防重放和服务端判定。
所以回到最初那个问题:用了 IL2CPP,代码保护就到位了吗?准确的说法是,它把逆向门槛抬高了一截,是一层有价值的防护,但不是全部。把它当一道门槛没问题,当成整套防线,签名、完整性、运行时检测、协议、服务端这些还得一层层补上。
(这几层加固的具体做法和取舍,我在主站版本里拆得更细,可以搜「字节暗面」。)
浙公网安备 33010602011771号