Unity AssetBundle効率的な暗号化ケースの共有

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)の方法に対して、Unity公式は「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の最後に1つの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.

公式ドキュメントのサンプルコードにはoffsetパラメーターのデモンストレーションを提供してありませんから、ここで表示しません。次に、自分が作成したテストコードを使用してデモンストレーションします。

まず、XAssetが生成したAssetBundleファイル内容をオフセットします。Unityがパッケージした後にすべてのAssetBundleファイルをトラバースして、ファイルにoffsetを添付してから覆います。コードは次のように、

foreach (string bundleName in bundleNames)
{
    string filepath = outputPath + "/" + bundleName;
    // hashcodeを利用してoffsetする
    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をロードする」を別々に行って、中の1つの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));
}

そして、上記の2つの関数で実行されたProfilerデータ分析とサンプリングの結果を見てみましょう。

テストシーンのスクリーンショット

offsetパラメーターを使用してAssetBundleをロードする前のメモリ状況

offsetパラメーターを使用してAssetBundleをロードする後のメモリ状況

LoadFromMemoryを使用してロードする前のメモリ状況

LoadFromMemoryを使用してロードする後のメモリ状況

比べたら、LoadFromMemoryを使用するとメモリが鮮明に増加し、さらにロードプロセス中に1つのメモリピークがあります。

AssetBundleのアセットをオフセットしたため、理論的にはAssetStudioがUnityプロジェクトのAssetBundleを直接分析できないことは間違いありません。次に、理論が実践のテストに耐えられるかどうかを確認しましょう。

テスト後、offsetが添付されていない場合には、AssetStudioを使用してAssetBundleにあるアセットを簡単にプレビューできます。次の図を参照してください(会社のプロジェクトリソースであるため、コーディングする必要があります)。

offsetのあるアセット

結果は理論上の推測結果と一致していることがわかりました。以下を参照してください。

テスト中に、AssetStudioの一部の古いバージョンは、offsetのあるアセットを解析すると、直接クラッシュすることがあるとわかりました。実際に、アセットの暗号化については、ほとんどの場合には初心者を防げますが専門家を防げません。シンプルな方法も複雑な方法も、逆コンパイラー専門家にとっては何度なく無力です。ある人がIDAを使用して通信暗号化アルゴリズムを逆コンパイラーしたのを見たことがあるので、ここではこれ以上詳細な分析は行いません。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意)https://answer.uwa4d.com