|
論壇說明 | 標記討論區已讀 |
歡迎您來到『史萊姆論壇』 ^___^ 您目前正以訪客的身份瀏覽本論壇,訪客所擁有的權限將受到限制,您可以瀏覽本論壇大部份的版區與文章,但您將無法參與任何討論或是使用私人訊息與其他會員交流。若您希望擁有完整的使用權限,請註冊成為我們的一份子,註冊的程序十分簡單、快速,而且最重要的是--註冊是完全免費的! 請點擊這裡:『註冊成為我們的一份子!』 |
|
主題工具 | 顯示模式 |
2003-10-25, 10:44 AM | #1 |
榮譽會員
|
揭開「掃雷(WinMine)」的秘密
揭開「掃雷(WinMine)」的秘密
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 ; ----------------------------------------------------------------------- 可能自然轉移到位址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"之後,如果想在滑鼠移動時知道當前 滑鼠所指位置底下是否有地雷,可以有兩個辦法: 1 當移動滑鼠時,左手按住Ctrl鍵不要放; 2 直接按一下Shift鍵,以後移動滑鼠時不需要按住Ctrl鍵 不管是哪種辦法,在滑鼠移動時,你只要仔細觀察桌面左上角有沒有黑 點,如果有黑點則表示滑鼠底下是地雷,如果是亮點則滑鼠底下沒有地雷。 要注意第2種辦法中的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鍵不放,同時移動滑鼠,觀察「掃雷」視窗左上角, 如果有黑點則滑鼠底下是地雷,若是亮點則滑鼠底下沒地雷。 「多羅羅羅」,啊,終於掃完了。 P.S.:附件是修改過的winmine.exe和winmine.ini的壓縮包。 |
送花文章: 3,
|