【文章标题】: 打造扫雷终极外挂
【文章作者】: CARY
【作者邮箱】: caryzyu@hotmail.com
【软件名称】: 扫雷5.1.2600.0
【下载地址】: XP自带
【编写语言】: VC++ 6.0
【使用工具】: OD
【操作平台】: Windows XP
【作者声明】: 只是感兴趣,没有其他目的。失误之处敬请诸位大侠赐教!
--------------------------------------------------------------------------------
【详细过程】
一日,向往常那样用OD随便加载一些东东小研究一翻,便把WINDOWS XP自带扫雷扔到了OD窗口里,随便调之,不料小有深入,拿出来与大家分享..嘻...
在这里我要感谢Backer老师在调试分析过程中给我的帮助!
(下面先是分析部分然后是代码的实践部分)
首先先要了解我们要做的外挂,在功能上我为大家准备了2个,一是让扫雷程序自动把所有有雷的地方自动换成用小红旗表示,二是自动帮我们完成扫雷任务,也就是代替我们完成游戏.哈哈 这个可是秒杀哦.
我们先分析这个游戏,要想完成上面的两个功能首先应该要猜测这个游戏的内部实践,只有知道了游戏的运行原理才能找到针对这个游戏的关键部分进行XXXX,才能实现功能嘛~~ 呵呵
OK,进入正题,先用PEID查壳为Microsoft Visual C++ 7.0 Method2 [调试] 没有加壳(太棒了!)在运行扫雷,如果是第一次运行则会默认为初级状态,也就是9*9小格的布局,如果你是老玩家了,那么也要把扫雷调到初级状态,因为关联数据少好分析嘛`
下面是我的猜测
猜测一:方格布局应该是随机生成
猜测二:扫雷的方格布局应该是数组或巨阵
猜测三:在我们点击方块时肯定有变量记录我们点击的是哪个方块 比如:x = 1 y = 3 既 第1列 第3排
我们先从第一个猜测开始,扫雷程序是用VC开发的,随机函数应该用到库函数rand 我们用OD下断rand(命令:bp rand)跟到主程序领空可以看到:
这是扫雷里计算随机数用到的函数
01003940 /$ FF15 B0110001 CALL DWORD PTR DS:[<&msvcrt.rand>] ; [rand
01003946 |. 99 CDQ
01003947 |. F77C24 04 IDIV DWORD PTR SS:[ESP+4] ;相当于EDX = rand % arg1
0100394B |. 8BC2 MOV EAX,EDX ;EDX给EAX做为返回值
0100394D \. C2 0400 RETN 4
我们来看看谁来调用上面的函数(被010036CD地址代码调用)
这个函数应该是随机布雷函数
0100367A /$ A1 AC560001 MOV EAX,DWORD PTR DS:[10056AC] ;横向方格数
0100367F |. 8B0D A8560001 MOV ECX,DWORD PTR DS:[10056A8] ;纵向方格数
01003685 |. 53 PUSH EBX
01003686 |. 56 PUSH ESI
01003687 |. 57 PUSH EDI
01003688 |. 33FF XOR EDI,EDI 0100368A |. 3B05 34530001 CMP EAX,DWORD PTR DS:[1005334] ;检测方格数是否改变
01003690 |. 893D 64510001 MOV DWORD PTR DS:[1005164],EDI
01003696 |. 75 0C JNZ SHORT winmine.010036A4 ;改变则跳
01003698 |. 3B0D 38530001 CMP ECX,DWORD PTR DS:[1005338] ;检测方格数是否改变
0100369E |. 75 04 JNZ SHORT winmine.010036A4 ;改变则跳
010036A0 |. 6A 04 PUSH 4
010036A2 |. EB 02 JMP SHORT winmine.010036A6
010036A4 |> 6A 06 PUSH 6
010036A6 |> 5B POP EBX
010036A7 |. A3 34530001 MOV DWORD PTR DS:[1005334],EAX
010036AC |. 890D 38530001 MOV DWORD PTR DS:[1005338],ECX
010036B2 |. E8 1EF8FFFF CALL winmine.01002ED5
010036B7 |. A1 A4560001 MOV EAX,DWORD PTR DS:[10056A4]
010036BC |. 893D 60510001 MOV DWORD PTR DS:[1005160],EDI
010036C2 |. A3 30530001 MOV DWORD PTR DS:[1005330],EAX
************************************************关键的随机布雷循环*************************************
010036C7 |> FF35 34530001 PUSH DWORD PTR DS:[1005334] ;横向方格数 布雷循环开始处
010036CD |. E8 6E020000 CALL winmine.01003940 ;根据横向方格数产生随机数
010036D2 |. FF35 38530001 PUSH DWORD PTR DS:[1005338] ;纵向方格数
010036D8 |. 8BF0 MOV ESI,EAX ;横向坐标给ESI
010036DA |. 46 INC ESI
010036DB |. E8 60020000 CALL winmine.01003940 ;根据纵向方格数产生随机数
010036E0 |. 40 INC EAX
010036E1 |. 8BC8 MOV ECX,EAX ;纵向坐标给ECX
010036E3 |. C1E1 05 SHL ECX,5
010036E6 |. F68431 405300>TEST BYTE PTR DS:[ECX+ESI+1005340],80
010036EE |.^ 75 D7 JNZ SHORT winmine.010036C7
010036F0 |. C1E0 05 SHL EAX,5 ;算出内存布局的纵坐标 相当于Y * 20
010036F3 |. 8D8430 405300>LEA EAX,DWORD PTR DS:[EAX+ESI+1005340] ;方格内存布局的地址给EAX
010036FA |. 8008 80 OR BYTE PTR DS:[EAX],80 ;把雷写入方格内存布局
010036FD |. FF0D 30530001 DEC DWORD PTR DS:[1005330] ;布雷的个数(也是循环次数)每次-1
01003703 |.^ 75 C2 JNZ SHORT winmine.010036C7 ;未布完则循环
*******************************************************************************************************
01003705 |. 8B0D 38530001 MOV ECX,DWORD PTR DS:[1005338]
0100370B |. 0FAF0D 345300>IMUL ECX,DWORD PTR DS:[1005334]
01003712 |. A1 A4560001 MOV EAX,DWORD PTR DS:[10056A4]
01003717 |. 2BC8 SUB ECX,EAX
01003719 |. 57 PUSH EDI
0100371A |. 893D 9C570001 MOV DWORD PTR DS:[100579C],EDI
01003720 |. A3 30530001 MOV DWORD PTR DS:[1005330],EAX
01003725 |. A3 94510001 MOV DWORD PTR DS:[1005194],EAX
0100372A |. 893D A4570001 MOV DWORD PTR DS:[10057A4],EDI
01003730 |. 890D A0570001 MOV DWORD PTR DS:[10057A0],ECX
01003736 |. C705 00500001>MOV DWORD PTR DS:[1005000],1
01003740 |. E8 25FDFFFF CALL winmine.0100346A
01003745 |. 53 PUSH EBX ; /Arg1
01003746 |. E8 05E2FFFF CALL winmine.01001950 ; \winmine.01001950
0100374B |. 5F POP EDI
0100374C |. 5E POP ESI
0100374D |. 5B POP EBX
0100374E \. C3 RETN
[ECX+ESI+1005340] 这个代码实际上是二维数组寻址 可以理解为 1005340[X + Y * 20]
010036FD |. FF0D 30530001 DEC DWORD PTR DS:[1005330] 这段代码的内存地址[1005330]存的是要布的雷数,你高兴也可以把它写为1个
我们知道了它的内存结构很容易就能算出每个方格在内存中的位置了,那我们先看看第一个方格在内存中是什么样的
注意: y * 20 是因为这条指令 010036E3 |. C1E1 05 SHL ECX,5 一个数左移5位等于这个数乘以20
第1个方格 1005340[1 + 1 * 20] = 1005361
最后1个方格 1005340[9 + 9 * 20] = 1005469
哈哈,是不是计算出来了呢.我们可以在OD内存窗口里看看(命令:d 1005361)
01005361 0F 0F 8F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F 0F ?
01005371 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
01005381 8F 0F 0F 8F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F 0F ??
01005391 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
010053A1 0F 0F 0F 0F 0F 0F 8F 0F 0F 10 0F 0F 0F 0F 0F 0F ?
010053B1 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
010053C1 0F 0F 0F 0F 0F 0F 8F 0F 0F 10 0F 0F 0F 0F 0F 0F ?
010053D1 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
010053E1 0F 0F 0F 0F 0F 0F 0F 0F 8F 10 0F 0F 0F 0F 0F 0F ?
010053F1 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
01005401 0F 0F 0F 0F 0F 8F 0F 8F 0F 10 0F 0F 0F 0F 0F 0F ??
01005411 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
01005421 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F 0F
01005431 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
01005441 0F 0F 0F 0F 0F 8F 0F 0F 0F 10 0F 0F 0F 0F 0F 0F ?
01005451 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 10
01005461 0F 0F 0F 0F 8F 0F 0F 0F 0F ?
哇这么多一片!那么我们怎么知道哪块是雷呢..... 看看下面代码
010036FA |. 8008 80 OR BYTE PTR DS:[EAX],80
这段代码每次都让0F与80做或运算 0F OR 80 = 8F 也就是说8F就是雷的标记喽.. 我们可以看到内存中有10个8F的标记
下面我们试着找小红旗的标记,我们先在没有雷的地方单击鼠标右键设上小红旗.然后在有雷的地方在设一个在看看内存布局,结果有雷的地方变成了8F,没有雷的地方是0E。
好了以上的线索先保留,让我们在看看如何让扫雷自己去消灭小方格,现在要先借助一些游戏修改工具了(我相信大家都用过吧),目的是要找到鼠标当前点击方格的 X Y 坐标,找这个很简单,只要先点开一个方格然后输入其X 或 Y,然后在任意点一个方格在搜索,直到搜索到为一个值时这个值就是需要的了.
我搜索的两个地址是 1005118 和 100511C
在这两个地方用OD下内存写入断点,断到了这里
01002068 |. 830D 18510001>or dword ptr [1005118], FFFFFFFF ;初始化变量
0100206F |. 830D 1C510001>or dword ptr [100511C], FFFFFFFF ;初始化变量
01002076 |. 53 push ebx
01002077 |. 891D 40510001 mov dword ptr [1005140], ebx
0100207D |. E8 91080000 call 01002913
01002082 |. 8B4D 14 mov ecx, dword ptr [ebp+14]
01002085 |> 393D 40510001 cmp dword ptr [1005140], edi
0100208B |. 74 34 je short 010020C1
0100208D |. 841D 00500001 test byte ptr [1005000], bl
01002093 |.^ 0F84 54FFFFFF je 01001FED
01002099 |. 8B45 14 mov eax, dword ptr [ebp+14]
0100209C |. C1E8 10 shr eax, 10
0100209F |. 83E8 27 sub eax, 27
010020A2 |. C1F8 04 sar eax, 4
010020A5 |. 50 push eax ; /Arg2
010020A6 |. 0FB745 14 movzx eax, word ptr [ebp+14] ; |
010020AA |. 83C0 04 add eax, 4 ; |
010020AD |. C1F8 04 sar eax, 4 ; |
010020B0 |. 50 push eax ; |Arg1
010020B1 |> E8 1E110000 call 010031D4 ; \
在往上翻到1001FDF 到鼠标消息处理分支
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.ReleaseCaptu>; [ReleaseCapture
01001FF9 |. 841D 00500001 test byte ptr [1005000], bl
01001FFF |. 0F84 B6000000 je 010020BB
01002005 |. E8 D7170000 call 010037E1 ;处理我们鼠标点击的函数
0100200A |. E9 9A010000 jmp 010021A9
在1001FDF下断点 跟到10037E1里去
010037E1 /$ A1 18510001 mov eax, dword ptr [1005118] ;横坐标给EAX
010037E6 |. 85C0 test eax, eax
010037E8 |. 0F8E C8000000 jle 010038B6
010037EE |. 8B0D 1C510001 mov ecx, dword ptr [100511C] ;纵坐标给ECX
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
01003814 |. 53 push ebx
01003815 |. 33DB xor ebx, ebx
01003817 |. 43 inc ebx
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 = 000B03F4 ('扫雷',class='扫雷')
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 ; /Arg1 = 00000004
0100385B |. E8 F0000000 call 01003950 ; \winmine.01003950
01003860 |> A1 18510001 mov eax, dword ptr [1005118]
01003865 |. 8B0D 1C510001 mov ecx, dword ptr [100511C]
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 [100511C], ecx
0100387F |. A3 18510001 mov dword ptr [1005118], eax
01003884 |> 833D 44510001>cmp dword ptr [1005144], 0
0100388B |. 74 09 je short 01003896
0100388D |. 51 push ecx
0100388E |. 50 push eax
0100388F |. E8 23FDFFFF call 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 [edx+eax+1005340]
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 ; 坐标X
010038B0 |. 50 push eax ; 坐标Y
010038B1 |. E8 5CFCFFFF call 01003512 ; 选种方格
010038B6 |> FF35 60510001 push dword ptr [1005160]
010038BC |. E8 52F0FFFF call 01002913
010038C1 \. C3 retn
哈哈 选种方格的函数找到了。
现在我们来整理一下线索
找到的数据
8F = 雷
8E = 有雷 AND 小红旗
0E = 无雷 AND 小红旗
找到的地址
1005334 横向方格数
1005338 纵向方格数
找到的函数
010038AF |. 51 push ecx ; 坐标X
010038B0 |. 50 push eax ; 坐标Y
010038B1 |. E8 5CFCFFFF call 01003512 ; 选种方格
下面说下实践的思路,
功能一 "查雷"; 把内存布局中的所有8F有雷 都换成8E小红旗
功能二 "排雷": 用一个DLL注入扫雷进程 然后帮它调用选种方格的函数 有雷的地方不选
这是功能一的代码:
DWORD x = 0x10056A8;
DWORD y = 0x10056AC;
HWND hwnd = ::FindWindow(NULL, "扫雷");
DWORD hProcessId;
::GetWindowThreadProcessId(hwnd, &hProcessId);
HANDLE Process = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, hProcessId);
int b = 0 , s = 0, nx = 0, ny = 0;
::ReadProcessMemory(Process, (LPCVOID)x, &nx, 1, NULL); //获取横向方格长度
::ReadProcessMemory(Process, (LPCVOID)y, &ny, 1, NULL); //获取纵向方格长度
for(int i = 0; i < nx * 32; i += 32)
{
for(int j = 0; j < ny; j++)
{
::ReadProcessMemory(Process, (LPCVOID)(addr + i+j), &b, 1, NULL);
if (b == 0x8F) //判断是否有雷
{
s = 0x8E; //如果有雷就画上小红旗
::WriteProcessMemory(Process, (LPVOID)(addr + i+j), &s, 1, NULL);
}
}
}
::InvalidateRect(hwnd, NULL, TRUE);
::CloseHandle(Process);
功能二 在注入的DLL中实现的,因为同一进程内才能调用同一进程的函数
DWORD addr = 0x1005361;
DWORD x = 0x10056A8;
DWORD y = 0x10056AC;
HWND hwnd = ::FindWindow(NULL, "扫雷");
DWORD hProcessId;
::GetWindowThreadProcessId(hwnd, &hProcessId);
HANDLE Process = ::OpenProcess(PROCESS_ALL_ACCESS, FALSE, hProcessId);
int b = 0 , nx = 0, ny = 0;
DWORD s = 0;
::ReadProcessMemory(Process, (LPCVOID)x, &nx, 1, NULL); //获取横向方格长度
::ReadProcessMemory(Process, (LPCVOID)y, &ny, 1, NULL); //获取纵向方格长度
DWORD xuanzong = 0x10037E1; //选方格的函数地址
DWORD x1 = 1;
DWORD y1 = 1;
int (*Rec)[900] = new int[nx][900];
for(int i = 0; i < nx * 32; i += 32)
{
for(int j = 0; j < ny; j++)
{
::ReadProcessMemory(Process, (LPCVOID)(addr + i+j), &b, 1, NULL);
if (b == 0x8E || b == 0x8F) //把内存布局写入自定义的数组
{
Rec[i/32][j] = 1;
}
else
Rec[i/32][j] = 0;
}
}
for(i = 0; i < nx; i ++)
{
for(int j = 0; j < ny; j++)
{
x1 = i + 1;
y1 = j + 1;
if(Rec[i][j] != TRUE) //选择没有雷的方格
{
_asm
{
push x1 //要选方格的X
push y1 //要选方格的Y
call xuanzong //调用选方格函数
}
}
}
}
哈哈,这样一个扫雷的秒杀外挂就出来了!由于我水平有限,如有错误之处请跟贴指正!(附件暂时还不能用 5555555 有时间找个地址我会贴出来```
--------------------------------------------------------------------------------
【版权声明】: 本文原创于看雪技术论坛, 转载请注明作者并保持文章的完整, 谢谢!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)