久远的故事——反汇编破解红白机游戏《炸弹人》密码系统

前言的前言

本文是我上大四时写的一篇颇有些纪念意义的文章,转换成了Markdown格式并稍作修改,在这里作为技术杂谈留个档吧。

前言

《炸弹人》(Bomberman)是由当时“六大软件商”之一的Hudson Soft开发,并于1985年12月19日在日本发行的经典红白机游戏,大家小的时候基本都玩过,所以笔者也就不多花时间介绍了。

1987年重新发行的美版box art

比较有趣的是,作为一个可以称得上是早期的红白机作品,它配有一个密码存档系统。当玩家不幸Game Over之后,屏幕下方会显示出一串由20个A~P的字母组成的字符串。在游戏标题选择CONTINUE,输入这串字符串,玩家就可以接着上一次的进度玩了,就算是复位或重新启动机器都没有关系。

笔者还小的时候,就觉得这串密码非常神奇,那看似杂乱无章的字符串后面一定隐藏着什么规律。现在,我们就可以利用反汇编技术,来揭示这个规律了。笔者使用以下两个软件:

  • FCEUX,是一个具备调试、监视和记录等功能的红白机模拟器。
  • 带有NESpackage插件的IDA Pro,是一个功能强大的反汇编器,可以将红白机的ROM文件(*.nes)反汇编为MOS 6502的汇编代码。

由于《炸弹人》的mapper=0(只有24KB大),因此由IDA Pro反汇编出来的代码大体上是准确的,不存在换页机制的问题。事实上,这个游戏是笔者庞大的红白机游戏库中,唯一一个带有密码系统的无mapper的ROM。不过美中不足的是,IDA Pro好像无法解释FCEUX做记录产生的CDL(Code/Data Log)文件,如果可以这样的话,IDA Pro就能更好地区分数据段和代码段,得到的汇编代码就应该是100%准确的了。写完这篇文章之后还可以试着做一个CDL Parser用IDAPython插件跑一跑。

本文共分为八节。除本节外,第二节讲述游戏状态数据及密码映射表的定址,第三到六节讲述探索各内存块存储的信息的过程,第七节讲述密码的最终形成,第八节附有C++写成的密码生成器的源码。

在正文开始之前,先明确一点:在行文过程中描述地址时,均采用十六进制。记法是:使用$addr表示内存地址,使用[$addr]来表示内存地址addr处存储的数据,使用[reg]来表示寄存器reg中的数据。

好了。Let's get our hands dirty.

基础工作——游戏状态数据及密码映射表的定址

首先要解决一个问题:游戏状态的数据到底存储在哪些内存块中?想要自行通过调试得出基本是不可能的,比较方便的做法是参考这个游戏的金手指,Google得到的一些金手指如:0058:00~5E(选关)、0073:50(炸弹5级威力)、0074:09(可同时放置10个炸弹)、0075~0079:01(得到全部特殊道具)。通过这些,我们便可以得知内存中的一个或某几个字节的具体含义。当然,金手指包含的信息也不完全,有许多还要手动得出结论。

接下来是艰苦的任务:逐一确定游戏状态数据的存储地址。

打开FCEUX,加载ROM,玩游戏直到小人耗掉最后一条命。当小人已经消失,但是Game Over和密码屏幕还未出来之前,暂停模拟器,在以上已知的内存区域中选取一个(这里选择$75~$79),使用FCEUX内置的调试器打上读取断点。然后让模拟器继续运行,直到停止在断点处,观察该处的内容,如下:
>00:E31B:B1 34 LDA ($34) ,Y @ $0077 = #$00

可以发现在$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

这是一段子程序代码,至于它的作用是什么,在后面揭晓。

我们关心一下LDA ($34),Y这条指令的寻址方式。在6502体系中,这种方式叫做零页间接索引寻址(zero-page indirect indexed addressing)。其过程是:取得[$34]和[$35](这两个地址都位于$00~$FF,亦即第0页的范围内),将[$35]作为高8位,[$34]作为低8位,再与[Y]做算术加法,得到16位的有效地址Z,最后将[Z]推入累加器A中。

那么,[$34]和[$35]怎么得来的?可以发现,在$E318处有一条JSR指令,它的功能相当于80x86汇编中的CALL指令,调用了标记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这条指令的寻址方式,这叫绝对索引寻址(absolute indexed addressing),其有效地址就是将[$E334]与[X]相加。STA *指令的含义就是将[A]存入地址*中,INX则是将[X]加1。

由此可见,$E334实际上是一个存储特定信息的向量的首地址,因此$E334及其之后的数据对我们而言十分重要。跳转到该处,内容是这样的(为了看起来舒服一些,把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

这显然是不正确的——这一区域存储的应当是数据(类似上面$E35A处),但是IDA Pro把它们解释成了代码。为了看到它们的本来面目,我们就对这些代码进行undefine(解除定义)操作,得到以下数据:

代码清单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

可以非常直观地看到,上面的数据明显分为$E334~$E35B、$E35C~$E36B这两块。而$E35C~$E36B这16个单元内,存储的正好就是组成密码的A~P这16个字符的ASCII码值,说明这一块区域是生成密码所用的映射表。那么,$E334~$E35B存储的又是什么?将它们的内容与前文金手指的地址做比较,可以大胆地推测这一块区域存储的是游戏状态数据的内存地址表。其内容为:
(67,77,DD,61,99,66,DC,64,79,9A,74,63,75,62,9B,65,94,DE,76,95)

按照上面的数据将其地址从低到高排列为:
(61,62,63,64,65,66,67,74,75,76,77,79,94,95,99,9A,9B,DC,DD,DE)

下面综合代码分析和内存监视两种方法来证明这个猜想,以及确定各个内存单元的作用。现在我们已知的信息是:$74存储最大炸弹数,$75~$79存储特殊道具是否取得的状态(但是各个单元与道具的对应关系未知)。

$DC~$DE地址块的分析——炸弹威力及关卡信息

取$E338处的字节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

由此还有意外收获:游戏状态数据地址表中的$DC、$DD和$DE这三个地址均被执行了STA指令。通过阅读代码,我们发现,[$DC]是[$73]右移4位得到的;[$DD]、[$DE]分别是[$58]取低4位(与F做逻辑与)和高4位(右移4位)得到的。根据金手指,可知$DC存储的是炸弹威力的信息,而$DD、$DE存储的是关卡信息。

实际表现出来是不是这样?我们可以通过游戏来进行检验。使用FCEUX运行游戏,输入一个在著名的国外游戏攻略网站GameFAQs上找到的密码:
DJFEMPBPCGJKEFEEFBAC

该密码的作用是定位到第37关,并且拥有炸弹威力5和炸弹数10,分数为6557400,得到了使行走速度变成原来的两倍的道具(图标是一只溜冰鞋)。让小人自杀三次Game Over之后,打开内存监视器,查看这些内存单元的内容,结果是:
[$58]=25H=00100101B [$73]=50H=01010000B [$DC]=[$DD]=05H=00000101B [$DE]=02H=00000010B

这与上面通过代码得出的结论完全一致。

$61~$67地址块的分析——游戏分数的存储方式

地址$61~$67是连续的,我们不难考虑到,它们存储的应该是同一数据项。事实是否如此?我们切换到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存在数据交叉引用(data xref)。我们跳转到子程序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

好了,接下来粗略地读一下代码。要将这些东西全部理解是非常困难的,所以我们无需关注太多的细节。

可以知道,刚进入这段子程序时,[X]的初值是6,[Y]的初值是0。将[A]与[$(61+[X])]做一次带进位加法,接着进入一个比较奇特的循环:这个循环每次将[A]暂存入[$(61+[X])],将[A]减去十进制的10(立即寻址),然后以程序状态寄存器中进位比特(C)为标识,检查差是否大于0。当C=1时,将[Y]增1,继续循环;当C=0时,退出循环。

重复执行上面的过程,每次执行完毕后,都将[Y]传送入A中,并将[X]减去1,直到[X]的终值为0。这样,这一块的数据就以地址从高到低的顺序得出来了。但是我们仍然不知道它们具体表示什么。所以,我们还要找到调用这段子程序的代码在哪里,下面代码段中$CEE7处的JSR语句即是。为了方便阐述,将这个语句的上下文,以及有分支指令要跳转到的代码段都抄录在这里:

代码清单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

通过这段代码,可以发现,在进入sub_DD1B这个子程序之前,[A]的初值是10。另外,注意观察BEQ指令跳转向的代码段,以loc_CF12处为例,它对$73进行操作,具体来讲是:先检查该单元的数据是不是等于50H,若不等,则将它的值加上10H。联系一下前面得到的成果,可以清楚地发现这一小段代码的作用是使炸弹威力提升一级。同理,对$74的操作是使最大炸弹数增加一个,对$75的操作是得到某种特殊道具。所以我们可以推定,第二段代码及其上下文的作用是修改游戏状态。

那么,sub_DD1B这个子程序操作的是什么状态呢?再回头看一眼代码清单4-B的逻辑,不难发现[$61]~[$67]可以大致表示为:

[$(61+[X])]=10*[Y]+N+Y',其中0<=X<=6,0<=N<=9,Y'是来自计算[$(62+[X])]时得到并由TYA指令传入A中的Y值。

这样就真相大白了:这一块内存中存放的是一个7位的十进制数,高地址为低位,低地址为高位,Y寄存器充当的是把256进制数转化为十进制数时产生的进位信号。满足“7位的十进制数”这个条件的,基本就可以确定是玩家的当前分数。当然,这只是一个猜测而已,我们还需要检验一下。利用前一节所述的密码进入游戏,监视[$61]~[$67],结果是:
(00,00,06,05,05,07,04)

这与密码所实现的分数是吻合的,[$61]~[$67]分别表示分数的百万位、十万位、万位、千位和百位。至于十位和个位则可以忽略,因为在这个游戏中,消灭怪物得到的分数都是整百的。

$75~$79地址块的分析——特殊道具的持有状态

根据金手指,我们已经知道了这一块区域中的每一个字节对应是否持有某种特殊道具的状态,下面需要知道的是它们分别对应哪一种道具。查找交叉引用得到的代码十分复杂,我们可以用一种傻瓜式的但是非常简单的方法:直接修改内存,然后观察游戏的表现即可。

正常开始游戏,打开FCEUX自带的十六进制编辑器,找到这一地址区域,分别将它们的值修改成01,看看会有什么不同。结论是:[$75]=01H时,小人的行走速度加倍;[$76]=01H时,小人可以在砖块上行走;[$77]=01H时,小人放置的炸弹可以由玩家遥控引爆;[$78]=01H时,小人可以在自己放置的炸弹上面行走;[$79]=01H时,小人不会被自己放置的炸弹炸死。

所以,$75~$79这六个地址分别对应的道具是(来自GameFAQs上的英文名称):speed-up、wall-walker、detonator、bomb-walker、flame-proof。不过,在上面的地址表中没有$78这个地址,因此bomb-walker这个道具的状态不会被记录在密码中。

$95~$9B地址块的分析——数据校验值

取$E33C处的字节99,查找代码交叉引用(code xref),选择读类型,来到子程序sub_E291的标记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

读上面的代码,发现首先调用了两个子程序:sub_E310和sub_E327,这两个子程序的代码可以参考前面的代码清单2-A和2-B。在这两个子程序之前,我们先看一下代码清单3的最后几句话,可以发现,在代码清单6-A的程序开始执行前,做了一些初始化工作:[X]=0,[Y]=0,[$1F]=3。在下面的程序中[Y]一直为0,所以我们就不考虑了。

分析sub_E327。前面已经提到,它的作用就是每次将[$(E334+[X])]与[$(E335+[X])]存入地址$34和$35中,而$E334恰好是游戏数据地址表的基址。观察一下代码清单2-D,可以发现每次存入时,[$34]就是一个游戏数据地址,[$35]=0。

接着看sub_E310。开始时,[$20]=4,[$24]=0,进入循环:先调用一次sub_E327,然后将[$34]作为有效地址Z(回想一下寻址方式),将[Z]载入A寄存器,与[$24]做加法并将和返回该处,将[$20]减1,直到其为0。由此可见,$20是作为循环变量使用的,[$24]就是四个[Z]的和,亦即游戏状态数据地址表中连续四个地址处的数据的和。例如,在第一次循环中,[$24]=[$67]+[$77]+[$DD]+[$61]。

我们的焦点转移到$24这个内存单元。在第一次执行了这两个子程序之后,可见[$34]就是第五个游戏状态数据地址,即$99。$E2B5和$E2B7两句完成的功能就是:将[$24]经由A传送到[$99]。在这里,$1F也作为循环变量使用,循环3次就退出。

仔细想一想,我们就可以总结出来:将游戏状态数据地址表划分为4组,每组5个地址,则$99、$9A、$9B这三个单元存放的分别就是它们所在组的前4个单元内数据的和,也就是说它们是作为校验值使用的。

上面的循环只有3次,那么第四组的校验值是如何计算出来的?继续向下走,发现得到的结果是:分别将[$99]、[$9A]、[$9B]取出,利用A各算术左移一位,加到[$24],然后将这个和存入$95,而$95正好就是第四组的校验值所在地址。也就是说,第四组的校验值与本组的数据无关,而与前三组的校验值有关。校验值就是这样生成的。

这样,游戏状态数据地址表中就只剩下$94了。不过查看与它相关的代码时,发现它除了在复位中断处理例程中被设置为0外,再无其他写入的操作。并且在长时间的实际游戏过程中,它的值也一直为0。所以,我们就假定它只是一个填充字节而已,没有实际的意义。

所有的数据都搞明白了,剩下的工作就是要把这些信息转换成密码。

最后一步——密码的生成

在代码清单6的程序中,最后初始化了[$54]=0以及[X]=0,可以察觉到必有用途。查找一下$54的出现位置,发现它正好就在下面,代码是:

代码清单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

这段代码虽短,但大有玄机。它是一个这样的循环:开始先调用子程序sub_E327,取得一个游戏数据地址到$34。然后将[$[$34]]——就是该地址所储存的游戏数据——载入A中,做如下运算:先取低4位,减去[$54],再减去7,取低4位。然后,将得到的结果同时存入[$(180+[X])]与[$54]中。循环直到[X]=28H为止。

我们还不知道经过这些运算后的数据终究是什么,但是我们可以明白:由于子程序sub_E327中有两条INX指令,因此,这些数据的存储起始地址是$182,并且循环一共执行28H/2=20次。从“20次”这一点可以猜测,这很可能就是最后的密码了。看一下子程序sub_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这个地址。这个地址是$2006,是红白机显存(VRAM)的地址寄存器之一。继续阅读代码,这是本文涉及到的最后一个循环了:

代码清单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开始循环,将[$(180+[X])]传送到Y寄存器。然后将[$(E35C+[Y])]送入VRAM_IOR。这个地址是$2007,是VRAM的I/O专用寄存器,用来向$2006指定地址的位置读出或写入信息。每次将[X]增加2,当[X]=2AH时,结束循环。
我们终于到达了终点——Y寄存器储存的是$182为起始地址的向量的内容,它其实是该码字在密码映射表中的偏移量。将[$(E35C+[Y])]送入显存中,屏幕上就能显示出A~P组成的密码串了。大功告成。

附言——密码生成器的C++实现

我们破解了《炸弹人》这个看似简单的游戏的密码系统,就可以使用密码作为游戏修改器了,也许会让游戏过程更加精彩。下面附上笔者用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

民那晚安。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266