AssetBundleの解析手法とその対策について

# はじめに

ゲームアプリの開発において、**リソースの解析・抽出を防ぐ**のはそこそこ重要な課題です。特にいわゆるソシャゲにおいては、**カード画像や音声データなどあらゆるリソースは重要なコンテンツの一部であり、これを不正に入手されることは避けたい**です。
そして、Unityでは外部リソースはAssetBundleを利用して配信することがほとんどです。この記事では、**AssetBundleによるコンテンツ配信における解析対策**についてまとめてみます。

# AssetBundle解析の手法について

解析対策を行うにあたって、まずは**解析の手法について知る**必要があります。
AssetBundle解析の流れは、基本的には以下の流れになります。

1. なんらかの方法でアプリのAssetBundleファイルを入手する
2. 入手したAssetBundleファイルからデータを抽出する

## AssetBundle内のデータを解析・抽出する方法

入手したAssetBundleからデータを解析・抽出するのは簡単で、いくつかのリバースエンジニアリングツールが公開されています。
最近でも更新されていて使い勝手が良さそうなのはこの辺りでしょうか。

- [Unity Assets Bundle Extractor](https://github.com/DerPopo/UABE)
- [AssetStudio](https://github.com/Perfare/AssetStudio)

ツールの使い方については特に説明するほどの事もないため、ここでは省きます。詳細は下記の記事などを参考にしてください。

参考：[【Unity】Unity 製のゲームやアセットバンドルからアセットを抽出できるリバースエンジニアリングツール「Unity Assets Bundle Extractor（UABE）」紹介 \- コガネブログ](http://baba-s.hatenablog.com/entry/2019/01/07/170000)

ちなみに、AssetBundleに含まれるメタ情報を確認する程度であれば**Unity公式の[AssetBundleBrowser](https://docs.unity3d.com/ja/2018.1/Manual/AssetBundles-Browser.html)でも可能**です。
<img src="https://qiita-image-store.s3.amazonaws.com/0/97014/40a58765-ae96-8c8a-1791-c03b383c40b4.png" width=500>

## AssetBundleファイルを入手する方法

### パケットキャプチャツールを用いて保存する

FiddlerやWiresharkなどのパケットキャプチャツールを用いてアプリの通信をキャプチャし、その中からAssetBundleのDLと思わしき通信を見つけて、そのペイロードを保存してしまうという方法です。

- [iPhoneのパケットキャプチャ \- Qiita](https://qiita.com/HIkaruSato/items/1f66c1a189bf9c19f838)
- [Android端末上のHTTP／HTTPS\(SSL\)通信内容を傍受・解析する方法](https://amimilab.com/2016/08/14/post-183/)

この方法は以下の特徴があります。

- メリット：URLが分からなくても入手することが可能
- デメリット：手間がかかる、パケットキャプチャ中にDLしたAssetBundleしか入手できない

### URLを直接叩いてDLする

古典的な手法ですが、**AssetBundleのURLが分かってしまえば直接DLすることが可能**です。
先述のパケットキャプチャと合わせて、AssetBundleのURLの法則性を推測することで、あらゆるリソースを入手できる可能性があります。
この方法は古典的ながら非常に強力で、例えばソシャゲで**自分が所持していないキャラクターのリソースでも入手が可能**、さらには**未公開の新キャラに関連するリソースも入手が可能**ということが有り得ます。

よくありの例としては、例えばキャラクターのリソースが`http://example.com/chara/{id}.bundle`のようなURL形式になっていた場合、IDは連番で容易に推測が可能なため、ゲーム上ではまだ公開されていないリソースがサーバー上にアップロードされていた場合、不正に入手が可能になってしまいます。
ちょっとしたスクリプトを書けば総当たりでDLするようなことも簡単にできてしまいます。

- メリット：楽、URLの法則性が分かってしまえばあらゆるリソースがDL可能
- デメリット：URLの法則性が分からないと不可能

### アプリ本体や端末上に保存されたAssetBundleファイルを抽出する

通常、**一度DLされたAssetBundleは端末上にキャッシュされています**。そのため、端末上にキャッシュされたAssetBundleファイルを抽出するという方法も考えられます。
Unity標準のAssetBundleのキャッシュの仕組みが使われていれば、キャッシュの保存場所を特定することは比較的容易です。但し、フォルダ構造やファイル名はUnityの仕様上やや取り扱いづらい構造になっています。

- 参考
  - [\[Unity 2018\.2\] AssetBundleのキャッシュを完全に理解する \- Qiita](https://qiita.com/k7a/items/23d909ffeea3bab7dfcb)
  - [【Unity】Androidで情報をローカル保存するときの話 \- KAYAC engineers' blog](https://techblog.kayac.com/unity_advent_calendar_2018_14)

また、予めStreamingAssets内にAssetBundleを格納しているようなアプリも存在します。
その場合はなんらかの方法でアプリ本体（apkやipa）を抽出・展開することで入手が可能です。
アプリ本体の抽出・展開自体は案外簡単で、すこし調べれば解説記事が山程出てきます。

- メリット：キャッシュがクリアされない限りいつでも抽出することが可能
- デメリット：一度DLしたAssetBundleしか抽出できない、フォルダ構造が扱いづらい

# AssetBundleの解析対策について

AssetBundleの解析手法について理解したところで、本題に移ります。
AssetBundleの解析を防ぐには、大きく分けて以下の2パターンあります。

- AssetBundleファイル自体の入手を防ぐ
- AssetBundleファイルからのデータ抽出を防ぐ

## URL（ファイル名）にハッシュ値を付加する

まず初めに一番お手軽かつ効果の高い方法として、**サーバー上にアップロードするAssetBundleのファイル名にファイルのハッシュ値を付加する**という方法が挙げられます。
要するに、例えばキャラクターのリソースの場合は`http://example.com/chara/{id}_{hash}.bundle`というような形式にします。

- 例
  - chara/1_f2f3cb2166c36a6fdd0e3bc17ef571cc.bundle
  - chara/2_2dc42e6831cc3166e89d3be1d3cc7895.bundle

要は**URLを推測されにくいものにする**ことで、直接アクセスによるDLを防ごうという発想です。
これによって、ID総当たりによるリソース解析は防ぐことができます。

### AssetBundle名とハッシュ値の対応の扱いについて

この方法ではハッシュ値が分からなければアプリからもAssetBundleをDLすることができないため、**結局は何らかの形でアプリはAssetBundle名とハッシュ値の対応を取得する**ことになります。そのため、当然**そこを解析されてしまえばURLが判明**してしまいます。なので、そこをどう防ぐかという話に続きます。

そもそも、**DLしたAssetBundleをキャッシュする為にはハッシュ値（やそれに代わる識別子）が必要**になるので、**AssetBundleを使う場合は何らかの形でハッシュ値を取得する実装が必要**になっています。
（その辺りの詳しい話は → [\[Unity 2018\.2\] AssetBundleのキャッシュを完全に理解する](https://qiita.com/k7a/items/23d909ffeea3bab7dfcb)）
その為、この話は解析対策以前にAssetBundleを扱うにあたっては避けては通れない話になります。

#### 起動時に一括で取得する

恐らくほとんどのアプリで用いられているのがこの方式で、**アプリ起動時に全てのAssetBundle名とハッシュ値の対応表をサーバーから取得する**という実装になります。
（旧来の[AssetBundleManifest](https://docs.unity3d.com/ja/2017.4/ScriptReference/AssetBundleManifest.html)を使用した仕組みも実質的には同じで、Addressable Asset Systemなんかも仕組み的には同じです）
この方法の場合、その**対応表を不正に取得されてしまえばURLの推測が可能**となってしまいます。
さらなる対策としては、対応表自体を暗号化してしまうことも考えられますが、最終的にはイタチごっこになります。

しかし、もう一つ効果的な策として、「サーバー上にはアップロードしておきたいけど、ユーザーにはまだ見られたくない（解析されたくない）リソースがある」という場合に、**未公開のリソースについてはユーザーに渡す対応表に含めない**という対応でコントロールする事も可能です。

##### 例：「キャラクターID4,5のリソースを追加したいが、ユーザーに公開するのは15時以降にしたい」

1. キャラクターID4,5のリソースをサーバー上にアップロード
2. キャラクターID4,5を含まないハッシュ値表（A）と、含むハッシュ値表（B）を用意
3. 15時になるまではAのハッシュ値表、15時以降にはBのハッシュ値表をユーザーに返すようにする

#### 必要になったら都度取得する

起動時ではなく、アプリが必要になった時に必要になった分だけの情報を取得するようにすれば、DLしたAssetBundle以外のURLは推測が不可能になります。

実装としては、サーバー側に**AssetBundle名を入力として対応するハッシュ値を返すAPIを実装**するなどが考えられます。しかし、そのAPIに対して不正なリクエストを飛ばすことでハッシュ値を入手できてしまう可能性はあります。
恐らく、以下の記事で解説されているシステム（Octo）では似たような仕組みでAssetBundle配信をマイクロサービス化していると思われます。

参考：[複雑化するAssetBundleの配信からロードまでを基盤化した話【CEDEC 2017】 \| CyberAgent](https://creator.game.cyberagent.co.jp/?p=4791)

## コンテンツのDLにアクセス制限を付ける

より高度な対策として、コンテンツのDLにアクセス制限を付けてしまい、アプリ外からの不正なリクエストによるDLを防ぐという方法が考えられます。
簡単な所ではリファラによるアクセス制限などが考えられますが、リファラは簡単に偽造が可能なのであまり効果は期待できません。

AssetBundle配信はCDNを使うことが多いですが、例えばAWSの場合は署名付きCookieを使うという方法があります。

- 参考
  - [署名付き URL と署名付き Cookie を使用してプライベートコンテンツを供給する \- Amazon CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html)
  - [CF : アクセス制限をつけてCDN配信する方法 \- aws memo](http://understeer.hatenablog.com/entry/2012/03/23/173414)

各ユーザー・各AssetBundleごとに署名付きURLを発行するのは現実的でないので、署名付きCookieを使う方法になるかと思います。
しかし、この場合アプリサーバー側に署名付きCookieを発行する実装が必要な他、UnityWebRequestはCookieはいい感じにしてはくれないので、自前で保持＆DL時にヘッダに付加する仕組みを実装する必要がありそうです。
また、そもそも不正にCookieを取得されてしまえば終わりです。アプリサーバーとの通信周りの認証・暗号化などがちゃんとしていればそう簡単には突破されなさそうですが、やはり最終的にはイタチごっこになると言えるでしょう。実装工数に対して見合った効果が得られるかは何とも言い難い所です。

## リソース、AssetBundle自体を暗号化してしまう

AssetBundleからのデータの抽出を防ぐためには、リソースやAssetBundle自体を暗号化してしまうという方法があります。
この手法は、大きく分けて以下の2パターンが考えられます。

### 暗号化したリソースをAssetBundle化する

リソースをAssetBundleにする前に暗号化し、AssetBundleにはTextAsset（バイナリ列）として格納するという方法です。
この場合、DLするファイル自体はAssetBundleになっているので、Unity標準のAssetBundleのキャッシュシステムが使えます。
その代わり、復号したバイナリ列から元のオブジェクトにデシリアライズする実装が必要になります。これは**リソースを適切なオブジェクトとして直接取得できるというAssetBundleの利点（というか存在意義）を潰してしまう**ことになります。

- メリット：Unity標準のAssetBundleのキャッシュシステムが使える
- デメリット：復号したバイナリ列から元のオブジェクトにデシリアライズする必要がある、復号によるオーバーヘッドが生じる

### AssetBundleを暗号化する

リソースをAssetBundle化した後に、そのAssetBundleを暗号化するという方法です。
暗号化することによって、Unityからすればただのバイナリ列となってしまうため、**Unity標準のAssetBundleのキャッシュシステムは使うことができなくなります**。
しかし、復号して[AssetBundle\.LoadFromMemory](https://docs.unity3d.com/ja/current/ScriptReference/AssetBundle.LoadFromMemory.html)でAssetBundleとしてロードすることができ、**AssetBundleから元のオブジェクトを直接取得することができます**。
その代わり復号したAssetBundleは常にメモリ上に置く形になり、無圧縮・LZ4圧縮のAssetBundleをディスク（キャッシュ）からロードする場合に比べて、**メモリ効率の面では不利**になります。
参考：[AssetBundle\.LoadFromFileとLoadFromMemoryの挙動の違いについて \- Qiita](https://qiita.com/k7a/items/974c2cc86ef012f6d68a)

- メリット：オブジェクトを直接取得できるというAssetBundleの利点を活かせる
- デメリット：Unity標準のAssetBundleのキャッシュシステムが使えない、復号によるオーバーヘッドが生じる、メモリ効率の面で不利

ちなみに、暗号化したAssetBundleをさらにTextAssetとしてAssetBundle化してしまうことで、Unity標準のAssetBundleのキャッシュシステムに乗せる…ということも出来ますが、**ビルドの手間が増え、展開・復号時のオーバーヘッドも増える**ことと、そもそも**Unity標準のAssetBundleのキャッシュシステム自体が現状微妙すぎる**ので、そこまでするくらいなら自前でキャッシュシステムを作ったほうが良いと思います。

# おわりに

個人的な結論としては

- ファイル名の末尾にハッシュ値を付けてアップロードする
- ハッシュ値表の公開タイミングをサーバー側でコントロールする
- （余裕があれば）AssetBundle自体を暗号化し、キャッシュシステムは自作する

という対応が、費用（工数）対効果的にもベストだと思います。
（というか、AssetBundleのキャッシュシステムはいい加減改修されてほしい…）

この記事ではAssetBundleの解析に絞って解説しましたが、これらの解析対策は、**アプリ全体での解析対策と一緒にやることでより効果が発揮**されます。
**解析対策をするにはまず解析手法について知ることが重要**であり、それには**実際にアプリを解析・ハックしてみる**のが一番です。（とは言え非常にグレー、というか場合によっては普通にアウトな行為ですし、**悪用は厳禁**です）
実際のアプリを解析するのはちょっとアレですが、SECCONなどではコンテストのために用意位されたUnity製のアプリを解析したり解析対策を行うというコンテストが行われていたりして、参加レポートがかなり参考になるのでオススメです。

参考：[SECCON 2018 x CEDEC CHALLENGE に参加しました \- st98 の日記帳](https://st98.github.io/diary/posts/2018-09-01-seccon-2018-x-cedec-challenge.html)

# 参考資料

- [ゲームの画像・音声などを開く\(Unity Engine編\) : アンセケターメンのメモ帳](http://sfttc.blog.jp/archives/1069286490.html)
- [AssetBundleの暗号化について図でまとめてみた \- Qiita](https://qiita.com/snaka/items/a4c16719888bbd23067f)