【文章标题】: win7自动扫雷
【作者声明】: 参考了看雪的一篇帖子http://bbs.pediy.com/showthread.php?t=40295,感谢该帖作者!
打开win7扫雷,感觉无从下手,来看雪搜索了一下,找到一篇帖子《Vista 的扫雷》,看完之后豁然开朗。该帖子分析得很清楚了,我就不多说了,直接开干了。
首先下断bp rand,直接就断下来了,执行到返回,发现是一个小函数,很多地方都会调用这个函数,而且会不停中断。
008DD7F3 /$ 8BFF mov edi, edi ;鼠标点到这一行的时候,OD会在代码窗口下方提示“本地调用来自。。。”
008DD7F5 |. 55 push ebp
008DD7F6 |. 8BEC mov ebp, esp
008DD7F8 |. FF15 B8168B00 call dword ptr [<&msvcrt.rand>] ; [rand
008DD7FE |. 8B4D 0C mov ecx, dword ptr [ebp+C]
008DD801 |. 2B4D 08 sub ecx, dword ptr [ebp+8]
008DD804 |. 99 cdq
008DD805 |. 41 inc ecx
008DD806 |. F7F9 idiv ecx
008DD808 |. 8BC2 mov eax, edx
008DD80A |. 0345 08 add eax, dword ptr [ebp+8]
008DD80D |. 5D pop ebp
008DD80E \. C2 0800 retn 8
在每个调用该函数的call都下断,然后清除掉一直中断的一个call(rand的中断也要清除掉),然后回到游戏,点击鼠标发现完全不会中断下来,于是开始新游戏,点击一个格子,断下来了。
008D0163 |. /EB 31 jmp short 008D0196
008D0165 |> |33FF xor edi, edi
008D0167 |. |EB 2D jmp short 008D0196
008D0169 |> |8B45 FC /mov eax, dword ptr [ebp-4]
008D016C |. |8B00 |mov eax, dword ptr [eax]
008D016E |. |85C0 |test eax, eax
008D0170 |. |76 2B |jbe short 008D019D
008D0172 |. |48 |dec eax
008D0173 |. |50 |push eax
008D0174 |. |6A 00 |push 0
008D0176 |. |E8 78D60000 |call 008DD7F3
008D017B |. |8BD8 |mov ebx, eax
008D017D |. |8B45 FC |mov eax, dword ptr [ebp-4]
008D0180 |. |8B40 0C |mov eax, dword ptr [eax+C]
008D0183 |. |FF3498 |push dword ptr [eax+ebx*4]
008D0186 |. |8BCF |mov ecx, edi
008D0188 |. |E8 C4580100 |call 008E5A51
008D018D |. |8B4D FC |mov ecx, dword ptr [ebp-4]
008D0190 |. |53 |push ebx
008D0191 |. |E8 08EB0000 |call 008DEC9E
008D0196 |> \8B07 mov eax, dword ptr [edi]
008D0198 |. 3B46 04 |cmp eax, dword ptr [esi+4]
008D019B |.^ 75 CC \jnz short 008D0169
此处代码跟《Vista 的扫雷》完全一样,我就不献丑了,直接Ctrl+F9,回到这里:
008D0C7F |. A1 B4689300 mov eax, dword ptr [9368B4] ;这里就是我们要的了
008D0C84 |. 3848 18 cmp byte ptr [eax+18], cl
008D0C87 |. 74 5C je short 008D0CE5
008D0C89 |. 51 push ecx
008D0C8A |. 51 push ecx
008D0C8B |. 51 push ecx
008D0C8C |. E8 58F90000 call 008E05E9
008D0C91 |. 33C9 xor ecx, ecx
008D0C93 |. EB 50 jmp short 008D0CE5
008D0C95 |> 394E 18 cmp dword ptr [esi+18], ecx
008D0C98 |. 75 20 jnz short 008D0CBA
008D0C9A |. 57 push edi
008D0C9B |. 53 push ebx
008D0C9C |. 8BCE mov ecx, esi
008D0C9E |. E8 18F4FFFF call 008D00BB
008D0CA3 |. 6A 00 push 0 ;Ctrl+F9后出现在这里
008D0CA5 |. 57 push edi
008D0CA6 |. 53 push ebx
008D0CA7 |. 6A 00 push 0
008D0CA9 |. 57 push edi
008D0CAA |. 53 push ebx
008D0CAB |. 8BCE mov ecx, esi
008D0CAD |. E8 90FDFFFF call 008D0A42
现在我们得到一个稳定的地址:minesweeper.exe+868B4,从这里可以知道某个格子有没有雷。公式是这样的:[[[[
minesweeper.exe+868B4]+10]+44]+c]+4*(x*高度+y),这个(x,y)的设定是假设扫雷的左上角是(0,0),往右x增加,往下y增加。
那么现在知道哪里有雷哪里没雷,想做手脚就很容易了,比如可以透视出哪里有雷,或者直接自动扫雷。《Vista 的扫雷》中是模拟鼠标的移动和点击来实现的,本文则是使用CE(Cheat Engine)注入代码,创建远程线程直接调用扫雷程序的call来扫雷,优点是不用计算客户区和格子的坐标,缺点是不稳定,容易使扫雷程序崩溃。。。
下面我们来找一下程序用来翻开格子的call,先在minesweeper.exe+868B4下硬件访问断点,发现一只断,只能清除掉,试试[
minesweeper.exe+868B4]+10,还是一直断,继续试[[
minesweeper.exe+868B4]+10]+44,不会断了,回到游戏,翻开一个格子,中断了。清除硬件断点,按Ctrl+F9四次之后来到这里,就是关键的call了:
008EA3F3 |> \8B0D 54749300 mov ecx, dword ptr [937454]
008EA3F9 |. 85C9 test ecx, ecx
008EA3FB |. 0F84 5E030000 je 008EA75F
008EA401 |. 6A 00 push 0
008EA403 |. FF75 10 push dword ptr [ebp+10]
008EA406 |. 53 push ebx
008EA407 |. E8 635DFFFF call 008E016F ;刚才我们就在这个call里
008EA40C |. /E9 4E030000 jmp 008EA75F
为什么要按四次呢?为什么判定这里就是关键call呢?因为我已经跟过了所以我知道。。。
现在马后炮分析一下,在刚才硬件断点中断的地方按一次Ctrl+F9,出来之后是一个switch的分支,在每个分支上下断点,回到游戏继续点格子,发现case 7是鼠标按下,case 14是鼠标弹起,我们不关心鼠标按下,所以只要分析case 14就好了。
看看case 14,发现这里除了用到minesweeper.exe+868B4之外,还有一个ESI的参数也参与了重要计算,追随一下ESI,发现在函数开头有一个Mov esi,ecx,于是继续Ctrl+F9,来到这里:
008E6835 /$ 8BFF mov edi, edi
008E6837 |. 55 push ebp
008E6838 |. 8BEC mov ebp, esp
008E683A |. 53 push ebx
008E683B |. 8B5D 08 mov ebx, dword ptr [ebp+8]
008E683E |. 56 push esi
008E683F |. 8BF1 mov esi, ecx
008E6841 |. 57 push edi
008E6842 |. 33FF xor edi, edi
008E6844 |. 8973 04 mov dword ptr [ebx+4], esi
008E6847 |. 39BE 90000000 cmp dword ptr [esi+90], edi
008E684D |. 76 17 jbe short 008E6866
008E684F |> 8B86 9C000000 /mov eax, dword ptr [esi+9C]
008E6855 |. 8B0CB8 |mov ecx, dword ptr [eax+edi*4]
008E6858 |. 8B01 |mov eax, dword ptr [ecx]
008E685A |. 53 |push ebx
008E685B |. FF10 |call dword ptr [eax]
008E685D |. 47 |inc edi ;出来的时候在这里
008E685E |. 3BBE 90000000 |cmp edi, dword ptr [esi+90]
008E6864 |.^ 72 E9 \jb short 008E684F
008E6866 |> 5F pop edi
008E6867 |. 5E pop esi
008E6868 |. 5B pop ebx
008E6869 |. 5D pop ebp
008E686A \. C2 0400 retn 4
可以看出,call的时候ecx是这么来的:[[esi+9C]+edi*4],edi是计数的不管他,esi呢?又一个Mov esi,ecx,那么再一次Ctrl+F9,来到这里:
008E048B |. 8B4D 08 mov ecx, dword ptr [ebp+8]
008E048E |. 8B01 mov eax, dword ptr [ecx]
008E0490 |. 57 push edi
008E0491 |. FF50 08 call dword ptr [eax+8]
008E0494 |> 838E 88000000>or dword ptr [esi+88], FFFFFFFF ;出来的时候在这里
看到一个mov ecx, dword ptr [ebp+8],感觉好像是参数传进来的,要不就是个临时变量,我们来分析一下这个[ebp+8]怎么来的。再一次Ctrl+F9(就是关键call啦),下个断点,回到游戏,翻开一个格子,中断在这里:
008EA407 |. E8 635DFFFF call 008E016F
F7跟进去,一路只注意[ebp+8]的内存变化,发现在执行完这个call之后,[ebp+8]就被赋值了:
008E0470 |. 8BCE mov ecx, esi
008E0472 |. 0F94C0 sete al
008E0475 |. 8847 0C mov byte ptr [edi+C], al
008E0478 |. 8D45 08 lea eax, dword ptr [ebp+8]
008E047B |. 50 push eax
008E047C |. FFB6 88000000 push dword ptr [esi+88]
008E0482 |. E8 BAF40000 call 008EF941
那么在这个call下断,回到游戏,翻开一个格子,中断到这个call的时候,F7跟进去:
008EF941 /$ 8BFF mov edi, edi
008EF943 |. 55 push ebp
008EF944 |. 8BEC mov ebp, esp
008EF946 |. 56 push esi
008EF947 |. 8BF1 mov esi, ecx
008EF949 |. FF76 08 push dword ptr [esi+8]
008EF94C |. 6A 00 push 0
008EF94E |. FF75 08 push dword ptr [ebp+8]
008EF951 |. E8 06F3FEFF call 008DEC5C
008EF956 |. 85C0 test eax, eax
008EF958 |. 7D 04 jge short 008EF95E
008EF95A |. 32C0 xor al, al
008EF95C |. EB 0D jmp short 008EF96B
008EF95E |> 8B4E 04 mov ecx, dword ptr [esi+4]
008EF961 |. 8B0481 mov eax, dword ptr [ecx+eax*4]
008EF964 |. 8B4D 0C mov ecx, dword ptr [ebp+C]
008EF967 |. 8901 mov dword ptr [ecx], eax
008EF969 |. B0 01 mov al, 1
008EF96B |> 5E pop esi
008EF96C |. 5D pop ebp
008EF96D \. C2 0800 retn 8
啥也不说了,还是一个mov esi, ecx,那这个函数就不看了,回到上层,找一下ECX吧。。。往上找了几行,就看到了:
008E0470 |. 8BCE mov ecx, esi
好,现在任务又变成找ESI了,往上找,正如你想到的,函数的开头有个mov esi, ecx,绕的好晕啊,Ctrl+F9回上层找吧:
008EA3F3 |> \8B0D 54749300 mov ecx, dword ptr [937454]
008EA3F9 |. 85C9 test ecx, ecx
008EA3FB |. 0F84 5E030000 je 008EA75F
008EA401 |. 6A 00 push 0
008EA403 |. FF75 10 push dword ptr [ebp+10]
008EA406 |. 53 push ebx
008EA407 |. E8 635DFFFF call 008E016F
看到了,mov ecx, dword ptr [937454],真不容易啊。。。ecx可以找到了,那么这个call的几个参数分别是干啥的呢?跟了几次,发现全是0。。。那么就当作全是push 0好了。
接下来分析ecx指向的结构,经过几次实验和观察,发现call内部使用到的地方也不多,而有意义的就这么几个:
[ecx+b8]:0表示鼠标按下,1表示鼠标弹起
[ebx+88],[ebx+10]:这两个地址的值都是格子的index=x*高度+y+1,因为这个index是从1开始计数的,所以后面有个+1。
[ebx+7c]:值为[[ecx+4]+4*(x*高度+y)],一个指针,指向的地方比较有意思,记录了翻开的格子上面显示的数字,也记录了没翻开的和有雷的格子,具体内容大家自己跟一下看就知道了。
大家可以试一下再Ctrl+F9,发现已经出了程序的领空了,来到USER32里面了,所以不能再往上找了,就在这里折腾就好了。
那么现在要这么做:
循环每个格子,遇到没有雷的格子,就mov ecx, dword ptr [937454],然后设置一下几个重要的参数,调用call,继续循环。
用CE的Auto assemble,输入以下内容:
alloc(myscript,1024)
globalalloc(mydata,256)
define(adr1,minesweeper.exe+868B4)
define(adr2,minesweeper.exe+87454)
define(adrcall,minesweeper.exe+3016F)
define(height,mydata+0)
define(width,mydata+4)
label(end)
label(in_width)
label(in_height)
myscript:
//Call this code to execute the script from assembler
mov ecx, dword ptr [adr2]
test ecx, ecx
je end
mov edi, dword ptr [adr1]
mov edi,[edi+10]
mov ebx,[edi+8]
mov [height],ebx
mov ebx,[edi+c]
mov [width],ebx
mov edi,[edi+44]
mov edi,[edi+c]
mov ecx,[width]
in_width:
test ecx,ecx
je end
dec ecx
mov eax,[edi+ecx*4]
mov eax,[eax+c]
mov edx,[height]
in_height:
test edx,edx
je in_width
dec edx
xor ebx,ebx
mov bl,byte ptr [eax+edx]
cmp bl,0
jnz in_height
pushad
mov ebx, dword ptr [adr2]
inc esi
mov [ebx+b8],esi
mov eax,[height]
push edx
mul ecx
pop edx
add eax,edx
push eax
shl eax,2
mov edx,[ebx+4]
mov edx,[edx+eax]
mov [ebx+7c],edx
pop eax
inc eax
mov [ebx+88],eax
mov [ebx+10],eax
mov ecx,ebx
push 0
push 0
push 0
call adrcall
push 32
call Sleep
popad
jmp in_height
end:
ret
execute一下,CE会告诉你代码注入到什么位置了,记下来,然后开多一个Auto assemble的窗口,输入这个:
createthread(地址)
这个地址就是刚才代码注入的地址了,然后execute,启动远程线程,就开始刷刷刷的自动扫雷了。 脚本写的不好请大家不吝赐教!还有就是这样注入经常会导致程序崩溃,哪位高人知道的请指点一下,多谢!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)