记一次 Unity 包的加固验收:几个差点被我判错的地方

前段时间接了个 Unity 包的上线前验收,甲方材料写着加固、资源加密都完成了。我照自己那套流程从静态过到运行时,本以为例行公事,结果有几处差点判错。记下来复盘一下。

静态我一般先解压翻 lib/arm64-v8a/,把加固 SO 拎出来扫三样:readelf -S.text 段大小,nm -D 看导出符号还剩不剩明文名,strings grep 一遍接口和密钥。这里有个常被念叨的误区:.text 只剩十几字节不一定是上了 VMP,代码也可能被挪进了自定义段;反过来还留着几百 KB,也可能只是关键函数单独保护了。所以段大小只当参考,真判断还得抽几个核心函数进 IDA,看能不能顺着读下去。DEX 顺手过一遍 JADX,IL2CPP 项目主逻辑虽不在 DEX 里,但 SDK 初始化、网络配置、native 加载入口还留着,类名混成 abc 只能说明做了改名,敏感参数几眼还是能顺出来。

这些都算常规,真正差点栽的是下面三处。

差点把“工具打不开”当成“防住了”

IL2CPP 这关,我把 libil2cpp.soglobal-metadata.dat 丢给 Il2CppDumper,它直接抛异常崩了,当时顺手就想记一句“metadata 已加密”。还好停了一下。Il2CppDumper 解析强依赖 metadata 头里的版本字段:文件开头是个 Il2CppGlobalMetadataHeadersanity 固定为 0xFAB11BAFxxd 看头四字节就是 AF 1B B1 FA),紧跟一个 version 决定后面一堆 offset/size 怎么切。工具是按写死的版本假设去读这些结构的,Unity 版本对不上、或者有人动过 header 里的字段,它一样崩,崩的表现和“被加密”几乎没区别。

分辨其实就一步:先 xxd 看那四个字节。magic 还是 AF 1B B1 FA、结构却解不动,大概率是版本或魔改,不是整体加密;magic 变了或成一片乱码,才可能是真加密(也可能只是抹了魔术字)。这次我拿了个没加固的同版本裸包做对照,工具在裸包上正常出 dump.cs,回到目标包才确认是加密拦的,不是工具自己趴窝。这个对照要是省了,我就把“没验准”写成“防住了”了。

补一句:就算 magic 变了、磁盘上真加密了,也别当终点。metadata 要用,引擎在 MetadataLoader::LoadMetadataFile 那里必然把它解成明文读进内存,root 机上 Zygisk-Il2CppDumper 这类工具直接从内存 dump,绕开你磁盘那层。所以磁盘加密拦的是“解包就 dump”的脚本党,真验到底得看内存那条路堵没堵。

资源那关,我一开始只看了文件头

资源 bundle 我先 xxd -l 32 看头,UnityFS55 6E 69 74 79 46 53)没了、magic 变了,差点打勾。后来想起有种省事的假加密只改文件头、或只加密头部几十字节,body 明文照旧,解析工具跳过头照样啃。于是从中间抽几段 dd if=res.bundle bs=64k skip=N count=1 出来喂 ent 测熵。

这里有个更绕的点:熵高不等于加密。AssetBundle 常带 LZ4/LZMA 压缩,压缩数据的熵一样能顶到 7.9 以上,跟加密看着一个样。所以熵只能证否、不能证是:某段熵明显偏低,那八成是明文或结构化数据,加密没覆盖到;熵高,还得看 AssetStudio、UABEA 能不能解开,能解开就是压缩,解不开才可能是加密。这次目标包就是头改了、body 熵不高,一个只换了魔术字的壳,资源基本裸着。

运行时:一台机器骗了我一次

运行时最容易被环境骗。我在常用测试机上 frida -U -f 挂起来,检测响了,本来要记“反 Frida 到位”。可那台机器 root 过、跑着默认 frida-server,本身就是最好逮的样本:默认 server 监听 27042 端口,注入后进程里会多出 gum-js-looppool-frida 这类线程名,/proc/pid/maps 里还能看到 frida-agent 的映射,很多反 Frida 就是扫这些默认特征。我把 server 换个名字、改了端口重新挂,有一处检测就没再触发,说明它盯的是默认特征,不是注入行为本身。

还有个时机问题:spawn 和 attach 得分开测。有的检测只在初始化那阵跑一遍,冷启动 -f 拉起时能逮到,等游戏跑起来再 attach 反而进得去;有的正相反。只测一个时机,容易把“某个时机没拦”当成“拦住了”。反调试是另一条线,看进程有没有自己占住 ptrace 的 tracer 位、/proc/self/statusTracerPid 会不会触发处置,跟反 Frida 不是一回事,得分开验。模拟器就更别在上面下结论,不少加固在模拟器上直接拒启动或行为两样,信号没法往真机套。

后来我固定下来的几个习惯

这次之后,几件事我提到验收最前面:

  • 动手前先对包指纹(签名、versionCode、关键 SO 的 hash),确认测的是要上架那版,别拿调试包或某个渠道包顶替。
  • 工具报错先看 magic、再拿裸包对照,别把“打不开”直接当“防住了”。
  • 资源别只看头,多段测熵,再用 AssetStudio 之类交叉验证是加密还是压缩。
  • 运行时 spawn 和 attach 分开、多环境验,一台脏机器的结果不作数。
  • 留一份基线(符号数、metadata 恢复情况、各步走到哪),下个版本一 diff 就知道哪层退回去了。

这次之后我对甲方那句“加固完成”留了个心眼。四个字是他们填的,到没到位,还得自己把包从头跑一遍才看得见。

完整的逐层验证流程我整理在字节暗面主站《Unity 加固怎么验》里。

posted @ 2026-09-03 11:48  字节暗面  阅读(11)  评论(0)    收藏  举报