本人最近刚刚开始学习逆向分析,突发奇想想弄一个Windows XP扫雷的辅助。发帖前,在论坛上一搜,好多人都做过这个了,还有人做过Windows7 32位,64位的,突然感觉自己好低级……于是我又认真地把windowsXP扫雷的几乎所有汇编代码都分析了一遍,最后做了一个修改扫雷内部代码而实现悬停显示雷的操作。
效果如下:
进入正题,这是我的预计任务
1. 找到回调函数
2. 找到并分析雷表
找到存放时间的位置
3. 分析绘图代码
首先,用OllyDbg打开windows xp的扫雷程序。
一开始如下图所示
01003E21 > $ 6A 70 push 70 01003E23 . 68 90130001 push 01001390 01003E28 . E8 DF010000 call 0100400C ; SEH安装 01003E2D . 33DB xor ebx, ebx 01003E2F . 53 push ebx ; /pModule => NULL 01003E30 . 8B3D 8C100001 mov edi, dword ptr [<&KERNEL32.GetMo>; |KERNEL32.GetModuleHandleA 01003E36 . FFD7 call edi ; \GetModuleHandleA 01003E38 . 66:8138 4D5A cmp word ptr [eax], 5A4D ; MZ头确认 01003E3D . 75 1F jnz short 01003E5E 01003E3F . 8B48 3C mov ecx, dword ptr [eax+3C] ; ecx=elfanew 01003E42 . 03C8 add ecx, eax 01003E44 . 8139 50450000 cmp dword ptr [ecx], 4550 ; PE头确认 01003E4A . 75 12 jnz short 01003E5E 01003E4C . 0FB741 18 movzx eax, word ptr [ecx+18] ; eax=OptionalHeader 01003E50 . 3D 0B010000 cmp eax, 10B ; Magic
显然,是属于程序的入口部分代码,包含有初始化SHE,获取ImageBase,测试PE文件的有效性等等等等。
一:找窗口回调函数
为了实现找到回调函数这一个目标,首先需要知道获取来源。
首先想到WNDCLASSA(或WNDCLASSW)和WNDCLASSEX这两个结构体,WNDCLASSW定义如下
typedef struct tagWNDCLASSW { UINT style; WNDPROC lpfnWndProc; int cbClsExtra; int cbWndExtra; HINSTANCE hInstance; HICON hIcon; HCURSOR hCursor; HBRUSH hbrBackground; LPCWSTR lpszMenuName; LPCWSTR lpszClassName; } WNDCLASSW, *PWNDCLASSW, NEAR *NPWNDCLASSW, FAR *LPWNDCLASSW;
这两个结构体都是用来初始化窗口类的,在RegisterClass中被引用,根据该结构体的定义,在此结构的(+0x04)位置处,就是窗口回调函数的地址。所以,从设置WNDCLASS成员时指定的地址或者从RegisterClass调用时的栈窗口,可以找到WNDCLASS的地址,进而找到lpfnWndProc的值。
RegisterClass一般在主线程的WinMain中被调用,我向下跟踪,来到此处
01003F8F . 50 push eax 01003F90 . E8 5BE2FFFF call 010021F0 01003F95 . 8BF0 mov esi, eax 01003F97 . 8975 84 mov dword ptr [ebp-7C], esi 01003F9A . 395D E4 cmp dword ptr [ebp-1C], ebx 01003F9D . 75 07 jnz short 01003FA6 01003F9F . 56 push esi ; /status 01003FA0 . FF15 94110001 call dword ptr [<&msvcrt.exit>] ; \exit 01003FA6 > FF15 9C110001 call dword ptr [<&msvcrt._cexit>] ; [msvcrt._cexit
目前窗口并没有创建,而接下来的第二个函数是exit的调用,标志着程序的退出,因此可以确认,下面的第一个函数,就是程序的WinMain函数。
跟踪进去
010021F0 /$ 55 push ebp 010021F1 |. 8BEC mov ebp, esp 010021F3 |. 83EC 4C sub esp, 4C 010021F6 |. 8B45 08 mov eax, dword ptr [ebp+8] 010021F9 |. 53 push ebx 010021FA |. 56 push esi 010021FB |. 57 push edi 010021FC |. A3 305B0001 mov dword ptr [1005B30], eax 01002201 |. E8 AA180000 call 01003AB0 ; 设置随机数种子 01002206 |. 33DB xor ebx, ebx 01002208 |. 43 inc ebx 01002209 |. 33FF xor edi, edi 0100220B |. 837D 14 07 cmp dword ptr [ebp+14], 7 0100220F |. 74 0C je short 0100221D 01002211 |. 837D 14 02 cmp dword ptr [ebp+14], 2 01002215 |. 893D 385B0001 mov dword ptr [1005B38], edi 0100221B |. 75 06 jnz short 01002223 0100221D |> 891D 385B0001 mov dword ptr [1005B38], ebx 01002223 |> 8D45 F8 lea eax, dword ptr [ebp-8] 01002226 |. 50 push eax ; /pInitEx 01002227 |. C745 F8 08000>mov dword ptr [ebp-8], 8 ; | 0100222E |. C745 FC FD160>mov dword ptr [ebp-4], 16FD ; | 01002235 |. FF15 1C100001 call dword ptr [<&COMCTL32.InitCommon>; \InitCommonControlsEx 0100223B |. 6A 64 push 64 ; /RsrcName = 100. 0100223D |. FF35 305B0001 push dword ptr [1005B30] ; |hInst = NULL 01002243 |. FF15 AC100001 call dword ptr [<&USER32.LoadIconW>] ; \LoadIconW 01002249 |. 8B0D 305B0001 mov ecx, dword ptr [1005B30] 0100224F |. 68 007F0000 push 7F00 ; /RsrcName = IDC_ARROW 01002254 |. 57 push edi ; |hInst 01002255 |. A3 285B0001 mov dword ptr [1005B28], eax ; | 0100225A |. 897D B4 mov dword ptr [ebp-4C], edi ; | 0100225D |. C745 B8 C91B0>mov dword ptr [ebp-48], <lpfnWndProc>; | 01002264 |. 897D BC mov dword ptr [ebp-44], edi ; | 01002267 |. 897D C0 mov dword ptr [ebp-40], edi ; | 0100226A |. 894D C4 mov dword ptr [ebp-3C], ecx ; | 0100226D |. 8945 C8 mov dword ptr [ebp-38], eax ; | 01002270 |. FF15 BC100001 call dword ptr [<&USER32.LoadCursorW>>; \LoadCursorW 01002276 |. 53 push ebx ; /ObjType 01002277 |. 8945 CC mov dword ptr [ebp-34], eax ; | 0100227A |. FF15 60100001 call dword ptr [<&GDI32.GetStockObjec>; \GetStockObject 01002280 |. 8945 D0 mov dword ptr [ebp-30], eax 01002283 |. 8D45 B4 lea eax, dword ptr [ebp-4C] 01002286 |. BE A05A0001 mov esi, 01005AA0 0100228B |. 50 push eax ; /pWndClass 0100228C |. 897D D4 mov dword ptr [ebp-2C], edi ; | 0100228F |. 8975 D8 mov dword ptr [ebp-28], esi ; | 01002292 |. FF15 CC100001 call dword ptr [<&USER32.RegisterClas>; \RegisterClassW
在第一个CALL处我进行了查看,发现是设置随机数种子,接下来明显地看到调用了LoadIconW,LoadCursorW,和GetStockObject三个API函数对WNDCLASSW进行设置,往下翻,立刻可以看见RegisterClassW。事实上,hIcon的位置在lpfnWndProc(+0x10)的位置,从LoadIcon的返回值的设置位置(2个地方)
01002255 |. A3 285B0001 mov dword ptr [1005B28], eax ; |
一开始我以为是这里,结果发现1005B18没有写入的代码,接着向下看:
0100226D |. 8945 C8 mov dword ptr [ebp-38], eax ; |
可知ebp-38-10=ebp-48就是回调函数的地址,接下来看到
0100225D |. C745 B8 C91B0>mov dword ptr [ebp-48], 01001BC9 ; |
可以确认,01001BC9就是回调函数的地址了,添加标签为lpfnWndProc
二:
进入窗口回调函数,马上看到一个switch
01001C05 . 48 dec eax ; Switch (cases 2..47)
根据对回调函数的整体浏览,发现对WM_LBUTTONDOWN的处理部分,有将(记住这个地址)1005144处的数据设为0这估计是为了实现左键按下时显示格子0的效果,添加标签为bLButtonUp(Down的非是Up (`へ´*)ノ)
接着对WM_LBUTTONDOWN和WM_LBUTTONUP这两个消息的处理部分下断
01001FA6 > \393D 48510001 cmp dword ptr [1005148], edi ; Case 201 (WM_LBUTTONDOWN) of switch 01001F5F 01001FAC .^ 75 D6 jnz short 01001F84 01001FAE . FF75 14 push dword ptr [ebp+14] 01001FB1 . E8 56F4FFFF call 0100140C 01001FB6 . 85C0 test eax, eax 01001FB8 .^ 0F85 A0FCFFFF jnz 01001C5E 01001FBE . 841D 00500001 test byte ptr [1005000], bl 01001FC4 . 0F84 DF010000 je 010021A9 01001FCA . 8B45 10 mov eax, dword ptr [ebp+10] 01001FCD . 24 06 and al, 6 01001FCF . F6D8 neg al 01001FD1 . 1BC0 sbb eax, eax 01001FD3 . F7D8 neg eax 01001FD5 . A3 44510001 mov dword ptr [<bLButtonUp>], eax 01001FDA . E9 80000000 jmp 0100205F 01001FDF > 33FF xor edi, edi ; Cases 202 (WM_LBUTTONUP),205 (WM_RBUTTONUP),208 (WM_MBUTTONUP) of switch 01001F5F 01001FE1 . 393D 40510001 cmp dword ptr [1005140], edi 01001FE7 . 0F84 BC010000 je 010021A9 01001FED > 893D 40510001 mov dword ptr [1005140], edi 01001FF3 . FF15 D8100001 call dword ptr [<&USER32.ReleaseCapture>]; ReleaseCapture 01001FF9 . 841D 00500001 test byte ptr [1005000], bl 01001FFF . 0F84 B6000000 je 010020BB 01002005 . E8 D7170000 call 010037E1 0100200A . E9 9A010000 jmp 010021A9
在扫雷点击一个格子,在WM_LBUTTONDOWN断下,继续运行,WM_LBUTTONUP自然不会断下,而游戏界面无变化,说明WM_LBUTTONDOWN不进行点击格子的操作
现在大概可以判断,是左键弹起表示了对一个块的点击。
取消WM_LBUTTONDOWN的断点,继续点击,跟踪WM_LBUTTONUP,来到此处
010037E1 /$ A1 18510001 mov eax, dword ptr [1005118] ; 左键弹起消息的处理函数 010037E6 |. 85C0 test eax, eax 010037E8 |. 0F8E C8000000 jle 010038B6 010037EE |. 8B0D 1C510001 mov ecx, dword ptr [100511C] 010037F4 |. 85C9 test ecx, ecx 010037F6 |. 0F8E BA000000 jle 010038B6 010037FC |. 3B05 34530001 cmp eax, dword ptr [1005334] 01003802 |. 0F8F AE000000 jg 010038B6 01003808 |. 3B0D 38530001 cmp ecx, dword ptr [1005338] 0100380E |. 0F8F A2000000 jg 010038B6
开始是四个判断,根据对数据的分析,1005118应该保存一个POINT结构体,是当前点击的单位坐标,而1005334保存了扫雷区域的单位长度,1005338则是单位宽度。于是添加标签g_xPos,g_yPos,Length,Height
那么此处的作用应该是,判断点击坐标是否在范围之内,否则函数跳到末尾。
接下来则是这部分
01003818 |. 833D A4570001>cmp dword ptr [10057A4], 0 0100381F |. 75 4A jnz short 0100386B 01003821 |. 833D 9C570001>cmp dword ptr [100579C], 0 01003828 |. 75 41 jnz short 0100386B 0100382A |. 53 push ebx 0100382B |. E8 BD000000 call 010038ED 01003830 |. FF05 9C570001 inc dword ptr [100579C] 01003836 |. E8 7AF0FFFF call 010028B5 0100383B |. 6A 00 push 0 ; /Timerproc = NULL 0100383D |. 68 E8030000 push 3E8 ; |Timeout = 1000. ms 01003842 |. 53 push ebx ; |TimerID 01003843 |. FF35 245B0001 push dword ptr [1005B24] ; |hWnd = NULL 01003849 |. 891D 64510001 mov dword ptr [1005164], ebx ; | 0100384F |. FF15 B4100001 call dword ptr [<&USER32.SetTimer>] ; \SetTimer 01003855 |. 85C0 test eax, eax 01003857 |. 75 07 jnz short 01003860 01003859 |. 6A 04 push 4 0100385B |. E8 F0000000 call 01003950 01003860 |> A1 18510001 mov eax, dword ptr [<g_xPos>] 01003865 |. 8B0D 1C510001 mov ecx, dword ptr [<g_yPos>] 0100386B |> 841D 00500001 test byte ptr [1005000], bl
一开始,判断了两个位置的数据,不为0则设置间隔为1s的定时器,于是我大胆猜测,这是第一次点击时会调用的代码,设置定时器,进行计时。那么,10057A4和100579C就可能:一个是时间的数据,一个是游戏是否开始的BOOL型数据。
然后让扫雷运行,再分别对10057A4和100579C下硬件写入断点,发现10057A4是每点开一个格子都会断下,并且计数+1,100579C是每秒都会断下,计数+1。那么,10057A4应该是翻开的格子数目,标签其为PressNum,10057C9应该是时间,标签其为Time
之后是设置值, eax=x,ecx=y
接下来的部分如图所示
0100386B |> \841D 00500001 test byte ptr [1005000], 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 [<g_yPos>], ecx 0100387F |. A3 18510001 mov dword ptr [<g_xPos>], eax 01003884 |> 833D 44510001>cmp dword ptr [<bLButtonUp>], 0 0100388B |. 74 09 je short 01003896
1003872的跳转一般不会实现,100388B的跳转一般一定实现,第一个跳转是判断1005000处的数据是否为零,通过对此地址设置硬件断点,可以发现,当“新游戏”时,此处的值被设置为1,而游戏结束后,此处的值被设置为16。
第二个跳转发现访问了bLButtonDown这个地址,为0则跳转。这个地址前面见到过,在左键按下的时候被设置成了0,而左键弹起前一定会有按下操作,因此,这个地方一般也会跳过。(也可以写监测程序查看值的变化)
然后就是
01003896 |> \8BD1 mov edx, ecx 01003898 |. C1E2 05 shl edx, 5 ; edx=ecx*32 0100389B |. 8A9402 405300>mov dl, byte ptr [edx+eax+1005340] ; 读取雷表(1维数组)数据 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 010038B0 |. 50 push eax 010038B1 |. E8 5CFCFFFF call <PressThisBlock> ; 处理
从前三行可以知道读取雷表的方法
例如有雷表BYTE arMine[..],需要读取第r行第c列
第一行将ecx:也就是r,赋给edx
shl相当于<<,第二行是将edx左移五位,五个二进制位就是乘以2^5(=32)
第三行读取
那么可以知道,1005340是雷表的头地址,标签其为arMine,读取方式为arMine[r*32+c]
通过不断地对数据进行更新和检查,可以知道储存规律:
雷表是一个以1个字节为元素长度的数组,0x10为游戏的边界
雷表元素的低四位:
i. 未翻开时,是0xF
ii. 插旗子时,是0xE
iii. 打问号时,是0xD
iv. 翻开时(非雷),是它的数字,是雷则为0xA(不是点击到的)
v. 左键按下时,是0x0
vi. 边界为0x1
雷表元素的高四位:
i. 未翻开时,是雷则为0x8,否则为0x0
ii. 翻开时(非雷),是0x4,是雷则为0x8(不是点击到的)
iii. 边界为0x0
雷表元素是点击到的雷,数据则为0xCC
三.
上面的基本说的都没用(众所周知),接下来才是重头戏,如何实现,把鼠标悬停在一个格子上,如果那个格子有雷,就把雷显示出来(透视的感觉有木有)
首先,需要寻找一个可以安心写代码并能保存到文件的位置,将代码向下翻,找到1004A60的位置,下面都是0数据,可以使用,不过要先判断这个区域是否在扫雷文件内(不然保存不到文件里),RVA=4A60,转为文件FileOffset=3E60,而文件中.text节区是到4000,那么把它标签为InjectCode,于是在此处就可以放心地写入代码了。
首先我在扫雷窗口生成后对WM_PAINT下断,然后在扫雷点开一格,并没有断下,说明重绘函数不会处理左键单击,格子变化之类的游戏进行中的小调整,在这里写入代码就没有用了(我一开始莽,直接在这里写,然并卵)。为了知道贴图的怎么变化的,我在BitBlt下了断,点击一下,来到此处:
01002646 >/$ 56 push esi 01002647 |. FF35 245B0001 push dword ptr [1005B24] ; /hWnd = NULL 0100264D |. FF15 2C110001 call dword ptr [<&USER32.GetDC>] ; \GetDC 01002653 |. 8B4C24 0C mov ecx, dword ptr [esp+C] 01002657 |. 68 2000CC00 push 0CC0020 ; /ROP = SRCCOPY 0100265C |. 8BF0 mov esi, eax ; | 0100265E |. 8B4424 0C mov eax, dword ptr [esp+C] ; | 01002662 |. 8BD1 mov edx, ecx ; | 01002664 |. 6A 00 push 0 ; |YSrc = 0 01002666 |. C1E2 05 shl edx, 5 ; | 01002669 |. 0FBE9402 4053>movsx edx, byte ptr [edx+eax+<arMine>] ; | 01002671 |. 6A 00 push 0 ; |XSrc = 0 01002673 |. 83E2 1F and edx, 1F ; | 01002676 |. FF3495 205A00>push dword ptr [edx*4+1005A20] ; |hSrcDC 0100267D |. C1E1 04 shl ecx, 4 ; | 01002680 |. 6A 10 push 10 ; |Height = 10 (16.) 01002682 |. 6A 10 push 10 ; |Width = 10 (16.) 01002684 |. 83C1 27 add ecx, 27 ; | 01002687 |. C1E0 04 shl eax, 4 ; | 0100268A |. 51 push ecx ; |YDest 0100268B |. 83E8 04 sub eax, 4 ; | 0100268E |. 50 push eax ; |XDest 0100268F |. 56 push esi ; |hDestDC 01002690 |. FF15 5C100001 call dword ptr [<&GDI32.BitBlt>] ; \BitBlt 01002696 |. 56 push esi ; /hDC 01002697 |. FF35 245B0001 push dword ptr [1005B24] ; |hWnd = NULL 0100269D |. FF15 28110001 call dword ptr [<&USER32.ReleaseDC>] ; \ReleaseDC 010026A3 |. 5E pop esi 010026A4 \. C2 0800 retn 8
发现两个参数分别是我点下的x,y坐标,那么这个函数就是根据一个格的数值来绘图。标签其为UpdateBlockPaint,观察栈,发现:把雷表中的相应元素&0x1F后,作为8字节为元素长度的数组1005A20的索引值,就可以得到相应贴图的HDC,那么,显示的雷是0x8A,&0x1F得0xA,于是位置就是4*0xA+0x1005A20=1005A48,这个地址之后BitBlt中会用到哦~
这个函数的调用最终是起源于回调函数中的:
01002085 |> \393D 40510001 cmp dword ptr [1005140], edi ; edi:0x00000000 0100208B |. 74 34 je short 010020C1 ; 左键没按下 0100208D |. 841D 00500001 test byte ptr [1005000], bl ; bl:0x01 01002093 |.^ 0F84 54FFFFFF je 01001FED ; 游戏没开始 01002099 |. 8B45 14 mov eax, dword ptr [ebp+14] ; lParam 0100209C |. C1E8 10 shr eax, 10 ; eax=HIWORD(lParam):yPos 0100209F |. 83E8 27 sub eax, 27 ; 减去边框后+16 010020A2 |. C1F8 04 sar eax, 4 ; eax/=16 010020A5 |. 50 push eax ; 参数2:y 010020A6 |. 0FB745 14 movzx eax, word ptr [ebp+14] ; eax=LOWORD(lParam):xPos 010020AA |. 83C0 04 add eax, 4 ; 减去边框后+16 010020AD |. C1F8 04 sar eax, 4 ; eax/=16 010020B0 |. 50 push eax ; 参数1:x 010020B1 |> E8 1E110000 call 010031D4
触发WM_MOUSEMOVE一定来到此处,左中右键按下也可能来到此处,一开始就判断左键是否按下,关于1005140的值,我用监测程序判断了一下
PROCESSENTRY32 pe32; HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); BOOL bRet = Process32First(hSnap, &pe32); DWORD dwPid = 0; while (bRet) { if (lstrcmp(pe32.szExeFile, "saolei.exe") == 0){ dwPid = pe32.th32ProcessID; break; } bRet = Process32Next(hSnap, &pe32); } if (dwPid == 0)return 0; CloseHandle(hSnap); g_hProcess = OpenProcess(PROCESS_ALL_ACCESS, 1, dwPid); int value; int last = 0; while (ReadProcessMemory(g_hProcess, (LPVOID)0x01005140, &value, 4, 0)){ Sleep(10); if (value != last){ char*szMsg = new char[100]; wsprintf(szMsg, "Turn to %d", value); printf("%s\n",szMsg); } last = value; }
左键按下为1,弹起后为0
接下来是10031D4函数的部分
010031D4 /$ 55 push ebp ; edx=x,edi=y(新的) eax=x,ecx=y(旧的) 010031D5 |. 8BEC mov ebp, esp 010031D7 |. 83EC 20 sub esp, 20 010031DA |. 8B55 08 mov edx, dword ptr [ebp+8] ; 参数1:x 010031DD |. A1 18510001 mov eax, dword ptr [<g_xPos>] 010031E2 |. 3BD0 cmp edx, eax 010031E4 |. 8B0D 1C510001 mov ecx, dword ptr [<g_yPos>] 010031EA |. 57 push edi 010031EB |. 8B7D 0C mov edi, dword ptr [ebp+C] ; 参数2:y 010031EE |. 75 08 jnz short 010031F8 ; 有变动,即刻更改g_xPos和g_yPos 010031F0 |. 3BF9 cmp edi, ecx 010031F2 |. 0F84 1F020000 je 01003417 ; 没变动,不需要更新数据与画面 010031F8 |> 833D 44510001>cmp dword ptr [<bLButtonUp>], 0 010031FF |. 53 push ebx 01003200 |. 56 push esi 01003201 |. 8BD8 mov ebx, eax 01003203 |. 8BF1 mov esi, ecx 01003205 |. 8915 18510001 mov dword ptr [<g_xPos>], edx ; 更新 0100320B |. 893D 1C510001 mov dword ptr [<g_yPos>], edi 01003211 |. 0F84 80010000 je 01003397 ; 当前情况下,一定跳转 ………… 01003397 |> \85DB test ebx, ebx ; (ebx,esi)上次移动时所在的格子 (edx,edi)这次移动时... 01003399 |. 7E 34 jle short 010033CF 0100339B |. 85F6 test esi, esi ; 上次点越界的判断 0100339D |. 7E 30 jle short 010033CF 0100339F |. 3B1D 34530001 cmp ebx, dword ptr [<Length>] 010033A5 |. 7F 28 jg short 010033CF 010033A7 |. 3B35 38530001 cmp esi, dword ptr [<Height>] 010033AD |. 7F 20 jg short 010033CF 010033AF |. 8BC6 mov eax, esi 010033B1 |. C1E0 05 shl eax, 5 010033B4 |. F68418 405300>test byte ptr [eax+ebx+<arMine>], 40 010033BC |. 75 11 jnz short 010033CF 010033BE |. 56 push esi 010033BF |. 53 push ebx 010033C0 |. E8 DBFDFFFF call <PressOff> ; 解除上一次点的按下 010033C5 |. 56 push esi 010033C6 |. 53 push ebx 010033C7 |. E8 7AF2FFFF call <UpdateBlockPaint> ; 处理目标位置的贴图 010033CC |. 8B55 08 mov edx, dword ptr [ebp+8] 010033CF |> 85D2 test edx, edx ; 当前点越界的判断 010033D1 |. 7E 42 jle short 01003415 010033D3 |. 85FF test edi, edi 010033D5 |. 7E 3E jle short 01003415 010033D7 |. 3B15 34530001 cmp edx, dword ptr [<Length>] 010033DD |. 7F 36 jg short 01003415 010033DF |. 3B3D 38530001 cmp edi, dword ptr [<Height>] 010033E5 |. 7F 2E jg short 01003415 010033E7 |. C1E7 05 shl edi, 5 ; 点击是否有效的判断(必须是未翻开格) 010033EA |. 8A8417 405300>mov al, byte ptr [edi+edx+<arMine>] 010033F1 |. A8 40 test al, 40 010033F3 |. 75 20 jnz short 01003415 010033F5 |. 24 1F and al, 1F 010033F7 |. 3C 0E cmp al, 0E 010033F9 |. 74 1A je short 01003415 010033FB |. 8B3D 1C510001 mov edi, dword ptr [<g_yPos>] 01003401 |. 8B35 18510001 mov esi, dword ptr [<g_xPos>] 01003407 |. 57 push edi 01003408 |. 56 push esi 01003409 |. E8 5DFDFFFF call <PressOn> ; 这一次点的按下 0100340E |. 57 push edi 0100340F |. 56 push esi 01003410 |. E8 31F2FFFF call <UpdateBlockPaint> 01003415 |> 5E pop esi 01003416 |. 5B pop ebx 01003417 |> 5F pop edi 01003418 |. C9 leave 01003419 \. C2 0800 retn 8
是为了实现,格子按下的效果,如果说左键按下情况下这次悬停在的格子和上次不同,就更改上次格子为不按下,这次格子为按下,关于PressOn和PressOff两个函数,还原后代码如下(我对IDA结果的类型表示不满意):
BYTE WINAPI PressOn(int x, int y){ BYTE *pElement; BYTE Value; BYTE ret; pElement = (BYTE*)&arMine[y * 32 + x]; Value = *pElement & 0x1F; if (Value == 0x0D){ Value = 0x09; } else if (Value == 0x0F){ Value = 0; } ret = Value | *pElement & 0xE0; *pElement = ret; return ret; } BYTE WINAPI PressOff(int x, int y){ BYTE *pElement; BYTE Value; BYTE ret; pElement = (BYTE*)&arMine[y * 32 + x]; Value = *pElement & 0x1F; if (Value == 0x09){ Value = 0x0D; } if ((*pElement & 0x1F) == 0){ Value = 0x0F; } ret = Value | *pElement & 0xE0; *pElement = ret; return ret; }
测试一下即知效果。
在回调函数中,WM_MOUSEMOVE时左键没按下,跳到此处
010020C1 |> \A1 54510001 mov eax, dword ptr [1005154] 010020C6 >|. 3BC7 cmp eax, edi
刚好可以在这里修改代码(字节刚好为5,强迫症福利),把前面写代码的位置标签为InjectCode,把10020C1处改为
010020C1 /E9 9A290000 jmp <InjectCode> 010020C6 >|. |3BC7 cmp eax, edi
并把10020C6标签为CodeReturn
接下来是InjectCode处代码
01004A60 > \8B45 14 mov eax, dword ptr [ebp+14] ; 这里其实是读取lParam 01004A63 . C1E8 10 shr eax, 10 ; 复制于 1002099 01004A66 . 83E8 27 sub eax, 27 01004A69 . C1F8 04 sar eax, 4 01004A6C . 50 push eax 01004A6D . 0FB745 14 movzx eax, word ptr [ebp+14] 01004A71 . 83C0 04 add eax, 4 01004A74 . C1F8 04 sar eax, 4 01004A77 . 50 push eax 01004A78 . 58 pop eax ; 接着它来 01004A79 . 59 pop ecx ; eax=x,ecx=y 01004A7A . 60 pushad ; 保存寄存器环境 01004A7B . 85C9 test ecx, ecx ; 越界判断 01004A7D . 0F8E A9000000 jle 01004B2C 01004A83 . 85C0 test eax, eax 01004A85 . 0F8E A1000000 jle 01004B2C 01004A8B . 3B05 34530001 cmp eax, dword ptr [<Length>] 01004A91 . 0F8F 95000000 jg 01004B2C 01004A97 . 3B0D 38530001 cmp ecx, dword ptr [<Height>] 01004A9D . 0F8F 89000000 jg 01004B2C 01004AA3 . 3B05 18510001 cmp eax, dword ptr [<g_xPos>] 01004AA9 . 75 08 jnz short 01004AB3 01004AAB . 3B0D 1C510001 cmp ecx, dword ptr [<g_yPos>] 01004AB1 . 74 20 je short 01004AD3 01004AB3 > 50 push eax ; 悬停的格子改变 01004AB4 . 51 push ecx 01004AB5 . FF35 1C510001 push dword ptr [<g_yPos>] 01004ABB . FF35 18510001 push dword ptr [<g_xPos>] 01004AC1 . E8 80DBFFFF call <UpdateBlockPaint> ; 覆盖上一次的绘制 01004AC6 . 59 pop ecx 01004AC7 . 58 pop eax 01004AC8 . A3 18510001 mov dword ptr [<g_xPos>], eax ; 更新值 01004ACD . 890D 1C510001 mov dword ptr [<g_yPos>], ecx 01004AD3 > 8BD1 mov edx, ecx 01004AD5 . C1E2 05 shl edx, 5 01004AD8 . 0FBE9410 4053>movsx edx, byte ptr [eax+edx+<arMine>] 01004AE0 . 80FA 8F cmp dl, 8F 01004AE3 . 75 47 jnz short 01004B2C ; 不是雷就跳走 01004AE5 . 51 push ecx 01004AE6 . 50 push eax 01004AE7 . FF35 245B0001 push dword ptr [1005B24] ; /hWnd = NULL 01004AED . FF15 2C110001 call dword ptr [<&USER32.GetDC>] ; \GetDC 01004AF3 . 8BF0 mov esi, eax 01004AF5 . 58 pop eax 01004AF6 . 59 pop ecx 01004AF7 . C1E1 04 shl ecx, 4 ; BitBlt参数设置 01004AFA . 83C1 27 add ecx, 27 ; 语法参考UpdateBlockPaint 01004AFD . C1E0 04 shl eax, 4 01004B00 . 83E8 04 sub eax, 4 01004B03 . 68 2000CC00 push 0CC0020 ; /ROP = SRCCOPY 01004B08 . 6A 00 push 0 ; |YSrc = 0 01004B0A . 6A 00 push 0 ; |XSrc = 0 01004B0C . FF35 485A0001 push dword ptr [1005A48] ; |hSrcDC = NULL 01004B12 . 6A 10 push 10 ; |Height = 10 (16.) 01004B14 . 6A 10 push 10 ; |Width = 10 (16.) 01004B16 . 51 push ecx ; |YDest 01004B17 . 50 push eax ; |XDest 01004B18 . 56 push esi ; |hDestDC 01004B19 . FF15 5C100001 call dword ptr [<&GDI32.BitBlt>] ; \绘制! 01004B1F . 56 push esi ; /hDC 01004B20 . FF35 245B0001 push dword ptr [1005B24] ; |hWnd = NULL 01004B26 . FF15 28110001 call dword ptr [<&USER32.ReleaseDC>] ; \ReleaseDC 01004B2C > 61 popad 01004B2D . A1 54510001 mov eax, dword ptr [1005154] ; 执行覆盖的代码 01004B32 .^ E9 8FD5FFFF jmp <CodeReturn>
然后其中UpdateBlockPaint中没有越界判断(其实是我不想写了,这段代码费了我4个小时,Ollydbg打汇编代码就是自闭啊,写到后面由于长度,要把一些短跳改成长跳,awsl)
所以,要把g_xPos和g_yPos直接初始化为0,就没有问题了
完成!
至于代码中UpdateBlockPaint的越界判断,以及执行前对游戏是否开始的判断,可以自行添加