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

口袋妖怪绿宝石——数据提取与代码分析(8-反作弊机制:校验码与数据打乱)

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

说在前面:

    前两期专栏介绍了原版绿宝石游戏反作弊机制的两种方法:指针跳动和数据加密。本期专栏继续介绍绿宝石游戏中其他的反作弊机制:校验码与数据打乱。

首宠修改

    在绿宝石系列游戏中,对队伍首位精灵(简称“首宠”)进行修改的金手指也是网上各种发布的金手指中的一大类,无论是修改技能、等级、道具、性格,还是修改个体值、努力值、亲密度、特性,甚至是修改闪光,基本上都是以首宠为准。接下来我们会看到,原版绿宝石把针对首宠修改的反作弊机制做得十分复杂,不仅对数据进行了加密,还通过校验码和数据打乱的方式进一步增加反作弊的力度。

    先来看看原版绿宝石中的队伍首位精灵信息在什么地方:

队伍精灵信息

    符号表中的gPlayerParty就是游戏中主角队伍里的精灵信息,转到它的定义:

gPlayerParty的定义

    从定义中可以看到,队伍信息gPlayerParty是一个大小为6的数组(PARTY_SIZE是6),类型是结构体struct Pokemon,再转到这个结构体的定义:

结构体Pokemon的定义

    结构体struct Pokemon里面,开头是一个类型为struct BoxPokemon的变量;它的含义是存储在电脑仓库内的精灵信息;换句话说,从第121行开始向下的这些变量,它们会出现在描述队伍精灵的信息里面,而不会出现在描述电脑仓库内的精灵信息里,我们马上就能看到什么样的信息会满足这样的条件。

    上图的右半边是struct BoxPokemon的定义,里面有很多可以被金手指修改的属性:

  • personality,精灵性格

  • otId,精灵主人的训练师ID

  • nickname,精灵昵称

  • otName,精灵主人的名称

    而在第111行定义了一个联合体变量union secure,这个变量需要好好解释一下。在解释之前,这里把之前的一期专栏(究极绿宝石5.3——科普向,什么是金手指(七))的图借用一下:

精灵数据表(来自之前的专栏)

    究极绿宝石作为绿宝石系列游戏的改版,在描述队伍精灵的信息上只做了一些小的改动,大体的框架还是和原版绿宝石相同的。有一处小的改动在相对地址1E处,由于原版绿宝石所处的第三世代还没有隐藏特性(另一种说法叫“梦特性”)这个设定,所以在原版绿宝石中就没有“是否是隐藏特性”这个变量。

    这个表格和上面的结构体定义对比一下,可以发现表格中绿色的部分就是struct BoxPokemon的范围,也就是“非面板值”,除此之外就是下面的蓝色部分“面板值”。在该图片出现的专栏中提到过:

图中绿色的区域是非面板值,蓝色的区域是面板值,也就是说蓝色区域中的变量取值是由绿色区域中的变量取值用游戏程序计算得到的。反映在游戏中,就是说如果使用金手指修改面板值(蓝色区域)的变量,那么将这只精灵放回电脑、在精灵中心回复、与训练师对战时,蓝色区域的变量会根据绿色区域的变量重新计算,因此修改蓝色区域变量的金手指实际上是起不到作用的。

    之所以放入电脑仓库中的精灵会像在精灵中心回复过一样,就是因为电脑仓库只会保留非面板值,也就是struct BoxPokemon的信息,后面的信息(面板值)都是从仓库中拿到队伍里面那一刻计算出来的。

    不过,在上面的非面板值中,还有好多信息(种族、道具等)没有在struct BoxPokemon的定义中看到,同时那个神秘的联合体变量secure还没有解释,它是做什么的呢?

精灵信息的存储方式:联合体变量

    C语言中的关键字union用于定义联合体变量,在该关键字包含的范围内,所有变量共用同一块空间,仔细看struct BoxPokemon中联合体变量的定义:

联合体变量secure

    这个联合体变量的名字叫secure,除了它的字面意思“安全”,现在还不清楚它有什么实际意义。这里面定义的两个变量raw和substructs,按照联合体的语法,是要共用同一块空间的,其中raw是一个数组,而substruct又是一个联合体类型的变量,也就是说,union PokemonSubstruct这个联合体中定义的所有变量也要共用同一块空间。

    共用一块空间的意思是说,如果在程序中修改了raw变量的内容,substructs的内容也会改变,有点像是同一个位置的数据拥有两个名字,通过哪个名字修改它都可以。

    不知道读者们看到这里会不会有些迷糊,似乎只看到一个联合体套着一个联合体,也不知道这些联合体是做什么的。要看懂接下来的内容,请务必保持清醒的头脑!

    再来到union PokemonSubstruct的定义处:

联合体PokemonSubstruct的定义

    这个联合体共定义了5个成员变量,前四个就是这个联合体的名字加上0,1,2,3共四个编号的后缀,最后一个也是叫做raw的数组,和上面的secure变量类似。这5个变量共用一块空间,也就是说,如果修改了其中一个变量,比如说type3的内容,那么其他的4个变量内容也会被一并修改。

    Substruct的含义是“子结构体”,下面就用这个名称来称呼这个名字很长的结构体。

    最后来看这些以数字为结尾的子结构体的定义:

子结构体0,1,2,3的定义

    直到此时,我们才看到了熟悉的精灵信息:种族、道具、经验值、亲密度在子结构体0;技能和技能PP数在子结构体1;努力值和选美值在子结构体2;遇到精灵的信息、个体值、特性和缎带在子结构体3。

    这些信息才是我们在上面那张精灵数据表格中看到的信息。原本它们应该按照顺序依次出现在BoxPokemon这个结构体中,可是原版绿宝石做了一些很奇怪的“包装”,让事情一下子变得复杂了起来。现在让我们从最底层的这些子结构体开始,一步一步推测上层的“包装”是做什么用的。回到PokemonSubstruct联合体的定义:

联合体PokemonSubstruct的定义(上面的图搬下来)

    这个被称作“子结构体”的联合体,实际上是代表了4种子结构体中的任意一个,最下面的raw变量起到的是申请空间的作用,为此我们可以看看NUM_SUBSTRUCT_BYTES这个宏定义,这个宏的名称字面意思是“子结构体所需字节数”:

子结构体所需字节数

    子结构体所需字节数,是所有子结构体中最大的那一个所占的字节数(尽管在这里,所有的子结构体是一样大的),这就是上面那些注释想要表达的含义。这样一来,PokemonSubstruct联合体就有了足够的空间可以存下任何一种子结构体,在它看来,这4种子结构体就没有区别了,它们都是占据了一块空间的变量,和数组raw没有区别。

    在往上层,回到BoxPokemon结构体中secure联合体的定义:

联合体变量secure(上面的图搬下来)

    secure联合体占据了4个子结构体的空间(注意substruct后面的[4],说明这是个数组)。

    现在可以做个结论了:存储在仓库中的精灵信息BoxPokemon,除了少部分(如性格、昵称等)信息按照正常的方式作为结构体成员变量存储之外,其他的信息被分割成了4块内容(子结构体),保存在secure联合体中。secure联合体只是分配好了用于存储4个子结构体的空间,但对于其中的子结构体不做区分。

    所有的这一切,都是为了数据打乱这个反作弊机制做准备工作。

从精灵信息的接口中分析反作弊机制

    绿宝石的源代码项目把精灵信息设计得这么复杂,真正在程序中要用的时候,访问某个成员变量不会很麻烦吗?为此,需要介绍一下“接口”的概念。

    接口(Interface)是程序开发的常用术语,放在绿宝石ROM这个语境下,接口可以理解为一种函数(这里就不提接口的专业化定义了),它可以提供一种对数据对象操作功能,让使用者可以不必关心被操作对象的细节。本期专栏提到的接口是对精灵信息进行操作的函数,这里的操作包括读操作和写操作。

    用一个直观的例子来解释,就像家里的下水道坏了需要维修,此时维修师傅的电话就是一个接口,利用这个接口,你可以把师傅请来修好下水道,但是下水道究竟是怎么修好的你可能并不知道,或者并不关心。

    在绿宝石的源代码项目中,有两个函数体现的就是接口的功能:GetMonData和SetMonData。

    这种以Get或者Set开头的函数名称具有典型的接口特点:Get用于读取数据,Set用于写入数据。如果在源代码项目中,“在文件中查找”:

    可以找到接近一千个结果:

GetMonData的查找结果

    而且看它的用法,都是从某个队伍中的精灵提取特定的信息,比如精灵种族(species)、性格(personality)之类。也就是说,对于游戏开发者而言,只需要写好GetMonData这个函数,就不需要再考虑复杂的精灵信息结构体了,它作为一个接口能够正常访问精灵信息就够用了。

    不过对于想要了解精灵信息反作弊机制的我们来说,就得详细地分析一下GetMonData这个函数:

GetMonData函数

        GetMonData函数的主体是一个switch结构,根据函数第二个参数field的不同,取出不同类型的数据。在上图中可以看到,对于面板值而言,游戏程序没有做任何加密,需要什么就直接返回什么。不过,GetMonData仅对部分数据直接返回结果,更多的信息还在它的default case里面:

GetBoxMonData函数

    与GetMonData对应的结构体是Pokemon,与GetBoxMonData对应的结构体是BoxPokemon。上图展示的GetBoxMonData的前面若干行函数,就是原版绿宝石对精灵信息采用的所有反作弊机制。

    其中,第3701~3704行的GetSubstruct对应的是数据打乱,第3706行DecryptBoxMon函数对应的是数据加密,第3708行CalculateBoxMonChecksum对应的是校验码。我们一个一个来说。

数据打乱

    在第3693~3696行,四个子结构体的变量各自定义了一个,准备利用GetSubstruct函数来获得每一个子结构体。可以看到,GetSubstruct函数的第二个参数是personality,也就是精灵性格值,它有什么用,需要到函数内部去看:

GetSubstruct函数

    GetSubstruct函数返回值是一个子结构体,函数主体仍然是一个switch代码块,条件是性格值对24取模的值,共有24种可能:从0到23。每个case用宏定义SUBSTRUCT_CASE来代替,这个宏定义有5个参数,仔细一看不难发现规律,第一个参数从0到23依次枚举下来,而后4个参数则是遍历了0,1,2,3这四个数的全排列。4个数的全排列恰好有24种可能性。

    来到宏定义SUBSTRUCT_CASE的定义:

    这里的宏定义是根据4个数字的某种排列取出对应的子结构体。现在可以举个例子来说明GetSubstruct的含义了:

    假设我们的一只精灵,它的性格值是0x0000001,对24取模之后结果是1,对应的0,1,2,3的排列是0,1,3,2,如果我们想要取出这只精灵的子结构体3,也就是取出PokemonSubstruct3(这里面存的是努力值、个体值、缎带信息等)的信息,就需要到substruct的下标2处去找,这里子结构体的编号3,对应到了联合体的下标2。

    头脑还清醒的读者应该能理解数据打乱的含义了。之所以要把一部分精灵信息拆成4个大小相同的子结构体,就是为了可以支持把它们的顺序打乱(彼此可以相互替代),打乱的规则按照精灵的性格值来确定,是24种排列其中的一种。这样一来,类似精灵个体值这样的数据就再也没有一个固定的地址(用作金手指),因为它和其他数据被混在一起打乱了顺序。

数据加密

    Decrypt的含义是解密,在取出精灵信息之前,需要先对被加密的精灵数据解密,然后才能取出正确的精灵信息。来到这个函数:

DecryptBoxMon函数

    和上期专栏一样,绿宝石ROM的加密方式就是简单的异或操作。使用的加密密钥有两个,一个是精灵主人的训练师ID,另一个是精灵的性格值。这样一来,不同的精灵使用的加密密钥几乎不会相同(性格值共有40多亿种可能性),给通过比对数据找金手指的方法带来极大的困难。

校验码

    哪怕你成功闯过了前两关,如果在校验码检测这一关没有通过,那么你修改的精灵就会变成蛋!

    校验码常用于判断一段数据是否被损坏,举一个很简单的例子,比如有这么一段0/1序列的数据:

    设计一个校验码,把这8个数字加起来再对2取模(这是一种常见的设计校验码的方法,因此校验码有时也被称作校验和),得到的结果是0,这个0就是这串0/1序列的校验码。

    如果数据出错了,例如其中一个0变成了1:

    再计算一遍校验和,会发现计算出来的校验和是1,与正确的校验和0不同,这就说明数据出错了。

    注意:如果校验码错误,能够说明数据一定出错了,但反之却不一定成立。错误的数据也有可能得到正确的校验和(比如上面的0/1序列有两个0变成了1)。一个校验和能够从多大程度上避免这种事情的发生,以及它本身占据的空间大小,是衡量校验和设计好坏的标准。

    从上面的代码可以看出来,绿宝石源代码在提取精灵信息时,如果发现校验码(英文名checksum)不正确,就会把这只精灵变成“坏蛋”(isBadEgg)。不知道有多少绿宝石系列游戏的玩家,在使用首宠修改的金手指时会碰到这种情况:用了金手指之后,队伍首位精灵就变成蛋了。

    计算校验码的函数CalculateBoxMonChecksum如下:

CalculateBoxMonChecksum函数

    绿宝石源代码计算校验码的方式符合传统:校验和,就是把所有的数据都加起来。

破解精灵信息的反作弊机制

    有了前两期专栏的知识基础,相信看到这里的读者们对于怎样用金手指破解这些反作弊机制已经有了自己的想法。

    对于数据打乱,破解的方式就是固定好一种排列顺序(比如按照上面给出的精灵数据表就很不错);对于数据加密,破解的方式就是让加密密钥为0(针对异或加密),或者干脆让程序跳过加密过程;对于校验码,我们甚至都不需要知道校验码是怎么计算的,只需要在判断校验码是不是正确的时候“动一下手脚”就可以了。下面依次来说。

破解数据打乱

    来看GetSubstruct的汇编代码(怎样找到GetSubstruct函数在ROM中的位置?还不熟悉的读者们需要到前两期专栏复习一下了):

GetSubstruct的汇编代码

    除了函数开头熟悉的push指令,我们还可以看看汇编代码是怎么实现C语言的switch结构的。注意这4行:

    前三行是典型的从数组中取数据的汇编代码写法(r1指向数组,r0作为下标);最后一行对pc寄存器赋值,pc寄存器永远指向当前需要执行的指令所在的地址,如果对pc寄存器进行修改,就相当于程序进行了跳转。而这种跳转和一个数组密切相关,说明汇编代码实现switch的方法就是把switch里各种case的跳转地址保存在一个数组内,然后根据判断条件的取值决定跳转到哪个分支。

    既然我们需要把数据打乱的顺序固定下来,只需要让下标r0固定为0就可以了,这可以对0806a28c处的add r0, r0, r1动一下手脚,改成

 这样就相当于总是从r1数组的下标0处跳转,金手指是:

破解数据加密

    来看DecryptBoxMon的汇编代码:

DecryptBoxMon的汇编代码

    里面那两行eor指令如此显眼,一看就是异或加密操作。还有一个比较高级的指令stmia需要介绍一下。

    stmia是由stm命令加上两个后缀i和a得到的,这条指令有两个操作数,指令的含义是将操作数2存入操作数1指向的地址中,并且还需要根据stm指令的后缀修改操作数1的值。具体来说:

    后缀i的含义是Increasement(增加),也就是stm指令在操作数1上加上一个值,由于stm只能存储4字节的数据,所以这个加上去的值只能是4。后缀a的含义是increasement After (在存储操作之后增加),也就是add操作在str之后。

    有了Increasement,就有Decreasement(减小),对应后缀d;有了increasement After,就有increasement Before(在存储操作之前增加),对应后缀b。所以stm指令共有4个版本:stmia,stmib,stmda,stmdb。这里只举一个stmdb的例子:

    注意str指令和对操作数1(r2)进行加减指令的顺序(对应后缀b),以及add和sub(减法)指令(对应后缀d)。

    另外,stm指令中操作数1后面跟的那个叹号也不能省略,省略的话指令的含义就会发生变化。关于这条指令更具体的分析可以看口袋妖怪绿宝石——数据提取与代码分析(5-THUMB汇编指令基础)给出的文档。

    稍微跑题一下,现在回来。对于DecryptBoxMon函数,有很多修改方案可以让解密(加密)操作失效,这里作者给出一个跳转指令的方案:

    这是在函数一开始就跳转到函数末尾,直接让函数什么都不做就返回了(当然必要的push和pop指令是不能跳过的)。为了能让在线网站成功翻译这条汇编指令,还是需要用到网站左下角的offset输入框:

在线网站代码转换

    因为b指令的操作数是相对地址,只有填入这条指令当前所在的地址时,才能进行绝对地址的转换。

    和解密函数DecryptBoxMon对应的还有一个加密函数EncryptBoxMon,这一对函数都需要修改才能让加解密的过程同时失效。这两个函数的汇编代码几乎一模一样(有兴趣的读者可以到VBA的反汇编器中去看)。所以,破解数据加密反作弊机制的金手指是:

破解校验码

    判断校验码是否正确的代码:

判断校验码是否正确

    如果能在这动一下手脚,让3708行的判断条件永远不会成立,也就永远不会执行括号内的内容(把精灵变成蛋),破解校验码的目的就达到了。

    来到GetBoxMonData的汇编代码中:

GetBoxMonData的汇编代码

    如何快速在汇编代码和C代码中找到相似性?这件事考验的是一个人对两种语言的熟练程度,以及阅读代码积累的经验。例如上图中,bl $0806a270这条函数调用语句出现了4次,能够迅速发现这个特征就可以和下面的C代码进行对应:

4个相同的函数调用

    同样是在GetBoxMonData的函数开头不久,同样是连续调用4个相同的函数,能找到这样的相似性,就可以不必一条一条指令去分析、去对应(这样很累的!),而是快速定位到自己感兴趣的代码处。

    根据相似性,接下来C代码还有对DecryptBoxMon和CalculateBoxMonChecksum的函数调用,因此在上面汇编代码图的最后,我们期待会看到接下来还有两条bl指令,其中第二条就是我们关注的计算校验码的函数:

GetBoxMonData的汇编代码(续)

    在最后一次调用完GetSubstruct函数(0806a6be)后,果然下面出现了两次bl指令,其中0806a6cc处的bl $08068c78就是对计算校验码函数的调用。接下来的两行先左移(lsl)后右移(lsr)的操作,在上次专栏讲到过,这是做一个类型强制转换。再接下来的两行:

    是典型的从结构体中取数据的汇编代码写法。r8的取值可以回到GetBoxMonData函数的开头去找,它存储的是函数的第一个参数boxMon,然后在这个结构体内相对地址0x1c处取出一个成员变量放在r1中。其实不需要回到BoxPokemon结构体去看0x1c处是什么成员变量,只需要对比一下C代码就知道:

上面的图搬下来继续用……

    这个地方取出来的值是boxMon->checksum。如果不出意外,下一步应该就是比较两个数值(计算校验码函数的返回值,和精灵信息中存储的校验码),果然:

    下面的beq指令,说明如果r0和r1相等,就跳过一段代码不执行(注意0806a6da和跳转到的地址0806a6f2之间还是有一段代码的)。如果要破解校验码的机制,我们就应该让这里的跳转总是发生。这样就有了两种修改方案:

    两种方案都能起到作用,读者可以自己体会一下它们的区别和联系。

    和GetBoxMonData对应的,还有SetBoxMonData,可以通过VS Code文件查找来验证:调用CalculateBoxMonChecksum进行校验码判断的只有这两个函数(其他的调用不是为了判断),这说明破解校验码和数据加密一样,也得改两处。如果采用上面的方案一,那么破解校验码的金手指就是:

    总结一下,破解三个对队伍精灵进行修改的反作弊机制,需要下面5行金手指(答案不是唯一的):

    本期专栏介绍的内容,说实话,难度很大(希望不是因为作者有限的表达能力带来的困惑)。但所谓“难者不会,会者不难”,如果能充分理解里面涉及到的各种知识,就会发现口袋妖怪绿宝石的反作弊机制也不过如此。它用了很难的数据加密算法了吗?没有。它的校验码计算过程很复杂吗?复不复杂根本不用管,反正破解它也不需要知道校验码的计算细节。它的数据打乱机制看起来有点创意,破解起来困难吗?还不是改一行汇编指令就可以……

    当然,作者在这里把破解反作弊机制说得这么轻松,完全是因为有源代码项目这个堪称上帝视角的资源。没有它的帮助,破解这些反作弊机制的速度就会成百上千倍地放慢。因此,我还是由衷地感谢源代码项目的作者们,TA们对开源项目社区的贡献是巨大的!

    再次感谢众位读者的支持,甚至都不指望能有多少读者能看完作者的长篇大论了……

一点可有可无的疑惑

    由于作者的上一系列专栏是介绍究极绿宝石系列金手指的,在当时技术水平还没达到现在写专栏的程度时,只能按照最简单通用的方法去找金手指。奇怪的是,作为绿宝石改版的究极绿宝石,原版绿宝石中出现的反作弊机制(上上期、上期和本期),在究绿中一个都没有。

    后来,作者在VBA的反汇编器中查看究绿的汇编代码时看到,究绿把这些破解反作弊机制的金手指已经写到改版游戏的ROM里面去了(和这几期作者给出的金手指不同,但是效果是一样的)!难以想象,贴吧里不知道有多少吧友强调:究极绿宝石是个反作弊机制很厉害的改版游戏!确实,非法的精灵或者非法的技能会被删除,努力值修改超过510,对战会直接导致游戏重启……但是,如果究绿选择把原版绿宝石的这几个反作弊机制保留下来,分分钟就可以让网上流传的绝大多数金手指瞬间失效。

    这样一来,作者就实在搞不清楚究绿制作组的成员们究竟是怎么想的了。

    你说TA们到底是支持金手指呢(破解了原版绿宝石的反作弊机制)?还是反对金手指呢(自己新添加了一些反作弊机制)?这是作者在利用这些工具进行分析时,产生的一点可有可无的疑惑……

口袋妖怪绿宝石——数据提取与代码分析(8-反作弊机制:校验码与数据打乱)的评论 (共 条)

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