“掃雷”游戲的幕后
發(fā)表時間:2024-02-06 來源:明輝站整理相關(guān)軟件相關(guān)文章人氣:
[摘要]介紹 曾想了解“掃雷”游戲在幕后所發(fā)生的一切嗎?嗯,我想過,還由此決定對其進行了研究。本文是我的研究結(jié)果,現(xiàn)公之于眾。主要概念1. 使用 P/Invoke 調(diào)用 Win32 API。2. 直接讀取另一個進程的內(nèi)存。注1:本文的第一部分包括一些匯編代碼,如果你不是很明白,無關(guān)要緊,這不是本文的...
介紹
曾想了解“掃雷”游戲在幕后所發(fā)生的一切嗎?嗯,我想過,還由此決定對其進行了研究。本文是我的研究結(jié)果,現(xiàn)公之于眾。
主要概念
1. 使用 P/Invoke 調(diào)用 Win32 API。
2. 直接讀取另一個進程的內(nèi)存。
注1:本文的第一部分包括一些匯編代碼,如果你不是很明白,無關(guān)要緊,這不是本文的目的,你盡可以跳過不管。然而,如果你想問我有關(guān)這些代碼的問題,非常歡迎你寫信給我。
注2:本程序是在Windows XP下測試的,所以如果它不能運行在其它的系統(tǒng)下,請注明該系統(tǒng)的信息,好讓我們大家都知道。
注2之更新: 本代碼現(xiàn)在經(jīng)過修改后也能在Windows 2000下運行。謝謝Ryan Schreiber找到了Win2K下的內(nèi)存地址。
第一步 – 探索 winmine.exe
如果你不是一個匯編迷,可以跳到這一步的最后,只看結(jié)論。
為了更好地了解“掃雷”幕后所發(fā)生的一切,我以一個調(diào)試器打開此文件作為開端。我個人最喜歡的調(diào)試器是Olly Debugger v1.08, 這是一個非常簡單且直觀的調(diào)試器?傊,我在調(diào)試器中打開winmine.exe,并查看該文件。 我發(fā)現(xiàn)在Import區(qū)(列出在程序中用到的所有dll函數(shù)的區(qū)域)有下面一行:
010011B0 8D52C377 DD msvcrt.rand
這就意味著“掃雷”用到了VC運行庫的隨機函數(shù),因此我認(rèn)為這對我可能有幫助。我搜索了該文件,看看到底在哪里調(diào)用了rand()函數(shù),不過只在一個地方找到了這個函數(shù):
01003940 FF15 B0110001 CALL DWORD PTR DS:[<&msvcrt.rand>]
接著我在這一行單步調(diào)用插入了一個斷點并運行程序。我發(fā)現(xiàn)每當(dāng)點擊笑臉圖標(biāo)時,一個新的布雷圖就生成了。布雷圖按以下步驟創(chuàng)建:
1. 首先,給布雷圖分配一塊內(nèi)存區(qū),并把所有的內(nèi)存字節(jié)都設(shè)置成0x0F,說明在該單元(cell)中沒有地雷。
2. 其次,按地雷數(shù)遍歷每一個地雷:
2.1. 隨機化 x 位置 (取值在1至寬度之間)。
2.2. 隨機化 y 位置 (取值在1至高度之間)。
2.3. 設(shè)置內(nèi)存塊中被選中的單元的值為0x8F,這意味著在該單元中有一個地雷。
下面是原碼,我已加入了一些注釋,并加粗了重點部分。
010036A7 MOV DWORD PTR DS:[1005334],EAX ; [0x1005334] = 寬度(即橫向格數(shù))
010036AC MOV DWORD PTR DS:[1005338],ECX ; [0x1005338] = 高度(即縱向格數(shù))
010036B2 CALL winmine.01002ED5 ; 生成空的內(nèi)存塊并進行清除
010036B7 MOV EAX,DWORD PTR DS:[10056A4]
010036BC MOV DWORD PTR DS:[1005160],EDI
010036C2 MOV DWORD PTR DS:[1005330],EAX ; [0x1005330] = 地雷的個數(shù)
; 以地雷個數(shù)進行循環(huán)
010036C7 PUSH DWORD PTR DS:[1005334] ; 把最大寬度(max width)壓入棧
010036CD CALL winmine.01003940 ; Mine_Width = 隨機化 x 位置 (0 至 max width-1) (即在0和max width-1之間隨機選一個值)
010036D2 PUSH DWORD PTR DS:[1005338] ; 把最大高度壓入棧
010036D8 MOV ESI,EAX
010036DA INC ESI ; Mine_Width = Mine_Width + 1
010036DB CALL winmine.01003940 ; Mine_Height =隨機化 y 位置
; (0 至 max height-1)
010036E0 INC EAX ; Mine_Height = Mine_Height +1
010036E1 MOV ECX,EAX ;計算單元在內(nèi)存塊(布雷圖)中的地址
010036E3 SHL ECX,5 ; 按這樣計算:
; 單元內(nèi)存地址 = 0x1005340 + 32 * height + width
010036E6 TEST BYTE PTR DS:[ECX+ESI+1005340],80 ; [單元內(nèi)存地址] ==是否已是地雷?
010036EE JNZ SHORT winmine.010036C7 ; 如果已是地雷,則重新迭代
010036F0 SHL EAX,5 ; 否則,設(shè)置此單元為地雷
010036F3 LEA EAX,DWORD PTR DS:[EAX+ESI+1005340]
010036FA OR BYTE PTR DS:[EAX],80
010036FD DEC DWORD PTR DS:[1005330]
01003703 JNZ SHORT winmine.010036C7 ; 進行下一次迭代
正如你從代碼所看到的,我發(fā)現(xiàn)了4個要點:
讀內(nèi)存地址[0x1005334]得出布雷圖的寬度。
讀內(nèi)存地址[0x1005338]得出布雷圖的高度。
讀內(nèi)存地址[0x1005330]得出布雷圖中地雷的個數(shù)。
給出x、y,它們代表布雷圖中的一個單元,位于x列,y行。地址 [0x1005340 + 32 * y + x] 給出了該單元的值,這樣我們就進入了下一步。
第2 步– 設(shè)計一個解決方案
你可能在想,我將會談到了哪一種解決方案呢?顯然,在發(fā)現(xiàn)了所有的地雷信息均可為我所用后,我所要做的就是從內(nèi)存中讀取數(shù)據(jù)。我決定編寫讀取這些信息的一個小程序,并給予說明。 它能自己繪出布雷圖,顯示出每一個被發(fā)現(xiàn)的地雷。
那么,怎么設(shè)計呢?我所做的就是把地址裝到一個指針中(是的,它在C#中還存在),并讀出其所指的數(shù)據(jù),這樣行嗎?嗯,并不完全如些。因為場合不同,存儲這些數(shù)據(jù)的內(nèi)存并不在我的應(yīng)用程序之中。要知道,每一個進程都擁有自己的地址空間,所以它就不會“意外地”訪問屬于別的程序的內(nèi)存。因此,為了能讀出這此數(shù)據(jù),就必須找到一種方法,用來讀取另一個進程的內(nèi)存。 在本例中,這個進程就是“掃雷”進程。
我決定寫一個小小的類庫,它將接收一個進程,并提供讀取該進程內(nèi)存地址的功能。之所以這樣做,是因為我還要在很多程序中用到它,沒有必要反反復(fù)復(fù)地編寫這些代碼。這樣,你就可以得到這個類,并在應(yīng)用程序中使用它,且是免費的。例如,如果你編寫一個調(diào)試器,這個類對你會有所幫助。據(jù)我所知,所有的調(diào)試器都具有讀取被調(diào)試程序內(nèi)存的能力。
那么,我們怎么才能讀取別的進程的內(nèi)存呢?答案在于一個叫做ReadProcessMemory的API。 這個API實際上可以讓你讀取進程內(nèi)存中的一個指定地址。但在進行此操作之前,必須以特定的模式打開進程,而在完成操作之后,就必須關(guān)閉句柄以避免資源泄漏。我們利用OpenProcess 和 CloseHandle這幾個API的幫助說明,完成了相應(yīng)的操作。
為了在C#中使用API,必須使用P/Invoke,這意味著在使用API之前需要先對其進行聲明。一般情況下都很簡單,但要是讓你以.NET的方式實現(xiàn)的話,有時就不那么容易了。我在MSDN中找到了這些API聲明:
HANDLE OpenProcess(
DWORD dwDesiredAccess, // 訪問標(biāo)志
BOOL bInheritHandle, // 句柄繼承選項
DWORD dwProcessId // 進程ID
);
BOOL ReadProcessMemory(
HANDLE hProcess, // 進程句柄
LPCVOID lpBaseAddress, // 內(nèi)存區(qū)基址
LPVOID lpBuffer, // 數(shù)據(jù)緩沖
SIZE_T nSize, // 要讀的字節(jié)數(shù)
SIZE_T * lpNumberOfBytesRead // 已讀字節(jié)數(shù)
);
BOOL CloseHandle(
HANDLE hObject // 進程句柄
);
這些聲明轉(zhuǎn)換為如下的C#聲明:
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(
UInt32 dwDesiredAccess,
Int32 bInheritHandle,
UInt32 dwProcessId
);
[DllImport("kernel32.dll")]
public static extern Int32 ReadProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
[In, Out] byte[] buffer,
UInt32 size,
out IntPtr lpNumberOfBytesRead
);
[DllImport("kernel32.dll")] public static extern Int32 CloseHandle(
IntPtr hObject
);
如果你想知道在c++和c#之間有關(guān)類型轉(zhuǎn)換的更多信息,我建議你從msdn.microsoft.com站點搜索此話題:“Marshaling Data with Platform Invoke”。 基本上, 如果你把邏輯上是正確的程序擱在那兒, 它便能運行, 但有時還需要一點點的調(diào)整。
在聲明了這些函數(shù)之后,我要做的是用一個簡單的類把它們包裝起來,并使用這個類。我把聲明放在一個叫做ProcessMemoryReaderApi的類中,這樣做更有條有理。主要的實用類稱為ProcessMemoryReade。這個類有一個ReadProcess屬性,它源于System.Diagnostics.Process類型,用于存放你要讀取其內(nèi)存的進程。類中有一個方法,用來以讀模式打開進程。
public void OpenProcess()
{
m_hProcess = ProcessMemoryReaderApi.OpenProcess(
ProcessMemoryReaderApi.PROCESS_VM_READ, 1,
(uint)m_ReadProcess.Id);
}
PROCESS_VM_READ 常量告訴系統(tǒng)以讀模式打開進程, 而m_ReadProcess.Id 聲明了我要打開的是什么進程。
在該類中最重要的是一個方法,它從進程中讀取內(nèi)存:
public byte[] ReadProcessMemory(IntPtr MemoryAddress, uint bytesToRead,
out int bytesReaded)
{
byte[] buffer = new byte[bytesToRead];
IntPtr ptrBytesReaded;
ProcessMemoryReaderApi.ReadProcessMemory(m_hProcess,MemoryAddress,buffer,
bytesToRead,out ptrBytesReaded);
bytesReaded = ptrBytesReaded.ToInt32();
return buffer;
}
這個函數(shù)以所請求的大小聲明一個字節(jié)數(shù)組,并使用API讀取內(nèi)存。就這么簡單!
最后,下面這個方法關(guān)閉了進程。
public void CloseHandle()
{
int iRetValue;
iRetValue = ProcessMemoryReaderApi.CloseHandle(m_hProcess);
if (iRetValue == 0)
throw new Exception("CloseHandle failed");
}
第三步 – 使用類
現(xiàn)在輪到了有趣的部分。使用這個類就是為了讀取“掃雷”的內(nèi)存并揭開布雷圖。要使用類,需要先對其進行初始化:
ProcessMemoryReaderLib.ProcessMemoryReader pReader
= new ProcessMemoryReaderLib.ProcessMemoryReader();
接著,必須設(shè)置你想要讀取其內(nèi)存的進程。以下是如何獲得“掃雷”進程的例子,這個進程一旦被裝入,就被設(shè)置為ReadProcess屬性:
System.Diagnostics.Process[] myProcesses
= System.Diagnostics.Process.GetProcessesByName("winmine");
pReader.ReadProcess = myProcesses[0];
我們現(xiàn)在需要做的是:打開進程,讀取內(nèi)存,并在完成后關(guān)閉它。下面還是有關(guān)操作的例子,它讀取代表布雷圖寬度的地址。
pReader.OpenProcess();
int iWidth;
byte[] memory;
memory = pReader.ReadProcessMemory((IntPtr)0x1005334,1,out bytesReaded);
iWidth = memory[0];
pReader.CloseHandle();
簡單吧!
在結(jié)論部分,我列出了顯示布雷圖的完整代碼。別忘了,我要訪問的所有內(nèi)存位置就是在本文第一部分中所找到位置。
// 布雷圖的資料管理器
System.Resources.ResourceManager resources = new System.Resources.ResourceManager(typeof(Form1));
ProcessMemoryReaderLib.ProcessMemoryReader pReader
= new ProcessMemoryReaderLib.ProcessMemoryReader();
System.Diagnostics.Process[] myProcesses
= System.Diagnostics.Process.GetProcessesByName("winmine");
// 獲得“掃雷”進程的第一個實列
if (myProcesses.Length == 0)
{
MessageBox.Show("No MineSweeper process found!");
return;
}
pReader.ReadProcess = myProcesses[0];
// 以讀內(nèi)存模式打開進程
pReader.OpenProcess();
int bytesReaded;
int iWidth, iHeight, iMines;
int iIsMine;
int iCellAddress;
byte[] memory;
memory = pReader.ReadProcessMemory((IntPtr)0x1005334,1,out bytesReaded);
iWidth = memory[0];
txtWidth.Text = iWidth.ToString();
memory = pReader.ReadProcessMemory((IntPtr)0x1005338,1,out bytesReaded);
iHeight = memory[0];
txtHeight.Text = iHeight.ToString();
memory = pReader.ReadProcessMemory((IntPtr)0x1005330,1,out bytesReaded);
iMines = memory[0];
txtMines.Text = iMines.ToString();
// 刪除以前的按鈕數(shù)組
this.Controls.Clear();
this.Controls.AddRange(MainControls);
// 創(chuàng)建一個按鈕數(shù)組, 用于畫出布雷圖的每一格
ButtonArray = new System.Windows.Forms.Button[iWidth,iHeight];
int x,y;
for (y=0 ; y<iHeight ; y++)
for (x=0 ; x<iWidth ; x++)
{
ButtonArray[x,y] = new System.Windows.Forms.Button();
ButtonArray[x,y].Location = new System.Drawing.Point(20 + x*16, 70 + y*16);
ButtonArray[x,y].Name = "";
ButtonArray[x,y].Size = new System.Drawing.Size(16,16);
iCellAddress = (0x1005340) + (32 * (y+1)) + (x+1);
memory = pReader.ReadProcessMemory((IntPtr)iCellAddress,1,out bytesReaded);
iIsMine = memory[0];
if (iIsMine == 0x8f)//如果有雷,則畫出地雷位圖
ButtonArray[x,y].Image = ((System.Drawing.Bitmap)
(resources.GetObject("button1.Image")));
this.Controls.Add(ButtonArray[x,y]);
}
// 關(guān)閉進程句柄
pReader.CloseHandle();
就是這些,希望你能學(xué)到新的東西。