口袋妖怪绿宝石——数据提取与代码分析(7-反作弊机制:数据加密)
说在前面:
上期专栏介绍了原版绿宝石中反作弊机制的一种:跳动的指针。本期专栏将继续介绍一种反作弊机制的常见方法:数据加密。

背包道具数量修改:加密的数字
在另一个系列的专栏究极绿宝石5.3——科普向,什么是金手指(三)中,作者提到了利用VBA模拟器中的“查找金手指”功能来找到游戏中的变量。如果一个游戏变量体现在游戏中恰好就是数字的形式(而不是文本、图片等),用“查找金手指”的“追踪式变量取值查找”功能可以将它快速定位。
然而,上面这个方法能够生效的关键在于:这个数字在游戏中和在程序代码中是一致的。比如金币数在游戏界面中显示是10000,在程序代码中代表金币的变量取值也是10000;花费一些金币让金币数变成9000,那么程序代码中相应的变量也会变成9000。只有这样查找变量取值才有意义。
于是,一种反作弊机制就是对变量的取值进行加密,让变量的取值和游戏界面上显示的数字不一致,这样就无法通过“查找金手指”中的“精确查找”功能找到想要的变量了。
关于这种反作弊机制,本期专栏给出例子是:原版绿宝石中背包道具数量的修改。
首先来看看源代码项目中背包道具的信息在ROM中的什么位置:

其实上期专栏也提到了背包道具,它位于SaveBlock1结构体内,在上图的第950~954行。这5行定义,每行都是bagPocket_后面跟着一个不同的后缀,Items是道具,KeyItems是重要道具,PokeBalls是精灵球,TMHM是技能机,Berries是树果,恰好对应了背包内道具的5种类型。
现在只关注一个:道具bagPocket_Items,里面存放的是类似伤药、驱虫喷雾剂、先制之爪之类的道具。它位于结构体内相对地址0x560处,这个数字之后要用。
用VS Code可以方便地查看到BAG_ITEMS_COUNT是30,也就是说道具栏最多只能存放30个道具,这就是原版绿宝石游戏的设定。转到struct ItemSlot的定义:

可以看到定义十分简单,用2个字节来表示道具的编号,再用2个字节来表示道具的数量。4字节表示一个道具,连数据对齐的问题都不需要考虑了(数据对齐的问题在之前的专栏口袋妖怪绿宝石——数据提取与代码分析(2-基于名称列表的详情信息提取)提到过)。
从上期专栏可以知道,想要找到背包道具的地址,首先需要到gSaveBlock1Ptr的位置去看这个指针指向了什么地址,该地址就是gSaveBlock1,然后在这个地址上加上0x560这个相对地址,就可以定位到背包道具了。作者在游戏中做如下尝试:

gSaveBlock1在03005D8C处,现在显示的取值是02025A44,再加上0x560就是02025FA4,然后再来到这里查看:

按理说,从02025FA4这里开始,就应该按照struct ItemSlot的定义,每4个字节描述一条道具,前两个字节是道具的编号,后两个字节是道具的数量。其中道具编号如何提取已经在之前的专栏口袋妖怪绿宝石——数据提取与代码分析(1-字符集与文本信息的提取)说得非常清楚了(也就是金手指中提到的道具代码)。
按照上图,0x0055对应脱洞绳(Escape Rope),0x006F对应心之鳞片(Heart Scale),0044对应神奇糖果(Rare Candy)……这都没有问题,直到0x0000表示这里没有道具了。但是,0055后面的那个BA70是什么意思?背包中的脱洞绳只有4个,这无论如何也跟BA70这个数字联系不上。再观察一下其他道具的数量,006F心之鳞片也是4个,在内存里面数字也是BA70;0044神奇糖果只有一个,内存中的数字却变成了BA75。4比1大,但是BA70比BA75要小,头一回看到这种对应关系的读者可能无法一眼看出来这两者之间有什么关系。
这就是加密,让游戏中展示的数字和程序代码中变量的取值不一致。如果想通过搜索道具数量来定位修改道具的金手指,就无法生效。
有的读者可能想去找找加密的规律了,逐一去比对:4加密到BA70、1加密到BA75、2加密到BA76……可惜的是,原版绿宝石的加密没那么容易破解。作者操控主角在周围走动一下,下楼上楼之后,再次打开“内存查看器”,发现gSaveBlock1Ptr指向的地址已经发生了变化(这在预料之内,上期专栏提到“跳动的指针”就有这个特点)。经过一番计算,找到新的背包道具所在地址:

可以看到,0055、006F等这些道具的编号并没有变化,但是道具数量对应的那些内存数据已面目全非,又变成了莫名其妙的9EAD、9EA8之类。这让之前试图找到加密规律的努力付之东流。看来,背包道具的数量不但被加密了,还会随着游戏流程不断被再次加密,毫无规律可循。
背包道具数量的反作弊机制,就是指针跳动+数据加密的组合拳。看来只好到源代码项目中,从原理上理解一下它的加密过程了。

绿宝石怎么对数据做加密
上期专栏提到了在寻找跳动的指针时,我们找到了load_save.c文件中的这个片段:

这是在函数MoveSaveBlocks_ResetHeap中。这里的函数名前半段MoveSaveBlocks说明了此函数的第一个功能:把存档信息“移动”一下。这里的“移动”其实就是图片中显示的这一段代码:改变指针指向的位置,将原本指针指向的内容完全复制过来。继续向下看:

下面出现了一个被叫做encrptionKey的变量,它的含义是“加密密钥”,是给数据加密用的。看代码看到这里,就会发现这个变量十分可疑,源代码项目给这个变量起这个名字肯定是有原因的。这个加密密钥在第129行通过随机数函数Random来取值,然后在第130行被用于ApplyNewEncryptionKeyToAllEncryptedData函数中,同时在131行还存储在了gSaveBlock2里面。上面的注释也提示(“create a new encryption key”),这部分代码是要生成一个新的加密密钥。
ApplyNewEncryptionKeyToAllEncryptedData函数的名称很长,但这就是源代码项目翻译质量高的体现之一,它的含义是“将新的加密密钥应用于所有的加密数据”。这种良好的命名方式让阅读代码的困难程度降低了太多太多,试想如果这个函数敷衍地用“Func_01”这种没有任何信息量的名字来命名,那和去看汇编代码还有什么区别?
看来加密算法就藏在这个名字很长的函数中了,来到它的定义:

这个函数又包含了5个函数,通过良好的命名,可以推测这些函数分别是给:游戏统计数据GameStats、背包道具BagItems、树果粉末BerryPowder、金币数money和游戏厅代币数coins加密。新世界的大门仿佛已经打开,这5个函数蕴含着丰富的信息量,有兴趣的读者不妨去读一读这里的源代码。本期专栏只看最相关的ApplyNewEncryptionKeyToBagItems_函数:

这个最后带有一个下划线的函数(不知道读者在第一眼看到它的时候有没有注意到)竟然只是给另一个函数套了一个壳,而这“另一个函数”的名字只是简单地去掉了下划线。这种看似毫无意义的代码写法让源代码项目的作者也会吐槽:
really GF?
GF你认真的吗?(代码写成这样就不要出来混了)
题外话打住,来看ApplyNewEncryptionKeyToBagItems函数,在第58行,终于出现了对道具数量quantity的加密函数ApplyNewEncryptionKeyToHword。这个函数有两个参数,第一个参数是道具数量,但前面还用了一个取地址运算符&,说明这个函数是要修改第一个参数的值;第二个参数就是新的加密密钥newKey。兜兜转转,我们来到了加密算法的最后一站:

这个加密算法是如此地简单:异或操作。这个函数在276行执行的其实是解密操作,也就是说,在第276行执行之后,在第277行执行之前,*hWord的取值就是它在游戏中对应的数字。对于异或运算来说,加密和解密的算法是一样的,因为对一个数字A来说,连续两次异或同一个数字B,得到的还是数字A本身。回过头来再看生成新密钥的地方:

再来看第131行就好理解了,gSaveBlock2里存储的加密密钥是老版本的加密密钥,这个函数做的事情是更新一下密钥,并把之前所有的加密数据重新用新密钥加密一遍。也就是为什么在游戏中,过一小段时间之后,道具数量那里对应的变量取值会出现毫无规律的变化。

破解数据加密的金手指
想要破解绿宝石的数据加密,需要对异或操作有一些基本的理解:
任何数字X和0异或,得到的结果还是数字X本身。
所以只需要把129行的C代码改成:
就大功告成了。
当然,使用金手指的话还是得到绿宝石的ROM中去找汇编代码。从符号表中可以找到MoveSaveBlocks_ResetHeap的地址是在08076c2c。

考虑到第129行是MoveSaveBlocks_ResetHeap的结尾位置,而汇编代码的一个特点就是函数结尾的地方往往会出现pop指令。从VBA模拟器的反汇编窗口内,来到08076c2c向下搜索,可以在08076ce8开始向后找到3个pop指令:

上图中最后一行bx r0可以看做是C代码函数的return指令,而开头的第1行和第3行(08076cc6和08076ccc)这两处调用了同一个函数,查找符号表可以知道就是Random函数,这和C代码的第129行,用两次Random函数生成encryptionKey就对的上了。这里还是用的寄存器r4来存储加密密钥。
顺带一提第5行和第6行(08076cd2和08076cd4)处对r0的两个操作:先lsl,也就是左移16位;再lsr,右移16位。这种先左移再右移,移动的还是相同的位数,带来的结果就是:r0的低16位保持不变,但高16位被清零了。也就是说,这两句话相当于对r0这个32位的数字进行了一个强制转换:
在08078cd6处,原本的汇编代码是为了完成计算encrytionKey的最后一步:
只需要把它改成:
加密密钥就被固定成0,再也起不到加密作用了。
通过在线网站https://armconverter.com/进行转换后,这条破解数据加密的金手指就是:

本期专栏我们又看到了反作弊机制的一种方法:数据加密。并且破解它的过程也体现出了上期专栏说到的特点:设计一个反作弊机制需要几十行甚至上百行的C代码,但是破解它往往只需要一两行汇编代码的修改。只不过想要找到这一两行代码位于何处,是需要对源代码有一定程度的了解才行的。
反作弊机制还不止指针跳动和数据加密,下期专栏还会继续介绍反作弊机制的其他方法。
再次感谢众位读者的支持!