diff --git a/WhiteMagic/ExternalReader.cs b/WhiteMagic/ExternalReader.cs new file mode 100644 index 0000000..b0d6826 --- /dev/null +++ b/WhiteMagic/ExternalReader.cs @@ -0,0 +1,86 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using WhiteMagic.Native; + +namespace WhiteMagic; + +/// +/// Out-of-process memory reader that accesses the target's memory through +/// and +/// . +/// +public sealed class ExternalReader : MemoryBase +{ + private readonly SafeMemoryHandle _handle; + private readonly IntPtr _imageBase; + private bool _disposed; + + /// + /// Opens a process for external memory access. + /// + /// The target process. + /// The access rights to request. Defaults to + /// . + 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; + } + + /// + public override IntPtr ImageBase => _imageBase; + + /// + public override SafeMemoryHandle Handle => _handle; + + /// + 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; + } + + /// + public override int WriteBytes(IntPtr address, ReadOnlySpan 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; + } + + /// + public override void Dispose() + { + if (!_disposed) + { + _disposed = true; + _handle.Dispose(); + } + } +} diff --git a/WhiteMagic/MemoryBase.cs b/WhiteMagic/MemoryBase.cs new file mode 100644 index 0000000..2f7ef33 --- /dev/null +++ b/WhiteMagic/MemoryBase.cs @@ -0,0 +1,215 @@ +using WhiteMagic.Native; +using System.Runtime.InteropServices; +using System.Text; + +namespace WhiteMagic; + +/// +/// Abstract base for all memory-access readers and writers. Provides typed +/// /, array IO, string IO, and +/// relative/absolute addressing. Subclasses implement the concrete +/// and methods. +/// +public abstract class MemoryBase : IDisposable +{ + /// The base address of the target process's main module. + public abstract IntPtr ImageBase { get; } + + /// The native handle to the target process. + public abstract SafeMemoryHandle Handle { get; } + + // ── Raw byte IO ──────────────────────────────────────────────────────── + + /// Reads a sequence of bytes from the target address. + public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false); + + /// Writes a sequence of bytes to the target address. + /// The number of bytes written. + public abstract int WriteBytes(IntPtr address, ReadOnlySpan bytes, bool isRelative = false); + + // ── Typed IO ─────────────────────────────────────────────────────────── + + /// Reads a value of type from the target address. + public T Read(IntPtr address, bool isRelative = false) where T : struct + { + if (isRelative) + address = GetAbsolute(address); + + int size = MarshalCache.Size; + Span buffer = stackalloc byte[size]; + byte[] raw = ReadBytes(address, size); + raw.CopyTo(buffer); + + if (MarshalCache.TypeRequiresMarshal) + { + return MarshalByteArrayToStructure(raw); + } + + return MemoryMarshal.Read(buffer); + } + + /// Writes a value of type to the target address. + /// if all bytes were written. + public bool Write(IntPtr address, T value, bool isRelative = false) where T : struct + { + if (isRelative) + address = GetAbsolute(address); + + int size = MarshalCache.Size; + Span buffer = stackalloc byte[size]; + + if (MarshalCache.TypeRequiresMarshal) + { + StructureToByteArray(value, buffer, size); + } + else + { + MemoryMarshal.Write(buffer, in value); + } + + int written = WriteBytes(address, buffer, false); + return written == size; + } + + /// Reads an array of values of type from the target address. + public T[] Read(IntPtr address, int count, bool isRelative = false) where T : struct + { + if (isRelative) + address = GetAbsolute(address); + + int elementSize = MarshalCache.Size; + int totalSize = elementSize * count; + byte[] raw = ReadBytes(address, totalSize); + + var result = new T[count]; + + if (MarshalCache.TypeRequiresMarshal) + { + for (int i = 0; i < count; i++) + { + var elementBytes = new ReadOnlySpan(raw, i * elementSize, elementSize); + result[i] = MarshalByteArrayToStructure(elementBytes.ToArray()); + } + } + else + { + ReadOnlySpan span = raw; + for (int i = 0; i < count; i++) + { + result[i] = MemoryMarshal.Read(span.Slice(i * elementSize, elementSize)); + } + } + + return result; + } + + /// Writes an array of values of type to the target address. + /// if all bytes were written. + public bool Write(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.Size; + int totalSize = elementSize * values.Length; + byte[] raw = new byte[totalSize]; + + Span span = raw; + for (int i = 0; i < values.Length; i++) + { + Span slice = span.Slice(i * elementSize, elementSize); + if (MarshalCache.TypeRequiresMarshal) + { + StructureToByteArray(values[i], slice, elementSize); + } + else + { + MemoryMarshal.Write(slice, in values[i]); + } + } + + int written = WriteBytes(address, raw, false); + return written == totalSize; + } + + // ── String IO ────────────────────────────────────────────────────────── + + /// Reads a null-terminated string from the target address. + public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false) + { + byte[] buffer = ReadBytes(address, maxLength, relative); + int nullIndex = Array.IndexOf(buffer, 0); + if (nullIndex >= 0) + { + return encoding.GetString(buffer, 0, nullIndex); + } + return encoding.GetString(buffer); + } + + /// Writes a null-terminated string to the target address. + 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 ───────────────────────────────────────────────────────── + + /// Converts a relative offset to an absolute address relative to . + public IntPtr GetAbsolute(IntPtr relative) + { + return ImageBase + (int)relative; + } + + /// Converts an absolute address to a relative offset from . + public IntPtr GetRelative(IntPtr absolute) + { + return (IntPtr)((int)ImageBase - (int)absolute); + } + + // ── Lifecycle ────────────────────────────────────────────────────────── + + /// + public virtual void Dispose() + { + Handle?.Dispose(); + } + + // ── Private helpers ──────────────────────────────────────────────────── + + private static T MarshalByteArrayToStructure(byte[] bytes) where T : struct + { + GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned); + try + { + return Marshal.PtrToStructure(pin.AddrOfPinnedObject()); + } + finally + { + pin.Free(); + } + } + + private static void StructureToByteArray(T value, Span 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(); + } + } +} diff --git a/WhiteMagicTest/MemoryBaseTests.cs b/WhiteMagicTest/MemoryBaseTests.cs new file mode 100644 index 0000000..223ba5a --- /dev/null +++ b/WhiteMagicTest/MemoryBaseTests.cs @@ -0,0 +1,219 @@ +using WhiteMagic.Native; +using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Text; +using WhiteMagic; + +namespace WhiteMagicTest; + +/// +/// Tests for abstract contract and +/// round-trip (Read<T>/Write<T>, arrays) using the current process as target. +/// +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(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(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(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(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(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(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 + } +} + +/// +/// A simple blittable struct for use in tests. +/// +[StructLayout(LayoutKind.Sequential)] +public struct TestStruct : IEquatable +{ + 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})"; +}