现象
一个 Unity 手游包,用了 IL2CPP,团队认为代码保护到位了:C# 编成 native,Assembly-CSharp.dll 那条 dnSpy 反编译路已经没了,拖进去一片空白。但实际拿到包做静态分析时,业务侧的类名、方法名、接口地址几乎没费什么劲就出来了。下面按排查顺序记录整个过程和结论。
排查
1. 解包,定位关键文件
先把 APK 解开,关注两个位置:
lib/arm64-v8a/libil2cpp.so # C# 编译后的 native 代码
assets/bin/Data/Managed/Metadata/global-metadata.dat # 元数据,运行时反射要用
libil2cpp.so 里是真正的逻辑,但直接 strings 扫,业务字符串基本扫不到——native 化把符号信息剥掉了。问题出在旁边那个 global-metadata.dat。
2. 看 metadata 文件头
把 global-metadata.dat 拖进十六进制编辑器,看开头四字节:
AF 1B B1 FA
这是 IL2CPP 元数据的 sanity 值(0xFAB11BAF,小端)。看到它是明文,说明 metadata 没做任何加密,是裸的。头结构往后是成对的 offset/size,把类型定义、方法签名、字段偏移、字符串表一段段划开,类名方法名就明文存在字符串表里。
3. 跑 Il2CppDumper
有了这两个明文文件,直接喂给 Il2CppDumper:
Il2CppDumper.exe libil2cpp.so global-metadata.dat <output_dir>
它把 libil2cpp.so 里的地址和 metadata 里的名字对起来,产出 dump.cs、il2cpp.h、以及可以直接跑的 IDA / Ghidra 脚本。原本 IDA 里满屏的 sub_XXXXXX,脚本一跑就还原成带类名的可读方法:
// dump.cs 里长这样
public void AddGold(int amount) { } // 对应 IDA 里的 PlayerController$$AddGold
到这一步,业务类、方法签名、接口字段基本都能对着看了。
分析
排查结论很明确:只上默认 IL2CPP,等于没锁。
原因在于 IL2CPP 的机制本身。native 这层丢掉了"关于代码的信息"——类叫什么、方法什么签名、字段偏移是多少,可 .NET 靠反射运行,运行时必须拿得到这些。IL2CPP 的办法是把它们从 native 里剥出来单独存成 global-metadata.dat,默认明文进包。也就是说,native 抹掉的符号,metadata 又原样交了回来。逆向根本不用碰 native 那层的算法,读文件就够。
方案
按成本从低到高,分两层加固,另外别把 IL2CPP 当成防护的全部。
第一层:加密 metadata。 在构建流水线里对 global-metadata.dat 加密,运行时在加载入口(MetadataCache::Initialize() 里调 LoadMetadataFile 那处)解回来。密钥别跟明文 metadata 进同一个仓库。
要清楚这一层的天花板:游戏要跑,metadata 就一定会在内存里被解成明文。攻击者不碰你的密钥,等进程解开直接内存 dump——Zygisk-Il2CppDumper 这类工具走的就是这条路。所以加密买到的是把攻击从"解包、跑 Il2CppDumper、导出"抬到"上 Frida、找 dump 点、绕反调试",能挡一批,但挡不死。
第二层:编译前混淆名字。 要让 dump 出来也读不懂,得在 IL2CPP 编译之前就把 C# 类名方法名混淆成无意义符号,写进字符串表的本来就是 a1、b2 这种。注意名字不能全抹:反射按字符串查找的类型、序列化用到的字段、GetComponent 按名字取的组件,运行时要靠原名对上号,一并混淆会崩,得在混淆规则里显式排除。
再往上还能自编译 libil2cpp、改元数据布局让现成工具解析失效,但维护成本和跟 Unity 版本对齐的成本要一起算,按项目价值定。
最后一点:IL2CPP 只管"读代码"这一段。包被改重签名分发、进程跑起来被 hook 改数值、协议被原样重放,它一概不接——那分别是签名完整性校验、反调试反 hook、防重放各自的活,还有服务端判定。
检查项
排查完可以对照过一遍:
-
global-metadata.dat头四字节是否还是明文AF 1B B1 FA(是则未加密) - Il2CppDumper 能否直接吐出带名字的
dump.cs/ IDA 脚本(能则符号未保护) - 类名方法名有没有在 IL2CPP 编译前混淆(否则 dump 到内存仍是可读名)
- 混淆是否排除了反射 / 序列化 /
GetComponent依赖的类型(否则运行时崩) - metadata 加密是否放进构建流水线、密钥是否与明文 metadata 分仓
- 签名完整性、防 hook、防重放、服务端判定是否各自落地(别指望 IL2CPP 兜)
(完整的两层加固思路和边界分析,在主站版本里拆得更细,可以搜「字节暗面 IL2CPP 代码保护」。)

7137

被折叠的 条评论
为什么被折叠?



