吞食魚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
結語
權當是休閑娛樂,沒做太多復雜的東西,想做成品修改器的小伙伴也可以寫一個。
今年最后一天了,祝大家玩得開心。