明輝手游網(wǎng)中心:是一個免費提供流行視頻軟件教程、在線學(xué)習(xí)分享的學(xué)習(xí)平臺!

“掃雷”游戲的幕后

[摘要]介紹 曾想了解“掃雷”游戲在幕后所發(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é)到新的東西。


標(biāo)簽:“掃雷”游戲的幕后