游戏里有些数值非常敏感:
金币、钻石、战斗属性、关键计数......
如果直接使用普通 int:
bash
public int mGold = 10000;
内存里就是一个非常明确的 10000。
一些简单的内存修改器只需要搜索数值、修改数值、再次筛选,就可能定位到目标地址。
MyFramework 中的 SafeInt / SafeLong / SafeFloat 没有试图让客户端数据"绝对不可修改",而是做了一件更加现实的事情:
不让真正的数据以固定明文、固定位置、固定密钥长期存在,同时主动检测内存是否被异常修改。
项目地址:
一、一个 int,实际用了 8 个密文位置
SafeInt 内部不是:
bash
private int mValue;
而是准备了 8 个密文槽:
bash
private int sidhgsg;
private int isdhy34;
private int hibsd;
private int sgkn;
private int jgo2;
private int hgwikg;
private int gnwe;
private int gwijhge;
再用:
bash
private int wghwe;
记录当前真正的数据到底藏在哪一个位置。
每次 set():
bash
wghwe = randomInt(0, 7);
因此同一个变量连续写入:
bash
100
100
100
100
真实密文位置都可能不同:
bash
第1次 → Slot 6
第2次 → Slot 1
第3次 → Slot 7
第4次 → Slot 3
修改器就不能长期盯住某个固定字段。
二、真正存进去的也不是原始值
写入时先重新生成两个随机密钥:
bash
gikoweg = randomInt(mValue0, mValue1);
woghjwe = randomInt(mValue2, mValue3);
真正存储的值经过多层运算:
bash
int newValue =
(value ^ (gikoweg - (gikoweg >> 2))) +
(woghjwe ^ 0x1238) -
(woghjwe ^ 0xFF123);
所以业务值:
bash
10000
内存里并不会长期出现:
bash
10000
而会随着每次 set() 的随机密钥发生变化。
也就是说,同样写入 10000 两次,最终看到的密文通常也完全不同。
三、连密钥自己都要做合法性校验
如果攻击者不改密文,直接尝试修改密钥呢?
MyFramework 又给两个密钥增加了一条约束:
bash
gikoweg =
(gikoweg + woghjwe) /
mValue5 *
mValue5 -
woghjwe;
其中 SafeInt:
bash
mValue5 = 437;
最终必须满足:
bash
(gikoweg + woghjwe) % 437 == 0
读取数据前先检查:
bash
if (gikoweg < mValue0 ||
gikoweg > mValue1 ||
woghjwe < mValue2 ||
woghjwe > mValue3 ||
(woghjwe + gikoweg) % mValue5 != 0)
{
mGameFrameworkHotFix.onMemoryModified(...);
}
所以密钥不仅要"看起来是个 int",还必须:
范围正确 + 两个密钥之间的数学关系正确。
四、解密以后,还要再做第二次校验
仅仅检查密钥还不够。
写入真实值时还保存了一份校验数据:
bash
gikowjeg = value ^ woghjwe;
读取时先解密:
bash
int curValue =
(value -
(woghjwe ^ 0x1238) +
(woghjwe ^ 0xFF123))
^
(gikoweg - (gikoweg >> 2));
再检查:
bash
if (curValue !=
(gikowjeg ^ woghjwe))
{
mGameFrameworkHotFix.onMemoryModified(...);
}
因此一次 get() 实际经历:
bash
检查密钥范围
↓
检查密钥数学关系
↓
找到真正密文槽
↓
解密
↓
用另一份校验数据验证
↓
返回真实值
攻击者只改中间任意一个字段,都可能导致校验失败。
五、每写一次,整套密钥都会换掉
这一点很关键。
set() 不是使用固定 Key:
bash
Value
↓
固定密钥
↓
Cipher
而是:
bash
每次set()
↓
生成新Key0
↓
生成新Key1
↓
重新选择8个槽中的一个
↓
生成新的Cipher
因此即使游戏数值完全没变,只要重新赋值,内存特征也会变化。
这比简单的:
bash
mValue = value ^ 12345678;
要难直接搜索得多。
六、MostSafeInt:觉得一份还不够,再存两套
框架还有一个更加激进的:
bash
public struct MostSafeInt
{
private SafeInt erhgre;
private SafeInt gwegihweg;
}
设置时两份一起写:
bash
public void set(int value)
{
gwegihweg.set(value);
erhgre.set(value);
}
读取时两边分别解密:
bash
int curValue = gwegihweg.get();
int checkValue = erhgre.get();
if (curValue != checkValue)
{
mGameFrameworkHotFix.onMemoryModified(...);
}
于是攻击者不仅要让一个 SafeInt 内部所有校验成立,还得保证:
bash
SafeInt A
==
SafeInt B
两套随机密钥、两套随机槽位最终必须解出同一个值。
七、Float 也不是直接把 IEEE 754 放在内存里
SafeFloat 会先把浮点数整数化:
bash
int newValue =
(round(value * 1000) ^
(qwfb - (qwfb >> 2))) +
(qgoqjg ^ 0x1238) -
(qgoqjg ^ 0xFF123);
也就是保留约 3 位小数后,再进入和 SafeInt 类似的密文体系。
校验数据则使用:
bash
asgihfasg =
(int)(value * 10000) ^ qgoqjg;
读取以后再通过误差范围比较。
所以框架目前提供:
bash
SafeInt
SafeLong
SafeFloat
MostSafeInt
MostSafeLong
MostSafeFloat
八、安全是有成本的,而且成本并不小
普通:
bash
int = 4 Byte
而一个 SafeInt 内部有 12 个 int:
bash
8个密文
1个当前下标
2个密钥
1个校验值
理论数据量就是:
bash
48 Byte
MostSafeInt 又包含两个 SafeInt:
bash
约96 Byte
所以这种类型绝对不应该无脑替换项目中所有 int。
更合理的用法是只保护真正敏感的数据:
bash
金币
付费货币
重要战斗属性
关键次数
客户端必须暂存的敏感值
而不是拿它存:
bash
数组下标
UI页签
普通动画状态
临时循环变量
另外当前实现依赖随机数,并明确限制:
只能在主线程使用。
九、它不是"客户端绝对安全"
这一点必须说清楚。
加密算法最终仍然存在于客户端代码中,足够有能力的攻击者可以反编译、调试甚至绕过检测。
所以 SafeInt 的定位不是密码学意义上的安全存储,也不能取代服务器权威校验。
它真正解决的是:
bash
明文搜索变困难
↓
固定地址修改变困难
↓
固定密文搜索变困难
↓
随意修改Key容易触发校验
↓
随意修改Cipher容易触发校验
↓
检测到异常后统一上报
最终异常都会进入:
bash
mGameFrameworkHotFix.onMemoryModified(
flag,
param0,
param1,
param2,
param3);
再由项目自己决定后续处理。
所以这套设计真正的核心不是:
"我把一个 int 加密了。"
而是:
我让真实值、密文位置和密钥不断变化,再用多层冗余信息检查这些数据有没有被人动过。
对于客户端防简单内存修改来说,这比一个永远躺在固定地址里的普通 int,已经完全是两回事了。
不过说真的,有点用,但不多