Click here to Skip to main content
16,016,227 members
Articles / Programming Languages / C#
Article

Minesweeper, Behind the scenes

Rate me:
Please Sign up or sign in to vote.
4.94/5 (203 votes)
13 Jan 2003Ms-PL7 min read 625.7K   12.8K   248   133
This article demonstrates directly reading another processes memory in C# using P/Invoke and Win32 Api's.

Sample Image - minememoryreader.jpg

Introduction

Ever wanted to know what is happening in the Minesweeper game behind the scenes?
Well, I did, and I decided to investigate it. This article is the result of my research, brought here just for you.

Main Concepts

1. Using P/Invoke to invoke Win32 API's.

2. Direct reading another process memory.

NOTE: the first part of this article involves some assembly code, if there something you don't understand it's NOT important to the goal of this article. you CAN skip it. Nevertheless, if you want to ask me about it, you're more then welcome to mail me your question.

NOTE 2: The program was tested on Windows XP, so if you got some other system it might not work.. if you got some other system, and it did worked, comment this article with your system info so we all could enjoy that knowledge.

Update to NOTE 2: the code is now fixed in order to work on Windows 2000 as well. thanks goes to Ryan Schreiber for finding the addresses in Win2K.

Step 1 - Investigate winmine.exe

If you're not an assembly fan, you might want to skip to the end of this step, to the conclusions..

So, in order to know better what is happening behind the scenes of Minesweeper I've started by opening the file in a debugger. My personally favorite debugger is Olly Debugger v1.08, this is a very simple and intuitive debugger. Anyway, I've open winmine.exe in the debugger and looked a bit on the file. I've found in the Import section (a section that lists all the dll functions that are used in the program) the following entry:

010011B0  8D52C377 DD msvcrt.rand
which means that the Minesweeper uses the randomize function of the MicroSoft Visual C RunTime dll, So I thought it might help me. I've search the file to find where the rand() function is being called, I've found only one such place:

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

Then I've put a breakpoint on this single call and ran the program. I've discovered that every time you click the smiley a new mines map is generated. the mines map is created as follows:

1. Allocate a block of memory for the mines map and set all the memory bytes to 0x0F, which means that there is no mine in that "cell".
2. second, loop over the number of mines and for each mine:
2.1. randomize x position (1 .. width).
2.2. randomize y position (1 .. height).
2.3. set the correct cell in the memory block to 0x8F, which represents a mine in this cell.

here is the original code, I've added some remarks, and bolded the important parts:

ASM
010036A7  MOV DWORD PTR DS:[1005334],EAX    ; [0x1005334] = Width
010036AC  MOV DWORD PTR DS:[1005338],ECX    ; [0x1005338] = Height
010036B2  CALL winmine.01002ED5  ; Generate empty block of memory and clears it
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 
010036CD  CALL winmine.01003940       ; Mine_Width  = randomize x position (0 .. max width-1)
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
010036E1  MOV ECX,EAX            ; calculate the address of the cell in the memory block 
                                 ; (the map)
010036E3  SHL ECX,5              ; the calculation goes:
                                 ; cell_memory_address = 0x1005340 + 32 * height + width
010036E6  TEST BYTE PTR DS:[ECX+ESI+1005340],80 ; [cell_memory_address] == is already mine?
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 
As you can see from the code I've discovered 4 important things:
  1. Reading the memory in address [0x1005334] gives me the Width of the map.
  2. Reading the memory in address [0x1005338] gives me the Height of the map.
  3. Reading the memory in address [0x1005330] gives me the number of mines in the map.
  4. Given x,y that represents a cell in the map, in column x, row y, the address [0x1005340 + 32 * y + x] gives me the cell value.

Which brings us to the next step..

Step 2 - Design a solution

You might wonder, what kind of solution am I talking about? Well, after discovering that all the mine information is available for me, and all I need to do is read the data from the memory I've decided to write a small app that reads this information and present it for you. It can even draw a map of its own, with a picture of a mine wherever I find one..

So, what is to design about that? all I need to do is put the address into a pointer (yes, they exist even in C#) and read the pointed data, right? well, not exactly, the reason why this is not the case is that the memory where this data is stored is not in my application. You see, each process has its own address space so it could not access "by accident" memory that belongs to another program, so in order to read this data I need to find a way to read the memory of another process, in this case, its the Minesweeper process.

I've decided to write a small class library, that will receive a process and will give me the functionality of reading a memory address from this process. the reason I've decided to make it a class library is because there is allot of cases you might want to use it so why develop it again and again? That way, you could easily take the class and use it in your own application, and you are free to do so. An example for where this class can help is if you write a debugger. All the debuggers I know have the ability to read memory of the debugged application..

So, how do we read another process memory? the answer resides in an API called ReadProcessMemory. this API actually let you read a process memory at a specific address. Only before you do it you must open the process in a special read mode and when you finish you must close the process handle, to avoid resource leaks. The following operations are performed with the help of two more API called OpenProcess and CloseHandle.

In order to use API's with C# you must use the P/Invoke, meaning you need to declare the API you're going to use, which is basically quite simple, but you need to do it in the .NET way, which is not that simple sometimes.. I've found the API declarations in the MSDN:

C++
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
    );

Well this declaration transformed to the following C# declarations:

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
    );
If you want to know more information on the types conversions between c++ and c#, I suggest you searched msdn.microsoft.com for the topics: "Marshaling Data with Platform Invoke". Basically, if you put there what is logically true, it will work. sometimes, a bit tuning is requested.

After I have these functions declared, all I need to do is wrap them with a simple class and use it. I've put the declarations in a separate class called ProcessMemoryReaderApi, only to be more organized. The main utility class is called ProcessMemoryReader. This class has a property named ReadProcess, this property is from the type System.Diagnostics.Process, this is where you put the process you want to read its memory.

The class has a method which opens the process in the read memory mode:

C#
public void OpenProcess()

{
    m_hProcess = ProcessMemoryReaderApi.OpenProcess(
                         ProcessMemoryReaderApi.PROCESS_VM_READ, 1, 
                         (uint)m_ReadProcess.Id);

}
The PROCESS_VM_READ constant tell the system to open the process in read mode, and the m_ReadProcess.Id states what process do I want to open.

The most important method in the class is the one that reads the memory from the process:

C#
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;

}
This function declares a byte array in the requested size and read the memory with the API. Simple as that.

And finally, the method that close it all:

C#
public void CloseHandle()

{
    int iRetValue;
    iRetValue = ProcessMemoryReaderApi.CloseHandle(m_hProcess);
    if (iRetValue == 0)
        throw new Exception("CloseHandle failed");

}

Step 3 - Using the class

Well, here comes the fun part. Using the class in order to read Minesweeper memory and reveal the map. In order to use the class you must first instantiate it:

C#
ProcessMemoryReaderLib.ProcessMemoryReader pReader
                   = new ProcessMemoryReaderLib.ProcessMemoryReader();

then, you need to set the process you want to read its memory, here is an example of how to get the Minesweeper progress, once it is loaded, and set it as the ReadProcess property:

C#
System.Diagnostics.Process[] myProcesses
                   = System.Diagnostics.Process.GetProcessesByName("winmine");
pReader.ReadProcess = myProcesses[0];
and all we need to do now is Open the process, read the memory, and close it when we finish. Again, here is an example of how its been done. Here I read the memory address that represent the Width of the map.
C#
pReader.OpenProcess();

int iWidth;
byte[] memory;
memory = pReader.ReadProcessMemory((IntPtr)0x1005334,1,out bytesReaded);
iWidth = memory[0];

pReader.CloseHandle(); 

that simple!

In conclusion I present you the full code that reveals the map of mines. Don't forget, all the memory places I'm accessing are places found in the first section of this article..

C#
// resource manager for the picture of the mine
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");

// take first instance of minesweeper you find
if (myProcesses.Length == 0)
{
    MessageBox.Show("No MineSweeper process found!");
    return;
}
pReader.ReadProcess = myProcesses[0];

// open process in read memory mode
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();

// delete the previous button array
this.Controls.Clear();
this.Controls.AddRange(MainControls);

// create new button array, for drawing the mine map
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]);
    }

// close process handle
pReader.CloseHandle();

That's it. Hope you leaned something new.

History

  • 14 Jan 2003 - updated demo code

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Software Developer (Senior) Verint
Israel Israel
Arik Poznanski is a senior software developer at Verint. He completed two B.Sc. degrees in Mathematics & Computer Science, summa cum laude, from the Technion in Israel.

Arik has extensive knowledge and experience in many Microsoft technologies, including .NET with C#, WPF, Silverlight, WinForms, Interop, COM/ATL programming, C++ Win32 programming and reverse engineering (assembly, IL).

Comments and Discussions

 
QuestionWhy use C#? Pin
Frank Furter11-Jun-03 15:59
sussFrank Furter11-Jun-03 15:59 
AnswerRe: Why use C#? Pin
Arik Poznanski16-Jun-03 5:18
Arik Poznanski16-Jun-03 5:18 
GeneralCouple of enhancements Pin
jasonee15-May-03 7:03
jasonee15-May-03 7:03 
GeneralI'm a dumb ass Pin
herbman24-Apr-03 7:48
herbman24-Apr-03 7:48 
GeneralRe: I'm a dumb ass Pin
Anonymous26-Nov-04 18:18
Anonymous26-Nov-04 18:18 
GeneralFirst Click Never Finds a Mine Pin
Mark Smithson8-Apr-03 13:35
Mark Smithson8-Apr-03 13:35 
GeneralRe: First Click Never Finds a Mine Pin
Arik Poznanski8-Apr-03 19:00
Arik Poznanski8-Apr-03 19:00 
GeneralRe: which memory Pin
Arik Poznanski21-Mar-03 21:29
Arik Poznanski21-Mar-03 21:29 
It can read any cell in the your given process address space. The stack and heap memory are both declared in the process address space memory.
If you declare memory using CoTaskMemAlloc Im not sure whether this memory is created in your process address space or in a special COM address space..

So, you can read stack and heap memory, the problem is that heap memory address changes between execution of the process, but stack variables have always the same address (in NATIVE languages!). So if you reference heap memory you should find the stack allocated pointer which points on the heap. so you can find the address at runtime.


Arik Poznanski
GeneralMineSweeper Solver Pin
obe4-Mar-03 10:34
obe4-Mar-03 10:34 
GeneralRe: MineSweeper Solver Pin
icewater5-Mar-03 13:41
icewater5-Mar-03 13:41 
GeneralRe: MineSweeper Solver Pin
obe6-Mar-03 2:21
obe6-Mar-03 2:21 
GeneralRe: MineSweeper Solver Pin
Georgik18-Dec-03 2:23
Georgik18-Dec-03 2:23 
GeneralRe: MineSweeper Solver Pin
Humanvegetable29-Apr-04 15:41
Humanvegetable29-Apr-04 15:41 
GeneralRe: MineSweeper Solver Pin
Hofver10-Mar-03 22:55
Hofver10-Mar-03 22:55 
GeneralRe: MineSweeper Solver Pin
Rage Kendel28-May-03 12:47
sussRage Kendel28-May-03 12:47 
GeneralRe: MineSweeper Solver Pin
Anonymous16-Apr-04 14:36
Anonymous16-Apr-04 14:36 
GeneralArik! Great article... Pin
zimanke28-Feb-03 4:36
zimanke28-Feb-03 4:36 
GeneralRe: Arik! Great article... Pin
zimanke28-Feb-03 4:48
zimanke28-Feb-03 4:48 
Generaljust one comment.. Pin
GuimaSun5-Feb-03 8:31
GuimaSun5-Feb-03 8:31 
GeneralFinally I can break all records !!! Pin
Flavio Maia4-Feb-03 2:20
Flavio Maia4-Feb-03 2:20 
GeneralRe: Finally I can break all records !!! Pin
King of Minesweeper23-Jan-04 12:54
sussKing of Minesweeper23-Jan-04 12:54 
GeneralFantasic Work out of my belife!! Pin
Zhefu Zhang1-Feb-03 8:49
Zhefu Zhang1-Feb-03 8:49 
GeneralRe: Fantasic Work out of my belife!! Pin
herbman24-Apr-03 7:50
herbman24-Apr-03 7:50 
GeneralRe: Fantasic Work out of my belife!! Pin
GLM212-Oct-03 3:00
GLM212-Oct-03 3:00 
GeneralCool - This could also be used to implement interprocess communication! Pin
sytelus27-Jan-03 17:55
sytelus27-Jan-03 17:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.