diff --git a/WhiteMagic/InProcessReader.cs b/WhiteMagic/InProcessReader.cs index 518275d..2e2f947 100644 --- a/WhiteMagic/InProcessReader.cs +++ b/WhiteMagic/InProcessReader.cs @@ -6,8 +6,11 @@ namespace WhiteMagic; /// /// In-process memory reader that accesses the owning process's memory through -/// direct pointer dereference (unsafe). Use this reader from within a -/// managed DLL injected into the target process. +/// and +/// on a handle to the current +/// process. Unlike the unsafe-deref approach, this fails softly (returns +/// empty / zero bytes) on invalid or protected addresses instead of crashing +/// the host process with an . /// public sealed class InProcessReader : MemoryBase { @@ -25,6 +28,12 @@ public sealed class InProcessReader : MemoryBase ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation, false, current.Id); + if (_handle.IsInvalid) + { + int error = Marshal.GetLastPInvokeError(); + throw new InvalidOperationException( + $"OpenProcess failed for PID {current.Id}: error {error}"); + } _imageBase = current.MainModule?.BaseAddress ?? IntPtr.Zero; } @@ -36,30 +45,37 @@ public sealed class InProcessReader : MemoryBase public override SafeMemoryHandle Handle => _handle; /// - public override unsafe byte[] ReadBytes(IntPtr address, int count, bool isRelative = false) + public override byte[] ReadBytes(IntPtr address, int count, bool isRelative = false) { if (isRelative) address = GetAbsolute(address); byte[] buffer = new byte[count]; - fixed (byte* ptr = buffer) + if (!NativeMethods.ReadProcessMemory(_handle, address, buffer, count, out nint bytesRead)) { - Buffer.MemoryCopy((void*)address, ptr, count, count); + return []; } + + if ((int)bytesRead != count) + { + Array.Resize(ref buffer, (int)bytesRead); + } + return buffer; } /// - public override unsafe int WriteBytes(IntPtr address, ReadOnlySpan bytes, bool isRelative = false) + public override int WriteBytes(IntPtr address, ReadOnlySpan bytes, bool isRelative = false) { if (isRelative) address = GetAbsolute(address); - fixed (byte* ptr = bytes) + if (!NativeMethods.WriteProcessMemory(_handle, address, bytes, bytes.Length, out nint written)) { - Buffer.MemoryCopy(ptr, (void*)address, bytes.Length, bytes.Length); + return 0; } - return bytes.Length; + + return (int)written; } /// diff --git a/WhiteMagic/MemoryBase.cs b/WhiteMagic/MemoryBase.cs index 931e8da..88f59d6 100644 --- a/WhiteMagic/MemoryBase.cs +++ b/WhiteMagic/MemoryBase.cs @@ -30,6 +30,8 @@ public abstract class MemoryBase : IDisposable // ── Typed IO ─────────────────────────────────────────────────────────── /// Reads a value of type from the target address. + /// The value, or default(T) when the read fails or returns fewer bytes than + /// . public T Read(IntPtr address, bool isRelative = false) where T : struct { if (isRelative) @@ -38,10 +40,11 @@ public abstract class MemoryBase : IDisposable int size = MarshalCache.Size; byte[] raw = ReadBytes(address, size); + if (raw.Length < size) + return default; + if (MarshalCache.TypeRequiresMarshal) - { return MarshalByteArrayToStructure(raw); - } return MemoryMarshal.Read(raw.AsSpan()); } @@ -57,9 +60,7 @@ public abstract class MemoryBase : IDisposable byte[] raw; if (MarshalCache.TypeRequiresMarshal) - { raw = StructureToByteArray(value, size); - } else { raw = new byte[size]; @@ -71,6 +72,8 @@ public abstract class MemoryBase : IDisposable } /// Reads an array of values of type from the target address. + /// An array of at most elements. May be shorter when the read + /// returns fewer bytes than expected. public T[] Read(IntPtr address, int count, bool isRelative = false) where T : struct { if (isRelative) @@ -79,24 +82,32 @@ public abstract class MemoryBase : IDisposable int elementSize = MarshalCache.Size; int totalSize = elementSize * count; byte[] raw = ReadBytes(address, totalSize); + int actualCount = Math.Min(count, raw.Length / elementSize); - var result = new T[count]; + var result = new T[actualCount]; + + if (actualCount == 0) + return result; if (MarshalCache.TypeRequiresMarshal) { - for (int i = 0; i < count; i++) + GCHandle pin = GCHandle.Alloc(raw, GCHandleType.Pinned); + try { - var elementBytes = new ReadOnlySpan(raw, i * elementSize, elementSize); - result[i] = MarshalByteArrayToStructure(elementBytes.ToArray()); + IntPtr basePtr = pin.AddrOfPinnedObject(); + for (int i = 0; i < actualCount; i++) + result[i] = Marshal.PtrToStructure(basePtr + (i * elementSize)); + } + finally + { + pin.Free(); } } else { ReadOnlySpan span = raw; - for (int i = 0; i < count; i++) - { + for (int i = 0; i < actualCount; i++) result[i] = MemoryMarshal.Read(span.Slice(i * elementSize, elementSize)); - } } return result; @@ -121,13 +132,9 @@ public abstract class MemoryBase : IDisposable { 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); @@ -136,17 +143,61 @@ public abstract class MemoryBase : IDisposable // ── String IO ────────────────────────────────────────────────────────── - /// Reads a null-terminated string from the target address. + /// Reads a null-terminated string from the target address by scanning in small + /// chunks. Stops at the null terminator, the maximum length, or the first page boundary + /// that fails to read (avoids an atomic failure when a 512-byte window crosses an unmapped + /// region). + /// The address to read from. + /// The text encoding. + /// The maximum number of bytes to read. + /// If , is relative + /// to . public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false) { - byte[] buffer = ReadBytes(address, maxLength, relative); - string decoded = encoding.GetString(buffer); - int nullIndex = decoded.IndexOf('\0'); - if (nullIndex >= 0) + if (relative) + address = GetAbsolute(address); + + // The encoded null terminator. For ASCII/UTF-8 this is a single 0x00 byte; + // for UTF-16 it is two zero bytes (0x00 0x00); for UTF-32 it is four. + byte[] nullTerminator = encoding.GetBytes("\0"); + + const int chunkSize = 64; + int remaining = maxLength; + var accumulated = new System.Collections.Generic.List(); + + while (remaining > 0) { - return decoded[..nullIndex]; + int take = Math.Min(chunkSize, remaining); + byte[] chunk = ReadBytes(address, take); + if (chunk.Length == 0) + break; + + int nullPos = IndexOfPattern(chunk, nullTerminator); + if (nullPos >= 0) + { + if (nullPos > 0) + accumulated.Add(chunk[..nullPos]); + break; + } + + accumulated.Add(chunk); + address += take; + remaining -= take; } - return decoded; + + int totalLength = 0; + foreach (byte[] part in accumulated) + totalLength += part.Length; + + byte[] combined = new byte[totalLength]; + int offset = 0; + foreach (byte[] part in accumulated) + { + part.CopyTo(combined, offset); + offset += part.Length; + } + + return encoding.GetString(combined); } /// Writes a null-terminated string to the target address. @@ -168,10 +219,11 @@ public abstract class MemoryBase : IDisposable return ImageBase + (nint)relative; } - /// Converts an absolute address to a relative offset from . + /// Converts an absolute address to a relative offset from . + /// This is the inverse of : GetAbsolute(GetRelative(a)) == a. public IntPtr GetRelative(IntPtr absolute) { - return (IntPtr)((nint)ImageBase - (nint)absolute); + return (IntPtr)((nint)absolute - (nint)ImageBase); } // ── Lifecycle ────────────────────────────────────────────────────────── @@ -214,16 +266,27 @@ public abstract class MemoryBase : IDisposable 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 + byte[] bytes = StructureToByteArray(value, size); + bytes.CopyTo(destination); + } + + private static int IndexOfPattern(byte[] data, byte[] pattern) + { + int lastStart = data.Length - pattern.Length; + for (int i = 0; i <= lastStart; i++) { - Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false); - temp.CopyTo(destination); - } - finally - { - pin.Free(); + bool match = true; + for (int j = 0; j < pattern.Length; j++) + { + if (data[i + j] != pattern[j]) + { + match = false; + break; + } + } + if (match) + return i; } + return -1; } } diff --git a/WhiteMagicTest/AddressingTests.cs b/WhiteMagicTest/AddressingTests.cs index 3245817..77ea5c2 100644 --- a/WhiteMagicTest/AddressingTests.cs +++ b/WhiteMagicTest/AddressingTests.cs @@ -7,6 +7,8 @@ namespace WhiteMagicTest; /// /// Tests for relative/absolute addressing in . +/// GetAbsolute(relative) = ImageBase + relative. +/// GetRelative(absolute) = absolute - ImageBase (inverse of GetAbsolute). /// public class AddressingTests { @@ -27,31 +29,45 @@ public class AddressingTests } [Fact] - public void GetRelative_computes_offset_from_image_base() + public void GetRelative_returns_absolute_minus_image_base() { using var reader = OpenSelf(); IntPtr imageBase = reader.ImageBase; IntPtr absolute = imageBase + 0x2000; IntPtr relative = reader.GetRelative(absolute); - // GetRelative returns ImageBase - absolute (GreyMagic convention) - Assert.Equal((IntPtr)((int)imageBase - (int)absolute), relative); + Assert.Equal((IntPtr)((nint)absolute - (nint)imageBase), relative); } [Fact] - public void GetAbsolute_after_GetRelative_at_image_base_returns_to_base() + public void GetAbsolute_and_GetRelative_are_inverses() { using var reader = OpenSelf(); - IntPtr atBase = reader.ImageBase; - IntPtr relative = reader.GetRelative(atBase); - IntPtr back = reader.GetAbsolute(relative); - Assert.Equal(atBase, back); + IntPtr offset = (IntPtr)0x3000; + + // Round-trip: offset -> absolute -> back to offset + IntPtr absolute = reader.GetAbsolute(offset); + IntPtr back = reader.GetRelative(absolute); + Assert.Equal(offset, back); + + // Reverse round-trip: absolute -> offset -> back to absolute + IntPtr relative = reader.GetRelative(absolute); + IntPtr absoluteAgain = reader.GetAbsolute(relative); + Assert.Equal(absolute, absoluteAgain); + } + + [Fact] + public void GetRelative_on_ImageBase_returns_zero() + { + using var reader = OpenSelf(); + IntPtr relative = reader.GetRelative(reader.ImageBase); + Assert.Equal(IntPtr.Zero, relative); } [Fact] public void Read_with_isRelative_true_uses_image_base() { using var reader = OpenSelf(); - // Read the first byte at ImageBase (should be MZ header: 0x4D = 'M') + // DOS header 'MZ' at the image base byte firstByte = reader.Read(IntPtr.Zero, isRelative: true); Assert.Equal(0x4D, firstByte); } @@ -65,10 +81,9 @@ public class AddressingTests try { IntPtr absolute = pin.AddrOfPinnedObject(); - nint relative = (nint)absolute - (nint)reader.ImageBase; - IntPtr relativePtr = (IntPtr)relative; + IntPtr relative = reader.GetRelative(absolute); - Assert.True(reader.Write(relativePtr, 42, isRelative: true)); + Assert.True(reader.Write(relative, 42, isRelative: true)); Assert.Equal(42, reader.Read(absolute)); } finally @@ -81,7 +96,6 @@ public class AddressingTests public void ReadBytes_with_isRelative_true_resolves_correctly() { using var reader = OpenSelf(); - // DOS header 'MZ' at the image base byte[] data = reader.ReadBytes(IntPtr.Zero, 2, isRelative: true); Assert.Equal(0x4D, data[0]); Assert.Equal(0x5A, data[1]); diff --git a/WhiteMagicTest/MemoryBaseTests.cs b/WhiteMagicTest/MemoryBaseTests.cs index 223ba5a..afc177f 100644 --- a/WhiteMagicTest/MemoryBaseTests.cs +++ b/WhiteMagicTest/MemoryBaseTests.cs @@ -1,8 +1,8 @@ -using WhiteMagic.Native; using System.Diagnostics; using System.Runtime.InteropServices; using System.Text; using WhiteMagic; +using WhiteMagic.Native; namespace WhiteMagicTest; @@ -31,7 +31,6 @@ public class MemoryBaseTests { 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 @@ -91,10 +90,7 @@ public class MemoryBaseTests 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); @@ -140,7 +136,6 @@ public class MemoryBaseTests int[] expected = [10, 20, 30, 40]; Assert.True(reader.Write(addr, expected)); - int[] actual = reader.Read(addr, 4); Assert.Equal(expected, actual); } @@ -168,7 +163,6 @@ public class MemoryBaseTests }; Assert.True(reader.Write(addr, expected)); - var actual = reader.Read(addr, 4); Assert.Equal(expected, actual); } @@ -199,7 +193,39 @@ public class MemoryBaseTests { var reader = OpenSelf(); reader.Dispose(); - reader.Dispose(); // Should not throw + reader.Dispose(); + } + + // ── Graceful failure on invalid addresses ─────────────────────────────── + + [Fact] + public void Read_int_on_invalid_address_returns_default() + { + using var reader = OpenSelf(); + Assert.Equal(0, reader.Read(IntPtr.Zero)); + } + + [Fact] + public void Read_struct_on_invalid_address_returns_default() + { + using var reader = OpenSelf(); + var result = reader.Read(IntPtr.Zero); + Assert.Equal(0, result.X); + Assert.Equal(0, result.Y); + } + + [Fact] + public void Read_int_array_on_invalid_address_returns_empty() + { + using var reader = OpenSelf(); + Assert.Empty(reader.Read(IntPtr.Zero, 10)); + } + + [Fact] + public void Read_bytes_on_invalid_address_returns_empty() + { + using var reader = OpenSelf(); + Assert.Empty(reader.ReadBytes(IntPtr.Zero, 10)); } }