第二十四章:逆向工程中的时间与随机数

时间与随机数,这两个东西在游戏里太常见了。计时器控制着技能冷却、Buff持续时间、倒计时任务;随机数则决定了暴击、掉落、抽卡结果。我敢说,百分之九十的游戏逆向需求,最后都会落到这两个点上。

今天我们就来聊聊,怎么把游戏里的计时器和随机数生成器给“拆”了。嗯,顺便教你怎么让随机数变得“不那么随机”。

一、游戏中的计时器逆向

计时器在内存里长什么样?说白了,就是一个不断变化的数值。可能是毫秒、秒,也可能是帧数。我见过最离谱的,是用浮点数存时间,精度高得离谱。

1.1 计时器的常见实现方式

游戏开发者一般用三种方式实现计时器:

  • 系统时间差:每次更新时取当前系统时间,减去上次记录的时间。这种计时器在内存里通常是一个DWORDQWORD,单位是毫秒。
  • 帧计数器:每渲染一帧就加1。这种计时器跟帧率挂钩,帧率高就跑得快。我遇到过一款老游戏,60帧下技能冷却10秒,改成30帧就变成20秒了——典型的帧计时器。
  • 自定义计时器:开发者自己维护一个递减或递增的变量。这种最容易被我们利用,因为变量就在那里,改就完了。

1.2 定位计时器变量

定位计时器的方法,我习惯用“变化扫描法”。举个例子:

  1. 游戏里有个技能冷却倒计时,显示“5秒”。
  2. 暂停游戏(如果支持),用CE扫描当前值。
  3. 等1秒,扫描减少后的值。
  4. 重复几次,直到找到唯一地址。

但这里有个坑——很多游戏用浮点数存时间。比如5.0秒,实际内存里是0x40A00000。你直接搜整数5是搜不到的。我建议你直接搜浮点数,或者用“未知初始值”+“减少的值”组合扫描。

小技巧: 如果计时器是递减的,你可以先搜一个较大的值(比如9999),然后等它减少,再搜“减少的值”。反复几次,地址就出来了。

1.3 修改计时器

找到地址后,修改就简单了。把递减的计时器改成0,技能立刻冷却完毕。或者直接冻结这个地址,让计时器永远不走。

我曾经逆向一款MMO,发现它的技能冷却计时器是“服务器+客户端”双重校验。客户端改了没用,服务器会强制同步。怎么办?我找到了客户端的计时器显示变量——只改显示,不改实际冷却。这样服务器以为还在冷却,但客户端显示已经好了。虽然不能真正无CD,但至少骗过了自己的眼睛。

注意: 修改计时器可能导致游戏逻辑异常。比如你把倒计时改成负数,游戏可能直接崩溃。建议先改成0或1,不要改得太离谱。

二、随机数生成器的逆向

随机数,其实是伪随机。游戏里用的都是伪随机数生成器(PRNG)。只要知道种子和算法,就能预测下一个随机数。这就是我们逆向的突破口。

2.1 常见的随机数算法

算法 特征 逆向难度
线性同余(LCG) 公式:X(n+1) = (a * X(n) + c) % m
梅森旋转(MT19937) 624字节的状态数组
Xorshift 异或和移位操作
系统API(rand/srand) 调用C标准库 极低

我个人最常遇到的是LCG和系统rand。为什么呢?因为简单,开发者随手就用。梅森旋转虽然随机性好,但实现复杂,中小型游戏很少用。

2.2 定位随机数生成器

定位随机数生成器,我一般用“断点法”:

  1. 在游戏里触发一个随机事件,比如抽卡、暴击。
  2. 用CE附加调试器,在randsrandRandom等API上下断点。
  3. 观察调用栈,找到游戏自己的随机数封装函数。

如果游戏没有调用系统API,而是自己实现了随机数算法,那就得靠特征搜索了。比如LCG的乘法常数0x41C64E6D(这是GBA游戏常用的LCG常数),搜这个立即数就能定位到算法。

核心思路: 随机数生成器在内存里一定有一个“状态变量”。LCG是一个32位整数,MT19937是624个32位整数。找到这个状态,就等于控制了随机数。

2.3 修改随机数结果

修改随机数结果,有三种常见手法:

  • 修改种子:把种子改成固定值,比如0。这样每次生成的随机数序列都一样。我见过一个抽卡游戏,把种子改成0后,每次十连抽结果完全一样——因为种子没变。
  • 修改状态:直接修改随机数生成器的当前状态。比如LCG的状态变量,改成你想要的值,下一个随机数就是你指定的。
  • Hook随机数函数:用代码注入,让随机数函数永远返回最大值。比如暴击判定,让rand() % 100永远返回99,暴击率直接拉满。

我曾经逆向一款卡牌游戏,它的抽卡逻辑是这样的:先调用Random()生成一个0-10000的整数,然后查表决定稀有度。我直接Hook了Random()函数,让它永远返回0。结果呢?每次抽卡都是最高稀有度。嗯,那天的服务器日志估计很精彩。

三、实战:修改游戏中的随机掉落

光说不练假把式。我们拿一个假设的场景来实操:

游戏里打怪有概率掉落稀有装备。我们想让它必掉。

3.1 分析流程

掉落判定一般是这样:

  1. 怪物死亡时,调用随机数生成器。
  2. 生成一个0-99的整数。
  3. 如果数值小于稀有掉落阈值(比如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,直接跳转到掉落逻辑。

避坑指南: 我曾经直接改jgejmp,结果游戏崩溃了。为什么?因为掉落逻辑里还有后续判断,比如背包满了要丢弃。直接跳过去绕过了这些检查,导致内存访问异常。所以,改比较值比改跳转更安全。

四、SVG:时间与随机数逆向知识体系

时间与随机数逆向知识体系 计时器逆向 随机数逆向 系统时间差 帧计数器 自定义计时器 LCG算法 MT19937 Xorshift 系统API 修改计时器值 修改随机数种子 Hook随机数函数 目标:控制时间与随机数,掌控游戏逻辑

五、总结

时间与随机数,说白了就是游戏逻辑的两个“开关”。计时器控制着“什么时候发生”,随机数控制着“会不会发生”。

逆向计时器,核心是找到那个不断变化的变量。逆向随机数,核心是找到那个状态变量或种子。一旦你控制了这两个东西,游戏里的冷却、掉落、暴击、抽卡,都成了你的提线木偶。

嗯,最后提醒一句:这些技术请用在单机游戏或你拥有权限的测试环境中。联网游戏修改随机数,轻则封号,重则吃官司。我见过太多人因为改抽卡概率被永久封禁了。

好了,这一章就到这里。记住我说的:找到变量,控制变量,你就赢了。


公众号:蓝海资料掘金营,微信deep3321