Unityにおける、Androidアプリの改変防止コード

2023/02/12追記

この記事にある手法は現在ではroot化に対してあまり意味がありません。公式のセキュリティのおすすめの方法や、

アプリのセキュリティに関するおすすめの方法 | Android デベロッパー | Android Developers

ライセンスチェックなどを行ってください。

ライセンス機能の概要 | Android デベロッパー | Android Developers


アプリ開発において、収益化やユーザーの体験を壊してしまうアプリの改変。
今回はこれについて、私が行っている防止のための処理を上げてみようと思います。

ただ最初に書いておくと、お金があるなら市販の改変、チート防止ライブラリを導入したほうがいいです。ノウハウの積み重ねがあるので、私が一生懸命考えたものより良いものになっているはずです。

今回はコードはC#になります。また、私が試した環境は、Unity2017.3fになります。

1.デバッガ防止
まず、しなければいけないことは、デバッガによるメモリの読み取りなどを防止することです。これができていないと、後述の署名の検証などはすぐに突破されてしまいます。
C#の関数には、System.Diagnostics.Debugger.IsAttachedという、デバッガを検知するプロパティがあります。
ただし、これはどうもMonoのデバッガにしか働かないようで、Android Studioからデバッグをしても、falseのままでした。
なので、Android側のandroid.os.Debug.isDebuggerConnected()を合わせて使って検出する必要があります。

2.署名の検証

2.1.署名とは?
ここでは、Google Play App Signingを使っている状態で、Google Playよってapkに付与されたデータを指します。
データの内容はリリース版を実行してデバッガで確認しないとわかりませんが、データのSHA-256はGoogle Developer Consoleのリリース管理>アプリの署名から確認することができます。
このSHA-256を使って検証を行います。SHA-256の計算方法はここでは省きます。
署名のデータ自体はGoogleしか知らないため、SHA-256の特性と合わせて外部の人間が事実上推測できないものになっています。

2.2.署名はどうやって取得する?
「へびのゆうしゃ」では、こういうコードでやりました。Javaソースファイルになります。

public class SnakeEntryPoint extends UnityPlayerActivity {

   public static PackageManager mPackageManager;
   static String mPackageName;
   protected void onCreate(Bundle savedInstanceState) {
       if(!android.os.Debug.isDebuggerConnected()){
           mPackageManager = getPackageManager();
           mPackageName = getPackageName();
       }
       // UnityPlayerActivity.onCreate() を呼び出す
       super.onCreate(savedInstanceState);
   }
   public void onBackPressed()
   {
       super.onBackPressed();
   }
   public static byte[] GetSignature(int i){
       try{
           PackageInfo packageInfo = mPackageManager.getPackageInfo(mPackageName,
                   PackageManager.GET_SIGNATURES);
           if(i < packageInfo.signatures.length){
               return packageInfo.signatures[i].toByteArray();
           }else{
               return null;
           }
       }catch (NameNotFoundException e) {
           return null;
       }
   }
}

UnityPlayerActivityを拡張した新しいエントリポイントを作り、その中でPackageManagerとパッケージ名を取得、そしてgetPackageInfo()で署名データを取得しています。
UnityPlayerActivityについては、以下の記事も参考にしてください。
https://docs.unity3d.com/ja/current/Manual/AndroidUnityPlayerActivity.html
コードにもありますが、取得の前には、必ずデバッガ防止のコードを忘れずに!

2.3.SHA-256の計算、検証
得られた署名データを元にSHA-256を計算し、Google Play Consoleの値と比較します。この計算を行うコードは、IL2CPPを有効にしたC#のコードで実行し、実行部分の関数名はわかりにくく、かつ他の関数名と比べて浮いていないものにしています。そうしないと、検証しているコードの位置を把握され、コードを無効化されて突破されてしまいます。(IL2CPPを使っていても、リフレクションの実現のために関数名のテーブルが残っている可能性があります)
また、先のコードのGetSignatureは、AndroidJavaClass.CallStaticでクラス名やメソッド名を指定して呼んでいますが、そのクラス名、メソッド名の文字列はC#コード中にそのまま置かず、シャッフルしたうえである値とxorしたものを置いて、実行時に復元しています。
Google Play Consoleから拾ったSHA-256はテキストファイル等には入れず、C#ソースコード中に配列形式で入れました。
SHA-256の計算にはSystem.Security.Cryptography.SHA256を使いました。が、これは前述の理由で良くないかもしれません。これは未検証です。

3.デバッガを検出した際、署名の検証に失敗した際の対処
デバッガを検出した際は、すぐにアプリケーションを終了します。
逆に、署名の検証に失敗した場合は、すぐにアプリケーションを終了していません。終了するまでに実行されたコードの部分が少ないと、署名の検出を行うためのコードを絞り込むヒントになります。

4.考えられる攻撃への効果
このアプリを攻撃する場合、デバッガの検出コードを無効化し、そのあとで署名の検証コードを探して無効化する必要があります。
攻撃者は署名の検証に成功した状態をデバッガで知ることができず、そのため署名の検証のコードの隠蔽が十分であれば、探すことが大変になると思われます。

まとめ
・デバッガを防止して署名検証に成功した状態を知られない
・署名の検証を行う
・署名の検証に失敗してもすぐにアプリを終了しない

不備等あれば、コメント欄に書き込んでいただくか、新たに記事を起こしていただけると助かります。

サポートでいただいたお金は、音素材やアセットの購入、グラフィックや曲の発注に使わせていただこうと思います。