分享

Unity开发游戏防作弊之内存加密

 勤奋不止 2021-12-30
对于单机游戏和弱联网游戏的开发者而言,总会遇到一些居心叵测的害群之马,喜欢通过修改器,修改游戏数据,进而满足自己甚至牟利,我对于这些使用GG修改器、CheatEngine等软件进行作弊的玩家痛深恶绝
那么,如果有数据需要在客户端运算,且不希望被玩家在运行时通过作弊器修改内存而篡改数据,该怎么做呢?这就引发出来了本文将讨论的技术实现——内存加密,该技术在JEngine框架中得到了实践,也成功阻挡了内存作弊。
古云知己知彼百战百胜,想要实现内存加密,自然得先了解如何内存作弊

内存作弊的原理

在这里,我们用一个闯关游戏举例,假设主角有100HP,而当HP=0时,则游戏失败。
在这个情况下,最直接的作弊方案就是,把主角的100HP改成1000HP,甚至更高,保证他的HP不会为0。
因为本文内容敏感,不会附带作弊器修改器使用的截图,只进行文字描述。
这种时候,只需要用GG修改器,或CheatEngine等工具,进行以下步骤:
  1. 找到游戏进程,全局搜索数值为100的存在内存中的数据(在这个时候游戏进程会被修改器暂停)
  2. 尝试修改这些数据的数值,回到游戏,看看是否奏效
  3. 重复第2步,直到奏效
  4. 作弊完成
可以说,内存作弊是傻瓜式操作,就看有没有耐心,以及有没有合适的工具,两者皆有,那么想要作弊便是手到擒来。

内存加密的实现

当知道了内存作弊的原理是通过扫描这个数值,定位到内存中的数据,从而进行篡改,那么防护就很好处理了。
最简单的方法,我们只需要对存在内存中的数据进行随机偏移即可,例如100的HP我们存为80,然后再存个20,获取HP的时候我们用80+20即可得到原结果。
这里需要注意,最好是每次设置HP的时候都随机一个偏移值,不然作弊者可以通过一些作弊软件的特殊功能发现偏移值的规律,继续肆意妄为的作弊
进行内存加密,我们需要以下步骤:
  1. 随机偏移值
  2. 定义加密结构体
  3. 重载类型转换(例如加密int类型转int类型等)
  4. 重载操作符(加减乘除求余数、全等不等、大于小于)
  5. 重载方法(ToString,GetHashCode,Equals)
  6. (可选)检测是否有内存作弊(抓人)

随机偏移值

因为不想对UnityEngine进行过多的依赖,这里对System的Random进行了封装:
using System; namespace JEngine.AntiCheat { public class JRandom { private static Random _random = new Random(); private JRandom() { } public static int RandomNum(int max = 1024) { return _random.Next(0, max < 0 ? 1024 : max); } } }
这里将JRandom定义为一个类型,不能在外部被创建实例,且持有一个System.Random的静态字段,同时有一个会返回一个随机的int数值的静态方法,参数可以自定义随机数的上限,如果上限是负数,则修改为1024再去随机
注,这里也可以把这个类改成静态类,在静态构造函数里修改一下random的seed

定义加密结构体

这里我们对Int32(int)类型进行内存加密,
我们需要:
  • 偏移值
  • 偏移后的数值
  • 可返回原数值的属性
  • 通过int生成出加密结构的构造函数
namespace JEngine.AntiCheat { public struct JInt { internal int ObscuredInt; internal int ObscuredKey; private int Value { get { var result = ObscuredInt - ObscuredKey; return result; } set { unchecked { ObscuredKey = JRandom.RandomNum(int.MaxValue - value); ObscuredInt = value + ObscuredKey; } } } public JInt(int val = 0) { ObscuredInt = 0; ObscuredKey = 0; Value = val; } } }
在这里,我们定义了两个int字段,分别是加密后的int数值(ObscuredInt)和偏移值(ObscuredKey),他们的修饰符是internal,可以理解为在同程序集下,他们是public,在不同程序集下,他们是private,当然,直接修饰为private也不是不可以;
接着,这个结构有个Value属性,有对应的getter和setter。
在getter内,我们通过使用加密后的数值减去随机的偏移值,就可以得到原值。
需要注意的是,如果原值是int.MaxValue,那么偏移值就是0
在setter内,我们使用了unchecked,防止出现刚刚提到的对int.MaxValue向上偏移造成的值越界问题,同时我们随机了新的偏移值,再次计算了加密后的数值
理论上可以通过setter内设置加密值=原值-偏移值,getter内设置原值=加密值+偏移值,来避免int.MaxValue无法加密的问题,但也需要改一下生成的随机偏移值,既改为ObscuredKey = JRandom.RandomNum(value),不需要再做减法了
最后,我们定义了JInt的构造函数,参数是int,代表了原数值,构造函数内我们初始化了偏移值和加密值,然后通过给Value赋值,调用其setter进而获得加密后的结构体

重载类型转换(例如加密int类型转int类型等)

那么我们如何把数据在int和JInt之间转换呢?我们就需要重载了,只需把这两行代码加入JInt代码即可:
public static implicit operator JInt(int val) => new JInt(val); public static implicit operator int(JInt val) => val.Value;
第一行代码是将int转为JInt,我们只需要通过JInt的构造参数返回一个JInt即可
第二行代码是JInt转为int,我们只需要调用JInt的Value属性的getter,返回原始数值即可

重载操作符(加减乘除求余数、全等不等、大于小于)

我们还需要让JInt支持数学运算,所以我们需要继续重载操作符,需要注意的是,我们需要支持JInt与JInt,以及JInt和int之间的数学运算,需要将以下代码加入JInt代码中:
public static bool operator ==(JInt a, JInt b) => a.Value == b.Value; public static bool operator ==(JInt a, int b) => a.Value == b; public static bool operator !=(JInt a, JInt b) => a.Value != b.Value; public static bool operator !=(JInt a, int b) => a.Value != b; public static JInt operator ++(JInt a) { a.Value++; return a; } public static JInt operator --(JInt a) { a.Value--; return a; } public static JInt operator + (JInt a, JInt b) => new JInt(a.Value + b.Value); public static JInt operator + (JInt a, int b) => new JInt(a.Value + b); public static JInt operator - (JInt a, JInt b) => new JInt(a.Value - b.Value); public static JInt operator - (JInt a, int b) => new JInt(a.Value - b); public static JInt operator * (JInt a, JInt b) => new JInt(a.Value * b.Value); public static JInt operator * (JInt a, int b) => new JInt(a.Value * b); public static JInt operator / (JInt a, JInt b) => new JInt(a.Value / b.Value); public static JInt operator / (JInt a, int b) => new JInt(a.Value / b); public static JInt operator % (JInt a, JInt b) => new JInt(a.Value % b.Value); public static JInt operator % (JInt a, int b) => new JInt(a.Value % b);
  • 通过对比Value,我们可以判断JInt与JInt(或int)是否全等或不等
  • 通过对Value的自增或自减,我们可以对JInt进行自增自减
  • 通过将两个JInt的Value增加到一起(或一个JInt的Value加上int的值),我们可以获得新的相加后的JInt结果
  • 通过将两个JInt的Value相减(或一个JInt的Value减去int的值),我们可以获得新的相减后的JInt结果
  • 通过将两个JInt的Value相乘(或一个JInt的Value乘以int的值),我们可以获得新的相乘后的JInt结果
  • 通过将两个JInt的Value相除(或一个JInt的Value除以int的值),我们可以获得新的相除后的JInt结果
  • 通过将两个JInt的Value求余(或一个JInt的Value除以int的值的余数),我们可以获得新的求余后的JInt结果
因为Value的setter中的unchecked操作,数学运算不会出现值越界,但是需要注意当数字达到int.MaxValue后就不会继续上升了

重载方法(ToString,GetHashCode,Equals)

需要重载的方法大致就3个,将下方代码加入JInt代码即可:
public override string ToString() => Value.ToString(); public override int GetHashCode() => Value.GetHashCode(); public override bool Equals(object obj) => Value.Equals(obj is JInt ? ((JInt) obj).Value : obj);
  • 首先是转字符串操作,将int的原值转字符串即可
  • 获取HashCode和转字符串一个道理,取原值的HashCode即可
  • 对比是否相等(这个是系统的Equals方法),需要判断是不是JInt,是的话则取其Value,不是的话则直接对比

(可选)检测是否有内存作弊(抓人)

想要检测是否内存作弊,其实也不难,只需要保存一个原值,然后看有没有和加密解密计算后的结果不匹配,不匹配的话就说明是有人修改了。
注,如果对float或double进行了内存加密,这里可能会因为精度问题导致结果不匹配
我们可以创建一个类,里面存抓到内存修改后的事件
using System; using UnityEngine; namespace JEngine.AntiCheat { public class AntiCheatHelper { public static Action OnDetected= () => { Debug.Log("被抓到修改内存了哦~"); }; internal static void Detected() { OnDetected?.Invoke(); } } }
使用的时候,我们只需往AntiCheatHelper.OnDetected += 事件即可。
现在我们修改一下JInt:
internal int ObscuredInt; internal int ObscuredKey; internal int OriginalValue; private int Value { get { var result = ObscuredInt - ObscuredKey; if (!OriginalValue.Equals(result)) { AntiCheatHelper.Detected(); } return result; } set { OriginalValue = value; unchecked { ObscuredKey = JRandom.RandomNum(int.MaxValue - value); ObscuredInt = value + ObscuredKey; } } }
可以看到,多了个字段,存原数值,在getter内对存原数值的字段赋值,在setter内对比解密结果是否匹配原数值,若不匹配,则代表内存作弊了,就会触发AntiCheatHelper内注册的对应事件。

测试

这里我在JInt的代码里定义了一个Log方法用于测试
public void Log() { var result = ObscuredInt - ObscuredKey; Console.WriteLine($"偏移值: {ObscuredKey}, 加密数值: {ObscuredInt}, 内存中钓鱼的数值: {OriginalValue},实际数值:{result}"); }
测试案例:
JInt a = 1; a.Log(); a++; a.Log(); a--; a.Log(); a+=10; a.Log(); a-=3; a.Log(); a*=2; a.Log(); a/=3; a.Log(); a%=4; a.Log();
测试结果:
可以看到,每次对数值进行操作后,都会改变偏移数值和加密数值,这也代表了我们的内存加密结构实现的很成功~

完整代码

JRandom和AntiCheatHelper在上文已提供完整代码,以下是JInt的完整代码:
// // JInt.cs // // Author: // JasonXuDeveloper(傑) <jasonxudeveloper@gmail.com> // // Copyright (c) 2020 JEngine // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. namespace JEngine.AntiCheat { public struct JInt { internal int ObscuredInt; internal int ObscuredKey; internal int OriginalValue; private int Value { get { var result = ObscuredInt - ObscuredKey; if (!OriginalValue.Equals(result)) { AntiCheatHelper.OnDetected(); } return result; } set { OriginalValue = value; unchecked { ObscuredKey = JRandom.RandomNum(int.MaxValue - value); ObscuredInt = value + ObscuredKey; } } } public JInt(int val = 0) { ObscuredInt = 0; ObscuredKey = 0; OriginalValue = 0; Value = val; } public static implicit operator JInt(int val) => new JInt(val); public static implicit operator int(JInt val) => val.Value; public static bool operator ==(JInt a, JInt b) => a.Value == b.Value; public static bool operator ==(JInt a, int b) => a.Value == b; public static bool operator !=(JInt a, JInt b) => a.Value != b.Value; public static bool operator !=(JInt a, int b) => a.Value != b; public static JInt operator ++(JInt a) { a.Value++; return a; } public static JInt operator --(JInt a) { a.Value--; return a; } public static JInt operator + (JInt a, JInt b) => new JInt(a.Value + b.Value); public static JInt operator + (JInt a, int b) => new JInt(a.Value + b); public static JInt operator - (JInt a, JInt b) => new JInt(a.Value - b.Value); public static JInt operator - (JInt a, int b) => new JInt(a.Value - b); public static JInt operator * (JInt a, JInt b) => new JInt(a.Value * b.Value); public static JInt operator * (JInt a, int b) => new JInt(a.Value * b); public static JInt operator / (JInt a, JInt b) => new JInt(a.Value / b.Value); public static JInt operator / (JInt a, int b) => new JInt(a.Value / b); public static JInt operator % (JInt a, JInt b) => new JInt(a.Value % b.Value); public static JInt operator % (JInt a, int b) => new JInt(a.Value % b); public override string ToString() => Value.ToString(); public override int GetHashCode() => Value.GetHashCode(); public override bool Equals(object obj) => Value.Equals(obj is JInt ? ((JInt) obj).Value : obj); } }

补充

除了用上面提到的相加相减加密外,也可以用异或加密(感谢评论区大佬的指出),只需要把Value替换成如下即可:
private int Value { get { var result = ObscuredInt ^ ObscuredKey; if (!OriginalValue.Equals(result)) { AntiCheatHelper.OnDetected(); } return result; } set { OriginalValue = value; unchecked { ObscuredKey = JRandom.RandomNum(int.MaxValue - value); ObscuredInt = value ^ ObscuredKey; } } }
这里就把之前的+和-去掉了,变成了^(异或符号)

最后

感谢大家的阅读!

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约