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鍵不放,同時移動鼠標,觀察“掃雷”窗口左上角,
如果有黑點則鼠標底下是地雷,若是亮點則鼠標底下沒地雷。
“多羅羅羅”,啊,終于掃完了。