Not sure if this should be here or in 'programming tutorials' but there still seem to be a lot of people who have trouble with it, so.
(I posted this at another forum, re-posting here)
The goal
To be able to read any memory address inside another process, like such.
Code:
Dim ourValue as Int32 = 0
ourValue = MemoryReader.ReadInt32(0x12345678)
Dim ourValue2 as Int64 = 0
ourValue2 = MemoryReader.ReadInt64(0x11223344)
Dim ourBytes() as Byte
ourBytes = MemoryReader.ReadBytes(0x1F00FF00,100) '100 = length to read
Basic Assumptions
I assume you can already program in .Net! That means variables,functions,classes. You feel comfortable with the OOP concepts of vb.net. If you don't know where to declare a variable so your program has access to it, I can't help you. I'm not going to specifically say when to create a class, what to name the files, etc. This guide is for those people who are really comfortable in .Net and have a basic conceptual understanding of what they are trying to do. I had quite hard time myself getting all of this to work (the ins-and-outs of OpenProcess, the mbi.Protect etc), and now that it does work, maybe the final results will help you avoid some frustration.
Before we get started
I AM NOT an expert. Some of my information may be incorrect or misleading. Trust content at own risk.
Some Background Information
Windows(R) Memory Management
Basically windows keeps track of a lot of complicated stuff for us, and as far as we care
each process (on Windows XP! 32bit) gets it's own memory range of 0-&FFFFFFF (32bit--4 gigs). This isn't possible. If the machine only has 4 gigs of ram, how can each process get 4 gigs? In reality it can't, and if each process actually used that much, your system would stop. But as far as each process is concerned, in theory, it can access 0-0xFFFFFFFF.
Windows API (I won't explain what they are, only declare them)
ReadProcessMemory() is the awesome API which actually lets us read into another processes ram. But, before we can just go reading away, there is a bit of security first. Basically, not just any program can/should/has any reason to, read the ram of another process. So before we can do that, we have to use another windows API, OpenProcess, which gives us special access rights (kind of). You probably have to be an administrator on the computer for this to work? (todo: experiment, if you care)
OpenProcess(). You pass in a reference to the desired process, and it returns a handle to that process which you can use for ReadProcessMemory (and WriteProcessMemory!) Be sure to call CloseHandle() when you're done with the handle, because it's an OS resource and might lead to mem leak.
Da Code
Code:
Private Declare Function OpenProcess Lib "kernel32.dll" (ByVal dwDesiredAcess As UInt32, ByVal bInheritHandle As Boolean, ByVal dwProcessId As Int32) As IntPtr
Private Declare Function ReadProcessMemory Lib "kernel32" (ByVal hProcess As IntPtr, ByVal lpBaseAddress As IntPtr, ByVal lpBuffer() As Byte, ByVal iSize As Integer, ByRef lpNumberOfBytesRead As Integer) As Boolean
Private Declare Function CloseHandle Lib "kernel32.dll" (ByVal hObject As IntPtr) As Boolean
So now that we have our APIs declared. Let's start using them! But first thing first. Which process do we want to 'read from' and how do we define it in vb.net? Thank goodness there is already a 'Process' class in .Net! The way I find the process is by using .Net functions to find it's main Window based on Title. There are some more complicated scenerios, such as, what if the process doesn't have a window being displayed?! I'm not going into that here.
There are several options are far as how we select(find) our target process. Two convenient .Net functions.
Process.GetProcesses() Returns an array of every process currently running.
Process.GetProcessByName() Tries to find process by name. Funny thing is, since you can run a program multiple times, this returns an array!
I chose .GetProcesses because of convenience (discovering process name vs window title - which seems easier)
It returns an array of all processes currently running. We loop over the array,comparing the Process's .MainWindowTitle until we find our process.
A note about OpenProcess(). You can actually ask for different access rights. The two we care about: READ and WRITE. In my code
I use PROCESS_ALL_ACCESS (which is Read,Write + some) but this value changes depending on what version of Windows you're on. If all you
plan to do is read, VM_READ should work. This code is made specifically to run on Windows Xp 32 bit. Any other OS might fail.
I actually overload my function so there are 2 ways to call it. I only explicitly use the 'window caption' version atm.
Code:
Public Class MemoryManager
Private _targetProcess as Process = Nothing 'to keep track of it. not used yet.
Private _targetProcessHandle as IntPtr = Intptr.Zero 'Used for ReadProcessMemory
Private PROCESS_ALL_ACCESS as UInt32 = &H1F0FFF
Private PROCESS_VM_READ as Uint32 = &H10
Public Function TryAttachToProcess(ByVal windowCaption As String) As Boolean
Dim _allProcesses() As Process = Process.GetProcesses
For Each pp As Process In _allProcesses
If pp.MainWindowTitle.ToLower.Contains(windowCaption.ToLower) Then
'found it! proceed.
Return TryAttachToProcess(pp)
End If
Next
MessageBox.Show("Unable to find process '" & windowCaption & ".' Is running?")
Return False
End Function
Public Function TryAttachToProcess(ByVal proc As Process) As Boolean
If _targetProcessHandle = IntPtr.Zero Then 'not already attached
_targetProcess = proc
_targetProcessHandle = OpenProcess(ReadProcessMemoryRights.PROCESS_ALL_ACCESS, False, _targetProces*****)
If _targetProcessHandle = 0 Then
TryAttachToProcess = False
MessageBox.Show("OpenProcess() FAIL! Are you Administrator??")
Else
'if we get here, all connected and ready to use ReadProcessMemory()
TryAttachToProcess = True
MessageBox.Show("OpenProcess() OK")
End If
Else
MessageBox.Show("Already attached! (Please Detach first?)")
TryAttachToProcess = False
End If
End Function
Public Sub DetachFromProcess()
If Not (_targetProcessHandle = IntPtr.Zero) Then
_targetProcess = Nothing
Try
CloseHandle(_targetProcessHandle)
_targetProcessHandle = IntPtr.Zero
MessageBox.Show("MemReader::Detach() OK")
Catch ex As Exception
MessageBox.Show("MemoryManager::DetachFromProcess::CloseHandle error " & Environment.NewLine & ex.Message)
End Try
End If
End Sub
End Class
I guess I should explain something before I continue too much further. We have a choice. Call OpenProcess() every time we want to ReadProcessMemory (which, if we're planning on reading dozens of values every second, might not be efficient) or call it once and store the value returned, the handle, in our class. This seems ideal, but means we must verify we have attached to the process before trying to read, which can sometimes be a pain. The other option is to add an extra parameter to your Read() functions, and make each function attach before it reads. Since I'm planning on doing a lot of ReadProcessMemory calls, I took the route of 'attaching' once, and storing the handle. Maybe later we can add shared methods so MemoryManager.ReadInt32(aProcess,0x1234) is possible.
ReadProcessMemory()
ReadProcessMemory(hProcess, lpBaseAddress, lpBuffer(), iSize,ByRef lpNumberOfBytesRead) As Boolean
[pre]
hProcess The process you want to read from. Duh. -- handle returned by OpenProcess()
lpBaseAddress The address you want to read (or start reading at) -- you can read as many bytes as you want. not just 1 address at a time!
lpBuffer The byte array where ReadProcessMemory stores the data it reads
iSize How many bytes to read
lpNumberOfBytesRead Actual number of bytes read (if this isn't == iSize, there was a partial error!)
As Boolean The function tells you if it succeeded or not.
demo. (no error checking)
Code:
Dim _myBytes(3) as Byte
_myBytes = ReadProcessMemory(_targetHandle,&H112233,_myBytes,4,vbNull) 'fills _myBytes with data stored at 0x112233, 0x112234, 0x112235, 0x112236
note. lpBuffer needs to be declared the correct size! ReadProcessMemory won't resize it for you.
If it's too small, it will return as normal and not throw any errors!
[/pre]
---------- Post added at 01:43 AM ---------- Previous post was at 01:40 AM ----------
The Goal
To verify that our MemoryManager code can, in fact, read another process's memory.
We will be using Cheat Engine (a memory scanner) to find addresses and to verify the data returned from our functions is correct.
Basically Cheat Engine scans all memory being used by a program. and can search it for a value. So using CE we can actually learn
the address where pinball stores our score! (note: this address can change each time you run pinball, we'll worry about that later if need be)
(crude demo on how to find memory address with Cheat Engine)
[pre]
-Open pinball. Play some. Make youre score go above 0
-Pause the game (F3) -- your score value needs to not change while CE scans
-Open Cheat Engine
-Attach CE to pinball
-Leave all options to default (4 bytes,range, etc)
-Do a first scan for your score
[/pre]
On my system, CE only finds the score stored at 1 location. If your score was, for example, 100, it probably would have found that value at a LOT more locations. Try changing your score and scanning again (new scan). If that doesn't work, read a tutorial on 'how to find addresses with Cheat Engine' ( 1) find all possible locs, 2)change your score, 3)see if those possible locs still == your score. CE will help you do this.) For pinball, there should only be 1 address. Either way, we're just trying to make sure our MemoryManager can actually read data. Any address could do: I chose reading pinball's score because it gives some "oh, I see it" satisfaction.
So, now we have an address to read! In my case 0x00AC4E4C (yours may/should be different, that's ok)
Back to the code. We haven't actually made a function in the MemoryManager class which can read data yet. We have a decision to make. There are actually two major design strategies we can use here: we can have lots of functions, one named for each data type we will read from ram. So our class will have ReadInt32(), ReadUint32(), ReadInt64(), ReadUint64 etc. Or, the other approach is create one function (or, prototype??) which lets you pass in what data type you want to read. For example, MemManager.ReadValue(0x123456,Int32) (This can only work with POD(?) types, which have a constant size in memory) You would want to do this, maybe, to read a custom structure from ram instead of just a plain old built-in type. It's part of
some new language features(function prototyping?) put into VB.Net. To be honest, I'm not really sure, and it's not something I've read about yet. I chose to instead, explicitly create a bunch of functions, each one made to read a specific data type. This means a LOT or duplicate code. Which isn't good. Keep that in mind.
MemoryManager class. The first Read() function. ReadInt32()
Code:
Public Function ReadInt32(Byval addr as IntPtr) As Int32
'Int32 data type-- is 32 bits long, 4 bytes.
Dim _dataBytes(3) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _dataBytes, 4, vbNull)
Return BitConverter.ToInt32(_bytes, 0)
End Function
[pre]
A couple things to note about the function
No error checking
lpBytesRead = vbNull ? This is used to know if RPM was actually able to read all data. If RPM only
read some of the memory locations, _bytesRead will be < iSize (in previous example, iSize=4)
If we pass in vbNull,because the parameter is ByRef, basically it's saying, I don't care, I won't use this value in my code.\
BitConverter A handy .Net class which converts an array of bytes to many core data types
[/pre]
error checking
Code:
Public Function ReadInt32(Byval addr as IntPtr) As Int32
Dim _dataBytes(3) As Byte
Dim _actualReadLength as Int32 = 0
If ReadProcessMemory(_targetProcessHandle, addr, _dataBytes, 4, _actualReadLength) Then
'success
If _actualReadLength = 4 Then
'all bytes read
Return BitConverter.ToInt32(_bytes, 0)
Else
'ReadProcessMemory only able to read *some* of the memory location.
Return 0 'or handle byte array at own risk? [should have been zero filled on creation?] + [some valid data?]
Else
'ReadProcessMemory returned FAIL. Possibly ram is inaccessable.
Return 0
End Function
The second Read() function. ReadInt64()
Code:
Public Function ReadInt32(Byval addr as IntPtr) As Int32
Dim _dataBytes(3) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _dataBytes, 4, vbNull)
Return BitConverter.ToInt32(_bytes, 0)
End Function
Public Function ReadInt64(Byval addr as IntPtr) As Int64
Dim _dataBytes(7) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _dataBytes, 8, vbNull)
Return BitConverter.ToInt64(_bytes, 0)
End Function
Side Note
Int32 and Uint32 are both 32 bits, 4 bytes, in length. You need to understand how signed and unsigned works! That's too much to explain inside another tutorial. I HIGHLY recomend physically writing on paper numbers in binary,decimal,hex..lining them up in different ways, and try to feel comfortable looking at numbers in different formats. This helps especially when debugging. For example, I kept reading a pointer and outputting it using .ToString(). Well I was getting a number like 110347892 instead of the hex address Cheat Engine was showing me. It was because .ToString() was displaying it in base 10, which is the default, we all use grew up using base 10. Fortunately, .ToString() can take a formatting parameter where it will convert it to hex for us! (and there are other .Net functions for converting bases). The point is that you need to understand numbers.
Tip. In vb.net hex numbers start with &H <-- get it..H, pretty obvious. In other languages the prefix is 0x. Sometimes you'll see me refer to a memory loc as 0x123456. In vb it would be &H123456 and in decimal it would be..you tell me.
Another Read() Function. This time UnSigned (if that should really matter..) Hooray BitCoverter class.
Code:
Public Function ReadInt32(Byval addr as IntPtr) As Int32
Dim _dataBytes(3) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _dataBytes, 4, vbNull)
Return BitConverter.ToInt32(_bytes, 0)
End Function
Public Function ReadInt64(Byval addr as IntPtr) As Int64
Dim _dataBytes(7) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _dataBytes, 8, vbNull)
Return BitConverter.ToInt64(_bytes, 0)
End Function
Public Function ReadUInt32(Byval addr as IntPtr) As UInt32
Dim _dataBytes(3) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _dataBytes, 4, vbNull)
Return BitConverter.ToUInt32(_bytes, 0)
End Function
Until you have one for each data type you might read
Code:
Public Function ReadInt16(ByVal addr As IntPtr) As Int16
Dim _rtnBytes(1) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 2, vbNull)
Return BitConverter.ToInt16(_rtnBytes, 0)
End Function
Public Function ReadInt32(ByVal addr As IntPtr) As Int32
Dim _rtnBytes(3) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 4, vbNull)
Return BitConverter.ToInt32(_rtnBytes, 0)
End Function
Public Function ReadInt64(ByVal addr As IntPtr) As Int64
Dim _rtnBytes(7) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 8, vbNull)
Return BitConverter.ToInt64(_rtnBytes, 0)
End Function
Public Function ReadUInt16(ByVal addr As IntPtr) As UInt16
Dim _rtnBytes(1) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 2, vbNull)
Return BitConverter.ToUInt16(_rtnBytes, 0)
End Function
Public Function ReadUInt32(ByVal addr As IntPtr) As UInt32
Dim _rtnBytes(3) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 4, vbNull)
Return BitConverter.ToUInt32(_rtnBytes, 0)
End Function
Public Function ReadUInt64(ByVal addr As IntPtr) As UInt64
Dim _rtnBytes(7) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 8, vbNull)
Return BitConverter.ToUInt64(_rtnBytes, 0)
End Function
Public Function ReadFloat(ByVal addr As IntPtr) As Single
Dim _rtnBytes(3) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 4, vbNull)
Return BitConverter.ToSingle(_rtnBytes, 0)
End Function
Public Function ReadDouble(ByVal addr As IntPtr) As Double
Dim _rtnBytes(7) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, 8, vbNull)
Return BitConverter.ToDouble(_rtnBytes, 0)
End Function
Public Function ReadIntPtr(ByVal addr As IntPtr) As IntPtr
Dim _rtnBytes(IntPtr.Size - 1) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, IntPtr.Size, Nothing)
If IntPtr.Size = 4 Then
Return New IntPtr(BitConverter.ToUInt32(_rtnBytes, 0))
Else
Return New IntPtr(BitConverter.ToInt64(_rtnBytes, 0))
End If
End Function
Public Function ReadBytes(ByVal addr As IntPtr, ByVal size As Int32) As Byte()
Dim _rtnBytes(size - 1) As Byte
ReadProcessMemory(_targetProcessHandle, addr, _rtnBytes, size, vbNull)
Return _rtnBytes
End Function
I also added one for IntPtr (which uses IntPtr.Size -- 32/64 problems ahead..) and one for ReadBytes. And forgot Boolean I hope they make sense. We'll be using ReadBytes() much later to scan the entire memory range because it's more efficient to read a large chunk of ram vs. 4 bytes at a time. It's the difference between calling ReadProcessMemory 10,000 times and 10,000,000 times...
I guess I forgot ReadByte(), singular. I don't see myself using it. If so desired, you should* be able to create it your self.
Back to actually testing if any of this works. I assume you've created a new project, and the MemoryManager class. And added all our new read functions.
Create a variable of the MemoryManager class. (Top of your form class? For demo purposes. I won't go into variable scoping) and use it!
Code:
Public Class Form1
Private _memManager as New MemoryManager 'New word is important! -I'm not explaining this.
Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Button1.Click
If _memManager.TryAttachToProcess("3D Pinball") Then
Dim _myScore as Int32 = _memManager.ReadInt32(&HAC4E4C)
' Done using MemoryManager for reading. Close it.
_memManager.DetachFromProcess()
MessageBox.Show("Your current 3D Pinball score is: " & _myScore.ToString())
Else
MessageBox.Show("MemoryManager unable to attach to 3D Pinball for ram reading")
End If
End Sub
End Class
That's it. You can now go poking around, trying to read the memory of different programs. But please don't.
-see pt. 2