task 2.3-2.4: implement MemoryBase + ExternalReader with tests
Add abstract MemoryBase base class with typed Read<T>/Write<T>, array IO, string IO, and relative/absolute addressing. Uses MarshalCache<T> to branch between blittable (MemoryMarshal) and marshal-required paths. Add ExternalReader (out-of-process via ReadProcessMemory/WriteProcessMemory) with SafeMemoryHandle lifecycle management. 11 new tests: ImageBase, Read/Write of int/byte/long/struct, byte array, int array, struct array, invalid address, dispose, double-dispose. All passing (total: 30).
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// Out-of-process memory reader that accesses the target's memory through
|
||||
/// <see cref="NativeMethods.ReadProcessMemory"/> and
|
||||
/// <see cref="NativeMethods.WriteProcessMemory"/>.
|
||||
/// </summary>
|
||||
public sealed class ExternalReader : MemoryBase
|
||||
{
|
||||
private readonly SafeMemoryHandle _handle;
|
||||
private readonly IntPtr _imageBase;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Opens a process for external memory access.
|
||||
/// </summary>
|
||||
/// <param name="process">The target process.</param>
|
||||
/// <param name="desiredAccess">The access rights to request. Defaults to
|
||||
/// <see cref="ProcessAccess.AllAccess"/>.</param>
|
||||
public ExternalReader(Process process, ProcessAccess desiredAccess = ProcessAccess.AllAccess)
|
||||
{
|
||||
_handle = NativeMethods.OpenProcess(desiredAccess, false, process.Id);
|
||||
if (_handle.IsInvalid)
|
||||
{
|
||||
int error = Marshal.GetLastPInvokeError();
|
||||
throw new InvalidOperationException(
|
||||
$"OpenProcess failed for PID {process.Id}: error {error}");
|
||||
}
|
||||
|
||||
_imageBase = process.MainModule?.BaseAddress ?? IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IntPtr ImageBase => _imageBase;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override SafeMemoryHandle Handle => _handle;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false)
|
||||
{
|
||||
if (isRelative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
byte[] buffer = new byte[count];
|
||||
if (!NativeMethods.ReadProcessMemory(_handle, address, buffer, count, out nint bytesRead))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
if ((int)bytesRead != count)
|
||||
{
|
||||
Array.Resize(ref buffer, (int)bytesRead);
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false)
|
||||
{
|
||||
if (isRelative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
if (!NativeMethods.WriteProcessMemory(_handle, address, bytes, bytes.Length, out nint written))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return (int)written;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
{
|
||||
_disposed = true;
|
||||
_handle.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
using WhiteMagic.Native;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// Abstract base for all memory-access readers and writers. Provides typed
|
||||
/// <see cref="Read{T}"/>/<see cref="Write{T}"/>, array IO, string IO, and
|
||||
/// relative/absolute addressing. Subclasses implement the concrete
|
||||
/// <see cref="ReadBytes"/> and <see cref="WriteBytes"/> methods.
|
||||
/// </summary>
|
||||
public abstract class MemoryBase : IDisposable
|
||||
{
|
||||
/// <summary>The base address of the target process's main module.</summary>
|
||||
public abstract IntPtr ImageBase { get; }
|
||||
|
||||
/// <summary>The native handle to the target process.</summary>
|
||||
public abstract SafeMemoryHandle Handle { get; }
|
||||
|
||||
// ── Raw byte IO ────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reads a sequence of bytes from the target address.</summary>
|
||||
public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false);
|
||||
|
||||
/// <summary>Writes a sequence of bytes to the target address.</summary>
|
||||
/// <returns>The number of bytes written.</returns>
|
||||
public abstract int WriteBytes(IntPtr address, ReadOnlySpan<byte> bytes, bool isRelative = false);
|
||||
|
||||
// ── Typed IO ───────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reads a value of type <typeparamref name="T"/> from the target address.</summary>
|
||||
public T Read<T>(IntPtr address, bool isRelative = false) where T : struct
|
||||
{
|
||||
if (isRelative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
int size = MarshalCache<T>.Size;
|
||||
Span<byte> buffer = stackalloc byte[size];
|
||||
byte[] raw = ReadBytes(address, size);
|
||||
raw.CopyTo(buffer);
|
||||
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
return MarshalByteArrayToStructure<T>(raw);
|
||||
}
|
||||
|
||||
return MemoryMarshal.Read<T>(buffer);
|
||||
}
|
||||
|
||||
/// <summary>Writes a value of type <typeparamref name="T"/> to the target address.</summary>
|
||||
/// <returns><see langword="true"/> if all bytes were written.</returns>
|
||||
public bool Write<T>(IntPtr address, T value, bool isRelative = false) where T : struct
|
||||
{
|
||||
if (isRelative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
int size = MarshalCache<T>.Size;
|
||||
Span<byte> buffer = stackalloc byte[size];
|
||||
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
StructureToByteArray(value, buffer, size);
|
||||
}
|
||||
else
|
||||
{
|
||||
MemoryMarshal.Write(buffer, in value);
|
||||
}
|
||||
|
||||
int written = WriteBytes(address, buffer, false);
|
||||
return written == size;
|
||||
}
|
||||
|
||||
/// <summary>Reads an array of values of type <typeparamref name="T"/> from the target address.</summary>
|
||||
public T[] Read<T>(IntPtr address, int count, bool isRelative = false) where T : struct
|
||||
{
|
||||
if (isRelative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
int elementSize = MarshalCache<T>.Size;
|
||||
int totalSize = elementSize * count;
|
||||
byte[] raw = ReadBytes(address, totalSize);
|
||||
|
||||
var result = new T[count];
|
||||
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var elementBytes = new ReadOnlySpan<byte>(raw, i * elementSize, elementSize);
|
||||
result[i] = MarshalByteArrayToStructure<T>(elementBytes.ToArray());
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadOnlySpan<byte> span = raw;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
result[i] = MemoryMarshal.Read<T>(span.Slice(i * elementSize, elementSize));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Writes an array of values of type <typeparamref name="T"/> to the target address.</summary>
|
||||
/// <returns><see langword="true"/> if all bytes were written.</returns>
|
||||
public bool Write<T>(IntPtr address, T[] values, bool isRelative = false) where T : struct
|
||||
{
|
||||
if (isRelative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
if (values is null || values.Length == 0)
|
||||
return true;
|
||||
|
||||
int elementSize = MarshalCache<T>.Size;
|
||||
int totalSize = elementSize * values.Length;
|
||||
byte[] raw = new byte[totalSize];
|
||||
|
||||
Span<byte> span = raw;
|
||||
for (int i = 0; i < values.Length; i++)
|
||||
{
|
||||
Span<byte> slice = span.Slice(i * elementSize, elementSize);
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
StructureToByteArray(values[i], slice, elementSize);
|
||||
}
|
||||
else
|
||||
{
|
||||
MemoryMarshal.Write(slice, in values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
int written = WriteBytes(address, raw, false);
|
||||
return written == totalSize;
|
||||
}
|
||||
|
||||
// ── String IO ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reads a null-terminated string from the target address.</summary>
|
||||
public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false)
|
||||
{
|
||||
byte[] buffer = ReadBytes(address, maxLength, relative);
|
||||
int nullIndex = Array.IndexOf<byte>(buffer, 0);
|
||||
if (nullIndex >= 0)
|
||||
{
|
||||
return encoding.GetString(buffer, 0, nullIndex);
|
||||
}
|
||||
return encoding.GetString(buffer);
|
||||
}
|
||||
|
||||
/// <summary>Writes a null-terminated string to the target address.</summary>
|
||||
public virtual bool WriteString(IntPtr address, string value, Encoding encoding, bool relative = false)
|
||||
{
|
||||
// Ensure null terminator
|
||||
if (value.Length == 0 || value[^1] != '\0')
|
||||
value += '\0';
|
||||
|
||||
byte[] bytes = encoding.GetBytes(value);
|
||||
int written = WriteBytes(address, bytes, relative);
|
||||
return written == bytes.Length;
|
||||
}
|
||||
|
||||
// ── Addressing ─────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Converts a relative offset to an absolute address relative to <see cref="ImageBase"/>.</summary>
|
||||
public IntPtr GetAbsolute(IntPtr relative)
|
||||
{
|
||||
return ImageBase + (int)relative;
|
||||
}
|
||||
|
||||
/// <summary>Converts an absolute address to a relative offset from <see cref="ImageBase"/>.</summary>
|
||||
public IntPtr GetRelative(IntPtr absolute)
|
||||
{
|
||||
return (IntPtr)((int)ImageBase - (int)absolute);
|
||||
}
|
||||
|
||||
// ── Lifecycle ──────────────────────────────────────────────────────────
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual void Dispose()
|
||||
{
|
||||
Handle?.Dispose();
|
||||
}
|
||||
|
||||
// ── Private helpers ────────────────────────────────────────────────────
|
||||
|
||||
private static T MarshalByteArrayToStructure<T>(byte[] bytes) where T : struct
|
||||
{
|
||||
GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
return Marshal.PtrToStructure<T>(pin.AddrOfPinnedObject());
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private static void StructureToByteArray<T>(T value, Span<byte> destination, int size) where T : struct
|
||||
{
|
||||
byte[] temp = destination.ToArray();
|
||||
GCHandle pin = GCHandle.Alloc(temp, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false);
|
||||
temp.CopyTo(destination);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
using WhiteMagic.Native;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WhiteMagic;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="MemoryBase"/> abstract contract and <see cref="ExternalReader"/>
|
||||
/// round-trip (Read<T>/Write<T>, arrays) using the current process as target.
|
||||
/// </summary>
|
||||
public class MemoryBaseTests
|
||||
{
|
||||
private static ExternalReader OpenSelf()
|
||||
{
|
||||
return new ExternalReader(
|
||||
Process.GetCurrentProcess(),
|
||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageBase_is_nonzero_for_self()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.NotEqual(IntPtr.Zero, reader.ImageBase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_int_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
|
||||
// Pin a local int to use as our "remote" address
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, 0x1BADB002));
|
||||
Assert.Equal(0x1BADB002, reader.Read<int>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_byte_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, (byte)0xAB));
|
||||
Assert.Equal(0xAB, reader.Read<byte>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_long_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
long slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, unchecked((long)0xDEADBEEF_CAFEBABE)));
|
||||
Assert.Equal(unchecked((long)0xDEADBEEF_CAFEBABE), reader.Read<long>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_blittable_struct_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
var slot = new TestStruct { X = 42, Y = 99 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
|
||||
// Write a new value
|
||||
Assert.True(reader.Write(addr, new TestStruct { X = 100, Y = 200 }));
|
||||
|
||||
var result = reader.Read<TestStruct>(addr);
|
||||
Assert.Equal(100, result.X);
|
||||
Assert.Equal(200, result.Y);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_bytes_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] buffer = new byte[16];
|
||||
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
byte[] expected = [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07];
|
||||
|
||||
int written = reader.WriteBytes(addr, expected);
|
||||
Assert.Equal(expected.Length, written);
|
||||
|
||||
byte[] actual = reader.ReadBytes(addr, expected.Length);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_int_array_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
int[] buffer = new int[4];
|
||||
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
int[] expected = [10, 20, 30, 40];
|
||||
|
||||
Assert.True(reader.Write(addr, expected));
|
||||
|
||||
int[] actual = reader.Read<int>(addr, 4);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_struct_array_writes_and_reads_back()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
var buffer = new TestStruct[4];
|
||||
GCHandle pin = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
var expected = new[]
|
||||
{
|
||||
new TestStruct { X = 1, Y = 2 },
|
||||
new TestStruct { X = 3, Y = 4 },
|
||||
new TestStruct { X = 5, Y = 6 },
|
||||
new TestStruct { X = 7, Y = 8 },
|
||||
};
|
||||
|
||||
Assert.True(reader.Write(addr, expected));
|
||||
|
||||
var actual = reader.Read<TestStruct>(addr, 4);
|
||||
Assert.Equal(expected, actual);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_returns_false_for_invalid_address()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.False(reader.Write(IntPtr.Zero, 42));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_closes_handle()
|
||||
{
|
||||
var reader = OpenSelf();
|
||||
Assert.False(reader.Handle.IsClosed);
|
||||
reader.Dispose();
|
||||
Assert.True(reader.Handle.IsClosed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Double_dispose_does_not_throw()
|
||||
{
|
||||
var reader = OpenSelf();
|
||||
reader.Dispose();
|
||||
reader.Dispose(); // Should not throw
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A simple blittable struct for use in tests.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct TestStruct : IEquatable<TestStruct>
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
|
||||
public bool Equals(TestStruct other) => X == other.X && Y == other.Y;
|
||||
public override bool Equals(object? obj) => obj is TestStruct other && Equals(other);
|
||||
public override int GetHashCode() => HashCode.Combine(X, Y);
|
||||
public override string ToString() => $"({X}, {Y})";
|
||||
}
|
||||
Reference in New Issue
Block a user