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:
kbe
2026-07-21 17:05:29 +02:00
parent ccde012e45
commit 6b34e0fab8
3 changed files with 520 additions and 0 deletions
+86
View File
@@ -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();
}
}
}
+215
View File
@@ -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();
}
}
}