Android
Unity
ゲーム制作
チート対策

改造apk(MOD)の作り方 & その対策


はじめに

改造apk(MOD)というものを聞いたことがありますか?

これはapkファイルを解析して内部を自分好みに書き換えるチート手段となります。

このチート手段は、ある程度のプログラミング知識を要しますが、チート界隈ではメモリ改ざんと同じくらい事例の多いものとなっています。

その訳は、apkの内部を書き換えるという特性上、そのapkを配布できるというところにあります。

実際、「改造apk」といったワードで検索すると、色々なゲームの改造apkが配布されています。

今回は、このチート手段を対策するという目的から、実際に改造apkを作る側になってみたいと思います。

そのため、作る過程でのツールの詳しい操作方法などは省いています。ご了承ください。


ゲーム開発の手段

開発者がゲームを制作する際、大抵の場合はゲームエンジンを利用します。現在よく使われるエンジンは、

となっています。

改造apkを作るためには、このゲームエンジンの特性を理解し解析する必要があります。

今回は、このゲームエンジンの中でも使用率が最も高いと思われるUnityを対象にしたいと思います。


使用ツール

上記のリンクからインストールが可能。


改造の対象

改造apkを作るにあたって、改造する対象となるapkが必要になります。

他人の作ったアプリケーションのapkファイルを改造することは法律に引っかかる恐れがありますので、今回は自作の以下のゲームを対象にしたいと思います。

61CA4AA8-2286-4CC2-8E84-E01871A99A1E.png

その名も「じゃんけんバトル」。

じゃんけんをして勝てば相手にダメージを与え、相手のHPを0にすれば勝利となります。

敵はコンピューターとなっており、ランダムにぐーちょきぱーを選択する仕様になっています。

後述しているMono・IL2CPPで個別にビルドし、それらを使って改造していきましょう。


作り方

Unityとは、PCやスマートフォンなどマルチプラットフォームに対応したゲームエンジンです。

これらのプラットフォーム用の実行ファイルを出力するために、MonoまたはIL2CPPでビルドを行いますが、このビルドによって改造apkの作り方手順が異なります。

一つ一つ順に見ていきましょう。


Monoの場合

Monoでビルドする際、開発者が書いたC#など各言語はILと呼ばれる中間言語に変換され、実行ファイルに含まれます。

そして、プラットフォームでアプリケーションを実行する際、中間言語が機械語に変換され、それが実行されることになります。

上記の実行ファイルというものがapkにあたり、改造apkを作りたいのであれば、その中に含まれている中間言語を解析・改ざんすればいいことがわかります。

そのために、まずはapkファイルを展開してきます。


1. apkファイルの展開

apkファイルを展開するのにApktoolを使用します。

$ apktool d XXXX.apk

こちらのコマンドを実行し、改造対象のapkを展開します。

これによって、apkファイルと同じディレクトリに展開されたものが追加されているかと思います。


2. デコンパイル

Monoでビルドした場合、展開したapkフォルダ内の/assets/bin/Data/Managed/Assembly-CSharp.dllに開発者の記述したコードが中間言語に変換された状態で含まれています。

つまり、これを改ざんすればアプリケーションの挙動を改変させることができます。

さて、改ざんするにあたって、まずはどんなコードが記述されているか知る必要があります。

アプローチとして、ildasmなどを使って逆アセンブルするという方法がありますが、それよりもやはりデコンパイルしてC#として読めた方が嬉しいですよね。

そのために、今回はDnSpyというデコンパイラを使っていきます。

DnSpyを起動し、File -> Open でAssembly-CSharp.dllを開きます。

8BF7C89D-77C7-45CE-AE5C-3EE6C742BF75.png

すると、このような画面になり、ここからどのようなクラスやメソッドが記述されているか知ることができます。


3. 改ざん

これでどんなコードが書かれているかわかりました。

では実際に改ざんしていきましょう。

今回はプレイヤーのHPを管理していると思われるPlayerController.TakeDamageを改変していきます。

改ざんするためには、改ざんしたいメソッドを選択して Edit -> Edit Method (C#) ... を選択します。

6B22D708-E1E1-4570-AFF7-66900C56B015.png

今回はTakeDamage内の処理を消してみました。

書き換えが完了したら、右下のCompileを押して、 File -> Save All... を押せば完了です。


4. apkファイルの再構築

これで改ざんが完了したので、展開したapkファイルを実行できるように再構築します。

apktool b XXXX

こちらのコマンドを実行すれば、apkファイルが作られます。

なお、検証の際には再署名も必要になります。

これをAndroidで実行してみると、こちら側がじゃんけんに負けてもTakeDamageによるダメージ処理が走らないため、ノーダメになります。

改造apkの完成です。


IL2CPPの場合

IL2CPPとは、その名前の通りILからC++に変換してくれるツールとなります。

C#をILに変換するまではMonoと同じですが、その後にIL2CPPがC++に変換し、実行ファイルとして出力するといった形になります。

開発者が書いたコードは、/assets/bin/Data/Managed/Assembly-CSharp.dllではなく/lib/各プロセッサ/libil2cpp.soに含まれることになります。

ただ、Monoの場合とは違って、クラス名やメソッド名などのmeta情報が/assets/bin/Data/Managed/Metadata/global-metadata.datに含まれているため、解析・改ざんするにあたって、こちらも一緒に利用することになります。

なお、IL2CPPの場合はバイナリを直接書き換えて改ざんを行います。

DnSpyのようにC#ベースで改ざんしない理由は、機械語に変換されてしまっており、逆アセンブルまでが限界のためです。

補足

逆アセンブルする手段として、よくIDAが選ばれますが、全ての機能を使うためには有料版が必要なため、今回は別のアプローチを取っています(無料版ではx86のみ可能)。


1. apkファイルの展開

ここはMonoの場合と同様です。


2. meta情報の展開

バイナリを書き換えるにあたって、改ざん目的の箇所のアドレスを知る必要があります。

そのために、Il2CppDumperを使用します。

Il2CppDumperを起動しましょう。

すると、はじめにlibil2cpp.so、その後にglobal-metadata.datの場所を聞かれるので、展開したapkの中から該当するファイルを選択します。

また、ダンプする際に使用するモードを尋ねられますが、公式のREADME.mdでは「4. Auto(Plus)」が推奨されており、今回は4を選択しています。

成功すれば、Il2CppDumper.exeと同じディレクトリにdump.csなどのファイルが生成されているはずです。


3. アドレスを特定する

生成されたdump.csを開いてみると、クラス名やメソッド名が羅列されて表示されます。

ここから、改ざんしたいメソッドを探し出します。

今回はMonoの場合と同様に、プレイヤーの受けるダメージを0にするため、TakeDamageで検索をかけます。

0B75EE04-3A05-4789-AEA2-71A0C9EE972E.png

見つかりました。

このメソッドのアドレスが、横にコメントとして記述されています。

これで、バイナリのどこを書き換えれば改ざんできるか特定できました。


4. バイナリを書き換える

libil2cpp.soをバイナリエディタで開きます。

今回使用するバイナリエディタはHxDです。File -> Open で開くことができます。

すると、libil2cpp.soの中が表示され、ここに開発者の書いた処理も含まれています。

なので、TakeDamageの処理もここに含まれており、その処理の位置を特定するためにさきほど見つけたアドレスを使用します。

Search -> Goto... で、さきほどのアドレスを入力し移動します。

D5DB93CD-FAB9-42AA-A627-1610397A94B6.png

ここから先がTakeDamageの処理となります。

つまり、ここを書き換えればTakeDamageの処理を改ざんすることができます。

書き換え内容は各プロセッサによって異なりますが、例えばarmだと、Online ARM To Hex Converterなどのサイトで、自身の書き換えたいアセンブリ言語をコンバートして利用するといいでしょう。

今回は何の処理もさせないため、先頭でreturn;するようにします。

Online ARM To Hex ConverterでBX LRを入力し、コンバートしましょう。

すると、

1EFF2FE1

にコンバートされるため、こちらをTakeDamageの処理に上書きします。

8A5B2E5B-0A90-4F7C-9469-61225BAB0575.png

これで上書きが完了しました。最後に、File -> Save all で上書きをセーブします。


5. apkファイルを再構築

ここもMonoの場合と同様です。

このapkをAndroidにインストールすると、Monoの際と同じようにプレイヤー側がノーダメになることが確認できると思います。


対策

改造apkの作り方を紹介しました。

とてもアプリケーションに影響度が大きいチート手段のため、しっかりと対策が必要です。

Unityのチート対策だと、Anti-Cheat Toolkitが有名どころですが、このツールでできるのは暗号化によるメモリ改ざん対策までで、バイナリ改ざんは対応していません。

そのため、バイナリ改ざんも考慮に入れた対策を考えていきましょう。


Monoの場合

Monoの場合は、そもそもデコンパイルするとC#ベースでコードが見えてしまうことが致命的です。

これはIL2CPPでビルドすれば解決するため、結局64bit対応が必須なことも考慮してIL2CPPを使用しましょう。


IL2CPPの場合

IL2CPPを使用した場合でも、さきほど作り方を提示した通り、改ざんすることができます。

その対策として、


  1. 署名の検証をする

  2. 難読化

  3. サーバーにデータを持つことで整合性を保つ

といった方法が考えられます。


1. 署名の検証をする

apkファイルを再構築する際には、前述通り、再署名する必要があります。

なので、署名を検証することで、それが再署名されたものか検出するようにします。

public static byte[] GetSignature()

{
AndroidJavaClass Player = new AndroidJavaClass("c#m.unity3d.player.UnityPlayer");
AndroidJavaObject Activity = Player.GetStatic<AndroidJavaObject>("currentActivity");
AndroidJavaObject PackageManager = Activity.Call<AndroidJavaObject>("getPackageManager");
int GET_SIGNATURES = PackageManager.GetStatic<int>("GET_SIGNATURES");
AndroidJavaObject PackageInfo = PackageManager.Call<AndroidJavaObject>("getPackageInfo", c_PackageName, GET_SIGNATURES);
AndroidJavaObject[] Signatures = PackageInfo.Get<AndroidJavaObject[]>("signatures");
return (Signatures != null && Signatures.Length > 0) ? Signatures[0].Call<byte[]>("toByteArray") : null;
}

引用:https://gist.github.com/zhenlinyang/3064ece280ce3177e14626d0df191100

こちらは例えですが、こういったコードを使用して、その署名が正しいものかチェックするといった手順になります。

補足

root化した端末(とエミュレータ)だとこの対策は効果がありません。

Lucky Patcherなどのツールを使えば、OS自体に「アプリのインストール時の署名検証を無効化する」パッチを適応することができるため、本来ならapk再構築時に必要な再署名をしていないapkでもインストールできるようになるためです。

つまり、いくらアプリ側で署名の検証を行おうが、再署名されていないものに関しては検知しようがないため、改造apkだと気付けないという問題が発生してしまいます。

配布されている改造apkの中には要root化のものがありますが、それが今回の場合に該当します。

実際、改造apkのファイル名を見てみると、unsigned-XXXX.apk(再署名されていないXXXXのapk)となっていたりします。

なので、本当に対策しようとすると、改造apkを使用していないユーザーも対象に含まれることを考慮した上で、


  • エミュレータ検知

  • root化した端末検知

  • USBデバッグ検知

なども検討するといいかもしれません(実際、某大手ソシャゲや銀行系のアプリでも取られている手法です)。


2. 難読化

作る際、ダメージを受ける処理をやっていそうなメソッド(TakeDamageなど)を見つける過程がありました。

これが見つかったことにより、ノーダメの改造apkが作られてしまいました。

また、先ほど対策として署名の検証を紹介しましたが、検証のコード自体が無効化されてしまえば、改造apkが作られてしまいます。

そのために、そもそもこれらが見つからないように難読化すれば良いことがわかります。

とはいえ、開発段階で難読化するのは可読性が下がってしまい非効率なので、Obfuscatorなどのツールを使い難読化をする方法が考えられます。


3. サーバーにデータを持つことで整合性を保つ

難読化を対策にあげましたが、結局のところ読みにくくしているだけなので、頑張れば改ざんすることはできてしまいます。

なので、絶対にチートされたくないということであれば、データ管理はサーバーで行い、怪しい行動を取るユーザー(ノーダメなど)がいればBANするというのが、一番安全かと思われます。


まとめ

改造apkの作り方とその対策を説明しました。

開発者側からすれば恐ろしいチートですが、逆に考えてみましょう。

チートされるアプリは人気アプリの証拠です!

どんなに対策しても、全くダウンロードされず、それが無意味と化してしまうことも少なくないはずです。

対策を立てることは重要ですが、それによって何かしらの制約が発生したり、工数が余計に増えてしまうこともあります。

アプリの規模感を考慮した上で、それにあった対策を行なっていきましょう。

Twitter: @YuukiDeveloper

Github: @YuukiDeveloper