Bagaimana cara melindungi data game di Unity dalam RAM?

gambar



Halo! Bukan rahasia lagi bahwa ada banyak program untuk meretas game dan aplikasi. Ada juga banyak cara untuk meretas. Misalnya, penguraian dan modifikasi kode sumber (dengan publikasi APK khusus berikutnya, misalnya, dengan emas tak terbatas dan semua pembelian berbayar). Atau cara paling serbaguna adalah memindai, memfilter, dan mengedit nilai dalam RAM. Bagaimana menangani yang terakhir, saya akan memberi tahu Anda di bawah pemotongan.



Secara umum, kami memiliki profil pemain dengan banyak parameter, yang diserialkan dalam Game Tersimpan dan dimuat / disimpan saat game dimulai / berakhir. Dan jika menambahkan enkripsi selama serialisasi cukup sederhana, maka melindungi profil yang sama di RAM agak lebih sulit. Saya akan mencoba memberikan contoh sederhana:



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.


Hal kedua yang perlu diperhatikan adalah penerapan perlindungan baru harus dilakukan dengan sedikit perubahan pada kode sumber permainan, di mana semuanya sudah berfungsi dengan baik dan telah diuji berkali-kali. Dalam metode saya, itu akan cukup untuk mengganti tipe int / long / float dengan ProtectedInt / ProtectedLong / ProtectedFloat . Selanjutnya saya akan memberikan komentar dan kode.



Kelas dasar Terlindungi menyimpan larik byte terenkripsi di bidang "_", ini juga bertanggung jawab untuk mengenkripsi dan mendekripsi data. Enkripsi ini primitif - XOR dengan Key . Enkripsi ini cepat, sehingga Anda dapat bekerja dengan variabel bahkan dalam Pembaruan... Kelas dasar bekerja dengan array byte. Kelas anak bertanggung jawab untuk mengubah tipe mereka ke dan dari array byte. Tapi yang paling penting, mereka "disamarkan" sebagai tipe sederhana menggunakan operator implisit , jadi pengembang mungkin tidak menyadari bahwa tipe variabel telah berubah. Anda mungkin juga melihat atribut pada beberapa metode dan properti yang diperlukan untuk serialisasi dengan JsonUtility dan Newtonsoft.Json (keduanya didukung pada waktu yang sama). Jika Anda tidak menggunakan Newtonsoft.Json, maka Anda perlu menghapus #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;
            }
        }
    }
}


Jika Anda lupa atau melakukan kesalahan di suatu tempat, tulis di komentar =) Selamat mencoba perkembangannya!



PS. Kucing itu bukan milikku, penulis fotonya adalah CatCosplay.



UPD. Dalam komentar dibuat pengamatan berikut pada kasus tersebut:

  1. Lebih baik pindah ke struct untuk membuat kode lebih dapat diprediksi (terlebih lagi jika kita menyamar sebagai tipe nilai sederhana).
  2. Pencarian di RAM dapat dilakukan bukan dengan nilai tertentu, tetapi oleh semua variabel yang diubah. XOR tidak akan membantu di sini. Cara lainnya, masukkan checksum.
  3. BitConverter lambat (dalam skala mikro, tentu saja). Lebih baik untuk menyingkirkannya (untuk int ternyata, untuk float - saya menunggu saran Anda).


Di bawah ini adalah versi kode yang diperbarui. ProtectedInt dan ProtectedFloat sekarang menjadi struktur. Saya menyingkirkan array byte. Selain itu memperkenalkan checksum _h sebagai solusi untuk masalah kedua. Saya menguji serialisasi dengan dua cara.



[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