はじめに
ゲームアプリの開発において、リソースの解析・抽出を防ぐのはそこそこ重要な課題です。特にいわゆるソシャゲにおいては、カード画像や音声データなどあらゆるリソースは重要なコンテンツの一部であり、これを不正に入手されることは避けたいです。
そして、Unityでは外部リソースはAssetBundleを利用して配信することがほとんどです。この記事では、AssetBundleによるコンテンツ配信における解析対策についてまとめてみます。
AssetBundle解析の手法について
解析対策を行うにあたって、まずは解析の手法について知る必要があります。
AssetBundle解析の流れは、基本的には以下の流れになります。
- なんらかの方法でアプリのAssetBundleファイルを入手する
- 入手したAssetBundleファイルからデータを抽出する
AssetBundle内のデータを解析・抽出する方法
入手したAssetBundleからデータを解析・抽出するのは簡単で、いくつかのリバースエンジニアリングツールが公開されています。
最近でも更新されていて使い勝手が良さそうなのはこの辺りでしょうか。
ツールの使い方については特に説明するほどの事もないため、ここでは省きます。詳細は下記の記事などを参考にしてください。
ちなみに、AssetBundleに含まれるメタ情報を確認する程度であればUnity公式のAssetBundleBrowserでも可能です。
AssetBundleファイルを入手する方法
パケットキャプチャツールを用いて保存する
FiddlerやWiresharkなどのパケットキャプチャツールを用いてアプリの通信をキャプチャし、その中からAssetBundleのDLと思わしき通信を見つけて、そのペイロードを保存してしまうという方法です。
この方法は以下の特徴があります。
- メリット: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の仕様上やや取り扱いづらい構造になっています。
- 参考
また、予め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のキャッシュを完全に理解する)
その為、この話は解析対策以前にAssetBundleを扱うにあたっては避けては通れない話になります。
起動時に一括で取得する
恐らくほとんどのアプリで用いられているのがこの方式で、アプリ起動時に全てのAssetBundle名とハッシュ値の対応表をサーバーから取得するという実装になります。
(旧来のAssetBundleManifestを使用した仕組みも実質的には同じで、Addressable Asset Systemなんかも仕組み的には同じです)
この方法の場合、その対応表を不正に取得されてしまえばURLの推測が可能となってしまいます。
さらなる対策としては、対応表自体を暗号化してしまうことも考えられますが、最終的にはイタチごっこになります。
しかし、もう一つ効果的な策として、「サーバー上にはアップロードしておきたいけど、ユーザーにはまだ見られたくない(解析されたくない)リソースがある」という場合に、未公開のリソースについてはユーザーに渡す対応表に含めないという対応でコントロールする事も可能です。
例:「キャラクターID4,5のリソースを追加したいが、ユーザーに公開するのは15時以降にしたい」
- キャラクターID4,5のリソースをサーバー上にアップロード
- キャラクターID4,5を含まないハッシュ値表(A)と、含むハッシュ値表(B)を用意
- 15時になるまではAのハッシュ値表、15時以降にはBのハッシュ値表をユーザーに返すようにする
必要になったら都度取得する
起動時ではなく、アプリが必要になった時に必要になった分だけの情報を取得するようにすれば、DLしたAssetBundle以外のURLは推測が不可能になります。
実装としては、サーバー側にAssetBundle名を入力として対応するハッシュ値を返すAPIを実装するなどが考えられます。しかし、そのAPIに対して不正なリクエストを飛ばすことでハッシュ値を入手できてしまう可能性はあります。
恐らく、以下の記事で解説されているシステム(Octo)では似たような仕組みでAssetBundle配信をマイクロサービス化していると思われます。
参考:複雑化するAssetBundleの配信からロードまでを基盤化した話【CEDEC 2017】 | CyberAgent
コンテンツのDLにアクセス制限を付ける
より高度な対策として、コンテンツのDLにアクセス制限を付けてしまい、アプリ外からの不正なリクエストによるDLを防ぐという方法が考えられます。
簡単な所ではリファラによるアクセス制限などが考えられますが、リファラは簡単に偽造が可能なのであまり効果は期待できません。
AssetBundle配信はCDNを使うことが多いですが、例えばAWSの場合は署名付きCookieを使うという方法があります。
- 参考
各ユーザー・各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でAssetBundleとしてロードすることができ、AssetBundleから元のオブジェクトを直接取得することができます。
その代わり復号したAssetBundleは常にメモリ上に置く形になり、無圧縮・LZ4圧縮のAssetBundleをディスク(キャッシュ)からロードする場合に比べて、メモリ効率の面では不利になります。
参考:AssetBundle.LoadFromFileとLoadFromMemoryの挙動の違いについて - Qiita
- メリット:オブジェクトを直接取得できるというAssetBundleの利点を活かせる
- デメリット:Unity標準のAssetBundleのキャッシュシステムが使えない、復号によるオーバーヘッドが生じる、メモリ効率の面で不利
ちなみに、暗号化したAssetBundleをさらにTextAssetとしてAssetBundle化してしまうことで、Unity標準のAssetBundleのキャッシュシステムに乗せる…ということも出来ますが、ビルドの手間が増え、展開・復号時のオーバーヘッドも増えることと、そもそもUnity標準のAssetBundleのキャッシュシステム自体が現状微妙すぎるので、そこまでするくらいなら自前でキャッシュシステムを作ったほうが良いと思います。
おわりに
個人的な結論としては
- ファイル名の末尾にハッシュ値を付けてアップロードする
- ハッシュ値表の公開タイミングをサーバー側でコントロールする
- (余裕があれば)AssetBundle自体を暗号化し、キャッシュシステムは自作する
という対応が、費用(工数)対効果的にもベストだと思います。
(というか、AssetBundleのキャッシュシステムはいい加減改修されてほしい…)
この記事ではAssetBundleの解析に絞って解説しましたが、これらの解析対策は、アプリ全体での解析対策と一緒にやることでより効果が発揮されます。
解析対策をするにはまず解析手法について知ることが重要であり、それには実際にアプリを解析・ハックしてみるのが一番です。(とは言え非常にグレー、というか場合によっては普通にアウトな行為ですし、悪用は厳禁です)
実際のアプリを解析するのはちょっとアレですが、SECCONなどではコンテストのために用意位されたUnity製のアプリを解析したり解析対策を行うというコンテストが行われていたりして、参加レポートがかなり参考になるのでオススメです。
参考:SECCON 2018 x CEDEC CHALLENGE に参加しました - st98 の日記帳