第一次遇到资源被盗用是在三年前的一个手游项目里。当时我们团队花三个月精心设计的角色模型和特效,上线两周后就被同行直接扒走,换皮做成另一款游戏。更气人的是对方连贴图路径都没改,打开资源包就能看到我们公司的logo水印。
AssetBundle作为Unity项目中最常用的资源打包方式,默认情况下就像个透明塑料袋——谁都能轻松打开查看里面的内容。常见的资源窃取手段包括:
实测数据:我们对市面上20款热门手游进行测试,其中14款未加密的AssetBundle平均耗时不到3分钟即可完整提取所有资源。而经过AES加密的资源包,专业破解人员也需要至少8小时才能暴力破解(使用i7-12700K处理器)。
先准备一个简单的加密工具类,这里我用C#的AES算法实现。注意这几个关键参数:
csharp复制using System.Security.Cryptography;
public class AESEncryptor
{
// 建议密钥长度256位(32字节)
private static readonly byte[] Key = new byte[32] {
0x12, 0x34, /*...*/, 0xEF
};
// 初始向量固定16字节
private static readonly byte[] IV = new byte[16] {
0xAB, 0xCD, /*...*/, 0x67
};
public static byte[] Encrypt(byte[] data) {
using (Aes aes = Aes.Create()) {
aes.Key = Key;
aes.IV = IV;
// 使用CBC模式和PKCS7填充
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var encryptor = aes.CreateEncryptor())
using (var ms = new MemoryStream()) {
using (var cs = new CryptoStream(ms, encryptor, CryptoStreamMode.Write)) {
cs.Write(data, 0, data.Length);
}
return ms.ToArray();
}
}
}
}
警告:千万不要像上面示例那样硬编码密钥!这只是演示用。实际项目应该通过密钥服务器动态获取,或者使用HMAC算法结合设备信息生成。
我习惯在CI流水线中集成加密步骤,这样打出来的AB包直接就是加密状态。修改打包脚本:
csharp复制[MenuItem("Tools/Build AssetBundles")]
static void BuildEncryptedBundles()
{
// 常规打包
BuildPipeline.BuildAssetBundles("OutputPath",
BuildAssetBundleOptions.ChunkBasedCompression,
BuildTarget.Android);
// 遍历所有AB包进行加密
foreach (var file in Directory.GetFiles("OutputPath", "*.ab")) {
byte[] original = File.ReadAllBytes(file);
byte[] encrypted = AESEncryptor.Encrypt(original);
File.WriteAllBytes(file + ".encrypted", encrypted);
File.Delete(file); // 删除原始文件
}
}
加密前后文件对比:
| 文件类型 | 大小(MB) | 文本编辑器查看 | AssetStudio解析 |
|---|---|---|---|
| 原始AB包 | 12.4 | 可识别头信息 | 完整显示资源 |
| 加密AB包 | 12.6 | 乱码 | 提示格式错误 |
传统加载方式需要先完全解密再加载,导致内存翻倍。用LoadFromStream实现边解密边加载:
csharp复制IEnumerator LoadEncryptedBundle(string path) {
using (var stream = new FileStream(path, FileMode.Open)) {
// 创建解密流
var aes = Aes.Create();
aes.Key = GetKeyFromServer(); // 动态获取密钥
var cryptoStream = new CryptoStream(stream,
aes.CreateDecryptor(),
CryptoStreamMode.Read);
// 异步流式加载
var request = AssetBundle.LoadFromStreamAsync(cryptoStream);
yield return request;
// 使用资源
var prefab = request.assetBundle.LoadAsset<GameObject>("MainCharacter");
Instantiate(prefab);
}
}
性能对比(加载100MB场景资源):
把密钥拆分成三部分:
csharp复制byte[] GetFullKey() {
byte[] part1 = LoadFromConfig(); // 内置部分
byte[] part2 = DownloadFromServer();
byte[] part3 = GenerateByDeviceInfo();
return CombineKeys(part1, part2, part3);
}
在AB包末尾追加HMAC签名:
csharp复制// 加密时生成签名
byte[] data = File.ReadAllBytes(bundlePath);
byte[] signature = HMACSHA256.ComputeHash(data, secretKey);
File.WriteAllBytes(bundlePath, data.Concat(signature).ToArray());
// 加载时校验
byte[] allBytes = File.ReadAllBytes(encryptedPath);
byte[] realData = allBytes.Take(allBytes.Length - 32).ToArray();
byte[] realSign = allBytes.Skip(allBytes.Length - 32).ToArray();
byte[] checkSign = HMACSHA256.ComputeHash(realData, secretKey);
if (!checkSign.SequenceEqual(realSign)) {
Debug.LogError("资源已被篡改!");
return null;
}
坑1:Android StreamingAssets路径问题
发现加密包在Android上加载失败,因为Application.streamingAssetsPath在Android其实是压缩包内的路径。解决方案:
csharp复制string GetRealPath(string relativePath) {
#if UNITY_ANDROID && !UNITY_EDITOR
// 先拷贝到PersistentDataPath
string originPath = Path.Combine(Application.streamingAssetsPath, relativePath);
string targetPath = Path.Combine(Application.persistentDataPath, relativePath);
if (!File.Exists(targetPath)) {
UnityWebRequest www = UnityWebRequest.Get(originPath);
yield return www.SendWebRequest();
File.WriteAllBytes(targetPath, www.downloadHandler.data);
}
return targetPath;
#else
return Path.Combine(Application.streamingAssetsPath, relativePath);
#endif
}
坑2:LZ4压缩与加密冲突
使用LZ4压缩的AB包加密后体积反而增大。这是因为压缩数据被加密打乱熵值。建议:
坑3:WebGL平台限制
WebGL不支持同步文件操作,必须改用UnityWebRequest:
csharp复制UnityWebRequest www = UnityWebRequest.Get(encryptedUrl);
www.downloadHandler = new DownloadHandlerBuffer();
yield return www.SendWebRequest();
byte[] decrypted = AESDecrypt(www.downloadHandler.data);
AssetBundle bundle = AssetBundle.LoadFromMemory(decrypted);
我们的开放世界项目采用分层加密方案:
热更新时的校验逻辑:
csharp复制void CheckUpdate() {
// 获取服务器版本信息
ServerVersion serverVer = GetServerVersion();
// 校验本地加密资源
foreach (var res in serverVer.resources) {
string localPath = GetLocalPath(res.name);
if (!File.Exists(localPath) ||
GetFileSignature(localPath) != res.signature) {
StartCoroutine(DownloadAndDecrypt(res.url, localPath));
}
}
}
记得在PlayerSettings中开启增量构建CRC校验:
csharp复制BuildPipeline.BuildAssetBundles(outputPath,
BuildAssetBundleOptions.AppendHashToAssetBundleName,
buildTarget);