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

内存作弊的原理
在这里,我们用一个闯关游戏举例,假设主角有100HP,而当HP=0时,则游戏失败。
在这个情况下,最直接的作弊方案就是,把主角的100HP改成1000HP,甚至更高,保证他的HP不会为0。
因为本文内容敏感,不会附带作弊器修改器使用的截图,只进行文字描述。
这种时候,只需要用GG修改器,或CheatEngine等工具,进行以下步骤:
-
找到游戏进程,全局搜索数值为100的存在内存中的数据(在这个时候游戏进程会被修改器暂停)
-
尝试修改这些数据的数值,回到游戏,看看是否奏效
-
重复第2步,直到奏效
-
作弊完成
可以说,内存作弊是傻瓜式操作,就看有没有耐心,以及有没有合适的工具,两者皆有,那么想要作弊便是手到擒来。
内存加密的实现
当知道了内存作弊的原理是通过扫描这个数值,定位到内存中的数据,从而进行篡改,那么防护就很好处理了。
最简单的方法,我们只需要对存在内存中的数据进行随机偏移即可,例如100的HP我们存为80,然后再存个20,获取HP的时候我们用80+20即可得到原结果。
这里需要注意,最好是每次设置HP的时候都随机一个偏移值,不然作弊者可以通过一些作弊软件的特殊功能发现偏移值的规律,继续肆意妄为的作弊
进行内存加密,我们需要以下步骤:
-
随机偏移值
-
定义加密结构体
-
重载类型转换(例如加密int类型转int类型等)
-
重载操作符(加减乘除求余数、全等不等、大于小于)
-
重载方法(ToString,GetHashCode,Equals)
-
(可选) 检测是否有内存作弊(抓人)
随机偏移值
因为不想对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
;
}
}
}
这里就把之前的+和-去掉了,变成了^(异或符号)
最后
感谢大家的阅读!