字节暗面

Unity 游戏包加固怎么验:从反编译到运行时 Hook


拿到加固后的包,我一般先确认一件事:手里的是不是最终要上架那版。调试包可能关了保护,不同渠道包也可能走不同配置。APK 的 hash、签名证书、版本号和渠道最好先记下来,再核对关键 SO、global-metadata.dat 和资源文件。

确认完包,再按静态、资源、运行时往下走。记录工具卡在哪:常规工具直接出结果,改一改还能继续,还是必须进运行时跟加载过程。

静态先看 DEX、SO 和 metadata

第一步通常是把 APK 扔进 JADX。Unity 的主要逻辑可能在 IL2CPP,但 SDK 初始化、网络配置和 Native 加载入口还会留在 DEX 里。类名变成 abc,只能证明做过改名。关键校验和敏感参数还是几眼就能顺下来,这层混淆带来的麻烦不会太大。

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 定义的导出项要分开。还留着一片 xxxSDKInitcheckEnvdecryptAsset,定位当然省事;只剩必要的 JNI 入口,符号清理得比较干净,不过函数体该看还是得看。

strings 命中也只是线索。token 可能是字段名,URL 也未必需要隐藏。真正值得追的是硬编码凭证、内部接口和能与调用代码对上的敏感值。

IL2CPP 项目还得把 libil2cpp.soglobal-metadata.dat 一起交给 Il2CppDumper。dump.cs 里还有 AddGoldPlayerController 这类业务名,类型和方法关系就很好还原,不过函数体保护可能还在。

工具报错先别记成“metadata 已加密”。Unity 版本不匹配、文件选错、参数不对,都可能让它停在这里。我通常拿同版本裸包先跑一次。裸包能出结果,目标包打不开,这个对比才有意义。修个文件头、换个解析器又能继续,最多算定制处理这一档。

静态检查不能只盯一个特征,DEX、SO 和 metadata 要放在一起看

资源别只看文件头

检查 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 看着很像加密,但压缩数据也可能长这样。文件头、几个区段和工具解析结果都对上后,再去查算法和密钥。

静态工具到这里卡住,再跟实际加载路径。项目可能走 LoadFromMemoryLoadFromFile,也可能在 Native 层自己解密。Hook 要下在真正的解密位置,见到 Unity 就固定挂一个函数,很容易扑空。

一挂就能拿到完整 UnityFS,这层主要挡住了静态批量提取。需要跟踪分段解密,再把数据拼起来,难度才算往上走了一档。

文件头和熵值只能提供线索,资源保护还得继续往加载过程里验

运行时直接挂上去试

静态这几项跑完,接下来就上 Frida。先从冷启动拉起一次,再等进程跑起来后 attach:

frida -U -f com.company.game   # 冷启动注入
frida -U -n game_process       # 连接已运行进程

如果两边都能进去,模块能枚举,内存也能正常读写,至少说明这台设备上没碰到什么拦截。先别急着写“反 Frida 没做”,有的方案不会当场闪退,检测结果可能只写进日志或发到后台。客户端日志和服务端记录都翻一下,往往比盯着进程还活不活更靠谱。

测试机本身也会影响结果。一台已经 root、跑着默认 Frida Server 的机器,基本就是送到检测面前的标准样本。我一般先用它确认基础检测,再换干净真机,改一下端口或进程特征。这样大致能看出它只是扫默认特征,还是确实盯了注入行为。

这轮测完,我还会顺手改一处无害资源,重新打包并换测试证书签名。包起不来先翻日志,第三方 SDK 绑定签名也会造成同样的结果;如果客户端照常运行,再去后台找有没有完整性异常上报。

Frida 能否接入只是第一步,日志和后台记录也要一起检查

留一份回归记录

这套东西只在上线前跑一次,过几个月就没什么参考价值了。Unity 升级、渠道重新打包,都可能让某层保护退回去。我一般会留下 APK 和证书 hash、DEX 与 SO 的可读信息、metadata 和 Bundle 的恢复情况,以及 Frida、资源 Dump、重签名测试分别走到了哪一步。工具版本和设备环境也顺手记上。

某次更新后符号突然多了一倍,或者原来要改脚本才能恢复的 metadata 现在直接出结果,这种变化一眼就能看出来。比给加固凑一个总分实用。

最后留张检查表

验证项 怎么测 结果怎么记
DEX 混淆 JADX 看命名、调用和敏感值 直接可读 / 需要追调用 / 关键逻辑已保护
SO 代码与符号 readelfnm 配合反编译抽查 直接恢复 / 需要定制 / 需运行时还原
IL2CPP metadata Il2CppDumper 配合裸包对照 默认恢复 / 定制后恢复 / 需运行时取数据
资源文件 看头、测多段熵、用资源工具解析 直接导出 / 修复后导出 / 需跟踪解密
防篡改 无害修改后重打包、重签名 正常运行 / 客户端发现 / 服务端联动处置
反调试与 Hook 分别测试 spawn 和 attach 默认接入 / 定制后接入 / 需先绕检测

跑完以后,把每一层卡住的位置写清楚就够了。开发和安全团队看到的是哪里还能直接拿、哪里已经需要定制处理,下一步补什么也比较明确。

← 返回列表