Wie schütze ich Spieldaten auf Unity im RAM?

Bild



Hallo! Es ist kein Geheimnis, dass es viele Programme zum Hacken von Spielen und Anwendungen gibt. Es gibt auch viele Möglichkeiten zu hacken. Zum Beispiel Dekompilierung und Änderung des Quellcodes (mit der anschließenden Veröffentlichung von benutzerdefinierten APKs, zum Beispiel mit unendlichem Gold und allen bezahlten Einkäufen). Am vielseitigsten ist es, die Werte im RAM zu scannen, zu filtern und zu bearbeiten. Wie man mit letzterem umgeht, werde ich Ihnen unter dem Schnitt sagen.



Im Allgemeinen haben wir ein Spielerprofil mit einer Reihe von Parametern, das im gespeicherten Spiel serialisiert und geladen / gespeichert wird, wenn das Spiel beginnt / endet. Und wenn es recht einfach ist, während der Serialisierung eine Verschlüsselung hinzuzufügen, ist es etwas schwieriger, dasselbe Profil im RAM zu schützen. Ich werde versuchen, ein einfaches Beispiel zu geben:



var money = 100; // "100" is present in RAM now (as four-byte integer value). Cheat apps can find, filter and replace it since it was declared.

money += 20; // Cheat apps can scan RAM for "120" values, filter them and discover the RAM address of our "money" variable.

Debug.Log(money); // We expect to see "120" in console. But cheat apps can deceive us!

ProtectedInt experience = 500; // four XOR-encrypted bytes are present in RAM now. Cheat apps can't find our value in RAM.

experience += 100;

Debug.Log(experience); // We can see "600" in console;

Debug.Log(JsonUtility.ToJson(experience)); // We can see four XOR-encrypted bytes here: {"_":[96,96,102,53]}. Our "experience" is hidden.


Der zweite Punkt, den Sie beachten sollten, ist, dass die Implementierung eines neuen Schutzes mit minimalen Änderungen am Quellcode des Spiels erfolgen sollte, bei dem alles bereits einwandfrei funktioniert und viele Male getestet wurde. In meiner Methode reicht es aus, die Typen int / long / float durch ProtectedInt / ProtectedLong / ProtectedFloat zu ersetzen . Als nächstes werde ich Kommentare und Code bereitstellen.



Die Basisklasse Protected speichert ein verschlüsseltes Array von Bytes im Feld "_" und ist auch für das Ver- und Entschlüsseln von Daten verantwortlich. Die Verschlüsselung ist primitiv - XOR mit Schlüssel . Diese Verschlüsselung ist schnell, sodass Sie auch in Update mit Variablen arbeiten können... Die Basisklasse arbeitet mit Bytearrays. Untergeordnete Klassen sind für die Konvertierung ihres Typs in und von einem Byte-Array verantwortlich. Am wichtigsten ist jedoch, dass sie mit dem impliziten Operator als einfache Typen "getarnt" werden , sodass der Entwickler möglicherweise nicht einmal bemerkt, dass sich der Typ der Variablen geändert hat. Möglicherweise bemerken Sie auch die Attribute einiger Methoden und Eigenschaften, die für die Serialisierung mit JsonUtility und Newtonsoft.Json erforderlich sind (beide werden gleichzeitig unterstützt). Wenn Sie Newtonsoft.Json nicht verwenden, müssen Sie das #define NEWTONSOFT_JSON entfernen .



#define NEWTONSOFT_JSON

using System;
using UnityEngine;

#if NEWTONSOFT_JSON
using Newtonsoft.Json;
#endif

namespace Assets
{
    [Serializable]
    public class ProtectedInt : Protected
    {
        #if NEWTONSOFT_JSON
        [JsonConstructor]
        #endif
        private ProtectedInt()
        {
        }

        protected ProtectedInt(byte[] bytes) : base(bytes)
        {
        }

        public static implicit operator ProtectedInt(int value)
        {
            return new ProtectedInt(BitConverter.GetBytes(value));
        }

        public static implicit operator int(ProtectedInt value) => value == null ? 0 : BitConverter.ToInt32(value.DecodedBytes, 0);

        public override string ToString()
        {
            return ((int) this).ToString();
        }
    }
    
    [Serializable]
    public class ProtectedFloat : Protected
    {
        #if NEWTONSOFT_JSON
        [JsonConstructor]
        #endif
        private ProtectedFloat()
        {
        }

        protected ProtectedFloat(byte[] bytes) : base(bytes)
        {
        }

        public static implicit operator ProtectedFloat(int value)
        {
            return new ProtectedFloat(BitConverter.GetBytes(value));
        }

        public static implicit operator float(ProtectedFloat value) => value == null ? 0 : BitConverter.ToSingle(value.DecodedBytes, 0);

        public override string ToString()
        {
            return ((float) this).ToString(System.Globalization.CultureInfo.InvariantCulture);
        }
    }

    public abstract class Protected
    {
        #if NEWTONSOFT_JSON
        [JsonProperty]
        #endif
        [SerializeField]
        private byte[] _;

        private static readonly byte[] Key = System.Text.Encoding.UTF8.GetBytes("8bf5b15ffef1f485f673ceb874fd6ef0");

        protected Protected()
        {
        }

        protected Protected(byte[] bytes)
        {
            _ = Encode(bytes);
        }

        private static byte[] Encode(byte[] bytes)
        {
            var encoded = new byte[bytes.Length];

            for (var i = 0; i < bytes.Length; i++)
            {
                encoded[i] = (byte) (bytes[i] ^ Key[i % Key.Length]);
            }

            return encoded;
        }

        protected byte[] DecodedBytes
        {
            get
            {
                var decoded = new byte[_.Length];

                for (var i = 0; i < decoded.Length; i++)
                {
                    decoded[i] = (byte) (_[i] ^ Key[i % Key.Length]);
                }

                return decoded;
            }
        }
    }
}


Wenn Sie irgendwo vergessen oder einen Fehler gemacht haben, schreiben Sie in die Kommentare =) Viel Glück bei der Entwicklung!



PS. Die Katze gehört nicht mir, der Autor des Fotos ist CatCosplay.



UPD. In den Kommentaren wurden folgende Bemerkungen zum Fall gemacht:

  1. Es ist besser, zu struct zu wechseln, um den Code vorhersehbarer zu machen (umso mehr, wenn wir uns als einfache Werttypen tarnen).
  2. Die Suche im RAM kann nicht nach bestimmten Werten, sondern nach allen geänderten Variablen durchgeführt werden. XOR wird hier nicht helfen. Alternativ können Sie eine Prüfsumme eingeben.
  3. BitConverter ist langsam (natürlich im Mikromaßstab). Besser, es loszuwerden (für int stellte sich heraus, für float - ich warte auf Ihre Vorschläge).


Unten finden Sie eine aktualisierte Version des Codes. ProtectedInt und ProtectedFloat sind jetzt Strukturen. Ich habe Byte-Arrays losgeworden. Zusätzlich wurde die Prüfsumme _h als Lösung für das zweite Problem eingeführt. Ich habe die Serialisierung auf beide Arten getestet.



[Serializable]
public struct ProtectedInt
{
	#if NEWTONSOFT_JSON
	[JsonProperty]
	#endif
	[SerializeField]
	private int _;

	#if NEWTONSOFT_JSON
	[JsonProperty]
	#endif
	[SerializeField]
	private byte _h;

	private const int XorKey = 514229;

	private ProtectedInt(int value)
	{
		_ = value ^ XorKey;
		_h = GetHash(_);
	}

	public static implicit operator ProtectedInt(int value)
	{
		return new ProtectedInt(value);
	}

	public static implicit operator int(ProtectedInt value) => value._ == 0 && value._h == 0 || value._h != GetHash(value._) ? 0 : value._ ^ XorKey;

	public override string ToString()
	{
		return ((int) this).ToString();
	}

	private static byte GetHash(int value)
	{
		return (byte) (255 - value % 256);
	}
}

[Serializable]
public struct ProtectedFloat
{
	#if NEWTONSOFT_JSON
	[JsonProperty]
	#endif
	[SerializeField]
	private int _;

	#if NEWTONSOFT_JSON
	[JsonProperty]
	#endif
	[SerializeField]
	private byte _h;

	private const int XorKey = 514229;

	private ProtectedFloat(int value)
	{
		_ = value ^ XorKey;
		_h = GetHash(_);
	}

	public static implicit operator ProtectedFloat(float value)
	{
		return new ProtectedFloat(BitConverter.ToInt32(BitConverter.GetBytes(value), 0));
	}

	public static implicit operator float(ProtectedFloat value) => value._ == 0 && value._h == 0 || value._h != GetHash(value._) ? 0f : BitConverter.ToSingle(BitConverter.GetBytes(value._ ^ XorKey), 0);

	public override string ToString()
	{
		return ((float) this).ToString(CultureInfo.InvariantCulture);
	}

	private static byte GetHash(int value)
	{
		return (byte) (255 - value % 256);
	}
}



All Articles