Schutz von Spielen und mobilen Anwendungen vor Hacking für Dummies (Unity, C #, Mono)

  • Tutorial
Hallo nochmal an alle! Wir haben einen coolen Artikel über ein sehr wichtiges Thema für Spieleentwickler geschrieben. Sprechen wir also über den Schutz Ihrer wertvollen Spiele und Anwendungen, die Sie auf Unity gesehen haben, in der Hoffnung, einen Laib Brot vor dem Zerbrechen durch böse Schulkinder zu verdienen. Warum Schulkinder? Denn a priori kann 100% zuverlässiger Schutz nicht sein. Und wer will es trotzdem brechen. Die Frage ist nur, wie viel Zeit und Mühe er dafür aufwenden wird. Und da Sicherheitsleute gerne scherzen, hat niemand die thermorektale Kryptoanalyse abgebrochen.

Deshalb werde ich in dem Artikel so viel wie möglich versuchen, über drei Aspekte zu sprechen (und natürlich werde ich eine Implementierung anbieten):
  • Anwendungsdatenschutz (speichert)
  • Anwendungsspeicherschutz
  • Kaufschutz im Spiel (Google Play)

Bild

1. Vorbereitung


Zuerst müssen Sie lernen, wie Sie Spieldaten (Typen, Klassen) in Zeichenfolgen konvertieren. Es lohnt sich, sich mit JSON- oder XML-Serialisierung zu befassen. Ich empfehle nicht, mit XML zu beginnen, da Ich habe Probleme mit iOS. Besser JSON lernen, hier ist der Link wiki.unity3d.com/index.php/SimpleJSON . Leider ist dies das Thema eines separaten Artikels und ich werde nicht weiter darauf eingehen. Wenn es nicht so schnell geht, können Sie eine Zeichenfolge manuell mit Trennzeichen formen. Zum Beispiel:

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

Sie müssen außerdem in der Lage sein, Zeichenfolgen in Byte-Arrays zu konvertieren und umgekehrt. Hier ist alles einfach:

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

Dann können Sie den String verschleiern, indem Sie darauf die base64-Konvertierung anwenden. Ich betone, dass base64 keine Verschlüsselung ist, es keinen Verschlüsselungsschlüssel hat und so weiter. base64 konvertiert Ihre Zeichenfolge in eine neue Zeichenfolge, die nur aus ASCII-Zeichen besteht. Sie können deutlich sehen, wie dies bei base64.ru geschieht . Ich werde nur den Implementierungscode geben:

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);
        }
    }
}

Ich stelle auch fest, dass base64 schnell und in der Geschwindigkeit mit der Additionsoperation vergleichbar ist. Sie können solche Konvertierungen auch in der Update-Schleife durchführen.

2. Schutz der Spieldaten (speichert)


Jetzt können wir Spieldaten in einen String konvertieren. Jetzt müssen Sie darüber nachdenken, wo Sie sie speichern können. Das erste, was mir in den Sinn kommt, ist das Speichern von Speichern in Dateien in Application.persistentDataPath. Die Nachteile dieser Methode sind zwei:
  • Application.persistentDataPath kann sich ändern, wenn die Anwendung aktualisiert wird (die Anwendung wird beispielsweise auf die SD-Karte verschoben). Dementsprechend wird die Sicherungsdatei nicht gefunden, und der Benutzer verliert jeglichen Fortschritt
  • Dies funktioniert nicht in Web-Player und Windows Phone

Der zweite und korrekteste Weg ist das Speichern in PlayerPrefs. Beispiel unten:

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);
}

Oh ja Baby, super! Jetzt müssen wir unseren Speicher verschlüsseln. Hier können Sie schnell eine Base64-Konvertierung durchführen. Dies schützt die Speicherung vor der Bearbeitung durch die meisten Hacking-Programme. Für Hardcore ist es jedoch an der Zeit, die normale Verschlüsselung zu verbessern. Nehmen Sie AES und verschlüsseln Sie. Wir kopieren die AES.cs-Datei und wundern uns nicht, wie es funktioniert:

using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
namespace Assets.Scripts.Common
{
    /// 
    /// 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)
    /// 
    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. Anwendungsspeicherschutz


Erinnert sich jeder an ein PC-Programm wie ArtMoney? Sie war in der Lage, nach Werten im RAM zu suchen und nach mehreren Wiederholungen des Screenings, die es ihr ermöglichten, Millionär im Spiel zu werden. Jetzt gibt es für Android und iOS viele ähnliche Programme, zum Beispiel das beliebteste - GameKiller.

Es ist ganz einfach, sich vor solchen Programmen zu schützen - Sie müssen die Werte im Anwendungsspeicher verschlüsseln. Verschlüsseln Sie JEDES MAL beim Schreiben und entschlüsseln Sie JEDES MAL beim Lesen. Und da die Operation ziemlich häufig ist, macht es keinen Sinn, schweres AES zu verwenden, und wir brauchen einen superschnellen Algorithmus. Ich schlage vor, unsere base64 ein wenig zu modifizieren und meine Verschlüsselung zu implementieren - effizient, schnell, mit Blackjack und XOR:

using System;
using System.Text;
namespace Assets.Scripts.Common
{
    /// 
    /// 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.
    /// 
    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;
        }
    }
}

Sobald wir nun das AES-te Profil aus dem Speicher gelesen und dekodiert haben, verschlüsseln wir sofort alle Werte mit diesem B64X (ich habe den Namen selbst erfunden). Und wir dekodieren jedes Mal, wenn Sie herausfinden möchten, wie viel Geld ein Spieler hat, welches Level er hat usw. Der B64X kann einen Schlüssel (Passwort) zur Verschlüsselung verwenden oder einen zufälligen Sitzungsschlüssel verwenden, damit wir nicht dämpfen, wo und wie er gespeichert wird.

4. Schutz der Einkäufe im Spiel


Für viele Entwickler ist dieses Thema nicht relevant und nur wenige Leute implementieren Schutz. Wenn Sie ein Multiplayer-Spiel haben, müssen Sie im Prinzip über den Schutz der Wirtschaft nachdenken. Es gibt so ein Programm - Freiheit. Es erfordert eine Wurzel und ersetzt, kurz gesagt, den In-Game-Einkaufsservice. Kurz gesagt: Ein Spieler kann kostenlos einkaufen.

Wir verzichten auf die Prüfung von Einkäufen auf dem Server des Entwicklers, da nicht jeder über einen solchen Mechanismus verfügt. Ich sage Ihnen, was Google in solchen Fällen anbietet.

UPD: Unity hat den Kaufmechanismus und dessen Überprüfung implementiert (http://docs.unity3d.com/Manual/UnityAnalyticsReceiptVerification.html), sodass die folgenden Informationen nur noch theoretisch geladen sind.

Wenn Sie eine Anwendung in der Entwicklerkonsole erstellen, generiert Google ein Schlüsselpaar für den RSA-Algorithmus - den öffentlichen und den privaten Schlüssel. Wenn Sie nicht wissen, was es ist - Google asymmetrische Verschlüsselung. Der öffentliche Schlüssel kann in der Entwicklerkonsole abgerufen werden:

Bild

Sie verwenden ihn weiterhin, wenn Sie den Game Store in der Anwendung implementieren.

Der private Schlüssel von Google wird Ihnen niemals angezeigt und zum digitalen Signieren von Einkäufen verwendet. Dementsprechend kann der private Schlüssel nur die Signatur verschlüsseln, und der öffentliche Schlüssel kann nur die Signatur entschlüsseln.

Der Schutzmechanismus ist recht einfach: Google signiert alle JSON-Antworten des Einkaufsservers, und niemand anderes kann eine solche Signatur vortäuschen. Der Entwickler, der den öffentlichen Schlüssel kennt, kann die digitale Signatur der Serverantworten überprüfen. Und wenn der Server mit Freedom hergestellt wurde, ist die digitale Signatur falsch.

Fahren wir mit der Implementierung fort. Zuerst müssen Sie eine unangenehme Operation durchführen. Sie müssen den öffentlichen base64-Schlüssel von der Entwicklerkonsole in einen XML-Schlüssel konvertieren, der zum Entschlüsseln der Signatur geeignet ist. Offensichtlich reicht es aus, base64 nur zu dekodieren. Aber das ist nicht so. Ich schlage vor, den Onlinedienst zu nutzen und sofort den XML-Schlüssel in der Anwendung einzugeben. Es lohnt sich nicht, sich um den Schutz zu kümmern - es ist derselbe öffentliche Schlüssel. Es kann hergestellt werden, aber das ist eine andere Geschichte. Also, hier ist der Service, geben Sie Ihren base64-Schlüssel ein und holen Sie sich den xml-Schlüssel: superdry.apphb.com/tools/online-rsa-key-converter

Bild

Im unteren Feld befindet sich unser XML-Schlüssel. Wir speichern es im Spiel oder in der Anwendung. Und dann ist alles einfach. Google sendet den Kauf an uns zurück. Wenn Sie das kostenlose Plug-In für die Implementierung von OpenIAB-Einkäufen in der Anwendung verwenden, ist dies ein Objekt der Purchase-Klasse. Es enthält zwei Felder, die wir benötigen:

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

Jetzt werde ich Ihnen eine Implementierung des Signaturprüfungsmechanismus geben:

using System;
using System.Security.Cryptography;
namespace Assets.Scripts.Common
{
    public static class GooglePlayPurchaseGuard
    {
        /// 
        /// 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/
        /// 
        /// Purchase JSON string
        /// Purchase signature string
        /// XML public key. Use http://superdry.apphb.com/tools/online-rsa-key-converter to convert RSA public key from Developer Console
        /// 
        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;
            }
        }
    }
}


Nun, als die Antwort von Google kam, dass der Kauf abgeschlossen wurde, überprüfen wir die Signatur und zeigen dem Spieler eine Feige, wenn die Signatur nicht übereinstimmt:

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

Ich möchte darauf hinweisen, dass es beim Kauf besser ist, der Anfrage eine zufällige Nutzlast hinzuzufügen, um Man-in-the-Middle-Angriffe zu verhindern, wenn Sie die richtige Serverantwort wiederholt mit der richtigen, aber der gleichen digitalen Signatur überfüllen können. Dies ist ein optionales Argument in der OpenIAB-Implementierung, auf das die meisten einen Bolzen setzen:

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

Eine detailliertere Beschreibung des Mechanismus finden Sie auf Englisch unter dem Link: mrtn.me/blog/2012/11/15/checking-google-play-signatures-on-net

5. Fazit


Ich hoffe der Artikel war nicht zu langweilig. Auf jeden Fall danke für Ihre Aufmerksamkeit, machen Sie hochwertige Spiele und geben Sie den Spielern neue Erfahrungen!
PS Kürzlich von der Gaming-Branche angezogen und ich möchte den Tätigkeitsbereich ändern)

Jetzt auch beliebt: