Comment protéger les données de jeu sur Unity en RAM?

image



salut! Ce n'est un secret pour personne qu'il existe de nombreux programmes de piratage de jeux et d'applications. Il existe également de nombreuses façons de pirater. Par exemple, décompilation et modification du code source (avec la publication ultérieure d'APK personnalisés, par exemple, avec de l'or infini et tous les achats payés). Ou le moyen le plus polyvalent est de scanner, filtrer et modifier les valeurs dans la RAM. Comment faire face à ce dernier, je vais vous dire sous la coupe.



En général, nous avons un profil de joueur avec un tas de paramètres, qui est sérialisé dans le jeu sauvegardé et chargé / sauvegardé lorsque le jeu commence / se termine. Et si l'ajout de chiffrement lors de la sérialisation est assez simple, la protection du même profil dans la RAM est un peu plus difficile. Je vais essayer de donner un exemple simple:



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.


Le deuxième point auquel il convient de prêter attention est que la mise en œuvre d'une nouvelle protection devrait avoir lieu avec des modifications minimes du code source du jeu, où tout fonctionne déjà bien et a été testé à plusieurs reprises. Dans ma méthode, il suffira de remplacer les types int / long / float par ProtectedInt / ProtectedLong / ProtectedFloat . Ensuite, je fournirai des commentaires et du code.



La classe de base Protected stocke un tableau chiffré d'octets dans le champ "_", elle est également responsable du chiffrement et du déchiffrement des données. Le cryptage est primitif - XOR avec clé . Ce cryptage est rapide, vous pouvez donc travailler avec des variables même dans Update... La classe de base fonctionne avec des tableaux d'octets. Les classes enfants sont responsables de la conversion de leur type vers et depuis un tableau d'octets. Mais surtout, ils sont "déguisés" en types simples utilisant l' opérateur implicite , de sorte que le développeur peut même ne pas remarquer que le type des variables a changé. Vous pouvez également remarquer les attributs de certaines méthodes et propriétés nécessaires pour la sérialisation avec JsonUtility et Newtonsoft.Json (les deux sont pris en charge en même temps). Si vous n'utilisez pas Newtonsoft.Json, vous devez supprimer le #define NEWTONSOFT_JSON .



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


Si vous avez oublié ou fait une erreur quelque part, écrivez dans les commentaires =) Bonne chance pour le développement!



PS. Le chat n'est pas le mien, l'auteur de la photo est CatCosplay.



UPD. Dans les commentaires ont fait les observations suivantes sur l'affaire:

  1. Mieux vaut passer à struct pour rendre le code plus prévisible (encore plus si nous nous déguisons en types valeur simples).
  2. La recherche dans la RAM peut être effectuée non pas par des valeurs spécifiques, mais par toutes les variables modifiées. XOR n'aidera pas ici. Vous pouvez également saisir une somme de contrôle.
  3. BitConverter est lent (à petite échelle, bien sûr). Mieux vaut s'en débarrasser (pour int il s'est avéré, pour float - j'attends vos suggestions).


Vous trouverez ci-dessous une version mise à jour du code. ProtectedInt et ProtectedFloat sont désormais des structures. Je me suis débarrassé des tableaux d'octets. Ajout de la somme de contrôle _h comme solution au deuxième problème. J'ai testé la sérialisation dans les deux sens.



[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