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

本文介绍了如何通过内存加密技术防止单机游戏和弱联网游戏中玩家使用修改器作弊。具体做法包括:随机偏移数值存储,重载类型转换、操作符以及方法,实现数据在加密和解密间的安全转换,以及检测内存篡改的机制。通过创建JInt结构体,实现了对int类型的加密,并提供了加减乘除等数学运算的支持。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

对于单机游戏和弱联网游戏的开发者而言,总会遇到一些居心叵测的害群之马,喜欢通过修改器,修改游戏数据,进而满足自己甚至牟利,我对于这些使用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 ; } } }
这里就把之前的+和-去掉了,变成了^(异或符号)

最后

感谢大家的阅读!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值