熱點推薦:
您现在的位置: 電腦知識網 >> 編程 >> .NET編程 >> 正文

“掃雷”游戲的幕後

2022-06-13   來源: .NET編程 

  介紹

  曾想了解掃雷游戲在幕後所發生的一切嗎?嗯我想過還由此決定對其進行了研究本文是我的研究結果現公之於眾

  主要概念

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

  就是這些希望你能學到新的東西


From:http://tw.wingwit.com/Article/program/net/201311/13469.html
    推薦文章
    Copyright © 2005-2022 電腦知識網 Computer Knowledge   All rights reserved.