拿到加固后的包,我一般先确认一件事:手里的是不是最终要上架那版。调试包可能关了保护,不同渠道包也可能走不同配置。APK 的 hash、签名证书、版本号和渠道最好先记下来,再核对关键 SO、global-metadata.dat 和资源文件。
确认完包,再按静态、资源、运行时往下走。记录工具卡在哪:常规工具直接出结果,改一改还能继续,还是必须进运行时跟加载过程。
静态先看 DEX、SO 和 metadata
第一步通常是把 APK 扔进 JADX。Unity 的主要逻辑可能在 IL2CPP,但 SDK 初始化、网络配置和 Native 加载入口还会留在 DEX 里。类名变成 a、b、c,只能证明做过改名。关键校验和敏感参数还是几眼就能顺下来,这层混淆带来的麻烦不会太大。
SO 可以先跑下面几条:
readelf -S -W libxxx.so
readelf -l -W libxxx.so
nm -D --defined-only libxxx.so
strings -n 8 libxxx.so | grep -iE 'https?://|/api/|secret|token|debug'
最容易误判的是 .text 大小。只剩很小一段,不一定就是上了 VMP,代码也可能被挪进自定义段;还有几百 KB,也可能是只保护了关键函数。我会再看可执行段、入口跳转,并抽几段核心函数扔进 IDA 或 Ghidra。能不能顺着读下去,比段大小更有用。
nm -D 查的是动态符号,导入项和当前 SO 定义的导出项要分开。还留着一片 xxxSDKInit、checkEnv、decryptAsset,定位当然省事;只剩必要的 JNI 入口,符号清理得比较干净,不过函数体该看还是得看。
strings 命中也只是线索。token 可能是字段名,URL 也未必需要隐藏。真正值得追的是硬编码凭证、内部接口和能与调用代码对上的敏感值。
IL2CPP 项目还得把 libil2cpp.so 和 global-metadata.dat 一起交给 Il2CppDumper。dump.cs 里还有 AddGold、PlayerController 这类业务名,类型和方法关系就很好还原,不过函数体保护可能还在。
工具报错先别记成“metadata 已加密”。Unity 版本不匹配、文件选错、参数不对,都可能让它停在这里。我通常拿同版本裸包先跑一次。裸包能出结果,目标包打不开,这个对比才有意义。修个文件头、换个解析器又能继续,最多算定制处理这一档。

资源别只看文件头
检查 AssetBundle,最省事的办法是先看头:
xxd -l 32 res.bundle
开头还是 UnityFS,说明没做完整的外层加密,可以继续丢给 AssetStudio、UABEA 试试。文件头变了也别急着下结论,有些处理只是换了 magic,正文基本没动。这时可以从中间抽一段测熵:
dd if=res.bundle bs=65536 skip=4 count=1 2>/dev/null | ent | grep Entropy
多换几个位置,再拿裸资源对照。熵接近 8.0 看着很像加密,但压缩数据也可能长这样。文件头、几个区段和工具解析结果都对上后,再去查算法和密钥。
静态工具到这里卡住,再跟实际加载路径。项目可能走 LoadFromMemory、LoadFromFile,也可能在 Native 层自己解密。Hook 要下在真正的解密位置,见到 Unity 就固定挂一个函数,很容易扑空。
一挂就能拿到完整 UnityFS,这层主要挡住了静态批量提取。需要跟踪分段解密,再把数据拼起来,难度才算往上走了一档。

运行时直接挂上去试
静态这几项跑完,接下来就上 Frida。先从冷启动拉起一次,再等进程跑起来后 attach:
frida -U -f com.company.game # 冷启动注入
frida -U -n game_process # 连接已运行进程
如果两边都能进去,模块能枚举,内存也能正常读写,至少说明这台设备上没碰到什么拦截。先别急着写“反 Frida 没做”,有的方案不会当场闪退,检测结果可能只写进日志或发到后台。客户端日志和服务端记录都翻一下,往往比盯着进程还活不活更靠谱。
测试机本身也会影响结果。一台已经 root、跑着默认 Frida Server 的机器,基本就是送到检测面前的标准样本。我一般先用它确认基础检测,再换干净真机,改一下端口或进程特征。这样大致能看出它只是扫默认特征,还是确实盯了注入行为。
这轮测完,我还会顺手改一处无害资源,重新打包并换测试证书签名。包起不来先翻日志,第三方 SDK 绑定签名也会造成同样的结果;如果客户端照常运行,再去后台找有没有完整性异常上报。

留一份回归记录
这套东西只在上线前跑一次,过几个月就没什么参考价值了。Unity 升级、渠道重新打包,都可能让某层保护退回去。我一般会留下 APK 和证书 hash、DEX 与 SO 的可读信息、metadata 和 Bundle 的恢复情况,以及 Frida、资源 Dump、重签名测试分别走到了哪一步。工具版本和设备环境也顺手记上。
某次更新后符号突然多了一倍,或者原来要改脚本才能恢复的 metadata 现在直接出结果,这种变化一眼就能看出来。比给加固凑一个总分实用。
最后留张检查表
| 验证项 | 怎么测 | 结果怎么记 |
|---|---|---|
| DEX 混淆 | JADX 看命名、调用和敏感值 | 直接可读 / 需要追调用 / 关键逻辑已保护 |
| SO 代码与符号 | readelf、nm 配合反编译抽查 |
直接恢复 / 需要定制 / 需运行时还原 |
| IL2CPP metadata | Il2CppDumper 配合裸包对照 | 默认恢复 / 定制后恢复 / 需运行时取数据 |
| 资源文件 | 看头、测多段熵、用资源工具解析 | 直接导出 / 修复后导出 / 需跟踪解密 |
| 防篡改 | 无害修改后重打包、重签名 | 正常运行 / 客户端发现 / 服务端联动处置 |
| 反调试与 Hook | 分别测试 spawn 和 attach | 默认接入 / 定制后接入 / 需先绕检测 |
跑完以后,把每一层卡住的位置写清楚就够了。开发和安全团队看到的是哪里还能直接拿、哪里已经需要定制处理,下一步补什么也比较明确。