怪物农场2修改日志2 - 奇妙能力歌
OK, Here is the bgm!
上篇,我们找到了名字"unigauld"存储的位置,并且成功修改了这个字符串.然而好像并没什么卵用,我并不准备汉化这个游戏.
之前也提到过了,文本通常是破解的切入点之一,但是目前上篇的发现似乎还没啥用.
所以现在该做什么呢?其实我也不知道,所以先随意逛逛好了.
我尝试用幻想传说的光盘生成了一只怪物,Hopper和冰狼合体的怪物クリック.
查看怪物图鉴,现在有2只怪物了,分别是编号278的モッチー.这里的怪物图鉴和口袋妖怪一样,当你生成一只新的怪物时,怪物图鉴就会更新该怪物的卡片.
还有编号169的クリック小朋友.
这里可以看到,在怪物卡片上,会显示该怪物的编号,,"169".如果我能在内存中找到"169"这段文本,我可以想办法知道169这个数据是从哪里得到的.
简单的推理可以知道,这个"169"明显来自某个列表.这个列表记录了当前玩家收集的怪物编号,当前这个列表中应该有"169,278"这两项.
当玩家生成新的怪物时,游戏程序会更新这个列表,例如你生成了一个新的怪物,假设编号是"222",那边游戏程序会将列表更新为"169,222,278".
当玩家打开怪物图鉴时,会去读这个列表,从而列出玩家当前拥有的所有怪物卡片.
如果我们能找到这个"收集了的怪物Id"列表.那么我们可以设置内存断点,在生成新怪物的时机中断,然后反推出生成怪物的数据来自何处.
所以找到"169"这个字符串,成了我们的切入点.
那么如何找到"169"这个字符串呢,首先我需要知道这个字符串在内存中会以什么样的数据存储.
我尝试生成了一只新的怪物,取名为"unigaul8".使用上篇的方法,我们已经可以找到存英文的字符串了,因此搜索"unigaul",这样就能找到"unigaul8"的位置,从而知道8在内存中的编码.打开怪物数据界面,发现名字就显示在上面"unigaul8".
我们保存一个存储快照,掏出我丑陋的python脚本,搜索"unigaul".找到了4处匹配.
打开二进制编辑器,首先看第一个地址,613849,也就是0x95dd9,对应的是2E.可以看出这里的数据从0x95dd8开始.
最后一个字符是AA FF.我们尝试修改这里的数据,0x95dd8是快照中的地址,内存中的地址为0x95dd8-0x2B0=0x95B28.
随意做些修改,发现"AA FF"对应"8","A4 FF"对应"2".
因此我们可以推出,"169"在内存中的表示应该是A3 FF A8 FF AB FF."278"在内存中的表示应该是A4 FF A9 FF AA FF.
再次打开怪物图鉴,保存存储快照,尝试搜索"278".也就是"A4 FF A9 FF AA FF".
发现找不到匹配.尝试查找A4 ? A9 ? AA ?,找到2个匹配.
明显0x765D8h处的看起来比较靠谱.我们直接修改内存试试,还是老方法,修改0x765D8-0x2B0=0x76328处的值.
发现不管如何修改,这里的值都会被写回a4 00 a9 00 aa 00,也就是"278".切换怪物,这里的值会变为a3 00 a8 00 ab 00,对应"169".
所以我们需要知道程序是从那里读取到数据(169或者278),用来更新这个地址的字符串.很明显数据应该来自我们前面假象的那个,保存玩家搜集的所有怪物的数组.
恩,开始调试,首先我们需要下个内存断点,看什么代码在往0x76328处写入数据.
按F9执行,发现这个内存地址被写入了很多次,我们一直继续,直到调试到写入A3 A8 AB的时机.如下图,此时已经写入了A3和A8,即将写入AB.
写入的指令为sh r3.0x0000(r2),就是把r3寄存器的值A8写入到r2的0x7632A处.sh的意思是Store Halfword,也就是存2个字节.playstation的cpu使用的是MIPS指令集.具体可以看MIPS指令相关.
我们再看上面两句,发现R3的值来自R16指定的地址,lbu r3.0x0000(r16). lbu的意思是Load Byte Unsigned.这里可以知道r3的数据来自地址0x76809.
我们查看0x76809处的数据.
可以看到这里存储的是a3 a8 ab ff.这里的数据是从哪来的呢?还是老方法,下个内存断点.
可以看出数据还是从R3流入R8指定的地址(0x76809)的.那么R3从哪里来的呢?往上看,0xB3088处,lbu r3.0x0000(r4),看起来是从r4这里读取的.我们在0xB3088处下个断点执行看看.
下完断点,0xB3088处变成了illegal,这代表断点下成功了.
发现这里从0x767EB处读取了A3,并且存储到R3,可以推断数据来自0x767E8处的FF AB A8 A3.我们增加一个内存断点在0x767E8处.
继续调试,直到0x767E8处被写入FF AB,可以知道接下来将要写的是A8和A3.
这里从sb r2.0x0000(r4)可以知道数据AB来自R2寄存器.我们再看上一句,addu r2.r9.r2,这条指令是把r2 = r2 + r9.此时r9为a2.r2为ab,可以反推出在执行addu r2.r9.r2前,r2的值为9.也就是ab代表的数字9."169"的第3个数字.
接下去代码应该会将A8和A3写0x767EA和0x767EB.我们单步继续执行.
bne r7.r0.0x000b3028,意思是r7不为0则跳转到0x000b3028处.这里为0x10,所以跳到0x000b3028处执行.
跳过无关的指令,继续执行,直到指定到addu r2.r9.r2处.此时r2为6."169"的第二个数字.
此时R7为1,依然跳转回0xB3028处,继续循环.执行到addu r2.r9.r2,此时r2为1,对应"169"的第一个数字.
大概可以看出,这里依次将9,6,1三个数字写入指定的位置.还原下代码,应该是这样的.
int id = 169;
while(id)
{
int num = id % 10;
char c = num+0xA2;
id = id / 10;
}
我们重新回到开始的时候,刚写入FF的时候.
这里一堆swl swr指令,将0x767E8后面的内存都清空为0.仔细观察寄存器,发现r7此时为a9,也就是169.
继续按F9,当AB和A8被写入的时候,r7的值变为10,也就是16.
继续按F9执行,r7变成1了.验证了上面还原的代码.
那么我们接下来需要找到r7这里的数据是从何而来.往上追溯,发现r7的值应该来自r4.
我们在0xb2f9c处下个断点.成功断在了这里.
可以看到r4的值为a9.我们需要知道r4的值是从哪里来的,尝试继续往上追溯,通过断点,定位到0xb2E10处.此时r4已经是a9了.
继续向上追溯.0xB2E08处,addu r4.r6.r0,可以知道r4的值来自r6.我们在上面一些的位置下个断点,就0xB2DEC了.
此时r6已经等于a9了,继续往上追溯,发现没法找到是从哪里跳到0xB2DEC的.我们单步执行,直到0xB2DEC返回上一层函数.
这里可以看到lhu r6.0x0000(r2),之后调用了jal 0x000B2DEC.jal是Jump and Link,会跳转到后面指定的地址,并将返回地址存到r31寄存器.可以看成是函数调用.
我们在上面点下个断点,发现r6的值是从r2,也就是0x15d000处读取的.
查看下0x15D000处的内存.0xA8和0x115,正好对应169-1和278-1.这就是我要找的列表了,"169,278".
直接修改,尝试将A9改为A3,发现显示的文本更新了.怪物编号从169变成了164.
关闭怪物图鉴,之后再次打开,数据又变回去了.
可以看出这个列表还不是最原始的信息来源,在打开怪物图鉴时,程序更新了0x15d000这里的列表.我们可以在0x15d000处下一个内存断点,然后重新打开怪物图鉴,看数据是从哪里来的.
我们断在了0x186930处,sh r4.0x0000(r2),数据来自r4.往上看代码,发现最近的一次,对r4的操作在这里.
我们在0x1858F0处下个断点试试,发现不会经过.接着在0x18592C处下断点.
发现,在打开菜单时,这个断点被清除了.illegal又变回了addu r2.r2.r3.看起来断点不知道因为什么原因被清除了.
在这个时机重新编辑断点并应用,发现会断在这个位置了.我们用同样的方法在0x1858F0处重新下个断点.
成功断在了这里.在继续之前,我们还是来找找看为什么断点会被清除.
下完断点后,我们查看0x1858F0的内存.发现这里被改成了44 44 44 4C,这应该代表一个正常的断点.所以指令那会显示illegal.
打开菜单,发现这里的44 44 44 4C被改了回去.
我们在0x1858f0处下个数据断点,重新打开菜单,发现不会触发内存断点,这里还是会被改回去.好吧,还是先继续找我们需要的数据.至少我们能区分断点是被清除了,还是没有执行到.
可以看到r4被清空为0,我们单步向下执行,看r4是怎么变成a8的.
执行发现,直接跳转到了0x185934,跳过了0x185930,我们继续执行看看.
执行后,发现这里是个循环,r2不等于r0时,就会跳转到0x185904.每执行一个循环,r4的值就会加1.让我们来过一次循环.
如上图,这里先将r4逻辑右移0x10,并保存到r2,这是r2为0x00050000.之后将r2算术右移0x10位,变成0x00000005.
不清楚这么做的意图,暂时认为是把r4的值保存到了r2.接下来,将r2加上r5,也就是0x9703c+5,取出值并保存在r2.
可以发现0x9703C+5处为0,因此r2读取到的值为0.
下一句beq r2.r0.0x00185934,beq是Branch on Equal的意思,即r2等于0(r0始终为0)时,跳转到0x185934.
仔细观察这段代码,假如r2不为0,则会执行到0x185930,也就是之前写入数据的那条指令.
简单分析,可以大概推出下面的代码.
char *base = 0x9703c; for(int i = 0; i < xx; i++) { if(*(base+i) == 0) { //继续循环. } else //0x9703c+i处的数据不为0. { //将i的值存储到0x15d000. } }
我们再看0x9703c处的数据.
可以看到,0x970E4和0x97151处为1,其他都为0.
0x970E4-0x9703C=A8=168=169-1.
0x97151-0x9703C=115=277=278-1.
可以知道,所有的数据都来自这个byte数组,每个byte代表了指定编号的怪物是否有被收集.这里+A8为1代表169号怪物已经收集,+115为1代表编号278的怪物已收集.
我们来修改这里的值试试,假设我们要让3号怪物变成已收集,应该要修改0x9703C+3-1的位置.也就是0x9703E.我们将0x9703E修改成1,然后重新进入怪物图鉴.
Here it is.ユニコ, 你可以叫她uni子.
绕了好长的路,总算是找到了"已收集怪物Id"这个列表.接下来我们可以看看,是否能从这个列表,反推出生成怪物时,使用的那些数据,并在运行时进行更改.