如何在Unity中的RAM中保护游戏数据?

图片



你好!秘密的是,有许多程序可以用来入侵游戏和应用程序。还有许多破解方法。例如,对源代码进行反编译和修改(随后发布自定义APK,例如,使用无数金和所有付费购买的商品)。或者最通用的方法是扫描,过滤和编辑RAM中的值。如何处理后者,我会在削减的情况下告诉您。



通常,我们有一个带有一系列参数的玩家配置文件,这些配置文件在“已保存的游戏”中序列化,并在游戏开始/结束时加载/保存。而且,如果在序列化过程中添加加密非常简单,那么在RAM中保护相同的配置文件就更加困难。我将尝试举一个简单的例子:



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.


值得注意的第二点是,引入新保护的过程中,应在对游戏源代码进行最小更改的情况下进行,其中一切都已经可以正常工作并且已经经过了多次测试。在我的方法中,ProtectedInt / ProtectedLong / ProtectedFloat替换int / long / float类型就足够了。接下来,我将提供注释和代码。 Protected基类在“ _”字段中存储了一个加密的字节数组,它还负责加密和解密数据。加密是原始的-与Key进行XOR 。这种加密速度很快,因此即使在Update中也可以使用变量



... 基类使用字节数组。子类负责将其类型与字节数组之间进行转换。但是最重​​要的是,使用隐式运算符将它们“伪装”为简单类型,因此开发人员甚至可能不会注意到变量的类型已更改。您可能还会注意到使用JsonUtility和Newtonsoft.Json进行序列化所需的某些方法和属性的属性(两者均受支持)。如果您不使用Newtonsoft.Json,则需要删除#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;
            }
        }
    }
}


如果您忘记了某个地方或在某个地方犯了一个错误,请在注释中写上=)祝您开发愉快!



PS。这只猫不是我的猫,照片的作者是CatCosplay。



UPD。在评论中,对该案提出了以下意见:

  1. 更好地使用结构以使代码更可预测(如果我们将自己伪装成简单的值类型,则更是如此)。
  2. RAM中的搜索不能由特定值执行,而可以由所有更改的变量执行。XOR在这里无济于事。或者,输入校验和。
  3. BitConverter很慢(当然是微尺度的)。最好摆脱它(因为它原来是int,所以它是float-我在等待您的建议)。


下面是代码的更新版本。现在,ProtectedInt和ProtectedFloat是结构。我摆脱了字节数组。另外介绍了_h校验和作为第二个问题的解决方案。我以两种方式测试了序列化。



[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