电脑故障

位置:IT落伍者 >> 电脑故障 >> 浏览文章

“扫雷”游戏的幕后


发布日期:2021/11/12
 

介绍

曾想了解扫雷游戏在幕后所发生的一切吗?嗯我想过还由此决定对其进行了研究本文是我的研究结果现公之于众

主要概念

使用 P/Invoke 调用 Win API

直接读取另一个进程的内存

本文的第一部分包括一些汇编代码如果你不是很明白无关要紧这不是本文的目的你尽可以跳过不管然而如果你想问我有关这些代码的问题非常欢迎你写信给我

本程序是在Windows XP下测试的所以如果它不能运行在其它的系统下请注明该系统的信息好让我们大家都知道

之更新: 本代码现在经过修改后也能在Windows 下运行谢谢Ryan Schreiber找到了WinK下的内存地址

第一步 – 探索 winmineexe

如果你不是一个汇编迷可以跳到这一步的最后只看结论

为了更好地了解扫雷幕后所发生的一切我以一个调试器打开此文件作为开端我个人最喜欢的调试器是Olly Debugger v 这是一个非常简单且直观的调试器总之我在调试器中打开winmineexe并查看该文件 我发现在Import区(列出在程序中用到的所有dll函数的区域)有下面一行

B DC DD msvcrtrand

这就意味着扫雷用到了VC运行库的随机函数因此我认为这对我可能有帮助我搜索了该文件看看到底在哪里调用了rand()函数不过只在一个地方找到了这个函数

FF B CALL DWORD PTR DS:[<&msvcrtrand>]

接着我在这一行单步调用插入了一个断点并运行程序我发现每当点击笑脸图标时一个新的布雷图就生成了布雷图按以下步骤创建

首先给布雷图分配一块内存区并把所有的内存字节都设置成xF说明在该单元(cell)中没有地雷

其次按地雷数遍历每一个地雷

随机化 x 位置 (取值在至宽度之间)

随机化 y 位置 (取值在至高度之间)

设置内存块中被选中的单元的值为xF这意味着在该单元中有一个地雷

下面是原码我已加入了一些注释并加粗了重点部分

A MOV DWORD PTR DS:[]EAX ; [x] = 宽度(即横向格数)

AC MOV DWORD PTR DS:[]ECX ; [x] = 高度(即纵向格数)

B CALL winmineED ; 生成空的内存块并进行清除

B MOV EAXDWORD PTR DS:[A]

BC MOV DWORD PTR DS:[]EDI

C MOV DWORD PTR DS:[]EAX ; [x] = 地雷的个数

; 以地雷个数进行循环

C PUSH DWORD PTR DS:[] ; 把最大宽度(max width)压入栈

CD CALL winmine ; Mine_Width = 随机化 x 位置 ( 至 max width) (即在和max width之间随机选一个值)

D PUSH DWORD PTR DS:[] ; 把最大高度压入栈

D MOV ESIEAX

DA INC ESI ; Mine_Width = Mine_Width +

DB CALL winmine ; Mine_Height =随机化 y 位置

; ( 至 max height)

E INC EAX ; Mine_Height = Mine_Height +

E MOV ECXEAX ;计算单元在内存块(布雷图)中的地址

E SHL ECX ; 按这样计算

; 单元内存地址 = x + * height + width

E TEST BYTE PTR DS:[ECX+ESI+] ; [单元内存地址] ==是否已是地雷?

EE JNZ SHORT winmineC ; 如果已是地雷则重新迭代

F SHL EAX ; 否则设置此单元为地雷

F LEA EAXDWORD PTR DS:[EAX+ESI+]

FA OR BYTE PTR DS:[EAX]

FD DEC DWORD PTR DS:[]

JNZ SHORT winmineC ; 进行下一次迭代

正如你从代码所看到的我发现了个要点

读内存地址[x]得出布雷图的宽度

读内存地址[x]得出布雷图的高度

读内存地址[x]得出布雷图中地雷的个数

给出xy它们代表布雷图中的一个单元位于x列y行地址 [x + * y + x] 给出了该单元的值这样我们就进入了下一步

步– 设计一个解决方案

你可能在想我将会谈到了哪一种解决方案呢?显然在发现了所有的地雷信息均可为我所用后我所要做的就是从内存中读取数据我决定编写读取这些信息的一个小程序并给予说明 它能自己绘出布雷图显示出每一个被发现的地雷

那么怎么设计呢?我所做的就是把地址装到一个指针中(是的它在C#中还存在)并读出其所指的数据这样行吗?嗯并不完全如些因为场合不同存储这些数据的内存并不在我的应用程序之中要知道每一个进程都拥有自己的地址空间所以它就不会意外地访问属于别的程序的内存因此为了能读出这此数据就必须找到一种方法用来读取另一个进程的内存 在本例中这个进程就是扫雷进程

我决定写一个小小的类库它将接收一个进程并提供读取该进程内存地址的功能之所以这样做是因为我还要在很多程序中用到它没有必要反反复复地编写这些代码这样你就可以得到这个类并在应用程序中使用它且是免费的例如如果你编写一个调试器这个类对你会有所帮助据我所知所有的调试器都具有读取被调试程序内存的能力

那么我们怎么才能读取别的进程的内存呢?答案在于一个叫做ReadProcessMemory的API 这个API实际上可以让你读取进程内存中的一个指定地址但在进行此操作之前必须以特定的模式打开进程而在完成操作之后就必须关闭句柄以避免资源洩漏我们利用OpenProcess 和 CloseHandle这几个API的帮助说明完成了相应的操作

为了在C#中使用API必须使用P/Invoke这意味着在使用API之前需要先对其进行声明一般情况下都很简单但要是让你以NET的方式实现的话有时就不那么容易了我在MSDN中找到了这些API声明

HANDLE OpenProcess(

DWORD dwDesiredAccess // 访问标志

BOOL bInheritHandle // 句柄继承选项

DWORD dwProcessId // 进程ID

);

BOOL ReadProcessMemory(

HANDLE hProcess // 进程句柄

LPCVOID lpBaseAddress // 内存区基址

LPVOID lpBuffer // 数据缓沖

SIZE_T nSize // 要读的字节数

SIZE_T * lpNumberOfBytesRead // 已读字节数

);

BOOL CloseHandle(

HANDLE hObject // 进程句柄

);

这些声明转换为如下的C#声明

[DllImport(kerneldll)]

public static extern IntPtr OpenProcess(

UInt dwDesiredAccess

Int bInheritHandle

UInt dwProcessId

);

[DllImport(kerneldll)]

public static extern Int ReadProcessMemory(

IntPtr hProcess

IntPtr lpBaseAddress

[In Out] byte[] buffer

UInt size

out IntPtr lpNumberOfBytesRead

);

[DllImport(kerneldll)] public static extern Int CloseHandle(

IntPtr hObject

);

如果你想知道在c++和c#之间有关类型转换的更多信息我建议你从站点搜索此话题Marshaling Data with Platform Invoke 基本上 如果你把逻辑上是正确的程序搁在那儿 它便能运行 但有时还需要一点点的调整

在声明了这些函数之后我要做的是用一个简单的类把它们包装起来并使用这个类我把声明放在一个叫做ProcessMemoryReaderApi的类中这样做更有条有理主要的实用类称为ProcessMemoryReade这个类有一个ReadProcess属性它源于SystemDiagnosticsProcess类型用于存放你要读取其内存的进程类中有一个方法用来以读模式打开进程

public void OpenProcess()

{

m_hProcess = ProcessMemoryReaderApiOpenProcess(

ProcessMemoryReaderApiPROCESS_VM_READ

(uint)m_ReadProcessId);

}

PROCESS_VM_READ 常量告诉系统以读模式打开进程 而m_ReadProcessId 声明了我要打开的是什么进程

在该类中最重要的是一个方法它从进程中读取内存

public byte[] ReadProcessMemory(IntPtr MemoryAddress uint bytesToRead

out int bytesReaded)

{

byte[] buffer = new byte[bytesToRead];

IntPtr ptrBytesReaded;

ProcessMemoryReaderApiReadProcessMemory(m_hProcessMemoryAddressbuffer

bytesToReadout ptrBytesReaded);

bytesReaded = ptrBytesReadedToInt();

return buffer;

}

这个函数以所请求的大小声明一个字节数组并使用API读取内存就这么简单!

最后下面这个方法关闭了进程

public void CloseHandle()

{

int iRetValue;

iRetValue = ProcessMemoryReaderApiCloseHandle(m_hProcess);

if (iRetValue == )

throw new Exception(CloseHandle failed);

第三步 – 使用类

现在轮到了有趣的部分使用这个类就是为了读取扫雷的内存并揭开布雷图要使用类需要先对其进行初始化

ProcessMemoryReaderLibProcessMemoryReader pReader

= new ProcessMemoryReaderLibProcessMemoryReader();

接着必须设置你想要读取其内存的进程以下是如何获得扫雷进程的例子这个进程一旦被装入就被设置为ReadProcess属性

SystemDiagnosticsProcess[] myProcesses

= SystemDiagnosticsProcessGetProcessesByName(winmine);

pReaderReadProcess = myProcesses[];

我们现在需要做的是打开进程读取内存并在完成后关闭它下面还是有关操作的例子它读取代表布雷图宽度的地址

pReaderOpenProcess();

int iWidth;

byte[] memory;

memory = pReaderReadProcessMemory((IntPtr)xout bytesReaded);

iWidth = memory[];

pReaderCloseHandle();

简单吧!

在结论部分我列出了显示布雷图的完整代码别忘了我要访问的所有内存位置就是在本文第一部分中所找到位置

// 布雷图的资料管理器

SystemResourcesResourceManager resources = new SystemResourcesResourceManager(typeof(Form));

ProcessMemoryReaderLibProcessMemoryReader pReader

= new ProcessMemoryReaderLibProcessMemoryReader();

SystemDiagnosticsProcess[] myProcesses

= SystemDiagnosticsProcessGetProcessesByName(winmine);

// 获得扫雷进程的第一个实列

if (myProcessesLength == )

{

MessageBoxShow(No MineSweeper process found!);

return;

}

pReaderReadProcess = myProcesses[];

// 以读内存模式打开进程

pReaderOpenProcess();

int bytesReaded;

int iWidth iHeight iMines;

int iIsMine;

int iCellAddress;

byte[] memory;

memory = pReaderReadProcessMemory((IntPtr)xout bytesReaded);

iWidth = memory[];

txtWidthText = iWidthToString();

memory = pReaderReadProcessMemory((IntPtr)xout bytesReaded);

iHeight = memory[];

txtHeightText = iHeightToString();

memory = pReaderReadProcessMemory((IntPtr)xout bytesReaded);

iMines = memory[];

txtMinesText = iMinesToString();

// 删除以前的按钮数组

thisControlsClear();

thisControlsAddRange(MainControls);

// 创建一个按钮数组 用于画出布雷图的每一格

ButtonArray = new SystemWindowsFormsButton[iWidthiHeight];

int xy;

for (y= ; y<iHeight ; y++)

for (x= ; x<iWidth ; x++)

{

ButtonArray[xy] = new SystemWindowsFormsButton();

ButtonArray[xy]Location = new SystemDrawingPoint( + x* + y*);

ButtonArray[xy]Name = ;

ButtonArray[xy]Size = new SystemDrawingSize();

iCellAddress = (x) + ( * (y+)) + (x+);

memory = pReaderReadProcessMemory((IntPtr)iCellAddressout bytesReaded);

iIsMine = memory[];

if (iIsMine == xf)//如果有雷则画出地雷位图

ButtonArray[xy]Image = ((SystemDrawingBitmap)

(resourcesGetObject(buttonImage)));

thisControlsAdd(ButtonArray[xy]);

}

// 关闭进程句柄

pReaderCloseHandle();

就是这些希望你能学到新的东西

上一篇:WinForm中的ListBox组件编程

下一篇:Entity Framework细节追蹤