Written by Black White
“扫雷(WinMine)”大家都是熟悉的,我相信凡是玩过电脑的人很少
没有玩过“扫雷”这个游戏的。但微软做的这个“扫雷”游戏中隐藏着一
些秘密,我估计知道的人并不多。
我以前听到过这样的传说:“扫雷”在被输入某个密码的情况下,你就
可以轻易知道哪个是地雷,哪个不是地雷。
我正是想证明这个传说是否真的存在才花了N个小时对扫雷程序进行了
分析,最后发现这个传说确实是真的,并且还发现了一些另外的秘密。
我分析的目标是Windows98下面的扫雷程序,程序名为“winmine.exe”,
该程序存放在C:\Windows这个文件夹下面,它的长度是24059字节,与它一
起的还有一个ini文件叫winmine.ini。
要分析这样一个看起来并不大的EXE程序其实并不容易,因为把它反汇
编(unassemble/disassemble)出来的代码仍旧是很长的。我决定采用静态反
汇编与动态跟踪相结合的办法来对它进行一个比较彻底的分析。我使用的静
态反汇编工具是俄罗斯人Ilfak Guilfanov写的IDA Pro,该工具软件的主页
是
http://www.datarescue.com。动态跟踪工具当然是SoftICE了。 以下这段代码是用IDA Pro反汇编出来的,它是WinMine的消息处理程序:
:03EE WindowProc:
:03EE enter 22h, 0
:03F2 push si
:03F3 mov ax, [bp+0Ch] ; AX=WMSG
:03F6 dec ax
:03F7 dec ax
:03F8 jz WM_DESTROY ; WM_DESTROY=2
:03FC dec ax
:03FD jz WM_MOVE ; WM_MOVE=3
:03FF sub ax, 3
:0402 jz WM_ACTIVATE ; WM_ACTIVATE=6
:0406 sub ax, 9
:0409 jz WM_PAINT ; WM_PAINT=0Fh
:040D sub ax, 7
:0410 jz WM_ENDSESSION ; WM_ENDSESSION=16h
:0414 sub ax, 0EAh
;
;这里对是否为键盘消息进行判断
:0417 IS_WM_KEYDOWN?: ; WM_KEYDOWN=100h
:0417 jz WM_KEYDOWN ; 若是键盘消息则转WM_KEYDOWN
:041B sub ax, 11h
:041E jz WM_COMMAND ; WM_COMMAND=111h
:0422 dec ax
:0423 jz WM_SYSCOMMAND ; WM_SYSCOMMAND=112h
:0425 dec ax
:0426 jz WM_TIMER ; WM_TIMER=113h
:042A sub ax, 0EDh
;
;这里对是否为鼠标移动消息进行判断
:042D jz WM_MOUSEMOVE ; WM_MOUSEMOVE=200h
; 若是鼠标移动则转WM_MOUSEMOVE
:0431 dec ax
:0432 jz WM_LBUTTONDOWN ; WM_LBUTTONDOWN=201h
:0436 dec ax
:0437 jz WM_LBUTTONUP ; WM_LBUTTONUP=202h
:043B dec ax
:043C dec ax
:043D jz WM_RBUTTONDOWN ; WM_RBUTTONDOWN=204h
:0441 dec ax
:0442 jz WM_LBUTTONUP ; WM_RBUTTONUP=205h
:0446 dec ax
:0447 dec ax
:0448 jz WM_MBUTTONDOWN ; WM_MBUTTONDOWN=207h
:044C dec ax
:044D jz WM_LBUTTONUP ; WM_MBUTTONUP=208h
:0451 sub ax, 9
:0454 jz WM_ENTERMENULOOP ; WM_ENTERMENULOOP=211h
:0458 dec ax
:0459 jz WM_EXITMENULOOP ; WM_EXITMENULOOP=212h
:045D OtherMessages:
:045D jmp GotoDefWindowProc
:0460 ;
-----------------------------------------------------------------------
以上这段代码的作用就是对各种消息进行判断并根据不同消息转到不同
的分支执行。这里我们就重点关注其中的键盘消息与鼠标移动消息的分支转
移。对于键盘消息WM_KEYDOWN,程序将转移到以下代码:
:0569 WM_KEYDOWN: ; 当有键被按下时,转到此处执行
:0569 mov ax, [bp+0Ah]
:056C cmp ax, 75h ; AL==75h (F6 Key)
:056F jz IsF6Key ; 若是F6键则转IsF6Key
:0573 ja CheckPassword
:0575 sub al, 10h ; AL==10h (Shift Key)
:0577 jz IsShiftKey ; 若是Shift键则转IsShiftKey
:057B sub al, 0Bh ; AL==1Bh (Esc Key)
:057D jz IsEscKey ; 若是Esc键则转IsEscKey
:057F sub al, 58h ; 'X' ; AL==73h (F4 Key)
:0581 jz IsF4Key ; 若是F4键则转IsF4Key
:0583 dec al ; AL==74h (F5 Key)
:0585 jz IsF5Key ; 若是F5键则转IsF5Key
;
;若不是以上这些键,则接下去判断输入的是否为密码
:0587 CheckPassword:
:0587 cmp PassCount, 5 ; 若密码字符个数大于等于5,
:058C jge GotoDefWindowProc ; 则不理它
;若已经输入的密码字符个数小于5,则继续判断
:0590 mov al, [bp+0Ah] ; AL=刚输入的字符
:0593 mov bx, PassCount ; BX=已输入的字符个数
:0597 cmp byte ptr password[bx], al ; "XYZZY"
; 判断刚输入的字符是否为正确的密码字符
:059B jnz ClearPassword ; 如果不正确则清除输入
:059D inc PassCount ; 如果正确,则密码字符个数+1
:05A1 jmp GotoDefWindowProc
:05A4 ;
-----------------------------------------------------------------------
:05A4 IsEscKey: ; 这里是对Esc键进行处理
:05A4 or byte ptr word_2376, 4
:05A9 push hwndMain
:05AD push large 112F020h
:05B3 push 0
:05B5 push 0
:05B7 call POSTMESSAGE
:05BC jmp GotoDefWindowProc
:05BF ;
-----------------------------------------------------------------------
:05BF IsF4Key: ; 这里是对F4键进行处理,它的功能就是开/关声音。
:05BF cmp SoundFlag, 1 ; 若声音标志小于等于1,则
:05C4 jle GotoDefWindowProc ; 不理它,转缺省消息处理
:05C8 cmp SoundFlag, 3 ; 若声音标志不等于3(等于2),
:05CD jnz SoundFlagIs2 ; 即无声时,则开声音。
;若声音标志等于3,即有声时,则关声音
:05CF SoundFlagIs3:
:05CF call DisableSound ; 开声音
:05D2 mov SoundFlag, 2 ; 若原先有声,则设成无声
:05D8 jmp GotoDefWindowProc
:05DB ;
-----------------------------------------------------------------------
:05DB SoundFlagIs2:
:05DB call EnableSound ; 关声音
:05DE mov SoundFlag, ax ; 若原先无声,则设成有声
:05E1 jmp GotoDefWindowProc
:05E4 ;
-----------------------------------------------------------------------
:05E4 IsF5Key: ; 这里是对F5键进行处理,它的功能是隐藏菜单。
:05E4 cmp MenuFlag, 0 ; 若菜单标志为0则不理它
:05E9 jz LetsGotoDefWindowProc
;若菜单标志不等于0,则隐藏菜单
:05EB HideMenu: ; 1 means to hide menu
:05EB push 1 ; 参数1表示隐藏菜单
:05ED ToHideShowMenu:
:05ED call HideShowMenu ; 调用隐藏/显示菜单函数
:05F0 jmp GotoDefWindowProc
:05F3 ;
-----------------------------------------------------------------------
:05F3 IsF6Key: ; 这里是对F6键进行处理,它的功能是显示菜单。
:05F3 cmp MenuFlag, 0 ; 若菜单标志为0则不理它
:05F8 jz LetsGotoDefWindowProc
;若菜单标志不等于0,则显示菜单
:05FA push 2 ; 参数2表示显示菜单
:05FC jmp short ToHideShowMenu
:05FE ;
-----------------------------------------------------------------------
:05FE IsShiftKey: ; 这里是对Shift键进行处理,它的功能是对PassCount
; ; 这个变量值进行切换,若原值为5则变成20(14h),若
; ; 原值为20(14h),则变成5。
:05FE cmp PassCount, 5
:0603 jl LetsGotoDefWindowProc
:0605 xor byte ptr PassCount, 14h
:060A jmp GotoDefWindowProc
:060D ;
-----------------------------------------------------------------------
:060D ClearPassword:
:060D mov PassCount, 0
:0613 jmp GotoDefWindowProc
:0616 ;
-----------------------------------------------------------------------
:0616 WM_DESTROY:
:0616 push hwndMain
:061A push 1
:061C call KILLTIMER
:0621 push 2
:0623 push 0
:0625 push 0
:0627 call sub_1734
:062A push 0
:062C call POSTQUITMESSAGE
:0631 WM_ENDSESSION:
:0631 cmp word_23AC, 0
:0636 jnz loc_63B
:0638 LetsGotoDefWindowProc:
:0638 jmp GotoDefWindowProc
:063B ;
------------------------------------------------------------------------
--
?
;这里是与password有关的一个变量及一个数组
:0034 PassCount dw 0
:0036 password db 'XYZZY',0
上面这段代码是对键盘输入进行处理,主要涉及到以下这些键:
F4、F5、F6、Shift、其它键
其中F4的作用是开关声音,F5的作用是隐藏菜单,F6的作用是显示菜单,
Shift键的作用是对变量PassCount的值进行切换,它的实际作用将在后面
部分分析。其它键其实就是用来输入密码的键,比如英文字母A到Z,数字
键0到9等。根据上面代码,我们已经知道,那传说中的密码就是:
XYZZY
这样5个字母。当你在玩“扫雷”时,只要连续输入这5个字母,那么扫雷
的阿里巴巴之门就从此为你打开。
现在先暂时不提在输入了密码之后如何去“照”出哪个是地雷,哪个
不是地雷。这里先讲一下F4、F5、F6以及Shift键的功能是如何分析出来的
的。
事实上,如果光是根据上面的代码是根本无法确定这些键的功能的,因为
IDA Pro的功能就算再强,它也不能达到理解代码甚至猜测代码作用的地步。
上面代码中所涉及到的变量名如PassCount、password、SoundFlag、MenuFlag
都是我根据分析手工加上去的,另外代码中提到的一些函数、标号名如:
DisableSound、EnableSound、HideShowMenu
CheckPassword、ClearPassword、GotoDefWindowProc
也都是我根据自己的理解加上的。只有那些全部是大写字母组成的函数名如:
POSTMESSAGE、KILLTIMER、POSTQUITMESSAGE
才是IDA Pro分析出来的。
要确定这些键的功能肯定需要进行动态跟踪。起先用SoftICE跟踪以上
这段代码时,仍旧看不出这些键的作用,因为总是在跟踪到一定时候就会发
现某些相关变量的初值为0。例如,摘录上面代码中与F6键有关的部分:
:05F3 ;
-----------------------------------------------------------------------
:05F3 IsF6Key: ; 这里是对F6键进行处理,它的功能是显示菜单。
:05F3 cmp MenuFlag, 0 ; 若菜单标志为0则不理它
:05F8 jz LetsGotoDefWindowProc
;若菜单标志不等于0,则显示菜单
:05FA push 2 ; 参数2表示显示菜单
:05FC jmp short ToHideShowMenu
:05FE ;
-----------------------------------------------------------------------
在跟踪到地址05F3时,我发现MenuFlag这个变量的值一直是0,这样程序就不
可能自然转移到地址05FA处执行。当然,我后来就设法强制改变量MenuFlag
的值,然后看程序继续执行之后会有什么后果,结果发现当该变量的值改成1
时菜单居然消失了,而改成2时则菜单重现。但这样仍旧没有从根本上解决问
题,因为我仍旧不知道这个变量的值在什么情况下会自然发生变化,比如在什
么情况下,MenuFlag的值会等于2。
要搞清楚变量MenuFlag的值究竟在什么情况下发生变化的,就应想办法
了解这个变量有没有在程序的其它地方被引用。这一点用IDA Pro可以轻松解
决,因为IDA Pro在反汇编时会指出某个变量在程序中的哪些地方被引用,这
个叫做Cross Reference(交叉引用)。根据MenuFlag的交叉引用,我就找到了
以下这段代码与MenuFlag的赋值有关:
;这些是相关的数据定义
:004E aWinmine_ini db 'winmine.ini',0
:005A aDifficulty db 'Difficulty',0
:0065 aMines db 'Mines',0
:006B aHeight db 'Height',0
:0072 aWidth db 'Width',0
:0078 aXpos db 'Xpos',0
:007D aYpos db 'Ypos',0
:0082 aSound db 'Sound',0
:0088 aMark db 'Mark',0
:008D aMenu db 'Menu',0
:0092 aTick db 'Tick',0
:0097 aColor db 'Color',0
:009D aTime1 db 'Time1',0
:00A3 aName1 db 'Name1',0
:00A9 aTime2 db 'Time2',0
:00AF aName2 db 'Name2',0
:00B5 aTime3 db 'Time3',0
:00BB aName3 db 'Name3',0
:00C1 align 2
;从C语言角度来理解,从地址00C2开始定义的是一个指针
;数组,不妨取名为IniItemPtr。
;其中IniItemPtr[0]等于字符串"Difficulty"的首地址;
; IniItemPtr[1]等于字符串"Mines"的首地址;
; IniItemPtr[2]等于字符串"Height"的首地址;
; ......
; IniItemPtr[6]等于字符串"Sound"的首地址;
; IniItemPtr[8]等于字符串"Menu"的首地址;
; IniItemPtr[9]等于字符串"Tick"的首地址;
; ......
:00C2 IniItemPtr dw offset aDifficulty ; "Difficulty"
:00C4 dw offset aMines ; "Mines"
:00C6 dw offset aHeight ; "Height"
:00C8 dw offset aWidth ; "Width"
:00CA dw offset aXpos ; "Xpos"
:00CC dw offset aYpos ; "Ypos"
:00CE dw offset aSound ; "Sound"
:00D0 dw offset aMark ; "Mark"
:00D2 dw offset aMenu ; "Menu"
:00D4 dw offset aTick ; "Tick"
:00D6 dw offset aColor ; "Color"
:00D8 dw offset aTime1 ; "Time1"
:00DA dw offset aName1 ; "Name1"
:00DC dw offset aTime2 ; "Time2"
:00DE dw offset aName2 ; "Name2"
:00E0 dw offset aTime3 ; "Time3"
:00E2 dw offset aName3 ; "Name3"
;--------------------------------------------------------------
;以下函数用来从winmine.ini读取各项的值,如Width、Menu、Sound、Tick
:2072 ReadWinMineIni proc near
:2072 push 2 ; 2是指针数组IniItemPtr的下标,
; IniItemPtr[2]的地址=2*2+C2=00C6
; 00C6 dw offset aHeight; "Height"
:2074 push 8
:2076 push 8
:2078 cmp word_2464, 1
:207D sbb ax, ax
:207F and ax, 9
:2082 add ax, 10h
:2085 push ax
:2086 call sub_1FBE ; 读取winmine.ini中Height的值
:2089 mov word_2558, ax
:208C mov Height, ax
;--------------------------------------------------------------
:208F push 3 ; 3*2+C2=00C8
; 00C8 dw offset aWidth ; "Width"
:2091 push 8
:2093 push 8
:2095 push 1Eh
:2097 call sub_1FBE
:209A mov word_255A, ax
:209D mov Width, ax
:20A0 push 0 ; 0*2+C2=00C2
; 00C2 IniItemPtr dw offset aDifficulty
:20A2 push 0
:20A4 push 0
:20A6 push 3
:20A8 call sub_1FBE ; 读取winmine.ini中Difficulty的值
:20AB mov word_2554, ax
;--------------------------------------------------------------
:20AE push 1 ; 1*2+C2=00C4
; 00C4 dw offset aMines ; "Mines"
:20B0 push 0Ah
:20B2 push 0Ah
:20B4 push 3E7h
:20B7 call sub_1FBE ; 读取Mines的值
:20BA mov word_2556, ax
;--------------------------------------------------------------
:20BD push 4 ; 4*2+C2=00CA
; 00CA dw offset aXpos ; "Xpos"
:20BF push 50h ; 'P'
:20C1 push 0
:20C3 push 400h
:20C6 call sub_1FBE ; 读取Xpos的值
:20C9 mov word_255C, ax
;--------------------------------------------------------------
:20CC push 5 ; 5*2+C2=00CC
; 00CC dw offset aYpos ; "Ypos"
:20CE push 50h ; 'P'
:20D0 push 0
:20D2 push 400h
:20D5 call sub_1FBE ; 读取Ypos的值
:20D8 mov word_255E, ax
;--------------------------------------------------------------
:20DB push 6 ; 6*2+C2=00CE
; 00CE dw offset aSound ; "Sound"
:20DD push 0
:20DF push 0
:20E1 push 3
:20E3 call sub_1FBE ; 读取Sound的值
:20E6 mov SoundFlag, ax
;--------------------------------------------------------------
:20E9 push 7 ; 7*2+C2=00D0
; 00D0 dw offset aMark ; "Mark"
:20EB push 1
:20ED push 0
:20EF push 1
:20F1 call sub_1FBE ; 读取Mark的值
:20F4 mov word_2562, ax
;--------------------------------------------------------------
:20F7 push 9 ; 9*2+C2=00D4
; 00D4 dw offset aTick ; "Tick"
:20F9 push 0
:20FB push 0
:20FD push 1
:20FF call sub_1FBE ; 读取Tick的值
:2102 mov TickFlag, ax
;--------------------------------------------------------------
:2105 push 8 ; 8*2+C2=00D2
; 00D2 dw offset aMenu ; "Menu"
:2107 push 0
:2109 push 0
:210B push 2
:210D call sub_1FBE ; 读取Menu的值
:2110 mov MenuFlag, ax
;--------------------------------------------------------------
:2113 push 0Bh ; B*2+C2=00D8
; 00D8 dw offset aTime1 ; "Time1"
:2115 push 3E7h
:2118 push 0
:211A push 3E7h
:211D call sub_1FBE ; 读取Time1的值
:2120 mov word_256A, ax
;--------------------------------------------------------------
:2123 push 0Dh ; D*2+C2=00DC
; 00DC dw offset aTime2 ; "Time2"
:2125 push 3E7h
:2128 push 0
:212A push 3E7h
:212D call sub_1FBE ; 读取Time2的值
:2130 mov word ptr dword_256C, ax
;--------------------------------------------------------------
:2133 push 0Fh ; F*2+C2=00E0
; 00E0 dw offset aTime3 ; "Time3"
:2135 push 3E7h
:2138 push 0
:213A push 3E7h
:213D call sub_1FBE ; 读取Time3的值
:2140 mov word ptr dword_256C+2, ax
;--------------------------------------------------------------
:2143 push 0Ch ; C*2+C2=00DA
; 00DA dw offset aName1 ; "Name1"
:2145 push ds
:2146 push offset byte_2570 ; LPSTR
:2149 call sub_204A ; 读取Name1的值
;--------------------------------------------------------------
:214C push 0Eh ; E*2+C2=00DE
; 00DE dw offset aName2 ; "Name2"
:214E push ds
:214F push offset byte_25B0 ; LPSTR
:2152 call sub_204A ; 读取Name2的值
;--------------------------------------------------------------
:2155 push 10h ; 10*2+C2=00E2
; 00E2 dw offset aName3 ; "Name3"
:2157 push ds
:2158 push offset byte_25F0 ; LPSTR
:215B call sub_204A ; 读取Name3的值
;--------------------------------------------------------------
:215E mov ax, word_2530
:2161 mov word_2568, ax
:2164 or ax, ax
:2166 jz loc_2175
:2168 push 0Ah ; A*2+C2=00D6
; 00D6 dw offset aColor ; "Color"
:216A push ax
:216B push 0
:216D push 1
:216F call sub_1FBE ; 读取Color的值
:2172 mov word_2568, ax
;--------------------------------------------------------------
:2175 loc_2175:
:2175 cmp SoundFlag, 3; 若Sound不等于3则不理它
:217A jnz locret_2182
:217C call EnableSound ; 若Sound等于3则开声音
:217F mov SoundFlag, ax
:2182
:2182 locret_2182:
:2182 retn
:2182 ReadWinMineIni endp
;--------------------------------------------------------------
;--------------------------------------------------------------
;以下这个函数sub_1FBE被上面的函数ReadWinMineIni调用。
;函数sub_1FBE的作用是读取winmine.ini文件中某一项的值。
:1FBE sub_1FBE proc near
:1FBE
:1FBE
:1FBE arg_0 = word ptr 4
:1FBE arg_2 = word ptr 6
:1FBE arg_4 = word ptr 8
:1FBE arg_6 = word ptr 0Ah
:1FBE
:1FBE enter 4, 0
:1FC2 push si
:1FC3 push ds
:1FC4 push offset byte_2532 ; LPCSTR
:1FC7 mov bx, [bp+arg_6] ; BX=指针数组IniItemPtr的下标
:1FCA add bx, bx ; 下标*2
:1FCC push ds
:1FCD push IniItemPtr[bx] ; 等于某一项名的首地址
:1FD1 push [bp+arg_4] ; int
:1FD4 push ds
:1FD5 push offset aWinmine_ini ; 指向"winmine.ini"
:1FD8 mov si, bx
:1FDA call GETPRIVATEPROFILEINT ; 读取某一项的整数值
:1FDF cmp ax, [bp+arg_0]
:1FE2 jg loc_1FFD
:1FE4 push ds
:1FE5 push offset byte_2532 ; LPCSTR
:1FE8 mov bx, si
:1FEA push ds
:1FEB push IniItemPtr[bx] ; LPCSTR
:1FEF push [bp+arg_4] ; int
:1FF2 push ds
:1FF3 push offset aWinmine_ini ; LPCSTR
:1FF6 call GETPRIVATEPROFILEINT
:1FFB jmp short loc_2000
:1FFD ;
-----------------------------------------------------------------------
:1FFD loc_1FFD:
:1FFD mov ax, [bp+arg_0]
:2000 loc_2000:
:2000 cmp ax, [bp+arg_2]
:2003 jge loc_200A
:2005 mov ax, [bp+arg_2]
:2008 jmp short loc_2045
:200A ;
-----------------------------------------------------------------------
:200A loc_200A:
:200A push ds
:200B push offset byte_2532 ; LPCSTR
:200E mov bx, [bp+arg_6]
:2011 add bx, bx
:2013 push ds
:2014 push IniItemPtr[bx] ; LPCSTR
:2018 push [bp+arg_4] ; int
:201B push ds
:201C push offset aWinmine_ini ; LPCSTR
:201F mov si, bx
:2021 call GETPRIVATEPROFILEINT
:2026 cmp ax, [bp+arg_0]
:2029 jg loc_2042
:202B push ds
:202C push offset byte_2532 ; LPCSTR
:202F push ds
:2030 push IniItemPtr[si] ; LPCSTR
:2034 push [bp+arg_4] ; int
:2037 push ds
:2038 push offset aWinmine_ini ; LPCSTR
:203B call GETPRIVATEPROFILEINT
:2040 jmp short loc_2045
:2042 ;
-----------------------------------------------------------------------
:2042 loc_2042:
:2042 mov ax, [bp+arg_0]
:2045 loc_2045:
:2045 pop si
:2046 leave
:2047 retn 8
:2047 sub_1FBE endp
我把上面代码中的第一个函数取名为ReadWinMineIni是因为它的作用就
是读取扫雷程序的winmine.ini文件中的各项。winmine.ini文件中允许包含
的各项包括:
Difficulty
Mines
Height
Width
Xpos
Ypos
Sound
Mark
Menu
Tick
Color
Time1
Name1
Time2
Name2
Time3
Name3
打开winmine.ini文件看一下,发现里面并不包括上面列出的所有项,
其中以下三项是没有的:
Sound
Menu
Tick
好,那就试着把这3项给它加上。在用记事本或者其它文本编辑器打开
winmine.ini之后,加上以下3行:
Sound=3
Menu=1
Tick=1
现在再重新双击winmine.exe运行扫雷。我们首先会发现扫雷的菜单消
失了,这是Menu的作用;当你开始挖雷之后,随着秒数的增加,你会听到
“滴滴”的声音,这个就是Tick的作用;当你不小心挖爆一个地雷时,你会
听到“嘟啊嘟啊”的声音,这个就是Sound的作用。
接下去再来试试功能键的作用:当你按F6时,菜单重新出现了;当你按
F5时菜单消失;当你按F4时声音消失,再按F4声音重新开启。
小结一下,Sound、Menu、Tick这3项的值代表的含义如下:
Sound=3 开启声音
Sound=2 关闭声音
Menu=1 隐藏菜单
Menu=2 显示菜单
Tick=0 关闭“滴滴”声
Tick=1 开启“滴滴”声
F4、F5、F6这3个功能键的作用如下:
F4 开启/关闭声音
F5 隐藏菜单
F6 显示菜单
现在,再回过头来关注一下在输入了正确密码"XYZZY"之后怎样轻易地
获知鼠标所指的位置下面是否有地雷。
那就再来看一段代码,这段代码与鼠标移动的消息有关:
:06A9 WM_MOUSEMOVE: ; 当鼠标移动时转到此处执行
:06A9 cmp LButtonDownFlag, 0 ; 若鼠标左键没有按下,则
:06AE jz IsPasswordOk ; 转IsPasswordOk判断密码
;若鼠标移动时左键被按下,则继续执行
:06B0 test byte ptr word_2376, 1
:06B5 jz loc_773
:06B9 mov ax, [bp+6]
:06BC add ax, 4
:06BF shr ax, 4
:06C2 push ax
:06C3 mov ax, [bp+8]
:06C6 sub ax, 27h
:06C9 shr ax, 4
:06CC push ax
:06CD
:06CD loc_6CD:
:06CD call sub_1154
:06D0 jmp GotoDefWindowProc
:06D3 ;
------------------------------------------------------------------------
--
?06D3 ; 当鼠标移动时左键没有按下,则转到此处执行
:06D3 IsPasswordOk:
:06D3 cmp PassCount, 0 ; 若已输入密码字符的个数为0,
:06D8 jz GotoDefWindowProc ; 则转缺省消息处理,不理会
:06DC cmp PassCount, 5 ; 若已输入密码字符的个数≠5
:06E1 jnz loc_6E9 ; 则转loc_6E9
;
;此时,已输入密码字符个数=5,即密码输入正确
:06E3 test byte ptr [bp+0Ah], 8 ; 若鼠标移动同时Ctrl键按下
:06E7 jnz CtrlIsHeldDown ; 则转CtrlIsHeldDown
; 注意这里需要两个条件同时
; 成立:密码正确、Ctrl按下
;注意这里有两种情形:
;(1) 如果输入密码字符个数不等于5时转到此处(回顾一下前面的代码,
; 当输入正确密码之后再按Shift键会使PassCount=20)
;(2) 如果输入密码字符个数等于5但Ctrl没有按下时也转到此处
:06E9 loc_6E9:
:06E9 cmp PassCount, 5 ; 若密码字符个数小于等于5
:06EE jle GotoDefWindowProc ; 则转缺省消息处理。
; 情形(2)符合此条件,所以
; 在鼠标移动时若Ctrl键没有
; 按下则不予理会。
;凡属以下两种情形之一,则转此处执行:
;(A) 密码输入正确(字符个数=5)并且鼠标移动时Ctrl键被按下
;(B) 密码输入正确(字符个数>5): 输入完正确密码后按一次Shift键使PassCount=20
:06F2
:06F2 CtrlIsHeldDown:
:06F2 mov ax, [bp+6] ; AX=coordinate X
:06F5 add ax, 4
:06F8 shr ax, 4
:06FB mov CoordinateX, ax ; 计算X坐标
:06FE mov cx, [bp+8] ; CX=Coordinate Y
:0701 sub cx, 27h
:0704 shr cx, 4
:0707 mov CoordinateY, cx ; 计算Y坐标
:070B or ax, ax
:070D jle InvalidCoordinate
:070F or cx, cx
:0711 jle InvalidCoordinate
:0713 cmp ax, Width ; 判断X坐标有否超过宽度
:0717 jg InvalidCoordinate
:0719 cmp cx, Height ; 判断Y坐标有否超过高度
:071D jle ValidCoordinate
:071F InvalidCoordinate:
:071F jmp GotoDefWindowProc
:0722 ;
-----------------------------------------------------------------------
;若坐标正确则转此处执行
:0722 ValidCoordinate:
:0722 call GETDESKTOPWINDOW ; 取得桌面窗口的句柄(handle)
:0727 push ax
:0728 call GETDC
:072D mov [bp-2], ax
:0730 push ax
:0731 push 0
:0733 push 0
:0735 mov si, CoordinateY ; 判断鼠标所指
:0739 shl si, 5 ; 位置下面是否
:073C mov bx, CoordinateX ; 有地雷,
:0740 test byte ptr [bx+si+460h], 80h ; 若没有地雷,
:0745 jz it_is_not_a_mine ; 则转
;
;若有地雷则转此处执行
:0747 it_is_a_mine: ; AX=0, DX=0
:0747 xor ax, ax ; RGB=0表示黑色
:0749 cwd ; means to show a black dot
:074A jmp short ShowDot ; 在窗口左上角显示一个黑点
:074C ;
-----------------------------------------------------------------------
:若没有地雷则转此处执行
:074C it_is_not_a_mine:
:074C mov ax, 0FFFFh ; AX=0FFFFh, DX=00FFh
:074F mov dx, 0FFh ; RGB=255表示白色
:074F ; 在窗口左上角显示一个亮点
:0752 ShowDot:
:0752 push dx
:0753 push ax
:0754 call SETPIXEL ; 画一个点!
:0759 call GETDESKTOPWINDOW; 重新获取桌面窗口句柄
:075E push ax
:075F push word ptr [bp-2]
:0762 call RELEASEDC ; 释放DC
:0767 jmp GotoDefWindowProc
:076A ;
-----------------------------------------------------------------------
通过对上面这段鼠标移动消息处理代码的分析,我们可以得出以下结论:
在正确输入5个字符的密码"XYZZY"之后,如果想在鼠标移动时知道当前
鼠标所指位置底下是否有地雷,可以有两个办法:
① 当移动鼠标时,左手按住Ctrl键不要放
② 直接按一下Shift键,以后移动鼠标时不需要按住Ctrl键
不管是哪种办法,在鼠标移动时,你只要仔细观察桌面左上角有没有黑
点,如果有黑点则表示鼠标底下是地雷,如果是亮点则鼠标底下没有地雷。
要注意第②种办法中的Shift是一个开关键,按奇数次开启探查功能,按偶数
次关闭探查功能。
在完成了上述分析之后,我发现只有在Windows3.1下面才能实现地雷探
查功能,而在Windows98下面则不行,也就是说,在正确输入密码之后,我看
不到桌面窗口左上角有黑点。
后来发现毛病出在GETDESKTOPWINDOW这个API上面。扫雷程序原先是运行
在Windows3.1上面的,它是NE格式的EXE,而不是现在常见的PE格式。即它是
一个16位的Windows程序,而非32位的Windows程序。所以它存在了一个兼容
性的问题,原先在Windows3.1的桌面窗口上可以画点,但在Windows98或者更
高版本的Windows XP上面则不行。我想正是由于这个原因,这个传说中的扫雷
密码才慢慢失传而不为人所知。
那么,现在该怎么办?办法还是有的,只能改程序了。只要把上面这段
鼠标移动消息处理代码中的两个API调用GETDESKTOPWINDOW改成另外一个API
调用GETACTIVEWINDOW就可以了。GETACTIVEWINDOW的意思就是获取当前活动
窗口的句柄,当你在玩扫雷时,活动窗口当然就是WinMine的窗口了。所以,
这样一来,当我们开启地雷探查功能时,我们看到的黑点与亮点不再显示在
桌面窗口的左上角,而是在扫雷窗口的左上角。
要改NE格式的EXE程序并不是件容易的事,因为我对这种格式并不熟悉。
后来是先到下面这个地址下载了一份NE格式文档:
http://www.wotsit.org/filestore/windoc.zip 仔细研读了许久,终于设法把winmine.exe修理好了。修改步骤如下:
用UltraEdit或者类似的EXE文件编辑器打开winmine.exe,搜索以下16进
制串:
02 00 1E 01
并替换为:
02 00 3C 00
实际只改了两个字节,即把1E改成3C,把01改成00。
总结一下,“扫雷”除了有探查地雷的密码,还有Menu、Sound、Tick等
秘密。
如果你想试验Menu、Sound和Tick的效果,请用记事本或其它文本编辑器
打开winmine.ini,增加以下3行并保存:
Sound=3
Menu=1
Tick=1
运行“扫雷”程序,按F4可以关闭/开启声音,按F6显示菜单,按F5隐藏菜单。
如果你想试验探查地雷的功能,请先按上面提到的步骤修改winmine.exe。
修改完之后运行“扫雷”程序,按顺序输入"XYZZY"这5个字母,然后按一下
Shift键放掉或者按住Ctrl键不放,同时移动鼠标,观察“扫雷”窗口左上角,
如果有黑点则鼠标底下是地雷,若是亮点则鼠标底下没地雷。
“多罗罗罗”,啊,终于扫完了。