ダミー(Unity、C#、Mono)のハッキングからのゲームとモバイルアプリケーションの保護

みなさんこんにちは! ゲーム開発者にとって非常に重要なトピックに関するクールな記事を書きました。 それで、一Unityのパンを邪悪な学童に壊されないように、Unityで見た貴重なゲームやアプリケーションを保護することについて話しましょう。 なぜ学童? 先験的に100%信頼できる保護はできないためです。 とにかくそれを破りたい人。 唯一の質問は、彼がどれだけの時間と労力を費やすかです。 そして、セキュリティの人々が冗談を言うのを好むので、誰も熱直腸暗号解読をキャンセルしませんでした。



そのため、この記事では、できる限り3つの側面についてお話しします(そしてもちろん、実装を提供します)。



画像



1.準備する



最初に、ゲームデータ(タイプ、クラス)を文字列に変換する方法を学ぶ必要があります。 JSONまたはXMLシリアル化を調査する価値があります。 XMLから始めることはお勧めしません。 iOSに問題があります。 JSONを学ぶ方が良いでしょう 。こちらはwiki.unity3d.com/index.php/SimpleJSONリンクです 。 残念ながら、これは別の記事のトピックであり、これについては詳しく語りません。 遅延ソートを行う場合-セパレータを使用して、昔ながらの方法で文字列を手動で彫刻できます。 例:



var profile = "name=player;money=999;level=80";
      
      





また、文字列をバイト配列に、またはその逆に変換できる必要があります。 ここではすべてが簡単です:



 var bytes = Encoding.Default.GetBytes(profile); profile = Encoding.Default.GetString(bytes);
      
      





次に、base64変換を適用して、文字列をベールできます。 base64は暗号化ではなく、暗号化キーなどを持たないことを強調します。 base64は、文字列をASCII文字のみで構成される新しい文字列に変換します。 base64.ruでこれがどのように行われるかを明確に見ることができます。 実装コードを提供します。



 using System; using System.Text; namespace Assets.Scripts.Common { public static class Base64 { public static string Encode(string plainText) { var plainTextBytes = Encoding.UTF8.GetBytes(plainText); return Convert.ToBase64String(plainTextBytes); } public static string Decode(string base64EncodedData) { var base64EncodedBytes = Convert.FromBase64String(base64EncodedData); return Encoding.UTF8.GetString(base64EncodedBytes); } } }
      
      





また、base64は高速で、加算操作と同等の速度であることに注意してください。 このような変換は、更新ループでも実行できます。



2.ゲームデータの保護(保存)



これで、ゲームデータを文字列に変換できるようになりました。 次に、どこに保存するかを考える必要があります。 最初に思い浮かぶのは、保存をApplication.persistentDataPathのファイルに保存することです。 この方法の欠点は2つです。



2番目の最も正しい方法は、PlayerPrefsに保存することです。 以下の例:



 const string key = "profile"; var profile = "name=player;money=999;level=80"; PlayerPrefs.SetString(key, profile); PlayerPrefs.Save(); if (PlayerPrefs.HasKey(key)) { profile = PlayerPrefs.GetString(key); }
      
      





ああ、ベイビー、スーパー! 次に、保存を暗号化する必要があります。 ここでは、base64変換をすばやく実行できます。これにより、ハッキング用のほとんどのプログラムを介した保存が編集から保護されます。 しかし、ハードコアの場合は、通常の暗号化を強化するときです。 すぐに、AESを取得して暗号化します。 AES.csファイルをコピーし、どのように機能するのか不思議ではありません。



 using System; using System.IO; using System.Security.Cryptography; using System.Text; namespace Assets.Scripts.Common { /// <summary> /// AES (Advanced Encryption Standard) implementation with 128-bit key (default) /// - 128-bit AES is approved by NIST, but not the 256-bit AES /// - 256-bit AES is slower than the 128-bit AES (by about 40%) /// - Use it for secure data protection /// - Do NOT use it for data protection in RAM (in most common scenarios) /// </summary> public static class AES { public static int KeyLength = 128; private const string SaltKey = "ShMG8hLyZ7k~Ge5@"; private const string VIKey = "~6YUi0Sv5@|{aOZO"; // TODO: Generate random VI each encryption and store it with encrypted value public static string Encrypt(byte[] value, string password) { var keyBytes = new Rfc2898DeriveBytes(password, Encoding.UTF8.GetBytes(SaltKey)).GetBytes(KeyLength / 8); var symmetricKey = new RijndaelManaged { Mode = CipherMode.CBC, Padding = PaddingMode.Zeros }; var encryptor = symmetricKey.CreateEncryptor(keyBytes, Encoding.UTF8.GetBytes(VIKey)); using (var memoryStream = new MemoryStream()) { using (var cryptoStream = new CryptoStream(memoryStream, encryptor, CryptoStreamMode.Write)) { cryptoStream.Write(value, 0, value.Length); cryptoStream.FlushFinalBlock(); cryptoStream.Close(); memoryStream.Close(); return Convert.ToBase64String(memoryStream.ToArray()); } } } public static string Encrypt(string value, string password) { return Encrypt(Encoding.UTF8.GetBytes(value), password); } public static string Decrypt(string value, string password) { var cipherTextBytes = Convert.FromBase64String(value); var keyBytes = new Rfc2898DeriveBytes(password, Encoding.UTF8.GetBytes(SaltKey)).GetBytes(KeyLength / 8); var symmetricKey = new RijndaelManaged { Mode = CipherMode.CBC, Padding = PaddingMode.None }; var decryptor = symmetricKey.CreateDecryptor(keyBytes, Encoding.UTF8.GetBytes(VIKey)); using (var memoryStream = new MemoryStream(cipherTextBytes)) { using (var cryptoStream = new CryptoStream(memoryStream, decryptor, CryptoStreamMode.Read)) { var plainTextBytes = new byte[cipherTextBytes.Length]; var decryptedByteCount = cryptoStream.Read(plainTextBytes, 0, plainTextBytes.Length); memoryStream.Close(); cryptoStream.Close(); return Encoding.UTF8.GetString(plainTextBytes, 0, decryptedByteCount).TrimEnd("\0".ToCharArray()); } } } } }
      
      





3.アプリケーションのメモリ保護



ArtMoneyのようなPCプログラムを誰もが覚えていますか? 彼女はRAMの値を探すことができ、数回のスクリーニングを繰り返してゲームの億万長者になれました。 現在、AndroidとiOSには、同様のプログラムが多数あります。たとえば、最も人気のあるGameKillerです。



このようなプログラムから身を守るのは非常に簡単です-アプリケーションメモリの値を暗号化する必要があります。 書き込み中は毎回暗号化し、読み取り中は毎回復号化します。 また、操作は非常に頻繁に行われるため、重いAESを使用しても意味がなく、超高速のアルゴリズムが必要です。 base64を少し変更し、暗号化を実装することをお勧めします-効率的、高速、ブラックジャックとXORを使用:



 using System; using System.Text; namespace Assets.Scripts.Common { /// <summary> /// Simple and fast Base64 XOR encoding with dynamic key (generated on each app run). Use for data protection in RAM. Do NOT use for data storing outside RAM. Do NOT use for secure data encryption. /// </summary> public class B64X { public static byte[] Key = Guid.NewGuid().ToByteArray(); public static string Encode(string value) { return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(value), Key)); } public static string Decode(string value) { return Encoding.UTF8.GetString(Encode(Convert.FromBase64String(value), Key)); } public static string Encrypt(string value, string key) { return Convert.ToBase64String(Encode(Encoding.UTF8.GetBytes(value), Encoding.UTF8.GetBytes(key))); } public static string Decrypt(string value, string key) { return Encoding.UTF8.GetString(Encode(Convert.FromBase64String(value), Encoding.UTF8.GetBytes(key))); } private static byte[] Encode(byte[] bytes, byte[] key) { var j = 0; for (var i = 0; i < bytes.Length; i++) { bytes[i] ^= key[j]; if (++j == key.Length) { j = 0; } } return bytes; } } }
      
      





これで、保存からAES番目のプロファイルを読み取ってデコードするとすぐに、このB64Xを使用してすべての値をすぐに暗号化します(自分で名前を発明しました)。 そして、プレイヤーがどれだけのお金を持っているか、どのレベルを持っているかなどを知る必要があるたびにデコードします。 B64Xは暗号化にキー(パスワード)を使用できます。または、ランダムなセッションキーを使用して、場所と方法を保存しないようにすることができます。



4.ゲーム内購入の保護



多くの開発者にとって、このトピックは関係がなく、保護を実装する人はほとんどいません。 原則として、マルチプレイヤーゲームがある場合は、その経済を保護することを考える必要があります。 そのようなプログラムがあります-自由。 ルートが必要であり、一言で言えば、ゲーム内のショッピングサービスを置き換えます。 つまり、プレーヤーは無料で購入できます。



開発者のサーバーで購入を確認するためのメカニズムの考慮は、誰もが持っているわけではないため省略します。 そのような場合にGoogleが提供するものを説明します。



UPD:Unityは購入メカニズムとその検証(http://docs.unity3d.com/Manual/UnityAnalyticsReceiptVerification.html)を実装したため、以下の情報には理論的な負荷しかありません。



開発者のコ​​ンソールでアプリケーションを作成すると、GoogleはRSAアルゴリズムのキーペア(公開キーと秘密キー)を生成します。 あなたがそれが何であるかわからない場合-Google非対称暗号化。 公開鍵は、開発者コンソールで取得できます。



画像



アプリケーションでゲームストアを実装するときにも引き続き使用します。



Googleの秘密鍵は決して​​表示されず、購入のデジタル署名に使用されます。 したがって、秘密鍵は署名のみを暗号化でき、公開鍵は署名のみを復号化できます。



保護メカニズムは非常に簡単です-Googleは購入サーバーのすべてのjson応答に署名し、他の誰もそのような署名を偽造することはできません。 開発者は、公開鍵を知っていれば、サーバー応答のデジタル署名を検証できます。 また、サーバーがFreedomを使用して作成された場合、デジタル署名は正しくありません。



実装に移りましょう。 最初に、1つの不快な操作を実行する必要があります。 開発者コンソールからbase64公開キーを、署名の復号化に適したxmlキーに変換する必要があります。 明らかに、base64をデコードするのに十分単純なようです。 しかし、これはそうではありません。 オンラインサービスを使用して、アプリケーションのxmlキーをすぐに掘ることをお勧めします。 特に保護について悩む価値はありません。同じ公開鍵です。 それは組み立てることができますが、それは別の話です。 だから、ここにサービスがあり、そこにbase64キーを挿入し、xmlキーを取得します: superdry.apphb.com/tools/online-rsa-key-converter



画像



下部のフィールドには、xmlキーがあります。 ゲームまたはアプリケーションに保存します。 そして、すべてが簡単です。 Googleは購入した商品を返品します。 アプリケーションでOpenIAB購入の実装に無料のプラグインを使用する場合、これはPurchaseクラスのオブジェクトであり、必要な2つのフィールドがあります。



 Purchase purchase; var json = purchase.OriginalJson; var signature = purchase.Signature;
      
      





次に、署名検証メカニズムの実装を紹介します。



 using System; using System.Security.Cryptography; namespace Assets.Scripts.Common { public static class GooglePlayPurchaseGuard { /// <summary> /// Verify Google Play purchase. Protect you app against hack via Freedom. More info: http://mrtn.me/blog/2012/11/15/checking-google-play-signatures-on-net/ /// </summary> /// <param name="purchaseJson">Purchase JSON string</param> /// <param name="base64Signature">Purchase signature string</param> /// <param name="xmlPublicKey">XML public key. Use http://superdry.apphb.com/tools/online-rsa-key-converter to convert RSA public key from Developer Console</param> /// <returns></returns> public static bool Verify(string purchaseJson, string base64Signature, string xmlPublicKey) { using (var provider = new RSACryptoServiceProvider()) { try { provider.FromXmlString(xmlPublicKey); var signature = Convert.FromBase64String(base64Signature); var sha = new SHA1Managed(); var data = System.Text.Encoding.UTF8.GetBytes(purchaseJson); return provider.VerifyData(data, sha, signature); } catch (Exception e) { UnityEngine.Debug.Log(e); } return false; } } } }
      
      







さて、今、購入が完了したという回答がGoogleから届いたとき、署名を検証し、署名が一致しない場合はプレーヤーに図を表示します。



 if (GooglePlayPurchaseGuard.Verify(purchase.OriginalJson, purchase.Signature, publicKeyXml)) { } else { }
      
      





購入時にランダムペイロードをリクエストに追加することをお勧めします。これにより、正しいが同じデジタル署名で正しいサーバーレスポンスを繰り返し詰め込むことができる場合、中間者攻撃から保護されます。 これはOpenIAB実装のオプションの引数であり、ほとんどの場合、これにボルトが付けられます。



 public static void purchaseProduct(string sku, string developerPayload = "")
      
      





メカニズムの詳細な説明は、英語のリンクmrtn.me/blog/2012/11/15/checking-google-play-signatures-on-netにあります。



5.結論



記事が退屈すぎないことを願っています。 いずれにしても、あなたの注意に感謝し、高品質のゲームを作り、プレイヤーに新しい体験を与えてください!

PS最近ゲーム業界に惹かれ、活動範囲を変更したい)



All Articles