كيفية حماية بيانات اللعبة على الوحدة في ذاكرة الوصول العشوائي؟

صورة



مرحبا! لا يخفى على أحد أن هناك العديد من البرامج لاختراق الألعاب والتطبيقات. هناك أيضًا طرق عديدة للاختراق. على سبيل المثال ، إلغاء ترجمة وتعديل شفرة المصدر (مع النشر اللاحق لملفات APK المخصصة ، على سبيل المثال ، مع الذهب اللانهائي وجميع المشتريات المدفوعة). أو الطريقة الأكثر تنوعًا هي مسح وتصفية وتحرير القيم في ذاكرة الوصول العشوائي. كيف تتعامل مع هذا الأخير ، سأخبرك تحت الخفض.



بشكل عام ، لدينا ملف تعريف لاعب مع مجموعة من المعلمات ، والتي يتم تسلسلها في اللعبة المحفوظة وتحميلها / حفظها عند بدء / انتهاء اللعبة. وإذا كان من السهل جدًا إضافة التشفير أثناء التسلسل ، فإن حماية نفس ملف التعريف في ذاكرة الوصول العشوائي تكون أكثر صعوبة إلى حد ما. سأحاول إعطاء مثال بسيط:



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.


النقطة الثانية الجديرة بالاهتمام هي أن إدخال حماية جديدة يجب أن يتم بأدنى حد من التغييرات في الكود المصدري للعبة ، حيث يعمل كل شيء بالفعل بشكل جيد وتم اختباره عدة مرات. في طريقتي ، سيكون كافيًا استبدال أنواع int / long / float بـ ProtectedInt / ProtectedLong / ProtectedFloat . بعد ذلك سأقدم التعليقات والرمز.



تخزن الفئة الأساسية Protected مجموعة مشفرة من البايتات في الحقل "_" ، كما أنها مسؤولة عن تشفير البيانات وفك تشفيرها. التشفير بدائي - XOR مع المفتاح . هذا التشفير سريع ، لذا يمكنك العمل مع المتغيرات حتى في التحديث... تعمل الفئة الأساسية مع مصفوفات البايت. تعتبر الفئات الفرعية مسؤولة عن تحويل نوعها من وإلى مصفوفة بايت. ولكن الأهم من ذلك ، أنها "متخفية" على أنها أنواع بسيطة باستخدام عامل التشغيل الضمني ، لذلك قد لا يلاحظ المطور حتى أن نوع المتغيرات قد تغير. قد تلاحظ أيضًا السمات في بعض الطرق والخصائص اللازمة للتسلسل مع 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;
            }
        }
    }
}


إذا نسيت أو ارتكبت خطأ في مكان ما ، فاكتب في التعليقات =) حظًا سعيدًا في التطوير!



ملاحظة. القطة ليست لي ، مؤلف الصورة هو CatCosplay.



محدث. في التعليقات أبديت الملاحظات التالية على القضية:

  1. من الأفضل الانتقال إلى الهيكلة لجعل الكود أكثر قابلية للتنبؤ (أكثر من ذلك إذا أخفنا أنفسنا بأنواع قيم بسيطة).
  2. لا يمكن إجراء البحث في ذاكرة الوصول العشوائي بقيم محددة ، ولكن من خلال جميع المتغيرات المتغيرة. لن يساعد XOR هنا. بدلاً من ذلك ، أدخل المجموع الاختباري.
  3. BitConverter بطيء (على نطاق صغير بالطبع). من الأفضل التخلص منه (لأنه اتضح أنه من أجل تعويم - أنا في انتظار اقتراحاتك).


يوجد أدناه نسخة محدثة من الكود. 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