天天看點

掃雷遊戲的秘密

作者: arikp

譯者: 賴鋒

前言

想知道掃雷遊戲的秘密嗎?

呵,我真的想知道,是以我決定去分析掃雷遊戲.這篇文章是我的分析成果,不敢獨享,是以在這兒分享給你們.

主要概念

  1. 利用 P/Invoke 調用 Win32 API.
  2. 直接讀取另一程序的記憶體.

注意:這篇文章的第一部分涉及一些彙編代碼,如果你感到有一些迷惑,這不會阻礙你了解掃雷遊戲的秘密.你也可以略過這一部分,假如你想了解更多,歡迎你來信詢問.

注意2:這個程式隻是在WindowsXP測試通過,是以如果你的系統不是XP,這個程式可能不會運作......假如你可以在非XP系統上運作,請在這篇文章的回饋上寫上你的作業系統類型,多謝.

注意2的更新:代碼已經修改,現在可以在Win2000系統上運作了,感謝Ryan Schreiber找到Win2K的運作位址.

第一步:深入Winmine.exe

假如你不對彙編感興趣,你可直接跳到這一步的總結部分......

為了更加清楚掃雷遊戲背後,我用調試工具打開了這個檔案(注:Windows/System32/WinMine.exe).我個人最喜歡的調試工具是Olly Debugger V1.08,這是一個非常簡單易用的調試工具.我打開了這個檔案并浏覽了一下,我發現了在導出表部份(這部分列出了所有被這個程式調用的dll函數).有一個入口:

010011B0 8D52C377 DD msvcrt.rand

這意味着Winmine.exe調用Microsoft Visual C runtime 動态庫的随機函數,是以我想這個會對有所幫助,我在Winmine.exe搜尋調用rand()這個函數的地方,隻在這個地方找到:

01003940 FF15 B0110001 CALL DWORD PTR DS:[<&msvcrt.rand>]

我在這個地方設定斷點并運作Winmine.exe.我發現每次點選重新開始開始的按鈕的時候,一個新的布雷地圖就會産生.這個布雷地圖的産生是按以下步驟産生的:

  1. 分布一塊記憶體并且初始這塊記憶體的所有位元組(bytes)的值為0x0F.這意味着在布雷的地圖中每個區域(cell)都沒有地雷
  2. 跟着,為每個地雷區循環的次數為布雷圖的地雷個數?
    1. 初始化雷區的x位置(1...width(雷區寬度))
    2. 初始化雷區的y位置(1...height(雷區高度))
  3. 設定布雷區中記憶體的适合位置的值為0x8F,這個值代表布雷圖中該位置設為地雷.
這是原始的代碼,我加上了一些标記,并在一些重要的地方加上了下劃線.(注:我儲存的是txt文檔,原始标記的地方我忘記了,我隻是憑記憶找了一些.呵)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

         ; [0x1005334] = Width

010036A7 MOV DWORD PTR DS:[1005334],EAX

         ; [0x1005338] = Height

010036AC MOV DWORD PTR DS:[1005338],ECX

         ; Generate empty block of memory and clears it

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] = number of mines

                                        ; loop over the number of mines

010036C7 PUSH DWORD PTR DS:[1005334] ; push Max Width into the stack

      ; Mine_Width = randomize x position (0 .. max width-1)

010036CD CALL winmine.01003940

010036D2 PUSH DWORD PTR DS:[1005338] ; push Max Height into the stack

010036D8 MOV ESI,EAX

010036DA INC ESI ; Mine_Width = Mine_Width + 1

010036DB CALL winmine.01003940 ; Mine_Height = randomize y position

; (0 .. max height-1)

010036E0 INC EAX ; Mine_Height = Mine_Height +1

; calculate the address of the cell in the memory block (the map)

010036E1 MOV ECX,EAX

010036E3 SHL ECX,5 ;the calculation goes:

                   ;cell_memory_address = 0x1005340 + 32 * height + width

           ; [cell_memory_address] == is already mine?

010036E6 TEST BYTE PTR DS:[ECX+ESI+1005340],80

010036EE JNZ SHORT winmine.010036C7 ; if already mine start over this iteration

010036F0 SHL EAX,5 ; otherwise, set this cell as mine

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 ; go to next iteration

正如你所看到的那樣,我從代碼中找到了四個重要的地方:

由位址[0x1005334]可以得到布雷圖的寬度

由位址[0x1005338]可以得到布雷圖的高度

由位址[0x1005330]可以得到布雷圖的地雷個數

給出值x,y代表布雷圖的坐标,在第x列,y行,由位址[0x1005340 + 32 * y + x]可以得到該布雷圖坐标的值

分析到此,我們進入下一部分.....

第二步:設計解決方法

你可能想知道,我會取用哪種解決方法?在我發現這些對有用的資訊後,我所需要做的是把這些資料從記憶體中讀出,我決定寫一個小程式讀取這些資料并且實作出給你們.并且這個程式可以畫出布雷圖,并且在有地雷的地方放置一個地雷标記.

跟着,需要設計哪部份呢?我所需要的是把一個位址放入一個指針(想不到吧,指針在C#中也存在),跟着,讀取指針值?呵呵,不全對,因為這些讀取的記憶體資料是不存在我的程式中.如你所知,每一個程序有它的獨立的位址空間以緻它不能"意外"通路屬于其它程式的記憶體區域,是以為了讀取這些資料我需要找一種讀取其它程序記憶體空間的方法,在這篇文章中,我要讀取的是Winmine.exe的程序記憶體區域.

我決定寫一個小類庫,這個類庫可以接收一個程序并且提供我讀取這個程序記憶體位址的功能,我設計成類庫主要是因為有時由于很多的情況下你也會用到它,為什麼三番四次重新開發呢?把它設計成類庫不更好嗎?你可以在你的應用中更加友善自由地使用.例如你可以利用這個類寫一個調試工具.據我所知,所有調試工具都有讀取調試的程式的記憶體的功能.

我該怎麼讀取其它程序的位址空間?答案在一個名叫ReadProcessMemory的API.這個API允許你讀取一個程序記憶體中的指定位址.在這之前你必須用一個特定的讀取模式打開程序,并且在完成後為了避免資源洩漏必須關閉程序句柄.這些操作是由兩個API,OpenProcess與CloseHandle幫助完成

為了在C#中使用這些API,你必須使用P/Invoke,意味着你使用這些API時必須進行使用聲明.這些非常簡單,你隻需用.Net的提供的方法就可以做到,有時候這些又不是很容易.....我在MSDN找到這些API的聲明.

HANDLE OpenProcess(

       DWORD dwDesiredAccess,// access flag

       BOOL bInheritHandle, // handle inheritance option

       DWORD dwProcessId // process identifier

       );

BOOL ReadProcessMemory(

       HANDLE hProcess, // handle to the process

       LPCVOID lpBaseAddress, // base of memory area

       LPVOID lpBuffer, // data buffer

       SIZE_T nSize, // number of bytes to read

       SIZE_T * lpNumberOfBytesRead // number of bytes read

       );

BOOL CloseHandle(

       HANDLE hObject // handle to object

       );

我們把這些聲明轉為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的類中,隻是為了更好地組織這些類.主要的輔助類被名名為ProcessMemoryReader.這個類有一個被名名為ReadProcess的屬性,這個屬性來自類型 System.Diagnostic.Process,假如你想讀取一個程序的記憶體,這個屬性儲存這個程序.

這個類有一個以隻讀模式打開程序的方法:

1

2

3

4

5

6

public void OpenProcess()

{

    m_hProcess = ProcessMemoryReaderApi.OpenProcess(

        ProcessMemoryReaderApi.PROCESS_VM_READ, 1,

        (uint)m_ReadProcess.Id );

}

常量PROCESS_VM_READ告訴系統以隻讀方式打開程序,m_ReadProcess.Id表明我想打開的程序.

在這個類最重要的是讀取這個程序記憶體的方法:

1

2

3

4

5

6

7

8

9

10

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;

}

這個函數聲明需要一個傳入指定數組大小的參數,一個位元組類型的(byte)數組,和一個讀取記憶體的API.就這麼簡單.

最後是一個關閉所有句柄的方法:

1

2

3

4

5

6

7

public void CloseHandle()

{

    int iRetValue;

    iRetValue = ProcessMemoryReaderApi.CloseHandle( m_hProcess );

    if ( iRetValue == 0 )

         throw new Exception( "CloseHandle failed" );

}

第三步:使用類

好了,到了最有趣的部分了.使用這個類讀取Winmine.exe的記憶體和破解布雷圖.使用這個類之前你必須初始化它:

1

2

ProcessMemoryReaderLib.ProcessMemoryReader pReader

         = new ProcessMemoryReaderLib.ProcessMemoryReader();

跟着,你需要設定你需要讀取記憶體的程序,這兒是教你如何獲得掃雷遊戲程序,它隻是裝載一次,并設該程序為ReadProcess的屬性.

1

2

3

System.Diagnostics.Process[] myProcesses

         = System.Diagnostics.Process.GetProcessesByName( "winmine" );

pReader.ReadProcess = myProcesses[ 0 ];

現在我們所需要做的是打開該程序,讀這個程序記憶體,當完成後關閉程序.這是進行這一過程的代碼.這兒我讀取布雷圖中的寬度的記憶體位址.

1

2

3

4

5

6

pReader.OpenProcess();

int iWidth;

byte[] memory;

memory = pReader.ReadProcessMemory( (IntPtr) 0x1005334, 1, out bytesReaded);

iWidth = memory[0];

pReader.CloseHandle();

就這麼簡單!

在總結部分,我會實作找出在布雷圖所有地雷的代碼.但是請記住,所有我所讀取的位址都在這篇文章的第一部分找到...

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

56

57

58

59

60

61

// 第一步先找到掃雷程式的Instance

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();

這是代碼的主要部分!希望你能從這裡學到一些新知識!

繼續閱讀