Unity AssetBundle高效加密案例分享

Unity AssetBundle高效加密案例分享

这是侑虎科技第585篇文章,感谢作者江鱼供稿。欢迎转发分享,未经作者授权请勿转载。如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

作者主页:https://www.zhihu.com/people/yu-jiang-3-65/,作者也是U Sparkle活动参与者,UWA欢迎更多开发朋友加入U Sparkle开发者计划,这个舞台有你更精彩!


随着AssetStudio的普及,Unity项目中使用的AssetBundle资源可以被各种小白用户解包提取,由此AssetBundle资源包的安全问题不得不引起我们重视。

过去,通过官方文档的了解,我们主要可以通过,AssetBundle.LoadFromMemory(Async)的方案来实现资源包加密,官方文档对这个方法是这样描述的:

Use this method to create an AssetBundle from an array of bytes. This is useful when you have downloaded the data with encryption and need to create the AssetBundle from the unencrypted bytes.

Compared to LoadFromMemoryAsync, this version is synchronous and will not return until it is done creating the AssetBundle object.

下面是官方文档中的示例代码:

using UnityEngine;
using UnityEngine.Networking;
using System.Collections;

public class ExampleClass : MonoBehaviour
{
    byte[] MyDecription(byte[] binary)
    {
        byte[] decrypted = new byte[1024];
        return decrypted;
    }

    IEnumerator Start()
    {
        var uwr = UnityWebRequest.Get("http://myserver/myBundle.unity3d");
        yield return uwr.SendWebRequest();
        byte[] decryptedBytes = MyDecription(uwr.downloadHandler.data);
        AssetBundle.LoadFromMemory(decryptedBytes);
    }
}

需要注意的是,对于AssetBundle.LoadFromMemory(Async)这个方法,在官方的AssetBundle foudamentals一文中,官方又非常明确的指出:

Unity's recommendation is not to use this API.

AssetBundle.LoadFromMemoryAsync loads an AssetBundle from a managed-code byte array (byte[] in C#). It will always copy the source data from the managed-code byte array into a newly-allocated, contiguous block of native memory. If the AssetBundle is LZMA compressed, it will decompress the AssetBundle while copying. Uncompressed and LZ4-compressed AssetBundles will be copied verbatim.

The peak amount of memory consumed by this API will be at least twice the size of the AssetBundle: one copy in native memory created by the API, and one copy in the managed byte array passed to the API. Assets loaded from an AssetBundle created via this API will therefore be duplicated three times in memory: once in the managed-code byte array, once in the native-memory copy of the AssetBundle and a third time in GPU or system memory for the asset itself.

Prior to Unity 5.3.3, this API was known as AssetBundle.CreateFromMemory. Its functionality has not changed.

从官方的解释中,我们可以看到AssetBundle.LoadFromMemory(Async)的使用成本非常高昂,不被推荐是自然而然的事情。但是,有没有更高效便捷的方式去对AssetBundle进行加密处理,防止被小白用户利用AssetStudio之类的工具轻易地提取到AssetBundle的资源呢?

在查看Unity API的时候发现LoadFromFile末尾有一个offset参数,那么这个参数有什么用呢?是否可以起到防止AssetBundle资源直接被AssetStudio提取呢?先看官方文档的接口说明:

public static AssetBundle LoadFromFile(string path, uint crc, along offset);

Parameters

Returns
AssetBundle Loaded AssetBundle object or null if failed.

Description
Synchronously loads an AssetBundle from a file on disk.

The function supports bundles of any compression type. In case of lzma compression, the data will be decompressed to the memory. Uncompressed and chunk-compressed bundles can be read directly from disk.

Compared to LoadFromFileAsync, this version is synchronous and will not return until it is done creating the AssetBundle object.

This is the fastest way to load an AssetBundle.

官方文档的代码示例并没有,提供offest参数的演示,所以在这里就不搬运了,接下来我会用自己写的测试代码来做演示。

首先,我们需将XAsset生成好的AssetBundle文件内容进行偏移处理,待Unity打包完成后遍历所有AssetBundle文件,并对文件添加offset后进行覆盖,代码如下:

foreach (string bundleName in bundleNames)
{
    string filepath = outputPath + "/" + bundleName;
    // 利用 hashcode 做偏移 
    string hashcode = manifest.GetAssetBundleHash(bundleName).ToString();
    ulong offset = Utility.GetOffset(hashcode);
    if ( offset > 0)
    {
        byte[] filedata = File.ReadAllBytes(filepath);
        int filelen = ((int)offset + filedata.Length);
        byte[] buffer = new byte[filelen];
        copyHead(filedata, buffer, (uint)offset);
        copyTo(filedata, buffer, (uint)offset);
        FileStream fs = File.OpenWrite(filepath);
        fs.Write(buffer, 0, filelen);
        fs.Close();
        offsets  += filepath + " offset:" + offset + "\n";
    }
    WriteItem(stream, bundleName, filepath, hashcode);
}

然后,我们再进行加载测试,我们分别使用offset参数加载AssetBundle,和模拟解密文件后从内存中加载AssetBundle然后读取其中的一个Texture用于显示,可以参考以下代码:

// 基于offset加载AssetBundle
async void onLoadWithOffsetClicked()
{
    if (offsetBundle)
        offsetBundle.Unload(true);

    var current_memory = Profiler.GetTotalAllocatedMemoryLong();
    display_image.texture = null;
    var path = System.IO.Path.Combine(Application.streamingAssetsPath, "assets_previews_offset");
    var assetBundleRequest = AssetBundle.LoadFromFileAsync(path, 0, 294);
    await assetBundleRequest;
    var texture = assetBundleRequest.assetBundle.LoadAsset<Texture2D>("download.jpg");
    display_image.texture = texture;
    offsetBundle = assetBundleRequest.assetBundle;

    Debug.Log("Offset Load Complete:" + (Profiler.GetTotalAllocatedMemoryLong() - current_memory));
}

// 基于Menmory加载AssetBundle
async void onLoadWithMemoryClicked()
{
    if (memoryBundle)
        memoryBundle.Unload(true);

    var current_memory = Profiler.GetTotalAllocatedMemoryLong();
    display_image.texture = null;
    var path = System.IO.Path.Combine(Application.streamingAssetsPath, "assets_previews");
    WWW www = new WWW("file://" + path);
    await www;
    var request = AssetBundle.LoadFromMemoryAsync( www.bytes);
    await request;
    var texture = request.assetBundle.LoadAsset<Texture2D>("download.jpg");
    display_image.texture = texture;

    memoryBundle = request.assetBundle;
    www.Dispose();

    Debug.Log("Memory Load Complete:"+ (Profiler.GetTotalAllocatedMemoryLong() - current_memory));
}

接下来,我们再看看对以上两个函数执行的Profiler数据分析采样的结果:


测试场景截图


使用offset参数加载之前的内存情况


使用offset参数加载AssetBundle之后的内存情况


使用LoadFromMemory加载之前的内存情况


使用LoadFromMemory加载之后的内存情况

通过对比发现,使用LoadFromMemory内存明显发生了增长,并且在加载过程中还出现了一个内存高峰。

由于我们对AssetBundle的资源进行了偏移,势必在理论上,AssetStudio无法直接解析出我们Unity工程中的AssetBundle,接下来我们再来看下,我们的理论是否经得起实践的考验。

经测试,没加offset的时候可以轻易地用AssetStudio预览AssetBundle中的资源,请参考下图(因为用的是公司项目的资源所以需要打码处理):

带offset的资源,发现和我们的理论推测结果一致,请参考:

在测试过程中发现,有些老版本的AssetStudio,在解析带offest的资源的时候甚至会直接奔溃。其实,对于资源加密,我们大多数时候能做到的是防小白不防专家,不管你是采用简单的或者复杂的,在反编译高手手里都有点苍白,我亲眼所见一个大佬用IDA把人家的通信加密算法反出来了,所以这里就不做更深入的分析了。


文末,再次感谢江鱼的分享,如果您有任何独到的见解或者发现也欢迎联系我们,一起探讨。(QQ群:793972859)

也欢迎大家来积极参与U Sparkle开发者计划,简称“US”,代表你和我,代表UWA和开发者在一起!