久遠(yuǎn)的故事——反匯編破解紅白機(jī)游戲《炸彈人》密碼系統(tǒng)

前言的前言

本文是我上大四時(shí)寫的一篇頗有些紀(jì)念意義的文章,轉(zhuǎn)換成了Markdown格式并稍作修改,在這里作為技術(shù)雜談留個(gè)檔吧。

前言

《炸彈人》(Bomberman)是由當(dāng)時(shí)“六大軟件商”之一的Hudson Soft開發(fā),并于1985年12月19日在日本發(fā)行的經(jīng)典紅白機(jī)游戲,大家小的時(shí)候基本都玩過,所以筆者也就不多花時(shí)間介紹了。

1987年重新發(fā)行的美版box art

比較有趣的是,作為一個(gè)可以稱得上是早期的紅白機(jī)作品,它配有一個(gè)密碼存檔系統(tǒng)。當(dāng)玩家不幸Game Over之后,屏幕下方會(huì)顯示出一串由20個(gè)A~P的字母組成的字符串。在游戲標(biāo)題選擇CONTINUE,輸入這串字符串,玩家就可以接著上一次的進(jìn)度玩了,就算是復(fù)位或重新啟動(dòng)機(jī)器都沒有關(guān)系。

筆者還小的時(shí)候,就覺得這串密碼非常神奇,那看似雜亂無章的字符串后面一定隱藏著什么規(guī)律?,F(xiàn)在,我們就可以利用反匯編技術(shù),來揭示這個(gè)規(guī)律了。筆者使用以下兩個(gè)軟件:

  • FCEUX,是一個(gè)具備調(diào)試、監(jiān)視和記錄等功能的紅白機(jī)模擬器。
  • 帶有NESpackage插件的IDA Pro,是一個(gè)功能強(qiáng)大的反匯編器,可以將紅白機(jī)的ROM文件(*.nes)反匯編為MOS 6502的匯編代碼。

由于《炸彈人》的mapper=0(只有24KB大),因此由IDA Pro反匯編出來的代碼大體上是準(zhǔn)確的,不存在換頁機(jī)制的問題。事實(shí)上,這個(gè)游戲是筆者龐大的紅白機(jī)游戲庫中,唯一一個(gè)帶有密碼系統(tǒng)的無mapper的ROM。不過美中不足的是,IDA Pro好像無法解釋FCEUX做記錄產(chǎn)生的CDL(Code/Data Log)文件,如果可以這樣的話,IDA Pro就能更好地區(qū)分?jǐn)?shù)據(jù)段和代碼段,得到的匯編代碼就應(yīng)該是100%準(zhǔn)確的了。寫完這篇文章之后還可以試著做一個(gè)CDL Parser用IDAPython插件跑一跑。

本文共分為八節(jié)。除本節(jié)外,第二節(jié)講述游戲狀態(tài)數(shù)據(jù)及密碼映射表的定址,第三到六節(jié)講述探索各內(nèi)存塊存儲(chǔ)的信息的過程,第七節(jié)講述密碼的最終形成,第八節(jié)附有C++寫成的密碼生成器的源碼。

在正文開始之前,先明確一點(diǎn):在行文過程中描述地址時(shí),均采用十六進(jìn)制。記法是:使用$addr表示內(nèi)存地址,使用[$addr]來表示內(nèi)存地址addr處存儲(chǔ)的數(shù)據(jù),使用[reg]來表示寄存器reg中的數(shù)據(jù)。

好了。Let's get our hands dirty.

基礎(chǔ)工作——游戲狀態(tài)數(shù)據(jù)及密碼映射表的定址

首先要解決一個(gè)問題:游戲狀態(tài)的數(shù)據(jù)到底存儲(chǔ)在哪些內(nèi)存塊中?想要自行通過調(diào)試得出基本是不可能的,比較方便的做法是參考這個(gè)游戲的金手指,Google得到的一些金手指如:0058:00~5E(選關(guān))、0073:50(炸彈5級(jí)威力)、0074:09(可同時(shí)放置10個(gè)炸彈)、0075~0079:01(得到全部特殊道具)。通過這些,我們便可以得知內(nèi)存中的一個(gè)或某幾個(gè)字節(jié)的具體含義。當(dāng)然,金手指包含的信息也不完全,有許多還要手動(dòng)得出結(jié)論。

接下來是艱苦的任務(wù):逐一確定游戲狀態(tài)數(shù)據(jù)的存儲(chǔ)地址。

打開FCEUX,加載ROM,玩游戲直到小人耗掉最后一條命。當(dāng)小人已經(jīng)消失,但是Game Over和密碼屏幕還未出來之前,暫停模擬器,在以上已知的內(nèi)存區(qū)域中選取一個(gè)(這里選擇$75~$79),使用FCEUX內(nèi)置的調(diào)試器打上讀取斷點(diǎn)。然后讓模擬器繼續(xù)運(yùn)行,直到停止在斷點(diǎn)處,觀察該處的內(nèi)容,如下:
>00:E31B:B1 34 LDA ($34) ,Y @ $0077 = #$00

可以發(fā)現(xiàn)在$E31B處讀取了[$77]。使用IDA Pro加載ROM文件,得到6502匯編代碼,定位到$E31B,觀察它的上下文,如下:

代碼清單2-A

ROM:E310 sub_E310: ; CODE XREF: sub_E291:loc_E2AF_p
ROM:E310 ; sub_E291+2C_p
ROM:E310 LDA #4
ROM:E312 STA byte_20
ROM:E314 LDA #0
ROM:E316 STA byte_24
ROM:E318 loc_E318: ; CODE XREF: sub_E310+14_j
ROM:E318 JSR sub_E327
ROM:E31B LDA ($34),Y
ROM:E31D CLC
ROM:E31E ADC byte_24
ROM:E320 STA byte_24
ROM:E322 DEC byte_20
ROM:E324 BNE loc_E318
ROM:E326 RTS

這是一段子程序代碼,至于它的作用是什么,在后面揭曉。

我們關(guān)心一下LDA ($34),Y這條指令的尋址方式。在6502體系中,這種方式叫做零頁間接索引尋址(zero-page indirect indexed addressing)。其過程是:取得[$34]和[$35](這兩個(gè)地址都位于$00~$FF,亦即第0頁的范圍內(nèi)),將[$35]作為高8位,[$34]作為低8位,再與[Y]做算術(shù)加法,得到16位的有效地址Z,最后將[Z]推入累加器A中。

那么,[$34]和[$35]怎么得來的?可以發(fā)現(xiàn),在$E318處有一條JSR指令,它的功能相當(dāng)于80x86匯編中的CALL指令,調(diào)用了標(biāo)記sub_E327處的子程序。該子程序的全貌是:

代碼清單2-B

ROM:E327 sub_E327: ; CODE XREF: sub_DA86:loc_DBCB_p
ROM:E327 ; sub_E291+21_p ...
ROM:E327 LDA $E334,X
ROM:E32A STA byte_34
ROM:E32C INX
ROM:E32D LDA $E334,X
ROM:E330 STA byte_35
ROM:E332 INX
ROM:E333 RTS

再看一下LDA $E334,X這條指令的尋址方式,這叫絕對(duì)索引尋址(absolute indexed addressing),其有效地址就是將[$E334]與[X]相加。STA *指令的含義就是將[A]存入地址*中,INX則是將[X]加1。

由此可見,$E334實(shí)際上是一個(gè)存儲(chǔ)特定信息的向量的首地址,因此$E334及其之后的數(shù)據(jù)對(duì)我們而言十分重要。跳轉(zhuǎn)到該處,內(nèi)容是這樣的(為了看起來舒服一些,把IDA Pro生成的橫線注釋都去掉了):

代碼清單2-C

ROM:E334 RRA byte_0
ROM:E336 RRA 0,X
ROM:E338 CMP $6100,X
ROM:E33B BRK
ROM:E33C STA $6600,Y
ROM:E33F BRK
ROM:E340 NOP
ROM:E343 BRK
ROM:E344 ADC $9A00,Y
ROM:E347 BRK
ROM:E348 aTcub:
ROM:E348 unicode 0, <tcub>
ROM:E348 .WORD $9B
ROM:E352 ADC byte_0
ROM:E354 STY 0,X
ROM:E356 DEC $7600,X
ROM:E359 BRK
ROM:E35A .BYTE $95, 0, $41, $4F, $46, $4B, $43, $50, $47, $45, $4C
ROM:E35A .BYTE $42, $48, $4D, $4A, $44, $4E, $49

這顯然是不正確的——這一區(qū)域存儲(chǔ)的應(yīng)當(dāng)是數(shù)據(jù)(類似上面$E35A處),但是IDA Pro把它們解釋成了代碼。為了看到它們的本來面目,我們就對(duì)這些代碼進(jìn)行undefine(解除定義)操作,得到以下數(shù)據(jù):

代碼清單2-D

ROM:E334 .BYTE $67 ; g
ROM:E335 .BYTE 0
ROM:E336 .BYTE $77 ; w
ROM:E337 .BYTE 0
ROM:E338 .BYTE $DD ; ?
ROM:E339 .BYTE 0
(……省略一部分)
ROM:E358 .BYTE $76 ; v
ROM:E359 .BYTE 0
ROM:E35A .BYTE $95 ; ?
ROM:E35B .BYTE 0
ROM:E35C .BYTE $41 ; A
ROM:E35D .BYTE $4F ; O
ROM:E35E .BYTE $46 ; F
(……省略一部分)
ROM:E36A .BYTE $4E ; N
ROM:E36B .BYTE $49 ; I

可以非常直觀地看到,上面的數(shù)據(jù)明顯分為$E334~$E35B、$E35C~$E36B這兩塊。而$E35C~$E36B這16個(gè)單元內(nèi),存儲(chǔ)的正好就是組成密碼的A~P這16個(gè)字符的ASCII碼值,說明這一塊區(qū)域是生成密碼所用的映射表。那么,$E334~$E35B存儲(chǔ)的又是什么?將它們的內(nèi)容與前文金手指的地址做比較,可以大膽地推測這一塊區(qū)域存儲(chǔ)的是游戲狀態(tài)數(shù)據(jù)的內(nèi)存地址表。其內(nèi)容為:
(67,77,DD,61,99,66,DC,64,79,9A,74,63,75,62,9B,65,94,DE,76,95)

按照上面的數(shù)據(jù)將其地址從低到高排列為:
(61,62,63,64,65,66,67,74,75,76,77,79,94,95,99,9A,9B,DC,DD,DE)

下面綜合代碼分析和內(nèi)存監(jiān)視兩種方法來證明這個(gè)猜想,以及確定各個(gè)內(nèi)存單元的作用?,F(xiàn)在我們已知的信息是:$74存儲(chǔ)最大炸彈數(shù),$75~$79存儲(chǔ)特殊道具是否取得的狀態(tài)(但是各個(gè)單元與道具的對(duì)應(yīng)關(guān)系未知)。

$DC~$DE地址塊的分析——炸彈威力及關(guān)卡信息

取$E338處的字節(jié)DD,查找代碼交叉引用(code xref),選擇寫類型,來到子程序sub_E291處:

代碼清單3

ROM:E291 sub_E291: ; CODE XREF: ROM:9CF1_p
ROM:E291 ; sub_DCD6+1B_p
ROM:E291 LDA byte_73
ROM:E293 LSR A
ROM:E294 LSR A
ROM:E295 LSR A
ROM:E296 LSR A
ROM:E297 STA byte_DC
ROM:E299 LDA byte_58
ROM:E29B AND #$F
ROM:E29D STA byte_DD
ROM:E29F LDA byte_58
ROM:E2A1 LSR A
ROM:E2A2 LSR A
ROM:E2A3 LSR A
ROM:E2A4 LSR A
ROM:E2A5 STA byte_DE
ROM:E2A7 LDY #0
ROM:E2A9 LDX #0
ROM:E2AB LDA #3
ROM:E2AD STA byte_1F

由此還有意外收獲:游戲狀態(tài)數(shù)據(jù)地址表中的$DC、$DD和$DE這三個(gè)地址均被執(zhí)行了STA指令。通過閱讀代碼,我們發(fā)現(xiàn),[$DC]是[$73]右移4位得到的;[$DD]、[$DE]分別是[$58]取低4位(與F做邏輯與)和高4位(右移4位)得到的。根據(jù)金手指,可知$DC存儲(chǔ)的是炸彈威力的信息,而$DD、$DE存儲(chǔ)的是關(guān)卡信息。

實(shí)際表現(xiàn)出來是不是這樣?我們可以通過游戲來進(jìn)行檢驗(yàn)。使用FCEUX運(yùn)行游戲,輸入一個(gè)在著名的國外游戲攻略網(wǎng)站GameFAQs上找到的密碼:
DJFEMPBPCGJKEFEEFBAC

該密碼的作用是定位到第37關(guān),并且擁有炸彈威力5和炸彈數(shù)10,分?jǐn)?shù)為6557400,得到了使行走速度變成原來的兩倍的道具(圖標(biāo)是一只溜冰鞋)。讓小人自殺三次Game Over之后,打開內(nèi)存監(jiān)視器,查看這些內(nèi)存單元的內(nèi)容,結(jié)果是:
[$58]=25H=00100101B [$73]=50H=01010000B [$DC]=[$DD]=05H=00000101B [$DE]=02H=00000010B

這與上面通過代碼得出的結(jié)論完全一致。

$61~$67地址塊的分析——游戲分?jǐn)?shù)的存儲(chǔ)方式

地址$61~$67是連續(xù)的,我們不難考慮到,它們存儲(chǔ)的應(yīng)該是同一數(shù)據(jù)項(xiàng)。事實(shí)是否如此?我們切換到RAM段,觀察其代碼:

代碼清單4-A

RAM:0061 byte_61: .BYTE 0 ; (uninited) ; DATA XREF: sub_DD1B+25_w
RAM:0062 ; 0 .BYTE uninited & unexplored
RAM:0063 ; 0 .BYTE uninited & unexplored
RAM:0064 ; 0 .BYTE uninited & unexplored
RAM:0065 ; 0 .BYTE uninited & unexplored
RAM:0066 .BYTE 0 ; (uninited)
RAM:0067 ; 0 .BYTE uninited & unexplored

由此可見,只有$61存在數(shù)據(jù)交叉引用(data xref)。我們跳轉(zhuǎn)到子程序sub_DD1B處,一部分代碼如下:

代碼清單4-B

ROM:DD1B sub_DD1B: ; CODE XREF: sub_CC2E+2B9_p
ROM:DD1B STX byte_24
ROM:DD1D STY byte_25
ROM:DD1F LDX byte_59
ROM:DD21 BNE loc_DD5A
ROM:DD23 LDX #6
ROM:DD25 loc_DD25: ; CODE XREF: sub_DD03+A_j
ROM:DD25 ; sub_DD0F+A_j ...
ROM:DD25 LDY #0
ROM:DD27 CLC
ROM:DD28 ADC $61,X
ROM:DD2A loc_DD2A: ; CODE XREF: sub_DD1B+19_j
ROM:DD2A STA $61,X
ROM:DD2C LDA $61,X
ROM:DD2E SEC
ROM:DD2F SBC #$A
ROM:DD31 BCC loc_DD36
ROM:DD33 INY
ROM:DD34 BNE loc_DD2A
ROM:DD36 loc_DD36: ; CODE XREF: sub_DD1B+16_j
ROM:DD36 CPY #0
ROM:DD38 BEQ loc_DD42
ROM:DD3A TYA
ROM:DD3B DEX
ROM:DD3C BPL loc_DD25
ROM:DD3E LDA #9
ROM:DD40 STA byte_61

好了,接下來粗略地讀一下代碼。要將這些東西全部理解是非常困難的,所以我們無需關(guān)注太多的細(xì)節(jié)。

可以知道,剛進(jìn)入這段子程序時(shí),[X]的初值是6,[Y]的初值是0。將[A]與[$(61+[X])]做一次帶進(jìn)位加法,接著進(jìn)入一個(gè)比較奇特的循環(huán):這個(gè)循環(huán)每次將[A]暫存入[$(61+[X])],將[A]減去十進(jìn)制的10(立即尋址),然后以程序狀態(tài)寄存器中進(jìn)位比特(C)為標(biāo)識(shí),檢查差是否大于0。當(dāng)C=1時(shí),將[Y]增1,繼續(xù)循環(huán);當(dāng)C=0時(shí),退出循環(huán)。

重復(fù)執(zhí)行上面的過程,每次執(zhí)行完畢后,都將[Y]傳送入A中,并將[X]減去1,直到[X]的終值為0。這樣,這一塊的數(shù)據(jù)就以地址從高到低的順序得出來了。但是我們?nèi)匀徊恢浪鼈兙唧w表示什么。所以,我們還要找到調(diào)用這段子程序的代碼在哪里,下面代碼段中$CEE7處的JSR語句即是。為了方便闡述,將這個(gè)語句的上下文,以及有分支指令要跳轉(zhuǎn)到的代碼段都抄錄在這里:

代碼清單4-C

ROM:CEE1 loc_CEE1: ; CODE XREF: sub_CC2E+5C_j
ROM:CEE1 LDA #4
ROM:CEE3 STA byte_DF
ROM:CEE5 LDA #$A
ROM:CEE7 JSR sub_DD1B
ROM:CEEA LDX byte_5B
ROM:CEEC DEX
ROM:CEED BEQ loc_CF05
ROM:CEEF DEX
ROM:CEF0 BEQ loc_CF12
ROM:CEF2 DEX
ROM:CEF3 BEQ loc_CF22
ROM:CEF5 DEX
(……省略一部分)
ROM:CF05 loc_CF05: ; CODE XREF: sub_CC2E+2BF_j
ROM:CF05 LDA byte_74
ROM:CF07 CMP #9
ROM:CF09 BEQ loc_CF0D
ROM:CF0B INC byte_74
ROM:CF0D loc_CF0D: ; CODE XREF: sub_CC2E+2DB_j
ROM:CF0D LDA #4
ROM:CF0F STA byte_B5
ROM:CF11 RTS
ROM:CF12 loc_CF12: ; CODE XREF: sub_CC2E+2C2_j
ROM:CF12 LDA byte_73
ROM:CF14 CMP #$50 ; 'P'
ROM:CF16 BEQ loc_CF1D
ROM:CF18 CLC
ROM:CF19 ADC #$10
ROM:CF1B STA byte_73
ROM:CF1D loc_CF1D: ; CODE XREF: sub_CC2E+2E8_j
ROM:CF1D LDA #4
ROM:CF1F STA byte_B5
ROM:CF21 RTS
ROM:CF22 loc_CF22: ; CODE XREF: sub_CC2E+2C5_j
ROM:CF22 LDA #1
ROM:CF24 STA byte_75
ROM:CF26 LDA #4
ROM:CF28 STA byte_B5
ROM:CF2A RTS

通過這段代碼,可以發(fā)現(xiàn),在進(jìn)入sub_DD1B這個(gè)子程序之前,[A]的初值是10。另外,注意觀察BEQ指令跳轉(zhuǎn)向的代碼段,以loc_CF12處為例,它對(duì)$73進(jìn)行操作,具體來講是:先檢查該單元的數(shù)據(jù)是不是等于50H,若不等,則將它的值加上10H。聯(lián)系一下前面得到的成果,可以清楚地發(fā)現(xiàn)這一小段代碼的作用是使炸彈威力提升一級(jí)。同理,對(duì)$74的操作是使最大炸彈數(shù)增加一個(gè),對(duì)$75的操作是得到某種特殊道具。所以我們可以推定,第二段代碼及其上下文的作用是修改游戲狀態(tài)。

那么,sub_DD1B這個(gè)子程序操作的是什么狀態(tài)呢?再回頭看一眼代碼清單4-B的邏輯,不難發(fā)現(xiàn)[$61]~[$67]可以大致表示為:

[$(61+[X])]=10*[Y]+N+Y',其中0<=X<=6,0<=N<=9,Y'是來自計(jì)算[$(62+[X])]時(shí)得到并由TYA指令傳入A中的Y值。

這樣就真相大白了:這一塊內(nèi)存中存放的是一個(gè)7位的十進(jìn)制數(shù),高地址為低位,低地址為高位,Y寄存器充當(dāng)?shù)氖前?56進(jìn)制數(shù)轉(zhuǎn)化為十進(jìn)制數(shù)時(shí)產(chǎn)生的進(jìn)位信號(hào)。滿足“7位的十進(jìn)制數(shù)”這個(gè)條件的,基本就可以確定是玩家的當(dāng)前分?jǐn)?shù)。當(dāng)然,這只是一個(gè)猜測而已,我們還需要檢驗(yàn)一下。利用前一節(jié)所述的密碼進(jìn)入游戲,監(jiān)視[$61]~[$67],結(jié)果是:
(00,00,06,05,05,07,04)

這與密碼所實(shí)現(xiàn)的分?jǐn)?shù)是吻合的,[$61]~[$67]分別表示分?jǐn)?shù)的百萬位、十萬位、萬位、千位和百位。至于十位和個(gè)位則可以忽略,因?yàn)樵谶@個(gè)游戲中,消滅怪物得到的分?jǐn)?shù)都是整百的。

$75~$79地址塊的分析——特殊道具的持有狀態(tài)

根據(jù)金手指,我們已經(jīng)知道了這一塊區(qū)域中的每一個(gè)字節(jié)對(duì)應(yīng)是否持有某種特殊道具的狀態(tài),下面需要知道的是它們分別對(duì)應(yīng)哪一種道具。查找交叉引用得到的代碼十分復(fù)雜,我們可以用一種傻瓜式的但是非常簡單的方法:直接修改內(nèi)存,然后觀察游戲的表現(xiàn)即可。

正常開始游戲,打開FCEUX自帶的十六進(jìn)制編輯器,找到這一地址區(qū)域,分別將它們的值修改成01,看看會(huì)有什么不同。結(jié)論是:[$75]=01H時(shí),小人的行走速度加倍;[$76]=01H時(shí),小人可以在磚塊上行走;[$77]=01H時(shí),小人放置的炸彈可以由玩家遙控引爆;[$78]=01H時(shí),小人可以在自己放置的炸彈上面行走;[$79]=01H時(shí),小人不會(huì)被自己放置的炸彈炸死。

所以,$75~$79這六個(gè)地址分別對(duì)應(yīng)的道具是(來自GameFAQs上的英文名稱):speed-up、wall-walker、detonator、bomb-walker、flame-proof。不過,在上面的地址表中沒有$78這個(gè)地址,因此bomb-walker這個(gè)道具的狀態(tài)不會(huì)被記錄在密碼中。

$95~$9B地址塊的分析——數(shù)據(jù)校驗(yàn)值

取$E33C處的字節(jié)99,查找代碼交叉引用(code xref),選擇讀類型,來到子程序sub_E291的標(biāo)記loc_E2AF處,這段代碼是緊接在代碼清單3后面的:

代碼清單6

ROM:E2AF loc_E2AF: ; CODE XREF: sub_E291+2Aj
ROM:E2AF JSR sub_E310
ROM:E2B2 JSR sub_E327
ROM:E2B5 LDA byte_24
ROM:E2B7 STA ($34),Y
ROM:E2B9 DEC byte_1F
ROM:E2BB BNE loc_E2AF
ROM:E2BD JSR sub_E310
ROM:E2C0 LDA byte_99
ROM:E2C2 ASL A
ROM:E2C3 CLC
ROM:E2C4 ADC byte_24
ROM:E2C6 STA byte_24
ROM:E2C8 LDA byte_9A
ROM:E2CA ASL A
ROM:E2CB CLC
ROM:E2CC ADC byte_24
ROM:E2CE STA byte_24
ROM:E2D0 LDA byte_9B
ROM:E2D2 ASL A
ROM:E2D3 CLC
ROM:E2D4 ADC byte_24
ROM:E2D6 STA byte_95
ROM:E2D8 LDY #0
ROM:E2DA STY byte_54
ROM:E2DC LDX #0

讀上面的代碼,發(fā)現(xiàn)首先調(diào)用了兩個(gè)子程序:sub_E310和sub_E327,這兩個(gè)子程序的代碼可以參考前面的代碼清單2-A和2-B。在這兩個(gè)子程序之前,我們先看一下代碼清單3的最后幾句話,可以發(fā)現(xiàn),在代碼清單6-A的程序開始執(zhí)行前,做了一些初始化工作:[X]=0,[Y]=0,[$1F]=3。在下面的程序中[Y]一直為0,所以我們就不考慮了。

分析sub_E327。前面已經(jīng)提到,它的作用就是每次將[$(E334+[X])]與[$(E335+[X])]存入地址$34和$35中,而$E334恰好是游戲數(shù)據(jù)地址表的基址。觀察一下代碼清單2-D,可以發(fā)現(xiàn)每次存入時(shí),[$34]就是一個(gè)游戲數(shù)據(jù)地址,[$35]=0。

接著看sub_E310。開始時(shí),[$20]=4,[$24]=0,進(jìn)入循環(huán):先調(diào)用一次sub_E327,然后將[$34]作為有效地址Z(回想一下尋址方式),將[Z]載入A寄存器,與[$24]做加法并將和返回該處,將[$20]減1,直到其為0。由此可見,$20是作為循環(huán)變量使用的,[$24]就是四個(gè)[Z]的和,亦即游戲狀態(tài)數(shù)據(jù)地址表中連續(xù)四個(gè)地址處的數(shù)據(jù)的和。例如,在第一次循環(huán)中,[$24]=[$67]+[$77]+[$DD]+[$61]。

我們的焦點(diǎn)轉(zhuǎn)移到$24這個(gè)內(nèi)存單元。在第一次執(zhí)行了這兩個(gè)子程序之后,可見[$34]就是第五個(gè)游戲狀態(tài)數(shù)據(jù)地址,即$99。$E2B5和$E2B7兩句完成的功能就是:將[$24]經(jīng)由A傳送到[$99]。在這里,$1F也作為循環(huán)變量使用,循環(huán)3次就退出。

仔細(xì)想一想,我們就可以總結(jié)出來:將游戲狀態(tài)數(shù)據(jù)地址表劃分為4組,每組5個(gè)地址,則$99、$9A、$9B這三個(gè)單元存放的分別就是它們所在組的前4個(gè)單元內(nèi)數(shù)據(jù)的和,也就是說它們是作為校驗(yàn)值使用的。

上面的循環(huán)只有3次,那么第四組的校驗(yàn)值是如何計(jì)算出來的?繼續(xù)向下走,發(fā)現(xiàn)得到的結(jié)果是:分別將[$99]、[$9A]、[$9B]取出,利用A各算術(shù)左移一位,加到[$24],然后將這個(gè)和存入$95,而$95正好就是第四組的校驗(yàn)值所在地址。也就是說,第四組的校驗(yàn)值與本組的數(shù)據(jù)無關(guān),而與前三組的校驗(yàn)值有關(guān)。校驗(yàn)值就是這樣生成的。

這樣,游戲狀態(tài)數(shù)據(jù)地址表中就只剩下$94了。不過查看與它相關(guān)的代碼時(shí),發(fā)現(xiàn)它除了在復(fù)位中斷處理例程中被設(shè)置為0外,再無其他寫入的操作。并且在長時(shí)間的實(shí)際游戲過程中,它的值也一直為0。所以,我們就假定它只是一個(gè)填充字節(jié)而已,沒有實(shí)際的意義。

所有的數(shù)據(jù)都搞明白了,剩下的工作就是要把這些信息轉(zhuǎn)換成密碼。

最后一步——密碼的生成

在代碼清單6的程序中,最后初始化了[$54]=0以及[X]=0,可以察覺到必有用途。查找一下$54的出現(xiàn)位置,發(fā)現(xiàn)它正好就在下面,代碼是:

代碼清單7-A

ROM:E2DE loc_E2DE: ; CODE XREF: sub_E291+63j
ROM:E2DE JSR sub_E327
ROM:E2E1 LDA ($34),Y
ROM:E2E3 AND #$F
ROM:E2E5 SEC
ROM:E2E6 SBC byte_54
ROM:E2E8 SEC
ROM:E2E9 SBC #7
ROM:E2EB AND #$F
ROM:E2ED STA $180,X
ROM:E2F0 STA byte_54
ROM:E2F2 CPX #$28 ; '('
ROM:E2F4 BNE loc_E2DE
ROM:E2F6 LDA #$23 ; '#'
ROM:E2F8 LDX #6
ROM:E2FA JSR sub_C18E
ROM:E2FD LDX #2

這段代碼雖短,但大有玄機(jī)。它是一個(gè)這樣的循環(huán):開始先調(diào)用子程序sub_E327,取得一個(gè)游戲數(shù)據(jù)地址到$34。然后將[$[$34]]——就是該地址所儲(chǔ)存的游戲數(shù)據(jù)——載入A中,做如下運(yùn)算:先取低4位,減去[$54],再減去7,取低4位。然后,將得到的結(jié)果同時(shí)存入[$(180+[X])]與[$54]中。循環(huán)直到[X]=28H為止。

我們還不知道經(jīng)過這些運(yùn)算后的數(shù)據(jù)終究是什么,但是我們可以明白:由于子程序sub_E327中有兩條INX指令,因此,這些數(shù)據(jù)的存儲(chǔ)起始地址是$182,并且循環(huán)一共執(zhí)行28H/2=20次。從“20次”這一點(diǎn)可以猜測,這很可能就是最后的密碼了??匆幌伦映绦騭ub_C18E:

代碼清單7-B

ROM:C18E sub_C18E: ; CODE XREF: ROM:80B9p
ROM:C18E ; ROM:80CBp ...
ROM:C18E STA VRAM_AR_2 ; VRAM Address Register #2 (W2)
ROM:C191 STX VRAM_AR_2 ; VRAM Address Register #2 (W2)
ROM:C194 RTS

可見,這是將[A]=23H和[X]=6寫入VRAM_AR_2這個(gè)地址。這個(gè)地址是$2006,是紅白機(jī)顯存(VRAM)的地址寄存器之一。繼續(xù)閱讀代碼,這是本文涉及到的最后一個(gè)循環(huán)了:

代碼清單7-C

ROM:E2FF loc_E2FF: ; CODE XREF: sub_E291+7Cj
ROM:E2FF LDA $180,X
ROM:E302 TAY
ROM:E303 LDA $E35C,Y
ROM:E306 STA VRAM_IOR ; VRAM I/O Register (RW)
ROM:E309 INX
ROM:E30A INX
ROM:E30B CPX #$2A ; '*'
ROM:E30D BNE loc_E2FF
ROM:E30F RTS

這段程序以[X]=2開始循環(huán),將[$(180+[X])]傳送到Y(jié)寄存器。然后將[$(E35C+[Y])]送入VRAM_IOR。這個(gè)地址是$2007,是VRAM的I/O專用寄存器,用來向$2006指定地址的位置讀出或?qū)懭胄畔?。每次將[X]增加2,當(dāng)[X]=2AH時(shí),結(jié)束循環(huán)。
我們終于到達(dá)了終點(diǎn)——Y寄存器儲(chǔ)存的是$182為起始地址的向量的內(nèi)容,它其實(shí)是該碼字在密碼映射表中的偏移量。將[$(E35C+[Y])]送入顯存中,屏幕上就能顯示出A~P組成的密碼串了。大功告成。

附言——密碼生成器的C++實(shí)現(xiàn)

我們破解了《炸彈人》這個(gè)看似簡單的游戲的密碼系統(tǒng),就可以使用密碼作為游戲修改器了,也許會(huì)讓游戲過程更加精彩。下面附上筆者用C++寫成的密碼生成器代碼,供參考。

代碼清單8

#include <iostream>
#include <cstdio>
#include <string>

using namespace std;

typedef unsigned char BYTE;
typedef unsigned int DWORD;

typedef struct Info {
  struct {
    BYTE score_hundred; //$67
    BYTE detonator; //$77
    BYTE stage_low; //$DD
    BYTE score_o;//$61
    BYTE checksum; //$99
  } J1;

  struct {
    BYTE score_thousand; //$66
    BYTE bomb_power; //$DC
    BYTE score_100_thousand; //$64
    BYTE flame_proof;//$79
    BYTE checksum; //$9A
  } J2;

  struct {
    BYTE bomb_count; //$74
    BYTE score_million; //$63
    BYTE speed_up; //$75
    BYTE score_o;//$62
    BYTE checksum; //$9B
  } J3;

  struct {
    BYTE score_10_thousand; //$65
    BYTE unknown;//$94
    BYTE stage_high; //$DE
    BYTE wall_walker;//$76
    BYTE checksum; //$95
  } J4;
} INFO;

const string PSW_MAP("AOFKCPGELBHMJDNI");

void process_score(INFO &info, DWORD score) {
  DWORD temp = score / 100;
  info.J1.score_hundred = temp % 10;
  temp /= 10;
  info.J2.score_thousand = temp % 10;
  temp /= 10;
  info.J4.score_10_thousand = temp % 10;
  temp /= 10;
  info.J2.score_100_thousand = temp % 10;
  info.J3.score_million = temp / 10;
  info.J1.score_o = 0;
  info.J3.score_o = 0;
}

void process_stage(INFO &info, BYTE stage) {
  info.J1.stage_low = stage & 0x0F;
  info.J4.stage_high = stage >> 4;
}

void process_checksum(INFO &info) {
  info.J1.checksum = info.J1.score_hundred + info.J1.detonator + info.J1.stage_low + info.J1.score_o;
  info.J2.checksum = info.J2.score_thousand + info.J2.bomb_power + info.J2.score_100_thousand + info.J2.flame_proof;
  info.J3.checksum = info.J3.bomb_count + info.J3.score_million + info.J3.speed_up + info.J3.score_o;
  info.J4.checksum = (info.J1.checksum << 1) + (info.J2.checksum << 1) + (info.J3.checksum << 1);
}

inline BYTE trans(BYTE b, BYTE &t) {
  t = ((b & 0x0F) - t - 7) & 0x0F;
  return (t);
}

string generate_password(INFO info) {
  string psw("");
  BYTE t = 0;
  psw += PSW_MAP[trans(info.J1.score_hundred, t)];
  psw += PSW_MAP[trans(info.J1.detonator, t)];
  psw += PSW_MAP[trans(info.J1.stage_low, t)];
  psw += PSW_MAP[trans(info.J1.score_o, t)];
  psw += PSW_MAP[trans(info.J1.checksum, t)];
  psw += PSW_MAP[trans(info.J2.score_thousand, t)];
  psw += PSW_MAP[trans(info.J2.bomb_power, t)];
  psw += PSW_MAP[trans(info.J2.score_100_thousand, t)];
  psw += PSW_MAP[trans(info.J2.flame_proof, t)];
  psw += PSW_MAP[trans(info.J2.checksum, t)];
  psw += PSW_MAP[trans(info.J3.bomb_count, t)];
  psw += PSW_MAP[trans(info.J3.score_million, t)];
  psw += PSW_MAP[trans(info.J3.speed_up, t)];
  psw += PSW_MAP[trans(info.J3.score_o, t)];
  psw += PSW_MAP[trans(info.J3.checksum, t)];
  psw += PSW_MAP[trans(info.J4.score_10_thousand, t)];
  psw += PSW_MAP[trans(info.J4.unknown, t)];
  psw += PSW_MAP[trans(info.J4.stage_high, t)];
  psw += PSW_MAP[trans(info.J4.wall_walker, t)];
  psw += PSW_MAP[trans(info.J4.checksum, t)];
  return (psw);
}

int main() {
  DWORD value_dw;
  BYTE value_bt;
  char value_ch;
  INFO info;
  printf("Bomberman (J).nes Password Generator\n");
  printf("Please input following items:\n\n");
  
  printf("Score (*****00) -> ");
  scanf("%u", &value_dw);
  process_score(info, value_dw);
  
  printf("Stage number (1~50) -> ");
  scanf("%hhu", &value_bt);
  process_stage(info, value_bt);
  
  printf("Bomb power (1~5) -> ");
  scanf("%hhu", &value_bt);
  info.J2.bomb_power = value_bt;
  
  printf("Bomb count (1~10) -> ");
  scanf("%hhu", &value_bt);
  getchar();
  info.J3.bomb_count = value_bt - 1;
  
  printf("Speed-up enabled [Y/N] -> ");
  scanf("%c", &value_ch);
  getchar();
  info.J3.speed_up = (value_ch == 'Y' ? 1 : 0);
  
  printf("Wall-walker enabled [Y/N] -> ");
  scanf("%c", &value_ch);
  getchar();
  info.J4.wall_walker = (value_ch == 'Y' ? 1 : 0);
  
  printf("Detonator enabled [Y/N] -> ");
  scanf("%c", &value_ch);
  getchar();
  info.J1.detonator = (value_ch == 'Y' ? 1 : 0);
  
  printf("Flame-proof enabled [Y/N] -> ");
  scanf("%c", &value_ch);
  getchar();
  info.J2.flame_proof = (value_ch == 'Y' ? 1 : 0);
  
  info.J4.unknown = 0;
  process_checksum(info);
  printf("\nThe generated password is: %s\n", generate_password(info).c_str());
  system("pause");
  return 0;
}

The End

民那晚安。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容