How to protect game data on Unity in RAM?

image



Hello! It is no secret that there are many programs for hacking games and applications. There are also many ways to hack. For example, decompilation and modification of the source code (with the subsequent publication of custom APKs, for example, with infinite gold and all paid purchases). Or the most versatile way is to scan, filter and edit values ​​in RAM. How to deal with the latter, I'll tell you under the cut.



In general, we have a player profile with a bunch of parameters, which is serialized in the Saved Game and loaded / saved when the game starts / ends. And if adding encryption during serialization is quite simple, then protecting the same profile in RAM is somewhat more difficult. I will try to give a simple example:



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.


The second point worth paying attention to is that the implementation of new protection should take place with minimal changes to the source code of the game, where everything already works fine and has been tested many times. In my method, it will be enough to replace the int / long / float types with ProtectedInt / ProtectedLong / ProtectedFloat . Next I will provide comments and code.



The base class Protected stores an encrypted array of bytes in the "_" field, it is also responsible for encrypting and decrypting data. The encryption is primitive - XOR with Key . This encryption is fast, so you can work with variables even in Update... The base class works with arrays of bytes. Child classes are responsible for converting their type to and from a byte array. But most importantly, they are "disguised" as simple types using the implicit operator , so the developer may not even notice that the type of the variables has changed. You may also notice the attributes on some of the methods and properties that are needed for serialization with JsonUtility and Newtonsoft.Json (both are supported at the same time). If you are not using Newtonsoft.Json, then you need to remove the #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;
            }
        }
    }
}


If you have forgotten or made a mistake somewhere, write in the comments =) Good luck with the development!



PS. The cat is not mine, the author of the photo is CatCosplay.



UPD. In the comments made the following observations on the case:

  1. Better to move to a struct to make the code more predictable (even more so if we disguise ourselves as simple value types).
  2. The search in RAM can be performed not by specific values, but by all changed variables. XOR won't help here. Alternatively, enter a checksum.
  3. BitConverter is slow (on a micro scale, of course). Better to get rid of it (for int it turned out, for float - I'm waiting for your suggestions).


Below is an updated version of the code. ProtectedInt and ProtectedFloat are now structures. I got rid of byte arrays. Additionally introduced the _h checksum as a solution to the second problem. I tested serialization in both ways.



[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