用IDA Pro + OD 来分析扫雷
【声明】小弟菜鸟一只,潜水多年,酷爱游戏,更爱逆向,喜欢修改游戏。坛子里关于扫雷的文章已经很多了,各位前辈都已经分析透彻了。小弟发此文,只是给和我一样的初级逆向爱好者提供一些思路,看看如何分析一个游戏,做出修改器,甚至是修改游戏规则。有不对的地方,各位前辈请指教,多谢。
工具:IDA Pro, OllyDbg, exeScope或者resource hacker
首先,我们知道,对于VC 写的windows编程来说,一般都是创建窗体,注册窗体,显示窗体,更新窗体,再来一个消息处理函数。我们先用IDA Pro来看看,至于为什么要用IDA,是因为我觉得IDA在某些方面比OD方便,比如函数名,变量名替换之类的,和OD配合使用,效果很好。
打开IDA Pro,自动进入EP,一开始肯定是一些加载器的初始化过程,没有必要仔细看,一路向下走。
.text:01003E21 public start
.text:01003E21 start proc near
.text:01003E21 push 70h
.text:01003E23 push offset dword_1001390
.text:01003E28 call sub_100400C
.text:01003E2D xor ebx, ebx
.text:01003E2F push ebx ; lpModuleName
.text:01003E30 mov edi, ds:GetModuleHandleA
.text:01003E36 call edi ; GetModuleHandleA
.text:01003E38 cmp word ptr [eax], 5A4Dh ; MZ
.text:01003E3D jnz short loc_1003E5E
.text:01003E3F mov ecx, [eax+3Ch]
.text:01003E42 add ecx, eax
.text:01003E44 cmp dword ptr [ecx], 4550h
.text:01003E4A jnz short loc_1003E5E
.text:01003E4C movzx eax, word ptr [ecx+18h]
.text:01003E50 cmp eax, 10Bh
.text:01003E55 jz short loc_1003E76
.text:01003E57 cmp eax, 20Bh
.text:01003E5C jz short loc_1003E63
当到达这里的时候:
text:01003F89 loc_1003F89: ; CODE XREF: start+158j
.text:01003F89 push eax ; hAccTable
.text:01003F8A push esi ; int
.text:01003F8B push ebx ; int
.text:01003F8C push ebx ; lpModuleName
.text:01003F8D call edi ; GetModuleHandleA
.text:01003F8F push eax ; int
.text:01003F90 call sub_10021F0 ;这是咱们的Main函数。
.text:01003F95 mov esi, eax
.text:01003F97 mov [ebp-7Ch], esi
.text:01003F9A cmp [ebp-1Ch], ebx
.text:01003F9D jnz short loc_1003FA6
.text:01003F9F push esi ; int
.text:01003FA0 call ds:exit
我们就发现了main函数,10021F0,为什么是这个?你看这个函数执行完了,程序就退出了啊。我们可以用IDA给它改名字,叫做Main。不废话,进入main函数看看。
看!IDA多智能,告诉我们了参数和临时变量,有WndClass和MSG,大家是不是眼前一亮。
.text:010021F0 Main proc near ; CODE XREF: start+16Fp
.text:010021F0
.text:010021F0 WndClass = WNDCLASSW ptr -4Ch
.text:010021F0 Msg = MSG ptr -24h
.text:010021F0 var_8 = tagINITCOMMONCONTROLSEX ptr -8
.text:010021F0 arg_0 = dword ptr 8
.text:010021F0 hAccTable = dword ptr 14h
.text:010021F0
.text:010021F0 push ebp
.text:010021F1 mov ebp, esp
.text:010021F3 sub esp, 4Ch
.text:010021F6 mov eax, [ebp+arg_0]
.text:010021F9 push ebx
继续走,往下几行就到这里了:
text:0100225A mov [ebp+WndClass.style], edi
.text:0100225D mov [ebp+WndClass.lpfnWndProc], offset WndProccess ;就是她!
.text:01002264 mov [ebp+WndClass.cbClsExtra], edi
.text:01002267 mov [ebp+WndClass.cbWndExtra], edi
.text:0100226A mov [ebp+WndClass.hInstance], ecx
.text:0100226D mov [ebp+WndClass.hIcon], eax
.text:01002270 call ds:LoadCursorW
.text:01002276 push ebx ; int
.text:01002277 mov [ebp+WndClass.hCursor], eax
.text:0100227A call ds:GetStockObject
.text:01002280 mov [ebp+WndClass.hbrBackground], eax
.text:01002283 lea eax, [ebp+WndClass]
.text:01002286 mov esi, offset AppName
.text:0100228B push eax ; lpWndClass
.text:0100228C mov [ebp+WndClass.lpszMenuName], edi
.text:0100228F mov [ebp+WndClass.lpszClassName], esi
.text:01002292 call ds:RegisterClassW
RegisterClassW,哈哈,我们这下子就知道消息处理函数了。先继续往下走,过一会儿我们看看这个消息处理函数。
.text:010022E1 push edi ; lpParam
.text:010022E2 push hInstance ; hInstance
.text:010022E8 add ecx, eax
.text:010022EA push edi ; hMenu
.text:010022EB push edi ; hWndParent
.text:010022EC push ecx ; nHeight
.text:010022ED mov ecx, dword_1005A90
.text:010022F3 add edx, ecx
.text:010022F5 push edx ; nWidth
.text:010022F6 mov edx, Y
.text:010022FC sub edx, eax
.text:010022FE mov eax, X
.text:01002303 push edx ; Y
.text:01002304 sub eax, ecx
.text:01002306 push eax ; X
.text:01002307 push 0CA0000h ; dwStyle
.text:0100230C push esi ; lpWindowName
.text:0100230D push esi ; lpClassName
.text:0100230E push edi ; dwExStyle
.text:0100230F call ds:CreateWindowExW
.text:01002315 cmp eax, edi
.text:01002317 mov hWnd, eax
.text:0100231C jnz short loc_1002325
.text:0100231E push 3E8h
.text:01002323 jmp short loc_1002336
嗯,创建窗体了。接着走,看返回的EAX不为空的话就跳。跳到这里:
text:01002325 loc_1002325: ; CODE XREF: Main+12Cj
.text:01002325 push ebx
.text:01002326 call sub_1001950
.text:0100232B call sub_1002B14
.text:01002330 test eax, eax
.text:01002332 jnz short loc_1002342
.text:01002334 push 5
.....
.....
.text:01002342 loc_1002342: ; CODE XREF: Main+142j
.text:01002342 push dword_10056C4
.text:01002348 call sub_1003CE5
.text:0100234D call sub_100367A
.text:01002352 push ebx ; nCmdShow
.text:01002353 push hWnd ; hWnd
.text:01002359 call ds:ShowWindow
.text:0100235F push hWnd ; hWnd
.text:01002365 call ds:UpdateWindow
.text:0100236B mov esi, ds:GetMessageW
.text:01002371 mov dword_1005B38, edi
.text:01002377 jmp short loc_10023A4
发现4个函数,我们要进去看看,因为凭咱们玩扫雷的经验来看,雷的产生是在窗体绘制之前就做好了,也就是showWindow之前,会有函数来布雷,所以,这四个函数,sub_1001950, sub_1002B14, sub_1003CE5,sub_100367A应该有一个或者多个来处理布雷。一个一个看看。
先看sub_1001950,函数太长,就不都贴了,贴一部分:
....
....
.text:01001978 mov edi, ds:GetMenuItemRect
.text:0100197E mov dword_1005B88, eax
.text:01001983 jnz short loc_10019DB
.text:01001985 mov edx, dword_1005B34
.text:0100198B add edx, eax
.text:0100198D mov eax, hMenu
.text:01001992 cmp eax, ebp
.text:01001994 mov dword_1005B88, edx
.text:0100199A jz short loc_10019DB
.text:0100199C lea edx, [esp+40h+rcItem]
.text:010019A0 push edx ; lprcItem
.text:010019A1 push ebp ; uItem
.text:010019A2 push eax ; hMenu
.text:010019A3 push ecx ; hWnd
.text:010019A4 call edi ; GetMenuItemRect
.text:010019A6 test eax, eax
.text:010019A8 jz short loc_10019DB
.text:010019AA lea eax, [esp+40h+var_20]
.text:010019AE push eax ; lprcItem
.text:010019AF push ebx ; uItem
.text:010019B0 push hMenu ; hMenu
.text:010019B6 push hWnd ; hWnd
.text:010019BC call edi ; GetMenuItemRect
....
....
从函数上看,是处理菜单那一块的东西,(如果错了,请各位指正,我当时一看函数不像,就没仔细看)。这个函数不像,那就下一个sub_1002B14:
.text:01002B14 sub_1002B14 proc near ; CODE XREF: Main+13Bp
.text:01002B14 call sub_1002414
.text:01002B19 test eax, eax
.text:01002B1B jnz short loc_1002B1E
.text:01002B1D retn
她居然call了另外一个函数,sub_1002414。进入sub_1002414看了就知道,是处理resource的,就不贴了,很明显的。
下一个函数,sub_1003CE5:
.text:01003CE5 arg_0 = dword ptr 4
.text:01003CE5
.text:01003CE5 mov eax, [esp+arg_0]
.text:01003CE9 mov dword_10056C4, eax
.text:01003CEE call sub_1001516
.text:01003CF3 mov eax, dword_10056C4
.text:01003CF8 and al, 1
.text:01003CFA neg al
.text:01003CFC sbb eax, eax
.text:01003CFE not eax
.text:01003D00 and eax, hMenu
.text:01003D06 push eax ; hMenu
.text:01003D07 push hWnd ; hWnd
.text:01003D0D call ds:SetMenu ; Assign a new menu to the specified window
.text:01003D13 push 2
.text:01003D15 call sub_1001950
.text:01003D1A retn 4
注意到一个SetMenu,应该是设置菜单的,其中,call了一个函数sub_1001516。不放心的话,就进去看看。
sub_1001516:
.text:01001516 sub_1001516 proc near ; CODE XREF: sub_1001B49+24p
.text:01001516 ; sub_1003CE5+9p
.text:01001516 xor eax, eax
.text:01001518 cmp word ptr dword_10056A0, ax
.text:0100151F setz al
.text:01001522 push eax
.text:01001523 push 209h ;521
.text:01001528 call sub_1003CC4
.text:0100152D xor eax, eax
.text:0100152F cmp word ptr dword_10056A0, 1
.text:01001537 setz al
.text:0100153A push eax
.text:0100153B push 20Ah ;522
.text:01001540 call sub_1003CC4
.text:01001545 xor eax, eax
.text:01001547 cmp word ptr dword_10056A0, 2
.text:0100154F setz al
.text:01001552 push eax
.text:01001553 push 20Bh ;523
.text:01001558 call sub_1003CC4
.text:0100155D xor eax, eax
.text:0100155F cmp word ptr dword_10056A0, 3
.text:01001567 setz al
.text:0100156A push eax
.text:0100156B push 20Ch ;524
.text:01001570 call sub_1003CC4
.text:01001575 push dword_10056C8
.text:0100157B push 211h ;529
.text:01001580 call sub_1003CC4
.text:01001585 push Data
.text:0100158B push 20Fh ;527
.text:01001590 call sub_1003CC4
.text:01001595 push dword_10056B8
.text:0100159B push 20Eh ;526
.text:010015A0 call sub_1003CC4
.text:010015A5 retn
.text:010015A5 sub_1001516 endp
发现每次都是压入一个数,然后call sub_1003CC4。因为她的父函数是处理菜单的,因此,我们怀疑压入的数是控件ID,用reource hacker看看。
果不其然:
500 MENU
LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US
{
POPUP "&Game"
{
MENUITEM "&New\tF2", 510
MENUITEM SEPARATOR
MENUITEM "&Beginner", 521
MENUITEM "&Intermediate", 522
MENUITEM "&Expert", 523
MENUITEM "&Custom...", 524
MENUITEM SEPARATOR
MENUITEM "&Marks (?)", 527
MENUITEM "Co&lor", 529
MENUITEM "&Sound", 526
MENUITEM SEPARATOR
MENUITEM "Best &Times...", 528
MENUITEM SEPARATOR
MENUITEM "E&xit", 512
}
POPUP "&Help"
{
MENUITEM "&Contents\tF1", 590
MENUITEM "&Search for Help on...", 591
MENUITEM "Using &Help", 592
MENUITEM SEPARATOR
MENUITEM "&About Minesweeper...", 593
}
}
是处理用户选的是初级,中级,高级之类的。所以,sub_1003CE5也不是。就剩下sub_100367A了:
sub_100367A函数很长,慢慢分析吧:
.text:0100367A sub_100367A proc near ; CODE XREF: sub_100140C+CAp
.text:0100367A ; sub_1001B49+33j ...
.text:0100367A mov eax, dword_10056AC
.text:0100367F mov ecx, uValue
.text:01003685 push ebx
.text:01003686 push esi
.text:01003687 push edi
.text:01003688 xor edi, edi
.text:0100368A cmp eax, dword_1005334
.text:01003690 mov dword_1005164, edi
.text:01003696 jnz short loc_10036A4
.text:01003698 cmp ecx, dword_1005338
.text:0100369E jnz short loc_10036A4
.text:010036A0 push 4
.text:010036A2 jmp short loc_10036A6
发现最终会进入loc_10036A6,走着:
.text:010036A6 loc_10036A6: ; CODE XREF: sub_100367A+28j
.text:010036A6 pop ebx
.text:010036A7 mov dword_1005334, eax
.text:010036AC mov dword_1005338, ecx
.text:010036B2 call sub_1002ED5
.text:010036B7 mov eax, dword_10056A4
.text:010036BC mov dword_1005160, edi
.text:010036C2 mov dword_1005330, eax
.text:010036C7
.text:010036C7 loc_10036C7: ; CODE XREF: sub_100367A+74j
.text:010036C7 ; sub_100367A+89j
.text:010036C7 push dword_1005334
.text:010036CD call RandomReminder
.text:010036D2 push dword_1005338
.text:010036D8 mov esi, eax
.text:010036DA inc esi
.text:010036DB call RandomReminder
.text:010036E0 inc eax
.text:010036E1 mov ecx, eax
.text:010036E3 shl ecx, 5
.text:010036E6 test byte ptr dword_1005340[ecx+esi], 80h
.text:010036EE jnz short loc_10036C7
.text:010036F0 shl eax, 5
.text:010036F3 lea eax, dword_1005340[eax+esi]
.text:010036FA or byte ptr [eax], 80h
.text:010036FD dec dword_1005330
.text:01003703 jnz short loc_10036C7
.text:01003705 mov ecx, dword_1005338
.text:0100370B imul ecx, dword_1005334
.text:01003712 mov eax, dword_10056A4
.text:01003717 sub ecx, eax
.text:01003719 push edi
.text:0100371A mov dword_100579C, edi
.text:01003720 mov dword_1005330, eax
.text:01003725 mov dword_1005194, eax
.text:0100372A mov dword_10057A4, edi
.text:01003730 mov dword_10057A0, ecx
.text:01003736 mov dword_1005000, 1
.text:01003740 call sub_100346A
.text:01003745 push ebx
.text:01003746 call sub_1001950
.text:0100374B pop edi
.text:0100374C pop esi
.text:0100374D pop ebx
.text:0100374E retn
.text:0100374E sub_100367A endp
在这一段中,我们发现了Rand()函数,就在01003940,我已经将她改名成了RandomReminder。进入发现就是随机产生一个数,然后除以参数,返回余数,这个应该就是随机布雷,而且发现调用2次,很容易想到是分别产生X和Y坐标的。两次调用的参数分别放在dword_1005334和dword_1005338中,怀疑是雷区的大小,进入OD,选择不同难度,然后断此函数看看参数,发现,果不其然,就是雷区大小。可以给dword_1005334和dword_1005338改名了,以后在遇到这两个参数就好识别了。接着走,发现这段话:
.text:010036E6 test byte ptr dword_1005340[ecx+esi], 80h
.text:010036EE jnz short loc_10036C7
.text:010036F0 shl eax, 5
.text:010036F3 lea eax, dword_1005340[eax+esi]
.text:010036FA or byte ptr [eax], 80h
哈哈,dword_1005340[ecx+esi],雷区地址。是这个样子的,dword_1005340[Y*32 + X]; dword_1005330很明显是雷数。
OK,到这里就找到雷区了,比较麻烦,还是各位前辈的直接下断Rand()简单,但是我比较笨笨,一开始怕万一不是用Rand()怎么办呢,于是就自己分析看看。先写到这里,下次主要靠OD分析按钮事件。谢谢。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!