元始天尊 发表于 2014-3-23 17:37:48

对xp扫雷程序的部分分析

初学ollydbg,便出此文
将xp的winmine.exe拖到ollydbg2中,F9运行,出现窗口后Alt+W选择扫雷的主窗口,如图所示
由于是顶层窗口,因此Parent一栏为Topmost,右键选择000B047C,选择Conditional Breakpoint(条件断点),
输入 == WM_LBUTTONUP,
问题1:为什么是 ESP+8?
问题2:为什么选择WM_LBUTTONUP?

选中一块雷区点击,ollydbg便停在入口处(由于程序内部释放焦点,因此我们由winmine切入到ollydbg窗口时)
01001BC9/.55            PUSH EBP
01001BCA|.8BEC          MOV EBP,ESP
01001BCC|.83EC 40       SUB ESP,40
01001BCF|.8B55 0C       MOV EDX,DWORD PTR SS:
01001BD2|.8B4D 14       MOV ECX,DWORD PTR SS:
01001BD5|.53            PUSH EBX
01001BD6|.56            PUSH ESI
01001BD7|.33DB          XOR EBX,EBX
01001BD9|.57            PUSH EDI
01001BDA|.BE 00020000   MOV ESI,200
01001BDF|.43            INC EBX
01001BE0|.33FF          XOR EDI,EDI
01001BE2|.3BD6          CMP EDX,ESI
01001BE4|.0F87 75030000 JA 01001F5F


01001F5F|> \8D82 FFFDFFFF LEA EAX,                        ; Switch (messages 201..212, 7 exits)
01001F65|.83F8 11       CMP EAX,11
01001F68|.0F87 3B020000 JA 010021A9
01001F6E|.0FB680 DE2100 MOVZX EAX,BYTE PTR DS:
01001F75|.FF2485 C22100 JMP DWORD PTR DS:



跳几次,就到WM_LBUTTONUP的位置了
01001FDF|> \33FF          XOR EDI,EDI                              ; Cases 202 (WM_LBUTTONUP), 205 (WM_RBUTTONUP), 208 (WM_MBUTTONUP) of switch winmine.1001F5F
01001FE1|.393D 40510001 CMP DWORD PTR DS:,EDI
01001FE7|.0F84 BC010000 JE 010021A9

01001FED|> /893D 40510001 MOV DWORD PTR DS:,EDI
01001FF3|. |FF15 D8100001 CALL DWORD PTR DS:[<&USER32.ReleaseCaptu ; [USER32.ReleaseCapture
01001FF9|. |841D 00500001 TEST BYTE PTR DS:,BL
01001FFF|. |0F84 B6000000 JZ 010020BB
01002005|. |E8 D7170000   CALL 010037E1                            ; [winmine.010037E1
0100200A|. |E9 9A010000   JMP 010021A9
0100200F|> |393D 48510001 CMP DWORD PTR DS:,EDI         ; Case 204 (WM_RBUTTONDOWN) of switch winmine.1001F5F


由于下面已经是WM_RBUTTONDOWN的判断了,因此CALL 010021A9是处理WM_LBUTTONUP的函数,我们F7跟进
010037E1/$A1 18510001   MOV EAX,DWORD PTR DS:         ; winmine.010037E1(guessed void)
010037E6|.85C0          TEST EAX,EAX
010037E8|.0F8E C8000000 JLE 010038B6
010037EE|.8B0D 1C510001 MOV ECX,DWORD PTR DS:
010037F4|.85C9          TEST ECX,ECX
010037F6|.0F8E BA000000 JLE 010038B6
010037FC|.3B05 34530001 CMP EAX,DWORD PTR DS:
01003802|.0F8F AE000000 JG 010038B6
01003808|.3B0D 38530001 CMP ECX,DWORD PTR DS:
0100380E|.0F8F A2000000 JG 010038B6
01003814|.53            PUSH EBX
01003815|.33DB          XOR EBX,EBX
01003817|.43            INC EBX
01003818|.833D A4570001 CMP DWORD PTR DS:,0             ; 次数?
0100381F|.75 4A         JNE SHORT 0100386B
01003821|.833D 9C570001 CMP DWORD PTR DS:,0             ; 时间?
01003828|.75 41         JNE SHORT 0100386B
0100382A|.53            PUSH EBX                                 ; /Arg1 => 1
0100382B|.E8 BD000000   CALL 010038ED                            ; \winmine.010038ED
01003830|.FF05 9C570001 INC DWORD PTR DS:
01003836|.E8 7AF0FFFF   CALL 010028B5                            ; [winmine.010028B5
0100383B|.6A 00         PUSH 0                                 ; /TimerFunc = 00000000
0100383D|.68 E8030000   PUSH 3E8                                 ; |Timeout = 1000. ms
01003842|.53            PUSH EBX                                 ; |TimerID
01003843|.FF35 245B0001 PUSH DWORD PTR DS:            ; |hWnd = 000B047C, class = 扫雷, text = 扫雷
01003849|.891D 64510001 MOV DWORD PTR DS:,EBX         ; |
0100384F|.FF15 B4100001 CALL DWORD PTR DS:[<&USER32.SetTimer>]   ; \USER32.SetTimer
01003855|.85C0          TEST EAX,EAX
01003857|.75 07         JNZ SHORT 01003860
01003859|.6A 04         PUSH 4                                 ; /Arg1 = 4
0100385B|.E8 F0000000   CALL 01003950                            ; \winmine.01003950
01003860|>A1 18510001   MOV EAX,DWORD PTR DS:
01003865|.8B0D 1C510001 MOV ECX,DWORD PTR DS:
0100386B|>841D 00500001 TEST BYTE PTR DS:,BL
01003871|.5B            POP EBX
01003872|.75 10         JNZ SHORT 01003884
01003874|.6A FE         PUSH -2
01003876|.59            POP ECX
01003877|.8BC1          MOV EAX,ECX
01003879|.890D 1C510001 MOV DWORD PTR DS:,ECX
0100387F|.A3 18510001   MOV DWORD PTR DS:,EAX
01003884|>833D 44510001 CMP DWORD PTR DS:,0
0100388B|.74 09         JE SHORT 01003896
0100388D|.51            PUSH ECX                                 ; /Arg2
0100388E|.50            PUSH EAX                                 ; |Arg1
0100388F|.E8 23FDFFFF   CALL 010035B7                            ; \winmine.010035B7
01003894|.EB 20         JMP SHORT 010038B6
01003896|>8BD1          MOV EDX,ECX
01003898|.C1E2 05       SHL EDX,5
0100389B|.8A9402 405300 MOV DL,BYTE PTR DS:
010038A2|.F6C2 40       TEST DL,40
010038A5|.75 0F         JNZ SHORT 010038B6
010038A7|.80E2 1F       AND DL,1F
010038AA|.80FA 0E       CMP DL,0E
010038AD|.74 07         JE SHORT 010038B6
010038AF|.51            PUSH ECX                                 ; /Arg2
010038B0|.50            PUSH EAX                                 ; |Arg1
010038B1|.E8 5CFCFFFF   CALL 01003512                            ; \winmine.01003512
010038B6|>FF35 60510001 PUSH DWORD PTR DS:            ; /Arg1 = 0
010038BC|.E8 52F0FFFF   CALL 01002913                            ; \winmine.01002913
010038C1\.C3            RETN


由于雷区数据不应该随函数的退出而消失,可以判断数据存放在静态或者全局变量区,所以着重关注DS:这种地方,多试几次可知
10057A4记录了用户挖雷次数,而100579C处记录着已用时间,真正的布雷函数显然在那几个call中。你在这些汇编代码中发现了SetTimer这个函数,你在游戏时可能已经发现,在用户挖第一个雷时才会计数,因此call 010038ED和call 010028B5处一定有猫腻
第一个点进去以后是这样:
010038ED/$833D B8560001 CMP DWORD PTR DS:,3             ; winmine.010038ED(guessed Arg1)
010038F4|.75 47         JNE SHORT 0100393D
010038F6|.8B4424 04   MOV EAX,DWORD PTR SS:
010038FA|.48            DEC EAX                                  ; Switch (cases 1..3, 4 exits)
010038FB|.74 2A         JZ SHORT 01003927
010038FD|.48            DEC EAX
010038FE|.74 15         JZ SHORT 01003915
01003900|.48            DEC EAX
01003901|.75 3A         JNZ SHORT 0100393D
01003903|.68 05000400   PUSH 40005                               ; Case 3 of switch winmine.10038FA
01003908|.FF35 305B0001 PUSH DWORD PTR DS:
0100390E|.68 B2010000   PUSH 1B2
01003913|.EB 22         JMP SHORT 01003937
01003915|>68 05000400   PUSH 40005                               ; Case 2 of switch winmine.10038FA
0100391A|.FF35 305B0001 PUSH DWORD PTR DS:
01003920|.68 B1010000   PUSH 1B1
01003925|.EB 10         JMP SHORT 01003937
01003927|>68 05000400   PUSH 40005                               ; Case 1 of switch winmine.10038FA
0100392C|.FF35 305B0001 PUSH DWORD PTR DS:
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 winmine.10038FA


可以看出这个函数就是在你点击了雷以后发出声音的,没什么价值,但是此时你注意到下面的一个函数用到了rand,
对啊,这个随机数在初始化雷阵的时候肯定用得到啊,先下个断点再说。开始新游戏,然后扫雷就断在rand函数处,
01003940/$FF15 B0110001 CALL DWORD PTR DS:[<&msvcrt.rand>]       ; [MSVCRT.rand
01003946|.99            CDQ
01003947|.F77C24 04   IDIV DWORD PTR SS:
0100394B|.8BC2          MOV EAX,EDX
0100394D\.C2 0400       RETN 4

这个函数显然是用来取rand()%N的

运行到winmine代码中,
会发现一个循环,很重要
010036C7|> /FF35 34530001 /PUSH DWORD PTR DS:             ; /Arg1 = 9
010036CD|. |E8 6E020000   |CALL 01003940                           ; \winmine.01003940
010036D2|. |FF35 38530001 |PUSH DWORD PTR DS:             ; /Arg1 = 9
010036D8|. |8BF0          |MOV ESI,EAX                           ; |
010036DA|. |46            |INC ESI                                 ; |
010036DB|. |E8 60020000   |CALL 01003940                           ; \winmine.01003940
010036E0|. |40            |INC EAX
010036E1|. |8BC8          |MOV ECX,EAX
010036E3|. |C1E1 05       |SHL ECX,5
010036E6|. |F68431 405300 |TEST BYTE PTR DS:,80
010036EE|.^\75 D7         |JNZ SHORT 010036C7
010036F0|. |C1E0 05       |SHL EAX,5
010036F3|. |8D8430 405300 |LEA EAX,
010036FA|. |8008 80       |OR BYTE PTR DS:,80
010036FD|. |FF0D 30530001 |DEC DWORD PTR DS:
01003703|.^\75 C2         \JNZ SHORT 010036C7


这段代码经过逆向分析可以得到伪代码:
处存放棋盘大小SIZE
处存放余下要摆放的雷数MineToSet
存放棋盘数据BYTE DATA   我们初级模式9*9只用到-的数据
while(MineToSet)
{
int line=rand%SIZE+1,column=rand%SIZE+1;
if((DATA&0x80) == 0)
{//如果不是雷
    DATA |= 0x80;//设置为雷
    MineToSet--;
}
}




我们从ollydbg的内存窗口取得1005340的数据,分析以后可以得到雷区数组




问题3:为什么初始布置的雷区和实际的不一样?如上图,左上角的雷本是没有的,而处本来应该有雷,为什么没有?
可以得知8F处的为雷,0F处的不是雷

这里仅对初级水平做了分析,其他的同学们自己研究!


问题1:为什么是 ESP+8?
这个问题式函数栈帧的结构问题,对函数下断点是断点总是断在入口处,而一般情况下(本例如此)进入该函数之前往往是:
push Z
push Y
.......
push B
push A
call func
??next??
(该函数原型为stdcall func(A,B,C,...,Z))

call是将下个指令地址压栈,因此进了func以后内存栈帧结构为这样
   低地址
       ??next?? <---ESP
       A      <---ESP+4
       B      <---ESP+8
       ...
       Y
       Z
    高地址

windows的窗口回调过程函数定义为:
    LRESULT CALLBACK WindowProc(HWND hwnd,      // handle to window
                                 UINT uMsg,      // message identifier
                                 WPARAM wParam,// first message parameter
                                 LPARAM lParam   // second message parameter);

数因此我们想截获uMsg == WM_LBUTTONUP消息,则判断处即可

问题2:为什么选择WM_LBUTTONUP?
这个你可以动手试试就知道了,你在雷区按下(设该点为A)鼠标不放,移动到别处(设该点为B),若B也在雷区,则B处鼠标释放后程序视为挖雷
而如果在外边则什么效果也没有。相反如果你在雷区之外A处按下鼠标不放,拖到雷区之内B处释放,则B处雷挖开。可见是否挖雷与LBUTTONDOWN的位置无关,而与LBUTTONUP位置有关

问题3:其实这2处在用户第一次点击雷区时被winmine偷偷替换掉了,也就是说用户第一次就点中了雷!但是这是不对的,第一次就中雷就没意思了
所以winmine釜底抽薪,吧这个雷换到别的地方去了

xp版winmine变态的地方在于,点人脸的时候会布雷,而点第一次的时候你永远不会踩中雷,因为他在第一次的时候偷偷把他换掉
如果你第一次就点中了布好的雷上,他就把这个位置换到别的地方,让他不是雷,如果你没点到雷上自然啥都不做,所以第一次永远点不到雷上。。。
-之外的,都是0x10    -初始化为0x0F,然后每次你按人脸的时候,布9科雷 布雷就是发现arr&0x80 == 0的时候进行
arr |= 0x80 高位置1
点了第一次,他会判断你是否中了雷,中了,就把雷放到别的地方去,反正总是让你第一次不中雷,中了,他自然再去获取随机数,找到一个没有雷的位置,然后设置为雷,然后把你踩的地方设为非雷


file:///G:\Users\Administrator\AppData\Roaming\Tencent\Users\571652571\QQ\WinTemp\RichOle\]}O_2GAOJK1{3E1_)A{1H(9.jpg


秦四喜 发表于 2014-3-26 17:12:27

{:soso_e179:}

Anonymous 发表于 2014-3-27 20:20:00

顶一个。。。。。。。。。。。。。
页: [1]
查看完整版本: 对xp扫雷程序的部分分析