平台回一句"加固完成",很多团队的加固验收就到此为止。但只打了勾、上线没几天就出事的包,见过不少:改了数值的破解版在群里传,查下来漏的不是"加没加固",是某几层没做到位,比如热更资源还是明文、越狱检测一处就能 patch 掉。验收不该停在平台回执,得把它拆成可勾选的几层,每层给一个当场能核对的点,最后附一张对照表。
分层清单
代码保护
- 关键字符串(接口地址、开关字段、检测标记)编译期加密,成品包里搜不到明文
- 控制流做了混淆,反编译出来不是一条直线读下去
- 保护做在编译阶段,而不是给成品包套壳(套壳能在运行时从内存 dump 出原始代码)
IL2CPP(Unity)
- 不只加密
global-metadata.dat,类名方法名在编译前就混淆过 - 还原出来是
a1、b2,不是PlayerController$$AddGold这种可读名 - 配了运行时反 dump(防止有人等内存里解开再整包捞走)
资源
- 首包和热更资源用同一套保护,热更目录里没有明文 bundle
- 按需分块解密,运行时内存里不出现整包完整明文
- per-resource key,解出一块不等于开全套
运行时
- 越狱检测不只探固定路径(rootless 越狱已把环境挪到别处)
- 越狱、注入、hook、内存修改器、变速多信号组合判断,不靠单一特征
- 检测不收敛成一个返回 bool 的总开关(否则 patch 一处就全关)
上报与服务端
- 客户端不自己判
safe/unsafe,结果带签名和随机数报给服务端 - "该报没报、安全模块缺失"本身也当风险信号
- 关键结果最终由服务端定性,客户端只负责抬高伪造成本
三个最容易"假到位"的点
IL2CPP 只加密 metadata。 文件确实加密了,但游戏一跑它必然在内存里解成明文,现成工具按特征一搜就 dump 走,名字原样还原。真正拦住的是编译前把名字混掉。
资源整包解密。 加载时把整包解开再交给引擎:
byte[] plain = Decrypt(File.ReadAllBytes(path), key); // 整包明文
AssetBundle ab = AssetBundle.LoadFromMemory(plain); // hook 这里就整包拿走
Decrypt 写得再复杂也没参与,因为攻击者截的是解完的 plain。按需分块能把这个明文窗口切碎。
检测收敛成一个 bool。
bool isSafeEnvironment() {
return checkJailbreak() && checkDebugger() && checkSignature();
}
一条指令把返回值改成 true,前面查得再全一起失效。检测要分散、结果掺进后续逻辑,别汇成一个能被一处 patch 掉的开关。
验收对照表
| 检查项 | 只做到这步 = 不够 | 验收看什么 |
|---|---|---|
| 代码 | Strip 完符号变少 | 明文字符串搜不到;保护在编译阶段 |
| IL2CPP | 只加密 global-metadata | 还原出来是 a1/b2 |
| 资源 | 只加密首包 | 首包+热更统一、按需分块、内存无整包明文 |
| 运行时 | 探几个越狱路径 | 多信号组合、不收敛成单点 bool |
| 上报 | 本地判 safe/unsafe | 签名+随机数报服务端,缺报也算信号 |
一句建议
验收别数"做了哪些",看"绕过一层之后能不能直接进下一层"。这几层连成一条没有断点的线才算数;缺哪一层,就从哪一层被穿过去。
每层的完整实操和原理在字节暗面主站原文:bytehidden.com/ios-game-ha… ,也可搜「字节暗面 iOS 游戏加固」。