吞食鱼2(FeedingFrenzyTwo) 修改器
童年回忆系列。小时候特别喜欢玩这类游戏,软件不大,很慢的网速也不会下载太久,然后对配置要求不高,很破的电脑也可以玩得很开心。不过也有糟心的时候啊,大鱼太多,无数次死于挑战咬梭子鱼的尾巴……今年最后一天,就休闲一小会吧。
小时候看不懂是啥意思,现在知道了,我来翻译下第一段。
警告!
发现梭子鱼在珊瑚礁附近游荡。留意警告标志,远离它张开的大嘴!要是你有迷之自信,可以试着咬它的尾巴。咬 4 次就会有惊喜……如果到那时候你还活着……
现在再玩已经没有当年的感觉了——鼠标换了……其实我老早就想,为什么到了下一关,我的鱼就变小了??太不爽了。游戏里各种对玩家不利的设定:小鱼不能吃大鱼、被大鱼追、被贝壳吃掉、被乌贼喷、被水母电、被大嘴鸟吞、被水雷炸、被河豚刺、吸水还要 CD、跳出水面翻滚的话进水眩晕……所以只有修改内存数据才是称霸海洋的秘诀!
工具:Cheat Engine 6.4 (以前搞的汉化版,其实翻译不完全,建议用英文版)
大,大,大
这个游戏的规则是吃小鱼,躲大鱼,等长大了以后就可以吃遍全图。成长值进度条在左上角,前面的关卡都是 3 个成长阶段,到了后面还有更多的级别。
好的,按照程序员的思路,姑且猜测 growth 数据用整型存储,每种鱼的加成不一样,小鱼少些,大鱼多些,初始值为 0,打开 CE ,开始搜索,每吃一条鱼就搜一次“增加的数值”。
好的,没搜几次,轻松搜到 2 个值。尝试修改第一个,发现鱼没反应;再尝试第二个,鱼长大了!然后自然是一顿操作:找出改写 -> 显示反汇编,然后看到了如下代码:
这也太舒服了,直接找到静态地址005AC624
,省的找基址了,直接手动添加地址,growth 就有了。基址005AC624
偏移+3C, +40
(跟基址写成 "FeedingFrenzyTwo.exe"+001AC624
是一样的)
四两吃千斤
修改 growth 的数值,就可以瞬间变大,通关,但是这一点也不爽啊,我一定要把被吃的仇报了才行!
还是刚才的进度条,用通常思路来猜,成长阶段数值分别是 0, 1, 2 ,姑且先试一下。借助修改 growth 值快速长大,搜索这几个值就很快了。
先尝试修改第一个,图中我修改 0245AE24
的值为 0 以后,我 2 阶的大鱼被比我小的鱼吃掉了,那应该就是它了。观察了一会发现,这个值就是用来确定会不会被吃的,但是查找访问它的代码并没找到什么关键判定,代码太多了,我也懒得看,反正只要不被吃复位,这个值就不会变,直接把它改大点就行了。
在鱼变大的时候查找改写它的地址,可以找到偏移量 EC
00496B47 - 89 99 EC000000 - mov [ecx+000000EC],ebx
再稍微调试跟踪下,找到基址。基址005AC624
偏移+40, +344, +0, +EC
,类型我选了 byte ,不过应该没啥影响。
然而事实是,我不会被大鱼吃掉了,但是在大鱼旁边的时候也不会触发吃鱼的动作了……是太难吃了吗?哈哈……不过,离成功不远了。经过一番探索,我用“增大的数值”搜索前边的成长阶段,找到了另一个数值。简单点来说,这两个值一个是玩家鱼在电脑鱼面前的大小,一个是电脑鱼在玩家鱼面前的小大。<- 我说小大,因为第二个值数值越大判定电脑鱼越小。
基址005AC624
偏移+40, +344, +0, +F8
,就在上一个值旁边,很狡猾啊,这个值是从 1 到 3 的,之前搜精确数值的时候没找到,早知道就先去看看数据结构了。
把这两个值同时改成 5 ,终于报了当年的血海深仇,啊哈哈哈哈哈哈哈……
速度和位置
吸取刚才的教训,现在来看看数据结构。
嘿嘿,果然有了意外收获。仔细观察鱼的状态和数值,可以发现上面的 4 组浮点数分别代表鱼的位置和速度,修改这些值可以让鱼瞬移到地图任何地方。
然后我就有了一个大胆的想法——能不能把地图里的其他鱼瞬移到我嘴边呢?省的乱跑了。事实是——YES! 首先要找到存放地图上所有鱼的地方。
拿玩家鱼的位置来说,地址是 基址005AC624
偏移+40, +344, +0, +98
,按照程序的对象模型来想,+344
指针应该是玩家指针,里面存放了很多和玩家相关的数据,刚才做四两吃千斤的时候,数据也在这个对象下,那么看下+0
指针应该就是玩家的鱼的指针了,我这次游戏的指针是 09397280
。好的现在假设有这么个全局鱼数组,那么这个 09397280
也一定在里面,直接搜索这个指针:
嘿嘿,我为什么单独标出来这个 07EDC488
呢?因为看数据结构,在之前的 +344
指针前面,在 +324
指针的地方,指向的数据不就是 07EDC488
嘛,这应该是个数组首地址,展开一看,果然全都是鱼!
鱼是有了,可是数量不知道……这个数组没有结束标识,貌似是像vector
那样管理的,有固定大小,靠整数标记结束的位置,而游戏本身可能不记录实时的鱼数量,所以我找了一遍,一直没找到数据,也可能是我找的方法不对。鱼在不同状态的时候地图上的鱼数量会变,但是因为不知道具体值,非常难找,我猜测是一些常数,规定了不同关卡鱼的数量上限。
我还强行试了下移动所有鱼直到空指针,会访问销毁过的鱼对象而导致访问越界崩溃,问题应该出在这里 mov ebx,[eax+edi]//4*n
,edi
的值增大以后,eax+edi
就不一定是有效地址了。然而令人惊奇的是,只要不点错误窗口上的确定键,就还可以继续游戏!真神奇……脚本如下:
[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
//0051A002
alloc(newm,512)
label(nextfish)
label(exit)
newm:
pushad
mov eax,["FeedingFrenzyTwo.exe"+001AC624]
mov eax,[eax+40]
//GetPlayersFish
mov esi,[eax+344]
mov esi,[esi]//ThisIsThePlayersFishClass
//PlayerPosition
mov ecx,[esi+98]
mov edx,[esi+9C]
//GetYou(heiheihei)
mov eax,[eax+324]
mov edi,0
nextfish:
mov ebx,[eax+edi]//4*n
cmp ebx,0//NoNextFish
je exit
add edi,4
cmp ebx,esi//IsPlayer?
je nextfish
mov [ebx+98],ecx
mov [ebx+9C],edx
jmp nextfish
exit:
popad
ret
createthread(newm)
LdrInitializeThunk:
DB 8B FF 55 8B EC
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newm)
可以不理它,别点确定,直接返回游戏:
如果想稳妥一点,就去掉 jmp nextfish
这句,这样就每次只移动 1 只鱼,因为地图上一直会有一堆鱼,所以低频调用移动 1 只鱼的脚本是没事的,也可以轻松叠满 FRENZY
因为已经可以四两吃千斤了,所以继续搞这个功能也没啥必要了,就到此一游吧,不找鱼总数了。
变小魔法
游戏里有很多有意思的道具,比如蘑菇,可以让周围的大鱼变小;还有个红色的疯狂鱼,可以时间停止然后自动吃掉屏幕里的鱼,如果能找到对应的 call 就爽了。下面就来试一试。
首先我大概想了个思路,因为之前得到了存储鱼 growth 数值的地方,电脑鱼与玩家鱼用的是同一个类,所以存储的偏移应该也是一样的,所以查看数据结构里全局鱼数组,随便找条大鱼,查找改写 growth 数值的地方,然后去吃个蘑菇。之后代码断在了 0041D744
,这里没什么有用信息,返回到上层函数,发现了有意思的东西。
0042863F - 68 08B35500 - push 0055B308 : ["shrinkBurstFx"]
这个单词 "shrink" 就是缩小的意思,再继续向上返回查找,又发现了一个位置:
0049AA93 - 68 B8E65400 - push 0054E6B8 : ["fishShrink"]
然后我再继续返回,发现到了外层大循环,而函数内部的断点是在魔法光球打在鱼身上的时候才会中断,我们需要的函数却是吃蘑菇的事件函数。好的,现在先暂停一下,缕缕思路:
- 玩家吃蘑菇
- 触发吃蘑菇事件函数,发出光球,目标是电脑大鱼
- 电脑大鱼被光球打中,触发缩小事件,缩小
我们刚才找的 "shrink" 相关的函数应该是步骤 3 ,需要找的是步骤 2 ,而步骤 2 到步骤 3 应该不在同一个函数中,它们之间应该只是消息传递的过程,所以这个线索就断了。
不过,嘿嘿,代码注释里已经给我们提供了新的线索——"shrink" 字符串。每次触发事件的时候,就会引用和 "shrink" 相关的字符串,所以新的思路有了——搜索 "shrink" 字符串,然后查找什么访问了字符串,看看吃蘑菇的时候会断在哪里。首先找到 078BBCF8
的位置是我们刚才找到的 "fishShrink" ,而就在它下面不远的位置,发现了 "shrinkPickup" ,地址是 078BBE10
!!!哇,运气好到爆炸!
查找什么访问了 078BBE10
,然后查看堆栈,在这个字符串附近的函数一个一个进去看,终于在 004747CD
的地方找到了线索。
在004747CD
下断点,然后吃东西就会中断,再向上返回,发现返回的位置和吃的东西有关。
吃蘑菇返回到这里
吃鱼返回到这里
试了几次,吃什么就会返回到什么地方,所以附近应该就有吃的函数了,传入的指针就是吃的东西,用多态的思想,吃什么就执行什么的事件函数,所以离胜利不远了……
经过一番调试,终于找到了吃东西的函数——call [eax+90]
,就在 0042A98C
的地方,esi
是被吃的对象,+90
大概是虚函数表里执行被吃事件的函数指针,参数是 edi
,储存发起吃东西事件对象的指针,还有个寄存器参数 ecx
,储存的是被吃对象指针。一个以吃东西为游戏内容的游戏,把吃东西的函数找到了,游戏结束!
后面的工作就轻而易举了,下断点在每次吃掉蘑菇的时候步入,就到了 004A249D
,这个 call 00493BD0
就是我们苦苦寻找的吃蘑菇的事件函数了,这个函数只有一个寄存器参数,就是 esi
储存发起事件的对象指针,这里我们把玩家填进去就好。还记得玩家指针在哪吗?没错,就是之前找四两吃千斤的时候找到的对象:基址005AC624
偏移+40, +344, +0
然后写出脚本,只要执行这个脚本,就相当于吃了蘑菇,变小魔法就会触发了,大功告成。
[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
//004A249D
alloc(newm,512)
label(exit)
newm:
pushad
mov eax,["FeedingFrenzyTwo.exe"+001AC624]
mov eax,[eax+40]
//GetPlayersFish
mov esi,[eax+344]
mov esi,[esi]//ThisIsThePlayersFishClass
call 00493BD0
exit:
popad
ret
createthread(newm)
LdrInitializeThunk:
DB 8B FF 55 8B EC
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newm)
狂吃
还是按照上面的思路,在0042A98C
的吃东西函数 call [eax+90]
下断点,然后吃个 "FEEDING FURY" 道具,找到对应的代码。很轻松就找到了,然后写出下面的脚本。
[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
//0053D05D
alloc(newm,512)
label(exit)
newm:
pushad
mov eax,["FeedingFrenzyTwo.exe"+001AC624]
mov eax,[eax+40]
//GetPlayersFish
mov esi,[eax+344]
mov esi,[esi]//ThisIsThePlayersFishClass
//feeding fury 00502399
add esi,00000154
mov eax,[esi]
mov ecx,esi
call [eax+20]
test eax,eax
je exit
mov eax,[esi]
mov ecx,esi
call [eax+20]
mov edx,[eax]
mov ecx,eax
call [edx+70]
exit:
popad
ret
createthread(newm)
LdrInitializeThunk:
DB 8B FF 55 8B EC
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newm)
防雷
直接找出是什么访问了玩家鱼指针,然后去撞水雷。查询过程非常卡,所以到了水雷旁边再开始查询。撞到水雷时,会出现一些新的代码,一个一个找。运气很不错,找第一个就发现了关键跳转。
0050B773 - 75 30 - jne 0050B7A5
有鱼死掉时的关键跳转
再向下找,就在下面找到了鱼撞雷死亡调用的函数
0050B798 - FF 90 DC000000 - call dword ptr [eax+000000DC]
当玩家单位触发时步入,里面只有 3 行汇编。
只要在玩家触发水雷的时候跳过这个函数就可以了
[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
alloc(newmem,2048)
label(returnhere)
label(originalcode)
label(exit)
newmem: //this is allocated memory, you have read,write,execute access
//place your code here
push eax
mov eax,["FeedingFrenzyTwo.exe"+001AC624]
mov eax,[eax+40]
//GetPlayersFish
mov eax,[eax+344]
mov eax,[eax]//ThisIsThePlayersFishClass
//if player hit a mine
//then jump
cmp eax,ecx
pop eax
je exit
originalcode:
mov eax,[ecx]
call dword ptr [eax+000000EC]
exit:
jmp returnhere
"FeedingFrenzyTwo.exe"+102BE3:
jmp newmem
nop
nop
nop
returnhere:
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newmem)
"FeedingFrenzyTwo.exe"+102BE3:
mov eax,[ecx]
call dword ptr [eax+000000EC]
//Alt: db 8B 01 FF 90 EC 00 00 00
因为这里的函数在其他事件触发时也有被调用到,所以不知道这么改有什么副作用,暂且先这样,等以后出了问题再回来看……(懒)
另外还发现了销毁玩家对象的函数,不知道以后用不用得上。
00438EBD - FF 50 1C - call dword ptr [eax+1C]
玩家鱼对象销毁
其他
- 吸水能量条 基址
"FeedingFrenzyTwo.exe"+001AC624
偏移+40, +344, +0, +20C
类型是float
从 0 到 1 - 吸水持久度 基址
005A7314
浮点数float
,数值越小持续时间越长 - 吸水恢复速度 基址
005A7318
浮点数float
,数值越大恢复速度越快 - 直接过关 基址
005AC624
偏移+40, +8C
字节byte
,改成 1 即可。大部分时候好用,个别关不行,不知道为啥
从空中翻滚后落到水里会晕一会,这个脚本可以防止眩晕。其实功能非常简单,就是跳过 mov byte ptr [ebx+0000019D],01
这一句。
[ENABLE]
//code from here to '[DISABLE]' will be used to enable the cheat
alloc(newmem,2048)
label(returnhere)
label(originalcode)
label(exit)
newmem: //this is allocated memory, you have read,write,execute access
//place your code here
jmp exit
originalcode:
mov byte ptr [ebx+0000019D],01
exit:
jmp returnhere
"FeedingFrenzyTwo.exe"+13FC74:
jmp newmem
nop
nop
returnhere:
[DISABLE]
//code from here till the end of the code will be used to disable the cheat
dealloc(newmem)
"FeedingFrenzyTwo.exe"+13FC74:
mov byte ptr [ebx+0000019D],01
//Alt: db C6 83 9D 01 00 00 01
结语
权当是休闲娱乐,没做太多复杂的东西,想做成品修改器的小伙伴也可以写一个。
今年最后一天了,祝大家玩得开心。