第二十四章:逆向工程中的时间与随机数
时间与随机数,这两个东西在游戏里太常见了。计时器控制着技能冷却、Buff持续时间、倒计时任务;随机数则决定了暴击、掉落、抽卡结果。我敢说,百分之九十的游戏逆向需求,最后都会落到这两个点上。
今天我们就来聊聊,怎么把游戏里的计时器和随机数生成器给“拆”了。嗯,顺便教你怎么让随机数变得“不那么随机”。
一、游戏中的计时器逆向
计时器在内存里长什么样?说白了,就是一个不断变化的数值。可能是毫秒、秒,也可能是帧数。我见过最离谱的,是用浮点数存时间,精度高得离谱。
1.1 计时器的常见实现方式
游戏开发者一般用三种方式实现计时器:
- 系统时间差:每次更新时取当前系统时间,减去上次记录的时间。这种计时器在内存里通常是一个
DWORD或QWORD,单位是毫秒。 - 帧计数器:每渲染一帧就加1。这种计时器跟帧率挂钩,帧率高就跑得快。我遇到过一款老游戏,60帧下技能冷却10秒,改成30帧就变成20秒了——典型的帧计时器。
- 自定义计时器:开发者自己维护一个递减或递增的变量。这种最容易被我们利用,因为变量就在那里,改就完了。
1.2 定位计时器变量
定位计时器的方法,我习惯用“变化扫描法”。举个例子:
- 游戏里有个技能冷却倒计时,显示“5秒”。
- 暂停游戏(如果支持),用CE扫描当前值。
- 等1秒,扫描减少后的值。
- 重复几次,直到找到唯一地址。
但这里有个坑——很多游戏用浮点数存时间。比如5.0秒,实际内存里是0x40A00000。你直接搜整数5是搜不到的。我建议你直接搜浮点数,或者用“未知初始值”+“减少的值”组合扫描。
1.3 修改计时器
找到地址后,修改就简单了。把递减的计时器改成0,技能立刻冷却完毕。或者直接冻结这个地址,让计时器永远不走。
我曾经逆向一款MMO,发现它的技能冷却计时器是“服务器+客户端”双重校验。客户端改了没用,服务器会强制同步。怎么办?我找到了客户端的计时器显示变量——只改显示,不改实际冷却。这样服务器以为还在冷却,但客户端显示已经好了。虽然不能真正无CD,但至少骗过了自己的眼睛。
二、随机数生成器的逆向
随机数,其实是伪随机。游戏里用的都是伪随机数生成器(PRNG)。只要知道种子和算法,就能预测下一个随机数。这就是我们逆向的突破口。
2.1 常见的随机数算法
| 算法 | 特征 | 逆向难度 |
|---|---|---|
| 线性同余(LCG) | 公式:X(n+1) = (a * X(n) + c) % m | 低 |
| 梅森旋转(MT19937) | 624字节的状态数组 | 中 |
| Xorshift | 异或和移位操作 | 低 |
| 系统API(rand/srand) | 调用C标准库 | 极低 |
我个人最常遇到的是LCG和系统rand。为什么呢?因为简单,开发者随手就用。梅森旋转虽然随机性好,但实现复杂,中小型游戏很少用。
2.2 定位随机数生成器
定位随机数生成器,我一般用“断点法”:
- 在游戏里触发一个随机事件,比如抽卡、暴击。
- 用CE附加调试器,在
rand、srand、Random等API上下断点。 - 观察调用栈,找到游戏自己的随机数封装函数。
如果游戏没有调用系统API,而是自己实现了随机数算法,那就得靠特征搜索了。比如LCG的乘法常数0x41C64E6D(这是GBA游戏常用的LCG常数),搜这个立即数就能定位到算法。
2.3 修改随机数结果
修改随机数结果,有三种常见手法:
- 修改种子:把种子改成固定值,比如0。这样每次生成的随机数序列都一样。我见过一个抽卡游戏,把种子改成0后,每次十连抽结果完全一样——因为种子没变。
- 修改状态:直接修改随机数生成器的当前状态。比如LCG的状态变量,改成你想要的值,下一个随机数就是你指定的。
- Hook随机数函数:用代码注入,让随机数函数永远返回最大值。比如暴击判定,让
rand() % 100永远返回99,暴击率直接拉满。
我曾经逆向一款卡牌游戏,它的抽卡逻辑是这样的:先调用Random()生成一个0-10000的整数,然后查表决定稀有度。我直接Hook了Random()函数,让它永远返回0。结果呢?每次抽卡都是最高稀有度。嗯,那天的服务器日志估计很精彩。
三、实战:修改游戏中的随机掉落
光说不练假把式。我们拿一个假设的场景来实操:
游戏里打怪有概率掉落稀有装备。我们想让它必掉。
3.1 分析流程
掉落判定一般是这样:
- 怪物死亡时,调用随机数生成器。
- 生成一个0-99的整数。
- 如果数值小于稀有掉落阈值(比如5),则掉落稀有装备。
我们的目标:让随机数永远小于5。
3.2 定位随机数函数
用CE附加游戏进程,在rand上下断点。打一只怪,断点触发。查看调用栈,找到游戏自己的掉落判定函数。
假设我们找到了这样一个函数:
00401234: call dword ptr [00405000] ; 调用随机数函数
0040123A: mov ecx, eax
0040123C: xor edx, edx
0040123E: mov eax, ecx
00401240: mov ecx, 64 ; 100
00401245: div ecx ; eax = 随机数 % 100
00401247: cmp eax, 05 ; 跟5比较
0040124A: jge 00401260 ; 如果>=5,跳过掉落
3.3 修改方案
最简单的改法:把cmp eax, 05改成cmp eax, 64(100)。这样只要随机数小于100就掉落,也就是必掉。
或者更暴力一点:把jge改成jmp,直接跳转到掉落逻辑。
jge为jmp,结果游戏崩溃了。为什么?因为掉落逻辑里还有后续判断,比如背包满了要丢弃。直接跳过去绕过了这些检查,导致内存访问异常。所以,改比较值比改跳转更安全。
四、SVG:时间与随机数逆向知识体系
五、总结
时间与随机数,说白了就是游戏逻辑的两个“开关”。计时器控制着“什么时候发生”,随机数控制着“会不会发生”。
逆向计时器,核心是找到那个不断变化的变量。逆向随机数,核心是找到那个状态变量或种子。一旦你控制了这两个东西,游戏里的冷却、掉落、暴击、抽卡,都成了你的提线木偶。
嗯,最后提醒一句:这些技术请用在单机游戏或你拥有权限的测试环境中。联网游戏修改随机数,轻则封号,重则吃官司。我见过太多人因为改抽卡概率被永久封禁了。
好了,这一章就到这里。记住我说的:找到变量,控制变量,你就赢了。
公众号:蓝海资料掘金营,微信deep3321