Let's take them one at a time.
WriteProcessMemory
In order to write something to somewhere in some process, you need to pass the correct arguments:
lpBuffer
The lpBuffer
argument must point to the data that needs to be written.
There's a couple of ways to produce a safe pointer to an existing object, I prefer this approach:
$dataToWrite = "ABC"
# Encode data in byte array
$dataArray = [Text.Encoding]::Unicode.GetBytes($dataToWrite)
# Pin the object in memory (ensures the garbage collector doesn't move the source data around)
$pinnedArray = [Runtime.InteropServices.GCHandle]::Alloc($dataArray, [Runtime.InteropServices.GCHandleType]::Pinned)
# Now we can obtain a safe pointer to the array
[IntPtr]$lpBuffer = $pinnedArray.AddrOfPinnedObject()
But the far easier approach would be to just declare lpBuffer
a byte[]
in the first place and have the runtime take care of all that (sans the string encoding):
[DllImport("kernel32.dll")]public static extern Boolean WriteProcessMemory(
IntPtr hProcess,
IntPtr lpBaseAddress,
byte[] lpBuffer,
UInt32 nSize,
ref UInt32 lpNumberOfBytesWritten
);
hProcess
For the hProcess
argument, you need a process handle - passing the PID
of the owning process directly won't work.
For the current process (ie. the executing PowerShell host application), you can use Get-Process
:
# Obtain handle to current process
$hProcess = (Get-Process -Id $PID).Handle
But for a different process, you'll need to call Kernel32!OpenProcess()
- this answer has an excellent example of how to import and declare the access flag parameter type:
[DllImport("kernel32.dll")]
public static extern IntPtr OpenProcess(
ProcessAccessFlags dwDesiredAccess,
[MarshalAs(UnmanagedType.Bool)]
bool bInheritHandle,
int dwProcessId);
[Flags]
public enum ProcessAccessFlags : uint
{
All = 0x001F0FFF,
Terminate = 0x00000001,
CreateThread = 0x00000002,
VMOperation = 0x00000008,
VMRead = 0x00000010,
VMWrite = 0x00000020,
DupHandle = 0x00000040,
SetInformation = 0x00000200,
QueryInformation = 0x00000400,
Synchronize = 0x00100000
}
lpBaseAddress
[Marshal]::AllocHGlobal($size)
will only help you when writing to the current process.
For allocation in a different process, you'll need to pass a writeable process handle obtained via OpenProcess()
to Kernel32!VirtualAllocEx()
:
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true) ]
public extern IntPtr VirtualAllocEx(
IntPtr hProcess,
IntPtr lpAddress,
UInt32 dwSize,
AllocationType flAllocationType,
MemoryProtection flProtect);
[Flags]
public enum AllocationType
{
Commit = 0x1000,
Reserve = 0x2000,
Decommit = 0x4000,
Release = 0x8000,
Reset = 0x80000,
Physical = 0x400000,
TopDown = 0x100000,
WriteWatch = 0x200000,
LargePages = 0x20000000
}
[Flags]
public enum MemoryProtection
{
Execute = 0x10,
ExecuteRead = 0x20,
ExecuteReadWrite = 0x40,
ExecuteWriteCopy = 0x80,
NoAccess = 0x01,
ReadOnly = 0x02,
ReadWrite = 0x04,
WriteCopy = 0x08,
GuardModifierflag = 0x100,
NoCacheModifierflag = 0x200,
WriteCombineModifierflag = 0x400
}
VirtualAllocEx
will write the base address of the allocation to the lpAddress
pointer on success.
ReadProcessMemory
Finally, to read the resulting data, you'll need to copy the data referenced by the lpBuffer
argument to an array:
$CallResult = [winapi]::ReadProcessMemory(
$hProcess,
$lpBaseAddress,
$lpBuffer,
$ReadSize,
[ref]$BytesRead
)
if($CallResult){
$destArray = [byte[]]::new($BytesRead)
[System.Runtime.InteropServices.Marshal]::Copy($lpBuffer, $destArray, 0, $BytesRead)
$stringRead = [Text.Encoding]::Unicode.GetString($destArray)
Write-Host "Read data from process memory: $stringRead"
}