介紹
曾想了解“掃雷”游戲在幕后所發生的一切嗎?嗯,我想過,還由此決定對其進行了研究。本文是我的研究結果,現公之于眾。
主要概念
1. 使用 P/Invoke 調用 Win32 API。
2. 直接讀取另一個進程的內存。
注1:本文的第一部分包括一些匯編代碼,如果你不是很明白,無關要緊,這不是本文的目的,你盡可以跳過不管。然而,如果你想問我有關這些代碼的問題,非常歡迎你寫信給我。
注2:本程序是在Windows XP下測試的,所以如果它不能運行在其它的系統下,請注明該系統的信息,好讓我們大家都知道。
注2之更新: 本代碼現在經過修改后也能在Windows 2000下運行。謝謝Ryan Schreiber找到了Win2K下的內存地址。
第一步 – 探索 winmine.exe
如果你不是一個匯編迷,可以跳到這一步的最后,只看結論。
為了更好地了解“掃雷”幕后所發生的一切,我以一個調試器打開此文件作為開端。我個人最喜歡的調試器是Olly Debugger v1.08, 這是一個非常簡單且直觀的調試器。總之,我在調試器中打開winmine.exe,并查看該文件。 我發現在Import區(列出在程序中用到的所有dll函數的區域)有下面一行:
010011B0 8D52C377 DD msvcrt.rand
這就意味著“掃雷”用到了VC運行庫的隨機函數,因此我認為這對我可能有幫助。我搜索了該文件,看看到底在哪里調用了rand()函數,不過只在一個地方找到了這個函數:
01003940 FF15 B0110001 CALL DWORD PTR DS:[<&msvcrt.rand>]
接著我在這一行單步調用插入了一個斷點并運行程序。我發現每當點擊笑臉圖標時,一個新的布雷圖就生成了。布雷圖按以下步驟創建:
1. 首先,給布雷圖分配一塊內存區,并把所有的內存字節都設置成0x0F,說明在該單元(cell)中沒有地雷。
2. 其次,按地雷數遍歷每一個地雷:
2.1. 隨機化 x 位置 (取值在1至寬度之間)。 2.2. 隨機化 y 位置 (取值在1至高度之間)。 2.3. 設置內存塊中被選中的單元的值為0x8F,這意味著在該單元中有一個地雷。
下面是原碼,我已加入了一些注釋,并加粗了重點部分。
010036A7 MOV DWORD PTR DS:[1005334],EAX ; [0x1005334] = 寬度(即橫向格數)
010036AC MOV DWORD PTR DS:[1005338],ECX ; [0x1005338] = 高度(即縱向格數)
010036B2 CALL winmine.01002ED5 ; 生成空的內存塊并進行清除
010036B7 MOV EAX,DWORD PTR DS:[10056A4]
010036BC MOV DWORD PTR DS:[1005160],EDI
010036C2 MOV DWORD PTR DS:[1005330],EAX ; [0x1005330] = 地雷的個數
; 以地雷個數進行循環
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 ;計算單元在內存塊(布雷圖)中的地址
010036E3 SHL ECX,5 ; 按這樣計算:
; 單元內存地址 = 0x1005340 + 32 * height + width
010036E6 TEST BYTE PTR DS:[ECX+ESI+1005340],80 ; [單元內存地址] ==是否已是地雷?
010036EE JNZ SHORT winmine.010036C7 ; 如果已是地雷,則重新迭代
010036F0 SHL EAX,5 ; 否則,設置此單元為地雷
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 ; 進行下一次迭代
正如你從代碼所看到的,我發現了4個要點:
讀內存地址[0x1005334]得出布雷圖的寬度。
讀內存地址[0x1005338]得出布雷圖的高度。
讀內存地址[0x1005330]得出布雷圖中地雷的個數。
給出x、y,它們代表布雷圖中的一個單元,位于x列,y行。地址 [0x1005340 + 32 * y + x] 給出了該單元的值,這樣我們就進入了下一步。
第2 步– 設計一個解決方案
你可能在想,我將會談到了哪一種解決方案呢?顯然,在發現了所有的地雷信息均可為我所用后,我所要做的就是從內存中讀取數據。我決定編寫讀取這些信息的一個小程序,并給予說明。 它能自己繪出布雷圖,顯示出每一個被發現的地雷。
那么,怎么設計呢?我所做的就是把地址裝到一個指針中(是的,它在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("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#之間有關類型轉換的更多信息,我建議你從msdn.microsoft.com站點搜索此話題:“Marshaling Data with Platform Invoke”。 基本上, 如果你把邏輯上是正確的程序擱在那兒, 它便能運行, 但有時還需要一點點的調整。
在聲明了這些函數之后,我要做的是用一個簡單的類把它們包裝起來,并使用這個類。我把聲明放在一個叫做ProcessMemoryReaderApi的類中,這樣做更有條有理。主要的實用類稱為ProcessMemoryReade。這個類有一個ReadProcess屬性,它源于System.Diagnostics.Process類型,用于存放你要讀取其內存的進程。類中有一個方法,用來以讀模式打開進程。
public void OpenProcess()
{
m_hProcess = ProcessMemoryReaderApi.OpenProcess(
ProcessMemoryReaderApi.PROCESS_VM_READ, 1,
(uint)m_ReadProcess.Id);
}
PROCESS_VM_READ 常量告訴系統以讀模式打開進程, 而m_ReadProcess.Id 聲明了我要打開的是什么進程。
在該類中最重要的是一個方法,它從進程中讀取內存:
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;
}
這個函數以所請求的大小聲明一個字節數組,并使用API讀取內存。就這么簡單!
最后,下面這個方法關閉了進程。
public void CloseHandle()
{
int iRetValue;
iRetValue = ProcessMemoryReaderApi.CloseHandle(m_hProcess);
if (iRetValue == 0)
throw new Exception("CloseHandle failed");
}
第三步 – 使用類
現在輪到了有趣的部分。使用這個類就是為了讀取“掃雷”的內存并揭開布雷圖。要使用類,需要先對其進行初始化:
ProcessMemoryReaderLib.ProcessMemoryReader pReader
= new ProcessMemoryReaderLib.ProcessMemoryReader();
接著,必須設置你想要讀取其內存的進程。以下是如何獲得“掃雷”進程的例子,這個進程一旦被裝入,就被設置為ReadProcess屬性:
System.Diagnostics.Process[] myProcesses
= System.Diagnostics.Process.GetProcessesByName("winmine");
pReader.ReadProcess = myProcesses[0];
我們現在需要做的是:打開進程,讀取內存,并在完成后關閉它。下面還是有關操作的例子,它讀取代表布雷圖寬度的地址。
pReader.OpenProcess();
int iWidth;
byte[] memory;
memory = pReader.ReadProcessMemory((IntPtr)0x1005334,1,out bytesReaded);
iWidth = memory[0];
pReader.CloseHandle();
簡單吧!
在結論部分,我列出了顯示布雷圖的完整代碼。別忘了,我要訪問的所有內存位置就是在本文第一部分中所找到位置。
// 布雷圖的資料管理器
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];
// 以讀內存模式打開進程
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();
// 刪除以前的按鈕數組
this.Controls.Clear();
this.Controls.AddRange(MainControls);
// 創建一個按鈕數組, 用于畫出布雷圖的每一格
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]);
}
// 關閉進程句柄
pReader.CloseHandle();
就是這些,希望你能學到新的東西。
|