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.


注意する価値のある2番目のポイントは、新しい保護の実装は、ゲームのソースコードへの最小限の変更で行われるべきであり、すべてがすでに正常に機能し、何度もテストされていることです。私の方法では、int / long / floatタイプProtectedInt / ProtectedLong / ProtectedFloatに置き換えるだけで十分です。次に、コメントとコードを提供します。



基本クラスProtectedは、暗号化されたバイト配列を「_」フィールドに格納し、データの暗号化と復号化も行います。暗号化は基本的です-キーとのXOR 。この暗号化は高速であるため、Updateでも変数を操作できます..。基本クラスは、バイトの配列で機能します。子クラスは、タイプをバイト配列との間で変換する責任があります。しかし、最も重要なことは、暗黙の演算子を使用して単純な型に「偽装」されているため、開発は変数の型が変更されたことに気付かない場合もあります。また、JsonUtilityおよびNewtonsoft.Json(両方が同時にサポートされている)を使用したシリアル化に必要ないくつかのメソッドとプロパティの属性に気付く場合があります。Newtonsoft.Jsonを使用していない場合は、#defineNEWTONSOFT_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が構造になりました。私はバイト配列を取り除きました。さらに、2番目の問題の解決策として_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