口袋妖怪綠寶石——數(shù)據(jù)提取與代碼分析(6-反作弊機(jī)制:跳動(dòng)的指針)
說(shuō)在前面:
????有了前面幾期專欄的基礎(chǔ),我們現(xiàn)在回過頭來(lái)再看看綠寶石游戲中常見的金手指修改的究竟是什么。另一個(gè)系列的專欄究極綠寶石5-金手指原理介紹主要是從數(shù)據(jù)對(duì)比的角度來(lái)理解金手指,現(xiàn)在可以從源代碼的角度來(lái)分析了。
????本期專欄主要介紹源代碼項(xiàng)目中的兩個(gè)符號(hào):gSaveblock1和gSaveblock2。這兩個(gè)變量覆蓋的范圍是綠寶石系列游戲金手指的重災(zāi)區(qū),但是在原版游戲中,它們的地址是不確定的,這給查找金手指帶來(lái)了一定的困難。

存檔信息SaveBlock
????回想一下,在使用綠寶石游戲中的“保存”功能(而不是模擬器提供的“快速保存”功能)時(shí),有哪些游戲數(shù)據(jù)會(huì)被保存到存檔文件中呢?
????太多太多了,隊(duì)伍中的精靈、背包里的道具、當(dāng)前的劇情進(jìn)度……這些都需要保存下來(lái),否則下次就無(wú)法繼續(xù)游戲了。在綠寶石源代碼項(xiàng)目中,存檔數(shù)據(jù)有很大一部分保存在gSaveblock1和gSaveblock2這兩個(gè)變量中,它們的類型分別是struct SaveBlock1和struct SaveBlock2,是兩個(gè)結(jié)構(gòu)體變量。下面統(tǒng)一用SaveBlock類型來(lái)稱呼它們。
????先來(lái)在源代碼項(xiàng)目中看看struct SaveBlock1的定義:

????每行的前面,源代碼都“貼心地”給出了結(jié)構(gòu)體成員變量的相對(duì)地址。從上面截圖里的變量來(lái)看,許多金手指已呼之欲出:
933行的location,角色當(dāng)前所在的地圖,瞬移金手指。
945行的playerParty,隊(duì)伍中的精靈信息,修改精靈的一系列金手指。
946行的money,擁有的金錢,修改金錢的金手指。
947行的coin,擁有的游戲廳代幣數(shù),修改游戲廳代幣的金手指。
949行的pcItems,電腦中的道具倉(cāng)庫(kù),修改電腦道具的金手指。
950~954行,各個(gè)背包,修改背包道具的金手指。
956行的seen1,精靈圖鑒中發(fā)現(xiàn)了哪些精靈,全圖鑒發(fā)現(xiàn)的金手指。
????繼續(xù)向下看,還能看到:
970~977行,各種裝飾和家具,獲得裝飾或家具的金手指。
1012行的seen2,精靈圖鑒中捕捉了哪些精靈,全圖鑒捕捉的金手指。
……
????再來(lái)看看SaveBlock2:

????同樣也有好多金手指:
475行的playerName,角色名稱,修改角色名稱的金手指。
478行的playerTrainerId,訓(xùn)練家ID(會(huì)顯示在訓(xùn)練師卡片上),修改角色I(xiàn)D的金手指。
479~481行,已經(jīng)進(jìn)行了多長(zhǎng)時(shí)間的游戲,修改訓(xùn)練師卡片上游戲時(shí)間的金手指。
……
????上面只是列舉了一部分網(wǎng)上常見的金手指,可以看到,SaveBlock類型定義的這兩個(gè)變量的確是金手指的“重災(zāi)區(qū)”,如果能弄明白里面每條數(shù)據(jù)的含義,這該會(huì)有多少條金手指!

嘗試金手指之前的一點(diǎn)提示
????熟悉前幾期專欄的讀者可能會(huì)迫不及待地到游戲中去試試這些金手指了。熟練的操作可以是:在符號(hào)表中找到gSaveBlock1和gSaveBlock2所在的地址、用結(jié)構(gòu)體定義中給出的相對(duì)地址計(jì)算出來(lái)金手指應(yīng)該修改哪個(gè)地址處的數(shù)據(jù)、金手指制作成功!
????然而實(shí)際上很有可能并不會(huì)成功,原因在接下來(lái)會(huì)逐步分析。
????首先,符號(hào)表中確實(shí)可以找到gSaveBlock1和gSaveBlock2:

????注意這兩個(gè)變量的地址是02開頭的,說(shuō)明它們不是ROM中的內(nèi)容。對(duì)哪部分地址代表什么含義還不太熟悉的讀者可以參考究極綠寶石5.3——科普向,什么是金手指(五)里面內(nèi)存視圖這個(gè)概念。
????這里顯示gSaveblock1在02025a00處,但是如果有的讀者真正到游戲中去這個(gè)地址查看,它本應(yīng)該對(duì)應(yīng)SaveBlock1中定義的第一個(gè)變量:pos,含義是角色在當(dāng)前地圖中的坐標(biāo)。如果角色到處走動(dòng)的話,這里的數(shù)值應(yīng)該會(huì)變化的。而實(shí)際上可能并沒有,是哪里出了問題呢?
????下面兩張圖為作者在一次游戲中的測(cè)試:


????可以看到,角色向右走動(dòng)了一步之后,02025A00處的數(shù)字并沒有發(fā)生變化,反而是02025A44處的0004變成了0005,這正好是角色X坐標(biāo)在當(dāng)前地圖中的變化。但是,02025A44這個(gè)地址也不是固定的,只要保存游戲之后再加載一次存檔,這個(gè)地址就又變了。
????這說(shuō)明,哪怕這一次把金手指改成有效果的版本,下一次可能就不起作用了。有過探索金手指經(jīng)驗(yàn)的讀者們很有可能會(huì)碰到這種問題。
????怎么從原理上來(lái)解釋這樣的現(xiàn)象呢?

跳動(dòng)的指針
????有些細(xì)節(jié)值得我們仔細(xì)觀察:

????上圖為SaveBlock1這個(gè)結(jié)構(gòu)體定義結(jié)尾的部分,有一行注釋:
????這行注釋表明SaveBlock1需要占據(jù)0x3D88個(gè)字節(jié),但它和符號(hào)表里的信息是不符的:

? ? 符號(hào)表中的gSaveBlock1占據(jù)了0x3e08個(gè)字節(jié),比0x3D88多出了0x80個(gè)字節(jié),這多出來(lái)的字節(jié)是用來(lái)做什么的呢?
????從剛才作者舉的游戲中的例子里可以看到,原本的02025A00地址變成了02025A44,地址向后偏移,但是為了把全部0x3D88的信息存儲(chǔ)下來(lái),它不能占用下一個(gè)變量(也就是gPokemonStorage)的空間,這就需要預(yù)留一部分的空間(或者叫緩沖區(qū))來(lái)避免這個(gè)變量的數(shù)據(jù)覆蓋了下一個(gè)變量的數(shù)據(jù)。多出來(lái)的0x80個(gè)字節(jié)就是作為緩沖區(qū)使用的。
????那么gSaveBlock1的地址真的就這么“飄忽不定”,難以尋覓了嗎?
????我們從游戲開發(fā)者的角度來(lái)想想,開發(fā)者肯定有辦法找到它的地址,否則代碼就沒法寫了?,F(xiàn)在我們有了源代碼項(xiàng)目,可以從中找到定位gSaveBlock1的關(guān)鍵。
????在符號(hào)表中搜索gSaveBlock1,會(huì)發(fā)現(xiàn)有兩個(gè)搜索結(jié)果,一個(gè)就是gSaveBlock1本身,還有一個(gè)叫做gSaveBlock1Ptr:

????這種一個(gè)變量名后面跟著Ptr的命名方式,表明這個(gè)變量是一個(gè)指針,它指向Ptr前面那個(gè)名稱的變量。還是以剛才作者在游戲里的角色舉例,查看一下03005D8C處的變量是什么:

????這里的變量是02025A44,正好就是之前我們得到的gSaveBlock1應(yīng)該所處的地址!
????這就是跳動(dòng)的指針,由一個(gè)變量APtr來(lái)保存另一個(gè)變量A的地址,APtr保存的數(shù)值可能會(huì)變化,這就導(dǎo)致了針對(duì)A變量的金手指無(wú)法一直起到作用。
????有的讀者也許會(huì)問,這個(gè)gSaveBlock1Ptr所在的地址會(huì)不會(huì)變呢?也就是說(shuō),有沒有g(shù)SaveBlock1PtrPtr這種東西?在符號(hào)表里是找不到的,這里游戲的開發(fā)者們的套娃只有一層,但就是這一層套娃,也是對(duì)抗金手指的一個(gè)有效辦法,或者說(shuō),這是反作弊的一個(gè)常用手法。
????跳動(dòng)的指針是反作弊的一種常用技術(shù)。

讓指針不再跳動(dòng)
????有句俗語(yǔ)叫“魔高一尺道高一丈”,有時(shí)它也會(huì)反過來(lái),叫成“道高一尺魔高一丈”?!暗馈焙汀澳А本烤拐l(shuí)高?其實(shí)這是一個(gè)此消彼長(zhǎng),“三十年河?xùn)|三十年河西”的變化過程,兩種說(shuō)法都有道理,體現(xiàn)的是辯證關(guān)系……(隨口一說(shuō)的哲學(xué)不要當(dāng)真)
????綠寶石游戲出現(xiàn)了,金手指就出現(xiàn)了。金手指一出現(xiàn),反作弊的手段(例如跳動(dòng)的指針)就出現(xiàn)了。反作弊的手段一出現(xiàn),就會(huì)有“反”反作弊的方法來(lái)應(yīng)對(duì)。只要有人愿意探索,這個(gè)過程就會(huì)一直持續(xù)下去。
????確實(shí)存在這么一條金手指,可以讓跳動(dòng)的指針固定下來(lái),不再跳動(dòng)。這條金手指一旦開啟,所有之前由于指針跳動(dòng)而無(wú)法使用的金手指就都可以用了。網(wǎng)上有的人給這類金手指起名叫做“解限碼”,含義是解除了反作弊機(jī)制的限制。
????首先找一下,源代碼項(xiàng)目里邊是怎么設(shè)置gSaveBlock1Ptr這個(gè)變量的。如果直接在VS Code中使用“編輯——在文件中查找”來(lái)查找gSaveBlock1Ptr的話,會(huì)看到上千個(gè)搜索結(jié)果:

????因?yàn)間SaveBlock1Ptr的使用實(shí)在是太頻繁了,如果要找到它被賦值的那條語(yǔ)句,上千條搜索結(jié)果還是太多了。那是不是把賦值用的等號(hào)“=”放在變量名后面搜索就可以了呢?

????為了避免不知道變量gSaveBlock1Ptr和等號(hào)“=”之間隔開了幾個(gè)空格,還特意用了正則表達(dá)式中的\s*來(lái)應(yīng)對(duì)這種情況?,F(xiàn)在只有一條搜索結(jié)果,位于load_save.c文件的第118行,但是它并不是對(duì)gSaveBlock1Ptr賦值,而是對(duì)它指向的內(nèi)容賦值(前面有一個(gè)星號(hào)“*”運(yùn)算符)。
????不過這一段代碼可以通過它的注釋大致了解它的功能:從103行開始,先把幾個(gè)指針指向的內(nèi)容暫存一下,然后在第110行重新設(shè)置每個(gè)指針的位置(注釋“change saveblock's pointers”),最后在第117行開始把之前暫存的內(nèi)容恢復(fù)到這些指針指向的變量中??磥?lái)問題的關(guān)鍵在于第110行調(diào)用的函數(shù)SetSaveBlocksPointers。
????轉(zhuǎn)到該函數(shù)的定義:

????終于發(fā)現(xiàn)了這些指針是怎么“跳動(dòng)”的了。關(guān)鍵在于第74行調(diào)用了Random函數(shù),生成了一個(gè)隨機(jī)數(shù),隨后在第76~78行,為每個(gè)Ptr變量設(shè)置了一個(gè)隨機(jī)偏移offset,這就是每次打開游戲這些指針會(huì)跳動(dòng)的原因!
????如果想讓這些指針的位置固定不變,在C代碼里面只需要在第76行之前把offset設(shè)置為0就可以了,比如把第74行改成這樣:
????但是,如果我們用金手指的話,面對(duì)的就不是C代碼,而是匯編代碼。并且這種金手指會(huì)不可避免地修改ROM,在究極綠寶石5.3——科普向,什么是金手指(五)里提到過,使用這種金手指相當(dāng)于修改游戲文件本身,也就相當(dāng)于對(duì)游戲進(jìn)行改版了。
????從符號(hào)表中查到SetSaveBlocksPointers的地址位于08076bdc處,在VBA的反匯編器里跳轉(zhuǎn)到這個(gè)位置查看:

????有了上期專欄分析匯編代碼的基礎(chǔ),我們可以把匯編代碼和C代碼進(jìn)行對(duì)應(yīng),找找看給offset這個(gè)變量賦值的語(yǔ)句對(duì)應(yīng)到哪些匯編代碼。首先注意這一行:
????可以通過符號(hào)表查到$0806f5cc就是Random函數(shù),這個(gè)函數(shù)有一個(gè)返回值,就是該函數(shù)生成的一個(gè)隨機(jī)數(shù)。
????綠寶石ROM的匯編代碼中,有返回值的函數(shù),默認(rèn)將返回值放在r0寄存器中。
????然后看下面三行代碼:
????第一行是在r4上加上r0的值,第三行的and指令是“按位與”操作,也就是C語(yǔ)言中的“&”運(yùn)算符,把r4和0x7C進(jìn)行按位與操作。這幾行匯編代碼對(duì)應(yīng)的就是源代碼第74行的:
????隨機(jī)數(shù)函數(shù)Random的調(diào)用、加上隨機(jī)數(shù)、按位與等等要素都能匹配得上,并且可以驗(yàn)證SAVEBLOCK_MOVE_RANGE的定義是128,也就是0x80,而0x80-4 = 0x7C,按位與的第二個(gè)操作數(shù)也是對(duì)得上的。順便一提,SAVEBLOCK_MOVE_RANGE的數(shù)值0x80和上一小節(jié)內(nèi)提到的“多出來(lái)的0x80個(gè)字節(jié)就是作為緩沖區(qū)使用的”恰好吻合。
????匯編代碼再往下,就是對(duì)各個(gè)Ptr變量賦值了,因此只需要把08076bee處的匯編代碼修改成讓offset等于0就大功告成了,也就是:
????現(xiàn)在就需要在線轉(zhuǎn)換網(wǎng)站完成最后一步:把匯編代碼翻譯為機(jī)器代碼:

????之前我們的用法是把字節(jié)序列翻譯為匯編代碼,那時(shí)在線網(wǎng)站左上角寫的是“HEX to ARM”,只要點(diǎn)擊一下就可以把它變成“ARM to HEX”,也就是從匯編代碼轉(zhuǎn)換到字節(jié)序列。結(jié)果顯示在右下角的THUMB一欄內(nèi),是0024。
????因此,這條固定指針位置的金手指就是:
????格式是原始代碼,注意按照小端序拼接后,順序會(huì)反過來(lái)。
????這條代碼的含義是:將原本SetSaveBlocksPointers函數(shù)中的一條語(yǔ)句“and r4, r0”替換為另一條語(yǔ)句“mov r4, 0”。

關(guān)于修改ROM程序金手指的一點(diǎn)思考
????上面那種金手指修改的是游戲邏輯,更準(zhǔn)確地來(lái)講,它修改的是游戲代碼。有一個(gè)值得注意的現(xiàn)象:為了實(shí)現(xiàn)指針跳動(dòng)這個(gè)功能,需要在空間上預(yù)留好緩沖區(qū),還需要寫一個(gè)函數(shù)來(lái)調(diào)用隨機(jī)數(shù)函數(shù),外加幾個(gè)賦值操作——沒有十幾行C代碼無(wú)法完成,而變成匯編代碼行數(shù)會(huì)更多;但是,讓這個(gè)功能失效只需要修改一行匯編代碼。
????這就好比:蓋好一座大樓可能需要幾年的時(shí)間,但是炸塌一座大樓一天就夠用了。
????這類修改ROM程序的金手指(修改ROM的金手指還包括修改ROM數(shù)據(jù)的金手指)大多具備這樣的特點(diǎn),說(shuō)的好聽一些叫“四兩撥千斤”,說(shuō)的難聽一點(diǎn)就是“動(dòng)一動(dòng)小指頭就讓開發(fā)者的辛苦付之東流”。這也是本系列專欄盡管時(shí)時(shí)提到改版游戲,但卻并沒有把任何一個(gè)改版游戲拿來(lái)“開刀”的原因。畢竟這種需要費(fèi)一些力氣才能實(shí)現(xiàn)出來(lái)的反作弊機(jī)制可以被一行或者兩行代碼擊破,可能在心理上不太好接受。
????另外,修改程序的金手指在使用時(shí)必須非常小心。修改數(shù)據(jù)的金手指如果地址不對(duì),后果可能還不是太嚴(yán)重,但是修改程序的金手指一旦地址不對(duì),一般來(lái)說(shuō)整個(gè)游戲都?xì)Я恕8鞣N卡檔、壞檔的罪魁禍?zhǔn)资邪司啪褪沁@種修改程序的金手指,所以它對(duì)探索金手指的知識(shí)儲(chǔ)備要求較高。

????“跳動(dòng)的指針”只是反作弊機(jī)制的一種,原版綠寶石還有其他的反作弊機(jī)制,將會(huì)在下一期專欄介紹。
????再次感謝眾位讀者的支持!