最近有个朋友拿着一款叫 SiNiSistar2 的独立游戏来问我,这游戏的资源包能不能解?我第一反应是:独立游戏还要搞加密,是不是太“卷”了点?结果下载下来用工具一扫,嘿,还真有东西。

既然遇到了,那就顺手来一波逆向实战,跟大家分享一下面对此类“常规加密”时的分析思路和解决路径。毕竟授人以鱼不如授人以渔,掌握了这套流程,下次遇到类似的 Unity 游戏也能心里有底。

第一步:眼见为实,确认加密状态

在动手做逆向之前,先得确认这货是不是真的穿了“马甲”。正常的 Unity AssetBundle 文件,头部特征是非常明显的。你随便找个没加密的 AB 包,用十六进制编辑器(比如 HxD)打开,开头的字节一定是 55 6E 69 74 79 46 53,对应的 ASCII 码就是大家熟悉的“UnityFS”。

我们进到游戏的 StreamingAssets 目录,挑个文件扔进 HxD 一看,好家伙,全是乱七八糟的随机字节,完全看不出任何字符串结构,更别提“UnityFS”签名了。到这一步,基本可以实锤:这资源包被加密了,而且是全量加密,不是简单的字段混淆。

第二步:顺藤摸瓜,定位加解密逻辑

既然确认了加密,下一步就是找到加密的“钥匙”和“锁孔”。现在的 Unity 游戏大多打成了 IL2CPP 包,代码都在 GameAssembly.dll 里,直接看 C# 源码是没戏了。这时候老牌神器 Il2CppDumper 就得登场了。

跑一遍 Il2CppDumper,它会吐出 dump.cs 文件,里面包含了所有的 C# 类定义和方法签名。不过光看这些定义还是有点大海捞针,这里有个现代化的小技巧:我们可以用 Python 脚本从 GameAssembly.dll 里导出一份全局字符串表(stringliteral.json),然后配合 AI(比如 Embedding 模型 + 代码分析模型)来快速检索。

为什么这么做?因为写加密逻辑的程序员,通常会留下一些极具特征的类名或方法名,比如“Encrypt”、“Decrypt”、“AES”、“Crypto”等等。把 dump.cs 扔给 AI,让它检索这些关键字,效率比人肉翻代码高多了。

在这次实战中,AI 很快就帮我们在一个名为 Util 的工具类里嗅到了“异味”。这个类里赫然出现了加密和解密相关的方法,而且算法类型也暴露无遗——RijndaelManaged(也就是 AES 的一种托管实现)。更有意思的是,这个类里还直接把两个静态字段 IV(初始化向量)和 K(Key)给暴露出来了。

第三步:内存取证,拿下密钥

虽然我们在代码里看到了 IVK 这两个字段,但它们的值通常不是写死在代码里的,而是在运行时动态生成的。这时候就需要动用内存修改工具 Cheat Engine(CE)了。

思路很简单:打开游戏,附加 CE,搜索 Util 这个类在内存中的地址,然后查看其静态字段的偏移量。通过对比 dump.cs 中的结构,我们能精准定位到存储 IVK 的内存位置。这一次运气不错,直接就在内存里把这两个关键字符串给“抄”了下来。

拿到密钥还不够,我们还得搞清楚具体的加密参数。回到 dump.cs 或者直接分析还原后的 C++ 代码(也就是 Util__get_aesManaged 方法),我们可以看到加密的具体配置:

  • 算法:Rijndael(AES 家族)
  • 密钥长度:256 位
  • 块大小:256 位(注意这里,标准 AES 只支持 128 位块,这也是为什么后面解密比较麻烦的原因)
  • 模式:CBC
  • 填充:PKCS7

这里有个小坑需要注意,代码里有一个小小的“障眼法”:设置 AES Key 的时候用的实际上是 Util.IV,而设置 IV 的时候用的却是 Util.K。这种 Key 和 IV 变量名与实际用途颠倒的做法,如果不仔细看代码逻辑,很容易在解密时搞反,导致解密失败。

第四步:编写脚本,还原资源

参数都齐活了,最后一步就是写代码解密。这里有个问题:标准的大多数 AES 库(比如 .NET 自带的 AesManaged 或 Python 的 Crypto.Cipher)默认只支持 128 位的块大小。而我们的目标使用的是 256 位块大小的 Rijndael。

这时候就得请出支持更多配置的加密库了,比如 .NET 生态里的 BouncyCastle。

下面是一个简单的解密逻辑示例(C# 版本)

using Org.BouncyCastle.Crypto.Engines;
using Org.BouncyCastle.Crypto.Modes;
using Org.BouncyCastle.Crypto.Paddings;
using Org.BouncyCastle.Crypto.Parameters;

// 假设 key 和 iv 已经从内存中提取并转换为 byte[]
// 注意:这里的 key 在代码逻辑里对应的是原 Util.IV,iv 对应的是 Util.K
var engine = new RijndaelEngine(256); // 256位块大小
var cipher = new CbcBlockCipher(engine);
var paddedCipher = new PaddedBufferedBlockCipher(cipher, new Pkcs7Padding());

// false 代表解密模式
KeyParameter keyParam = new KeyParameter(key);
ParametersWithIV ivParam = new ParametersWithIV(keyParam, iv);
paddedCipher.Init(false, ivParam);

byte[] result = new byte[paddedCipher.GetOutputSize(ciphertext.Length)];
int len = paddedCipher.ProcessBytes(ciphertext, 0, ciphertext.Length, result, 0);
len += paddedCipher.DoFinal(result, len);

// result 就是解密后的完整 AssetBundle 数据

跑完这段代码,把解密出来的二进制数据再次扔进 HxD。这时候奇迹发生了:原本的乱码消失不见,文件头赫然出现了“UnityFS”,紧随其后的是引擎版本号和文件列表。大功告成!接下来直接丢进 AssetStudio,模型、贴图、音频统统现出原形。

总结与思考

复盘一下整个流程,其实不难看出,对于 SiNiSistar2 这种体量的独立游戏,所谓的“加密”更多的是防君子不防小人。它利用了 Il2CPP 的复杂性来提升第一眼的分析门槛,但在密钥管理和算法实现上并没有做得非常彻底(静态字段暴露、内存明文存储)。

不过,现实中的大型商业游戏往往不会这么简单。如果你遇到的目标对 UnityPlayer 进行了深度魔改、加上了 VM 保护(虚拟化混淆)、或者开启了强力的反调试机制,那么上述步骤中的每一步都可能变成“地狱模式”。那时候可能就需要动用更高级的动态调试技巧(如 Frida)或者对抗性脱壳手段了。

总的来说,逆向分析就像是一场解谜游戏,关键在于耐心和对原理的理解。希望这次的实战经验能帮到大家,下次打开Unity游戏的时候,不妨也去 StreamingAssets 里逛逛,说不定会有意外收获?

标签: none

评论已关闭