欢迎光临散文网 会员登陆 & 注册

口袋妖怪绿宝石——数据提取与代码分析(6-反作弊机制:跳动的指针)

2022-08-20 16:51 作者:围巾胖头鱼  | 我要投稿

说在前面:

    有了前面几期专栏的基础,我们现在回过头来再看看绿宝石游戏中常见的金手指修改的究竟是什么。另一个系列的专栏究极绿宝石5-金手指原理介绍主要是从数据对比的角度来理解金手指,现在可以从源代码的角度来分析了。

    本期专栏主要介绍源代码项目中的两个符号:gSaveblock1和gSaveblock2。这两个变量覆盖的范围是绿宝石系列游戏金手指的重灾区,但是在原版游戏中,它们的地址是不确定的,这给查找金手指带来了一定的困难。

存档信息SaveBlock

    回想一下,在使用绿宝石游戏中的“保存”功能(而不是模拟器提供的“快速保存”功能)时,有哪些游戏数据会被保存到存档文件中呢?

    太多太多了,队伍中的精灵、背包里的道具、当前的剧情进度……这些都需要保存下来,否则下次就无法继续游戏了。在绿宝石源代码项目中,存档数据有很大一部分保存在gSaveblock1和gSaveblock2这两个变量中,它们的类型分别是struct SaveBlock1和struct SaveBlock2,是两个结构体变量。下面统一用SaveBlock类型来称呼它们。

    先来在源代码项目中看看struct SaveBlock1的定义:

SaveBlock1的定义(部分)

    每行的前面,源代码都“贴心地”给出了结构体成员变量的相对地址。从上面截图里的变量来看,许多金手指已呼之欲出:

  • 933行的location,角色当前所在的地图,瞬移金手指。

  • 945行的playerParty,队伍中的精灵信息,修改精灵的一系列金手指。

  • 946行的money,拥有的金钱,修改金钱的金手指。

  • 947行的coin,拥有的游戏厅代币数,修改游戏厅代币的金手指。

  • 949行的pcItems,电脑中的道具仓库,修改电脑道具的金手指。

  • 950~954行,各个背包,修改背包道具的金手指。

  • 956行的seen1,精灵图鉴中发现了哪些精灵,全图鉴发现的金手指。

    继续向下看,还能看到:

  • 970~977行,各种装饰和家具,获得装饰或家具的金手指。

  • 1012行的seen2,精灵图鉴中捕捉了哪些精灵,全图鉴捕捉的金手指。

  • ……

    再来看看SaveBlock2:

SaveBlock2的定义(部分)

    同样也有好多金手指:

  • 475行的playerName,角色名称,修改角色名称的金手指。

  • 478行的playerTrainerId,训练家ID(会显示在训练师卡片上),修改角色ID的金手指。

  • 479~481行,已经进行了多长时间的游戏,修改训练师卡片上游戏时间的金手指。

  • ……

    上面只是列举了一部分网上常见的金手指,可以看到,SaveBlock类型定义的这两个变量的确是金手指的“重灾区”,如果能弄明白里面每条数据的含义,这该会有多少条金手指!

尝试金手指之前的一点提示

    熟悉前几期专栏的读者可能会迫不及待地到游戏中去试试这些金手指了。熟练的操作可以是:在符号表中找到gSaveBlock1和gSaveBlock2所在的地址、用结构体定义中给出的相对地址计算出来金手指应该修改哪个地址处的数据、金手指制作成功!

    然而实际上很有可能并不会成功,原因在接下来会逐步分析。

    首先,符号表中确实可以找到gSaveBlock1和gSaveBlock2:

gSaveblock变量所在地址

    注意这两个变量的地址是02开头的,说明它们不是ROM中的内容。对哪部分地址代表什么含义还不太熟悉的读者可以参考究极绿宝石5.3——科普向,什么是金手指(五)里面内存视图这个概念。

    这里显示gSaveblock1在02025a00处,但是如果有的读者真正到游戏中去这个地址查看,它本应该对应SaveBlock1中定义的第一个变量:pos,含义是角色在当前地图中的坐标。如果角色到处走动的话,这里的数值应该会变化的。而实际上可能并没有,是哪里出了问题呢?

    下面两张图为作者在一次游戏中的测试:

测试1:02025A44处为0004
测试2:02025A44处为0005

    可以看到,角色向右走动了一步之后,02025A00处的数字并没有发生变化,反而是02025A44处的0004变成了0005,这正好是角色X坐标在当前地图中的变化。但是,02025A44这个地址也不是固定的,只要保存游戏之后再加载一次存档,这个地址就又变了。

    这说明,哪怕这一次把金手指改成有效果的版本,下一次可能就不起作用了。有过探索金手指经验的读者们很有可能会碰到这种问题。

    怎么从原理上来解释这样的现象呢?

跳动的指针

    有些细节值得我们仔细观察:

SaveBlock1定义的结尾

    上图为SaveBlock1这个结构体定义结尾的部分,有一行注释:

    这行注释表明SaveBlock1需要占据0x3D88个字节,但它和符号表里的信息是不符的:

符号表中的gSaveBlock1

    符号表中的gSaveBlock1占据了0x3e08个字节,比0x3D88多出了0x80个字节,这多出来的字节是用来做什么的呢?

    从刚才作者举的游戏中的例子里可以看到,原本的02025A00地址变成了02025A44,地址向后偏移,但是为了把全部0x3D88的信息存储下来,它不能占用下一个变量(也就是gPokemonStorage)的空间,这就需要预留一部分的空间(或者叫缓冲区)来避免这个变量的数据覆盖了下一个变量的数据。多出来的0x80个字节就是作为缓冲区使用的。

    那么gSaveBlock1的地址真的就这么“飘忽不定”,难以寻觅了吗?

    我们从游戏开发者的角度来想想,开发者肯定有办法找到它的地址,否则代码就没法写了。现在我们有了源代码项目,可以从中找到定位gSaveBlock1的关键。

    在符号表中搜索gSaveBlock1,会发现有两个搜索结果,一个就是gSaveBlock1本身,还有一个叫做gSaveBlock1Ptr:

gSaveBlock1Ptr

    这种一个变量名后面跟着Ptr的命名方式,表明这个变量是一个指针,它指向Ptr前面那个名称的变量。还是以刚才作者在游戏里的角色举例,查看一下03005D8C处的变量是什么:

0305D8C处的变量

    这里的变量是02025A44,正好就是之前我们得到的gSaveBlock1应该所处的地址!

    这就是跳动的指针,由一个变量APtr来保存另一个变量A的地址,APtr保存的数值可能会变化,这就导致了针对A变量的金手指无法一直起到作用。

    有的读者也许会问,这个gSaveBlock1Ptr所在的地址会不会变呢?也就是说,有没有gSaveBlock1PtrPtr这种东西?在符号表里是找不到的,这里游戏的开发者们的套娃只有一层,但就是这一层套娃,也是对抗金手指的一个有效办法,或者说,这是反作弊的一个常用手法。

    跳动的指针是反作弊的一种常用技术。

让指针不再跳动

    有句俗语叫“魔高一尺道高一丈”,有时它也会反过来,叫成“道高一尺魔高一丈”。“道”和“魔”究竟谁高?其实这是一个此消彼长,“三十年河东三十年河西”的变化过程,两种说法都有道理,体现的是辩证关系……(随口一说的哲学不要当真)

    绿宝石游戏出现了,金手指就出现了。金手指一出现,反作弊的手段(例如跳动的指针)就出现了。反作弊的手段一出现,就会有“反”反作弊的方法来应对。只要有人愿意探索,这个过程就会一直持续下去。

    确实存在这么一条金手指,可以让跳动的指针固定下来,不再跳动。这条金手指一旦开启,所有之前由于指针跳动而无法使用的金手指就都可以用了。网上有的人给这类金手指起名叫做“解限码”,含义是解除了反作弊机制的限制。

    首先找一下,源代码项目里边是怎么设置gSaveBlock1Ptr这个变量的。如果直接在VS Code中使用“编辑——在文件中查找”来查找gSaveBlock1Ptr的话,会看到上千个搜索结果:

上百个文件内的上千个搜索结果

    因为gSaveBlock1Ptr的使用实在是太频繁了,如果要找到它被赋值的那条语句,上千条搜索结果还是太多了。那是不是把赋值用的等号“=”放在变量名后面搜索就可以了呢?

加上赋值运算符“=”之后的搜索结果

    为了避免不知道变量gSaveBlock1Ptr和等号“=”之间隔开了几个空格,还特意用了正则表达式中的\s*来应对这种情况。现在只有一条搜索结果,位于load_save.c文件的第118行,但是它并不是对gSaveBlock1Ptr赋值,而是对它指向的内容赋值(前面有一个星号“*”运算符)。

    不过这一段代码可以通过它的注释大致了解它的功能:从103行开始,先把几个指针指向的内容暂存一下,然后在第110行重新设置每个指针的位置(注释“change saveblock's pointers”),最后在第117行开始把之前暂存的内容恢复到这些指针指向的变量中。看来问题的关键在于第110行调用的函数SetSaveBlocksPointers。

    转到该函数的定义:

SetSaveBlocksPointers的定义

    终于发现了这些指针是怎么“跳动”的了。关键在于第74行调用了Random函数,生成了一个随机数,随后在第76~78行,为每个Ptr变量设置了一个随机偏移offset,这就是每次打开游戏这些指针会跳动的原因!

    如果想让这些指针的位置固定不变,在C代码里面只需要在第76行之前把offset设置为0就可以了,比如把第74行改成这样:

    但是,如果我们用金手指的话,面对的就不是C代码,而是汇编代码。并且这种金手指会不可避免地修改ROM,在究极绿宝石5.3——科普向,什么是金手指(五)里提到过,使用这种金手指相当于修改游戏文件本身,也就相当于对游戏进行改版了。

    从符号表中查到SetSaveBlocksPointers的地址位于08076bdc处,在VBA的反汇编器里跳转到这个位置查看:

SetSaveBlocksPointers的汇编代码

    有了上期专栏分析汇编代码的基础,我们可以把汇编代码和C代码进行对应,找找看给offset这个变量赋值的语句对应到哪些汇编代码。首先注意这一行:

    可以通过符号表查到$0806f5cc就是Random函数,这个函数有一个返回值,就是该函数生成的一个随机数。

    绿宝石ROM的汇编代码中,有返回值的函数,默认将返回值放在r0寄存器中。

    然后看下面三行代码:

    第一行是在r4上加上r0的值,第三行的and指令是“按位与”操作,也就是C语言中的“&”运算符,把r4和0x7C进行按位与操作。这几行汇编代码对应的就是源代码第74行的:

    随机数函数Random的调用、加上随机数、按位与等等要素都能匹配得上,并且可以验证SAVEBLOCK_MOVE_RANGE的定义是128,也就是0x80,而0x80-4 = 0x7C,按位与的第二个操作数也是对得上的。顺便一提,SAVEBLOCK_MOVE_RANGE的数值0x80和上一小节内提到的“多出来的0x80个字节就是作为缓冲区使用的”恰好吻合。

    汇编代码再往下,就是对各个Ptr变量赋值了,因此只需要把08076bee处的汇编代码修改成让offset等于0就大功告成了,也就是:

    现在就需要在线转换网站完成最后一步:把汇编代码翻译为机器代码:

在线网站的翻译功能

    之前我们的用法是把字节序列翻译为汇编代码,那时在线网站左上角写的是“HEX to ARM”,只要点击一下就可以把它变成“ARM to HEX”,也就是从汇编代码转换到字节序列。结果显示在右下角的THUMB一栏内,是0024。

    因此,这条固定指针位置的金手指就是:

    格式是原始代码,注意按照小端序拼接后,顺序会反过来。

    这条代码的含义是:将原本SetSaveBlocksPointers函数中的一条语句“and r4, r0”替换为另一条语句“mov r4, 0”。

关于修改ROM程序金手指的一点思考

    上面那种金手指修改的是游戏逻辑,更准确地来讲,它修改的是游戏代码。有一个值得注意的现象:为了实现指针跳动这个功能,需要在空间上预留好缓冲区,还需要写一个函数来调用随机数函数,外加几个赋值操作——没有十几行C代码无法完成,而变成汇编代码行数会更多;但是,让这个功能失效只需要修改一行汇编代码。

    这就好比:盖好一座大楼可能需要几年的时间,但是炸塌一座大楼一天就够用了。

    这类修改ROM程序的金手指(修改ROM的金手指还包括修改ROM数据的金手指)大多具备这样的特点,说的好听一些叫“四两拨千斤”,说的难听一点就是“动一动小指头就让开发者的辛苦付之东流”。这也是本系列专栏尽管时时提到改版游戏,但却并没有把任何一个改版游戏拿来“开刀”的原因。毕竟这种需要费一些力气才能实现出来的反作弊机制可以被一行或者两行代码击破,可能在心理上不太好接受。

    另外,修改程序的金手指在使用时必须非常小心。修改数据的金手指如果地址不对,后果可能还不是太严重,但是修改程序的金手指一旦地址不对,一般来说整个游戏都毁了。各种卡档、坏档的罪魁祸首十有八九就是这种修改程序的金手指,所以它对探索金手指的知识储备要求较高。

    “跳动的指针”只是反作弊机制的一种,原版绿宝石还有其他的反作弊机制,将会在下一期专栏介绍。

    再次感谢众位读者的支持!


口袋妖怪绿宝石——数据提取与代码分析(6-反作弊机制:跳动的指针)的评论 (共 条)

分享到微博请遵守国家法律