揭开“扫雷(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, ; 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,
: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, ; AL=刚输入的字符
:0593 mov bx, PassCount; BX=已输入的字符个数
:0597 cmp byte ptr password, 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等于字符串"Difficulty"的首地址;
; IniItemPtr等于字符串"Mines"的首地址;
; IniItemPtr等于字符串"Height"的首地址;
; ......
; IniItemPtr等于字符串"Sound"的首地址;
; IniItemPtr等于字符串"Menu"的首地址;
; IniItemPtr等于字符串"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+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, ; BX=指针数组IniItemPtr的下标
:1FCA add bx, bx; 下标*2
:1FCC push ds
:1FCD push IniItemPtr; 等于某一项名的首地址
:1FD1 push ; int
:1FD4 push ds
:1FD5 push offset aWinmine_ini; 指向"winmine.ini"
:1FD8 mov si, bx
:1FDA call GETPRIVATEPROFILEINT; 读取某一项的整数值
:1FDF cmp ax,
:1FE2 jg loc_1FFD
:1FE4 push ds
:1FE5 push offset byte_2532; LPCSTR
:1FE8 mov bx, si
:1FEA push ds
:1FEB push IniItemPtr; LPCSTR
:1FEF push ; 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,
:2000 loc_2000:
:2000 cmp ax,
:2003 jge loc_200A
:2005 mov ax,
:2008 jmp short loc_2045
:200A; -----------------------------------------------------------------------
:200A loc_200A:
:200A push ds
:200B push offset byte_2532; LPCSTR
:200E mov bx,
:2011 add bx, bx
:2013 push ds
:2014 push IniItemPtr; LPCSTR
:2018 push ; int
:201B push ds
:201C push offset aWinmine_ini; LPCSTR
:201F mov si, bx
:2021 call GETPRIVATEPROFILEINT
:2026 cmp ax,
:2029 jg loc_2042
:202B push ds
:202C push offset byte_2532; LPCSTR
:202F push ds
:2030 push IniItemPtr; LPCSTR
:2034 push ; 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,
: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,
:06BC add ax, 4
:06BF shr ax, 4
:06C2 push ax
:06C3 mov ax,
: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 , 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, ; AX=coordinate X
:06F5 add ax, 4
:06F8 shr ax, 4
:06FB mov CoordinateX, ax; 计算X坐标
:06FE mov cx, ; 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 , 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 , 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
: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的窗口了。所以,
这样一来,当我们开启地雷探查功能时,我们看到的黑点与亮点不再显示在
桌面窗口的左上角,而是在扫雷窗口的左上角。具体位置请看下图:
BlackWhite
I'm a nutcracker 要改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的压缩包。
(全文完) 标 题: 【分享】微软扫雷PEDIY尝试
作 者: sungy
时 间: 2008-12-24,13:41:59
链 接: http://bbs.pediy.com/showthread.php?t=79338
【文章标题】: 微软扫雷PEDIY尝试
【文章作者】: 浮海观云
【作者邮箱】: [email protected]
【作者主页】: www.ifgoogle.com
【作者QQ号】: 250341858
【软件名称】: windows自带
【软件大小】: 120K
【下载地址】: 电脑上就有啊
【加壳方式】: 无
【保护方式】: 无
【编写语言】: VC
【使用工具】: OD,Winhex,ResHacker.exe
【操作平台】: WINDOWS
【软件介绍】: 扫雷就是电脑上那个扫雷啊
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
扫雷程序原来就有隐藏的作弊模式(xyzzy),现在通过PEDIY来修改一下玩个新花样,功力有限只做点小改动,剪刀加糨糊
让扫雷程序可以发声音报警。
先用OD分析程序,找到下面几处作弊点:
1,可以秒表停止的地方
01002FE0/$833D 64510001>CMP DWORD PTR DS:,0
01002FE7|.74 1E JE SHORT winmine.01003007
01002FE9|.813D 9C570001>CMP DWORD PTR DS:,3E7
01002FF3|.7D 12 JGE SHORT winmine.01003007
01002FF5 FF05 9C570001 INC DWORD PTR DS: ;加1秒的地方
01002FFB|.E8 B5F8FFFF CALL winmine.010028B5
01003000|.6A 01 PUSH 1
01003002|.E8 E6080000 CALL winmine.010038ED
01003007\>C3 RETN
2,下面比较连续输入的字符是不是"XYZZY",是则计数加1,否则变0,当DWORD PTR DS:=5时进入作弊等
待模式,以后按下shift键可以开关作弊
01001CBF|.66:8B0C45 34500001 MOV CX,WORD PTR DS: ;作弊码XYZZY(宽字符)
01001CC7|.66:2B4D 10 SUB CX,WORD PTR SS:
01001CCB|.40 INC EAX
01001CCC|.66:F7D9 NEG CX
01001CCF|.1BC9 SBB ECX,ECX
01001CD1|.F7D1 NOT ECX
01001CD3|.23C8 AND ECX,EAX
01001CD5 890D 54510001 MOV DWORD PTR DS:,ECX ;当DS: 里的值为5时,进入作弊模式
01001CDB E9 C9040000 JMP winmine.010021A9
010020C1|> \A1 54510001 MOV EAX,DWORD PTR DS: ;作弊是否
01001D40|> \833D 54510001>CMP DWORD PTR DS:,5 ;Case 10 of switch 01001C9D
01001D47|.0F8C 5C040000 JL winmine.010021A9
01001D4D|.8335 54510001>XOR DWORD PTR DS:,14 ;作弊开关
01001D54|.E9 50040000 JMP winmine.010021A9
01001CA0|. /0F84 9A000000 JE winmine.01001D40 ;是不是按下了shif键(ASCII-16)
3,原来的作弊方法是在屏幕的左上角画一个象素的黑/白点
01002126|.57 PUSH EDI ; /hWnd
01002127|.FF15 2C110001 CALL DWORD PTR DS:[<&USER32.GetDC>] ; \GetDC
0100212D|.8B0D 18510001 MOV ECX,DWORD PTR DS:
01002133|.8BF0 MOV ESI,EAX
01002135|.A1 1C510001 MOV EAX,DWORD PTR DS:
0100213A|.C1E0 05 SHL EAX,5
0100213D|.8A8408 405300>MOV AL,BYTE PTR DS: ;0F没雷,8F有雷
01002144|.24 80 AND AL,80
01002146|.F6D8 NEG AL
01002148|.1BC0 SBB EAX,EAX
0100214A|.25 010000FF AND EAX,FF000001
0100214F|.05 FFFFFF00 ADD EAX,0FFFFFF ;EAX值决定画黑点还是白点
01002154|.50 PUSH EAX ; /Color
01002155|.57 PUSH EDI ; |Y
01002156|.57 PUSH EDI ; |X
01002157|.56 PUSH ESI ; |hDC
01002158|.FF15 58100001 CALL DWORD PTR DS:[<&GDI32.SetPixel>] ; \SetPixel
0100215E|.56 PUSH ESI ; /hDC
0100215F|.57 PUSH EDI ; |hWnd
01002160|.FF15 28110001 CALL DWORD PTR DS:[<&USER32.ReleaseDC>]; \ReleaseDC
01002166|.EB 41 JMP SHORT winmine_.010021A9
4,看一个播放声音的地方
010038ED/$833D B8560001>CMP DWORD PTR DS:,3
010038F4|.75 47 JNZ SHORT winmine.0100393D
010038F6|.8B4424 04 MOV EAX,DWORD PTR SS:
010038FA|.48 DEC EAX ;Switch (cases 1..3)
010038FB|.74 2A JE SHORT winmine.01003927
010038FD|.48 DEC EAX
010038FE|.74 15 JE SHORT winmine.01003915
01003900|.48 DEC EAX
01003901|.75 3A JNZ SHORT winmine.0100393D
01003903|.68 05000400 PUSH 40005 ;Case 3 of switch 010038FA
01003908|.FF35 305B0001 PUSH DWORD PTR DS: ;winmine.01000000
0100390E|.68 B2010000 PUSH 1B2
01003913|.EB 22 JMP SHORT winmine.01003937 ;跳去播放炸雷声音
01003915|>68 05000400 PUSH 40005 ;Case 2 of switch 010038FA
0100391A|.FF35 305B0001 PUSH DWORD PTR DS: ;winmine.01000000
01003920|.68 B1010000 PUSH 1B1
01003925|.EB 10 JMP SHORT winmine.01003937 ;播放另一种怪声
01003927|>68 05000400 PUSH 40005 ;Case 1 of switch 010038FA
0100392C|.FF35 305B0001 PUSH DWORD PTR DS: ;winmine.01000000
01003932|.68 B0010000 PUSH 1B0 ;秒表嘀哒声
01003937|>FF15 68110001 CALL DWORD PTR DS:[<&WINMM.PlaySoundW>];WINMM.PlaySoundW
0100393D\>C2 0400 RETN 4 ;Default case of switch 010038FA
现在来改一下功能,PEDIY一下。可以增加一下菜单命令添加作弊选项,我用reshacker
POPUP "游戏(&G)"
{
MENUITEM "开局(&N)\tF2",510
MENUITEM SEPARATOR
MENUITEM "初级(&B)",521
MENUITEM "中级(&I)",522
MENUITEM "高级(&E)",523
MENUITEM "自定义(&C)...",524
MENUITEM "探雷器",525 (自己添加一项,编译保存)
MENUITEM SEPARATOR
MENUITEM "标记(?)(&M)",527
MENUITEM "颜色(&L)",529
MENUITEM "声音(&S)",526
MENUITEM SEPARATOR
MENUITEM "扫雷英雄榜(&T)...",528
MENUITEM SEPARATOR
MENUITEM "退出(&X)",512
}
这样游戏下拉菜单中就多出了一项作弊模式“探雷器”,用OD载入修过的”winmine.exe“,在下面一行下断点
01001DBC|> \0FB745 10 MOVZX EAX,WORD PTR SS: ;Case 111 (WM_COMMAND) of switch 01001D5B
01001DC0|.B9 10020000 MOV ECX,210 ;扫雷英雄榜
01001DC5|.3BC1 CMP EAX,ECX
01001DC7|.0F8F 0F010000 JG winmine.01001EDC
01001DCD|.0F84 FF000000 JE winmine.01001ED2
01001DD3|.3D FE010000 CMP EAX,1FE ;开局
01001DD8|.0F84 EA000000 JE winmine.01001EC8
01001DDE|.3BC6 CMP EAX,ESI
01001DE0|.0F84 B7000000 JE winmine.01001E9D
01001DE6|.3D 08020000 CMP EAX,208
01001DEB|.0F8E B8030000 JLE winmine.010021A9
01001DF1|.3D 0B020000 CMP EAX,20B
01001DF6|.7E 61 JLE SHORT winmine.01001E59 ;初级从这里跳走
01001DF8|.3D 0C020000 CMP EAX,20C
01001DFD|.74 50 JE SHORT winmine.01001E4F
01001DFF|.3D 0E020000 CMP EAX,20E
01001E04|.74 20 JE SHORT winmine.01001E26
01001E06|.3D 0F020000 CMP EAX,20F
01001E0B|.0F85 98030000 JNZ winmine.010021A9
01001E11|.33C0 XOR EAX,EAX
01001E13|.3905 BC560001 CMP DWORD PTR DS:,EAX
01001E19|.0F94C0 SETE AL
01001E1C|.A3 BC560001 MOV DWORD PTR DS:,EAX
01001E21|.E9 24010000 JMP winmine.01001F4A
运行winmine.exe,打开游戏菜单,选择“扫雷器”后,程序断在“01001DBC”(自己设定的断点) ,那么EAX将被赋值为:
020D(十进制525)。下面增加一行判断如果等于525 就打开作弊模式。看了一下在这地方加点代码也麻烦,反正玩初级的
是不太会玩的就把初级的变成作弊模式算了。
01001E59|> \8B45 10 MOV EAX,DWORD PTR SS: ;初级跳到这里设置参数
01001E5C|.05 F7FDFFFF ADD EAX,-209 ;209就是521,EAX在这里变成了0
01001E61|.66:A3 A056000>MOV WORD PTR DS:,AX
01001E67|.0FB7C0 MOVZX EAX,AX
01001E6A|.8D0440 LEA EAX,DWORD PTR DS:
01001E6D|.C1E0 02 SHL EAX,2
01001E70|.8B88 10500001 MOV ECX,DWORD PTR DS: ;初级的是十个雷吧?
01001E76|.890D A4560001 MOV DWORD PTR DS:,ECX ;雷数放到DS:
01001E7C|.8B88 14500001 MOV ECX,DWORD PTR DS: ;9行
01001E82|.8B80 18500001 MOV EAX,DWORD PTR DS: ;9列
01001E88|.890D A8560001 MOV DWORD PTR DS:,ECX
01001E8E|.A3 AC560001 MOV DWORD PTR DS:,EAX
01001E93|.E8 E2170000 CALL winmine.0100367A
01001E98|.E9 AD000000 JMP winmine.01001F4A ; 改为:jmp 01004a76
改动方案:在程序中找一块空白处(如:01004a76),添加代码:
01004A76 C705 54510001>MOV DWORD PTR DS:,5
01004A80 ^ E9 C5D4FFFF JMP winmine.01001F4A
现在程序已经实现,打开一下初级模式就开通了作弊模式,以后我们只要按‘SHIFT’键就可以打开关闭“黑白点”提示了!
接下来,改成声音报警的
方案1,用系统的BEEP,优点是没有声卡也能听到或者说没有耳机也能听到声音?、、
改成下面的代码,原代码看上面的第3条,优点
0100214F|.05 FFFFFF00 ADD EAX,0FFFFFF ;修改下面的
01002154 85C0 TEST EAX,EAX
01002156 75 51 JNZ SHORT winmine.010021A9
01002158 05 00010000 ADD EAX,100
0100215D 90 NOP
0100215E 50 PUSH EAX
0100215F 50 PUSH EAX
01002160 FF15 C411B176 CALL DWORD PTR DS:[<&KERNEL32.Beep>] ;kernel32.Beep
01002166|.EB 41 JMP SHORT winmine.010021A9
01002168|>393D 4C510001 CMP DWORD PTR DS:,EDI
0100216E|.^ 0F85 EAFAFFFF JNZ winmine.01001C5E
方案2,用程序中使用的WINMM.PlaySoundW
修改代码如下:
0100213D 8A8408 40530001 MOV AL,BYTE PTR DS: ;0F没雷与8F有雷
01002144 04 71 ADD AL,71
01002146 85C0 TEST EAX,EAX
01002148 75 5F JNZ SHORT winmine.010021A9
0100214A 68 05000400 PUSH 40005
0100214F FF35 305B0001 PUSH DWORD PTR DS: ;winmine.01000000
01002155 68 B2010000 PUSH 1B2
0100215A FF15 68110001 CALL DWORD PTR DS:[<&WINMM.PlaySoundW>] ;WINMM.PlaySoundW
01002160 90 NOP
01002161 90 NOP
01002162 90 NOP
01002163 90 NOP
01002164 90 NOP
01002165 90 NOP
然后保存修改内容到可执行文件,用winhex修改对应的字节就行了。
运行程序享受一下PEDIY的成果吧。爽吗?。。。。不爽?那声音是不是太烦人了,戴着声音听那爆炸声可不是种享受,换
一下喜欢的音乐吧,哈哈。
打开ResHacker.exe替换增加一下WAVE资源Action->add a new resource,选择自己喜欢的wav文件,资料类型为WAVE,资源
名称为435(1B3),然后把下面的代码改一下:
01002155 68 B2010000 PUSH 1B2 改成push 1B3 就是自己喜欢的声音啦。
好了,先写到这吧。
终于过了吧PEDIY的ying,估计只有初学者感兴趣就发到这里吧。有耐心看完的说明你有时间,特别是有耐心或者是同情
心。附近中是我改好的一个,无聊时可以拿来玩玩。 技术差太远
我是肉鸟型的- -! 好象没有啊 楼主的水平真高 楼主的冲劲比地雷还强悍,我准备拜师了
另:快去搞定 VMP ,你行的师傅 完全看不懂。
页:
[1]
2