这段时间在研究雷索纳斯的ab文件修改,游戏打包的时候用了AssetBundle.SetAssetBundleEncryptKey();也就是Unity中国区的官方加密。这一点可以通过dump il2cpp.so查看有没有SetAssetBundleDecryptKey函数或者直接观察ab文件头确定。

55 6E 69 74 79 46 53 00 00 00 00 07 35 2E 78 2E UnityFS.....5.x.

78 00 32 30 31 39 2E 34 2E 34 30 66 31 63 31 00 x.2019.4.40f1c1.
00 00 00 00 00 1F A8 B5 00 00 01 7D 00 00 03 23
00 00 02 43 00 00 00 00 26 6B 63 5F ED 5D F6 4C
2C C3 76 C5 17 CF 23 CC 55 26 44 73 3F 37 75 3E
62 31 6E 30 4B 4D 52 22 00 04 6C 53 56 0D 82 42
90 FB CB FF 8C 20 F3 E3 D3 55 26 44 73 3F 37 75
3E 62 31 6E 30 4B 4D 52 22 00 00 00 00 00 00 00
1E 00 01 00 30 42 00 02 07 00 42 5F 30 01 03 0A

一直到00 00 02 43都是正常的文件头部分,从26 6B 63 5F到4B 4D 52 22部分则是UnityCN加密相关的信息。主要特征是文件头后32字节 + 00 + 32字节末端对齐16字节。

实际上单纯提取UnityCN加密后的ab文件内的资产是有现成的工具的,比如Razmoth维护的Studio和K0lb3的UnityPy都支持根据Key解密读取加密的资产。其中Razmoth维护的Studio在Perfare佬的版本上实现了更多feature也做了不少优化。但是修改文件不只需要解密还需要加密。Studio并不直接支持导出解密之后的ab文件,UnityPy能导出解密后的ab文件但是没有提供直接的函数反向的加密。

我在GitHub上大致搜索了一圈并没有看到有提供解密/加密功能的项目,不过既然已经有前辈实现了解密,加密也只是倒过来操作一遍就是了。因为需求原因,我只实现了对UnityFS类型的文件的加解密,实际上似乎还有其他类型的文件会用UnityCN加密?因为我没找到比较系统的文档。这里大致讲一下加解密的流程(以下解密部分的逻辑均参考自Razmoth的Studio项目中的BundleFile.cs文件)。

以上面的文件头为例先讲讲对于UnityFS类型的BundleFile的大概结构:

55 6E 69 74 79 46 53 00 00 00 00 07 35 2E 78 2E UnityFS.....5.x.
----signature-------  --version-- -unityVersion
78 00 32 30 31 39 2E 34 2E 34 30 66 31 63 31 00 x.2019.4.40f1c1.
--  -------------unityRevision------------
00 00 00 00 00 1F A8 B5 00 00 01 7D       00 00 03 23
-------file size------- compressedBlocksInfoSize uncompressedBlocksInfoSize
00 00 02 43 00 00 00 00
---flags---

上面是文件头部分,file size是整个文件的大小,flags按位保存了各种flag信息。这里出现了一个Blocks,因为BundleFile像一个容器一样,具体的资产数据是被储存在block里的,而blocks的信息是存储在BlocksInfo里面的。在对文件进行lz4/lzma压缩的时候也会对BlocksInfo信息做压缩,所以需要存储压缩前后的信息以用作解压缩。正常来讲,如果是没使用UnityCN加密的文件,上面的头部信息之后compressedBlocksInfoSize长度的内容就是压缩后的BlocksInfo,解压之后就能拿到关于Block的信息。或者在flags中有提到BlocksInfo在文件尾部,就从末尾读取对应长度。具体可以查看源码。

我们先不考虑UnityCN的部分,拿到blockinfo之后剩下的数据就是block的内容了。虽然说是blockinfo,但是记录的内容包括blockinfo和DirectoryInfo。blockinfo里面记录了每个block的uncompressedSize、compressedSize和flags,每个block的长度是compressedSize。按compressedSize读取block然后根据flag解压再将所有block拼接就得到了完整的解压后的数据。而前面提到的DirectoryInfo是基于解压后的数据的。序列化输出一下这两个信息大概是这样的

Block Info: compressedSize: 0x00005A64 | uncompressedSize: 0x00020000 | flags: 0x00000003

Block Info: compressedSize: 0x00004C2A | uncompressedSize: 0x00020000 | flags: 0x00000003
Block Info: compressedSize: 0x000009F7 | uncompressedSize: 0x00020000 | flags: 0x00000003
Block Info: compressedSize: 0x0000025E | uncompressedSize: 0x00018A6C | flags: 0x00000003
Directory Info: offset: 0x00000000 | size: 0x00038A6C | flags: 4 | path: CAB-7cad85440178f2bd1acbf08ce895972f
Directory Info: offset: 0x00038A6C | size: 0x00800000 | flags: 0 | path: CAB-7cad85440178f2bd1acbf08ce895972f.resS

这个Directory 部分先按下不表,因为他不影响加解密部分。有了block的信息就需要读取并解压了,而加/解密就是在这里进行的,源码中是使用UnityCN.DecryptBlock(compressedBytes, compressedSize, i);对解压之前的数据块进行解密之后再解压的。那进行加密的核心部分就是对压缩之后的每个block进行加密然后写入,稍微看一下解密的逻辑就能写出加密函数了。

但是只是这样并不能输出一个有效的解密后的ab文件,还需要修改block flag和header里的flag,表示文件不是加密的。以及其他一些细节。

最后的成品UnityCN-Helper