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键不放,同时移动鼠标,观察“扫雷”窗口左上角, 
如果有黑点则鼠标底下是地雷,若是亮点则鼠标底下没地雷。 
    “多罗罗罗”,啊,终于扫完了。