内存加密
内存加密
目标 & 背景
- 抵御市面上常见的内存模糊搜索工具
- 没有致命性能问题
在手游的内存搜索工具上,一般来说 Android 需要 root 权限,iOS 需要越狱,但是在 PlayCover1出现后,打破了这个平衡
现在在 iOS 的版本下,对当前 iPA 进行砸壳后,使用 PlayCover1 运行,可以直接用 CE2 等工具,在开启 SIP3的情况下,可以直接对当前游戏进行内存搜索和修改,这样修改内存的门槛就大大降低了,所以针对内存修改的加密方案就变的势在必行
该方案已经开源,可点击查看 仓库地址
本文参考了 CSEncryptType4 这个方案的设计思路,并在此基础上解决了堆内存消耗过大的问题
设计思路
在开始之前,我们要搞清楚这些内存搜索工具的工作原理,一般来说,市面上常见的原理大致上就是多次输入一个值,然后取交集,最终确定这片内存的地址,在此基础上还有些工具会有计算差值等方式
在这个前提下,我们在设计加密时,就需要解决这两个问题
- 搜不到
- 无法进行有效差值计算
搜不到这件事情很简单,我们可以通过简单的位运算,解决这个问题,Set 时做一次位运算,Get 时再做一次位运算,这样实际显示/使用的值,和内存的值完全不一致,就可以达成搜不到这件事了
但是,针对第二项的无法进行有效差值计算这条就比较麻烦了,虽然我们设计可以基于位运算来加密这块内存,但是假设你这个值本次发生变化的大小为 10,即使做了位运算,这块加密的内存实际值的偏移量也仍然是 10,所以如果不做处理,针对这种搜索仍然是无效的
针对这个问题,我们可以给加密类型开辟多个存放的内存,每次 Set 时,存放在不同的内存上,这样即使去搜偏移,在大部分情况下搜到的结果都是错误的
改进 CSEncryptType4
下面截取了部分关键代码,从这几行代码中我们可以看到一个致命的问题,如果每次直接使用 EncryptByte byte = 0
这种方式进行赋值,实际上发生的是 new xxxClass
,这样就会导致非常严重的堆内存问题,每次赋值都有内存分配,这个并不是我们期望看到的,而且在游戏关键数据逻辑,如:角色属性修改上,是非常非常频繁的,这样会对战斗逻辑造成比较大的额外性能开销
public abstract class EncryptTypeBase<T, KVType, DType> // ...
{
private KVType[] _values;
// ...
protected static DType Box(T v)
{
DType d = new DType();
d.SetValue(v);
return d;
}
}
public class EncryptByte : EncryptTypeBase<byte, byte, EncryptByte>
{
// ...
public static implicit operator EncryptByte(byte v) => Box(v);
public static implicit operator byte(EncryptByte d) => Unbox(d);
}
针对这个问题,我们的解决思路也非常简单,要将 class 转成 struct 实现,但是在 _values
这个数组的实现上,就比较 tricky 了, 由于 struct 不允许有空的构造函数,int[] _values = new[3]
这种写法就是不支持的
这里我们直接定义
_1
、_2
、_3
三个变量,模拟这个数组
public struct EncryptInt : IComparable<EncryptInt>, IEquatable<EncryptInt>
{
private int _0;
private int _1;
private int _2;
}
然后直接通过操作这片内存,把这个 struct 看做数组,根据 _index
的值获取当前值存放在那个位置上即可
public unsafe int Get()
{
fixed(EncryptInt* array = &this)
{
return((int*) array)[_index] ^ _KEY;
}
}
Set 也是同理
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public unsafe void Set(int value)
{
if(++_index == 3)
{
_index = 0;
}
fixed(EncryptInt* array = &this)
{
((int*) array)[_index] = value ^ _KEY;
}
}
配表内存加密
在 Luban_Example 仓库中已经实现了对应的 tpl
生成模板,如有需要可以参考具体实现
注意事项
该库已经添加了较为完善的单元测试和性能测试,下图为 long
类型的加密和原生的性能对比,这里我们可以看到,Get
接口与原生访问的性能在同一个数量级内,而 EncryptLong encrypt = 0
和 encrypt.Set(0)
这两种使用方式存在一个数量级的差距
那么在实际使用时就需要注意了,需要频繁修改的场景,请务必使用
Set
接口,不那么频繁的可以直接使用=
赋值
最后
我们做这个内存加密的意义是为了提高游戏内存修改的门槛,即使使用了这个加密方案,也并不能认为 100% 没有问题
市面上还有一些加密方案是做了一些偏移,可以做到感知内存被修改了,但是当这片内存被搜索出来的那一刻,事情就不对了,例如 CE1 可以反向找出哪段代码对这片内存进行了修改,这样攻击者就可以通过 hook
等方法对这片代码地址进行注入,从而快速的修改你游戏的逻辑
参考
- 感谢你赐予我前进的力量