Merge feature/native-surface: native P/Invoke surface + Phase 2 core memory
Task 1.4 (Native/ LibraryImport surface, SafeMemoryHandle) and Phase 2 (MarshalCache, MemoryBase, ExternalReader, InProcessReader, string IO, addressing). Reviewed high-effort: 4 correctness + 3 cleanup fixed and verified. Deviation D1 (InProcessReader RPM-on-self) reconciled in spec. Known follow-up: task 2.10 (ReadString UTF-16 null alignment). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,4 @@ reference/
|
||||
|
||||
# Scratch
|
||||
*.tmp
|
||||
*.log
|
||||
|
||||
@@ -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,90 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// In-process memory reader that accesses the owning process's memory through
|
||||
/// <see cref="NativeMethods.ReadProcessMemory"/> and
|
||||
/// <see cref="NativeMethods.WriteProcessMemory"/> 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 <see cref="AccessViolationException"/>.
|
||||
/// </summary>
|
||||
public sealed class InProcessReader : MemoryBase
|
||||
{
|
||||
private readonly SafeMemoryHandle _handle;
|
||||
private readonly IntPtr _imageBase;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an in-process reader for the current process.
|
||||
/// </summary>
|
||||
public InProcessReader()
|
||||
{
|
||||
Process current = Process.GetCurrentProcess();
|
||||
_handle = NativeMethods.OpenProcess(
|
||||
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;
|
||||
}
|
||||
|
||||
/// <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,69 @@
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// Computes and caches marshal-related metadata for type <typeparamref name="T"/>
|
||||
/// exactly once. <see cref="MemoryBase.Read{T}"/> and <see cref="MemoryBase.Write{T}"/>
|
||||
/// branch on these cached flags to decide between blittable <c>Span</c>/<c>MemoryMarshal</c>
|
||||
/// paths and the fallback marshal path.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type to cache metadata for.</typeparam>
|
||||
public static class MarshalCache<T>
|
||||
{
|
||||
/// <summary>The unmanaged size of <typeparamref name="T"/> in bytes.</summary>
|
||||
public static readonly int Size;
|
||||
|
||||
/// <summary>The unmanaged size of <typeparamref name="T"/> as an unsigned integer.</summary>
|
||||
public static readonly uint SizeU;
|
||||
|
||||
/// <summary>
|
||||
/// <see langword="true"/> when <typeparamref name="T"/> has at least one field
|
||||
/// decorated with <see cref="MarshalAsAttribute"/>, meaning it cannot be copied
|
||||
/// via a simple pointer dereference.
|
||||
/// </summary>
|
||||
public static readonly bool TypeRequiresMarshal;
|
||||
|
||||
/// <summary><see langword="true"/> when <typeparamref name="T"/> is <see cref="IntPtr"/>.</summary>
|
||||
public static readonly bool IsIntPtr;
|
||||
|
||||
/// <summary>The underlying type code of <typeparamref name="T"/>.</summary>
|
||||
public static readonly TypeCode TypeCode;
|
||||
|
||||
/// <summary>
|
||||
/// The effective type that the marshaler uses. For an enum this is the underlying
|
||||
/// integer type; for all other types it is <typeparamref name="T"/> itself.
|
||||
/// </summary>
|
||||
public static readonly Type RealType;
|
||||
|
||||
static MarshalCache()
|
||||
{
|
||||
TypeCode = Type.GetTypeCode(typeof(T));
|
||||
|
||||
if (typeof(T) == typeof(bool))
|
||||
{
|
||||
Size = 1;
|
||||
RealType = typeof(T);
|
||||
}
|
||||
else if (typeof(T).IsEnum)
|
||||
{
|
||||
Type underlying = typeof(T).GetEnumUnderlyingType();
|
||||
Size = Marshal.SizeOf(underlying);
|
||||
RealType = underlying;
|
||||
TypeCode = Type.GetTypeCode(underlying);
|
||||
}
|
||||
else
|
||||
{
|
||||
Size = Marshal.SizeOf(typeof(T));
|
||||
RealType = typeof(T);
|
||||
}
|
||||
|
||||
SizeU = (uint)Size;
|
||||
IsIntPtr = RealType == typeof(IntPtr);
|
||||
|
||||
TypeRequiresMarshal =
|
||||
RealType.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
.Any(f => f.GetCustomAttributes(typeof(MarshalAsAttribute), true).Length != 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,292 @@
|
||||
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>
|
||||
/// <returns>The value, or <c>default(T)</c> when the read fails or returns fewer bytes than
|
||||
/// <see cref="MarshalCache{T}.Size"/>.</returns>
|
||||
public T Read<T>(IntPtr address, bool isRelative = false) where T : struct
|
||||
{
|
||||
if (isRelative)
|
||||
address = GetAbsolute(address);
|
||||
|
||||
int size = MarshalCache<T>.Size;
|
||||
byte[] raw = ReadBytes(address, size);
|
||||
|
||||
if (raw.Length < size)
|
||||
return default;
|
||||
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
return MarshalByteArrayToStructure<T>(raw);
|
||||
|
||||
return MemoryMarshal.Read<T>(raw.AsSpan());
|
||||
}
|
||||
|
||||
/// <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;
|
||||
|
||||
byte[] raw;
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
raw = StructureToByteArray(value, size);
|
||||
else
|
||||
{
|
||||
raw = new byte[size];
|
||||
MemoryMarshal.Write(raw.AsSpan(), in value);
|
||||
}
|
||||
|
||||
int written = WriteBytes(address, raw, false);
|
||||
return written == size;
|
||||
}
|
||||
|
||||
/// <summary>Reads an array of values of type <typeparamref name="T"/> from the target address.</summary>
|
||||
/// <returns>An array of at most <paramref name="count"/> elements. May be shorter when the read
|
||||
/// returns fewer bytes than expected.</returns>
|
||||
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);
|
||||
int actualCount = Math.Min(count, raw.Length / elementSize);
|
||||
|
||||
var result = new T[actualCount];
|
||||
|
||||
if (actualCount == 0)
|
||||
return result;
|
||||
|
||||
if (MarshalCache<T>.TypeRequiresMarshal)
|
||||
{
|
||||
GCHandle pin = GCHandle.Alloc(raw, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr basePtr = pin.AddrOfPinnedObject();
|
||||
for (int i = 0; i < actualCount; i++)
|
||||
result[i] = Marshal.PtrToStructure<T>(basePtr + (i * elementSize));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadOnlySpan<byte> span = raw;
|
||||
for (int i = 0; i < actualCount; 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 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).</summary>
|
||||
/// <param name="address">The address to read from.</param>
|
||||
/// <param name="encoding">The text encoding.</param>
|
||||
/// <param name="maxLength">The maximum number of bytes to read.</param>
|
||||
/// <param name="relative">If <see langword="true"/>, <paramref name="address"/> is relative
|
||||
/// to <see cref="ImageBase"/>.</param>
|
||||
public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false)
|
||||
{
|
||||
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<byte[]>();
|
||||
|
||||
while (remaining > 0)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Writes a null-terminated string to the target address.</summary>
|
||||
public virtual bool WriteString(IntPtr address, string value, Encoding encoding, bool relative = false)
|
||||
{
|
||||
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 + (nint)relative;
|
||||
}
|
||||
|
||||
/// <summary>Converts an absolute address to a relative offset from <see cref="ImageBase"/>.
|
||||
/// This is the inverse of <see cref="GetAbsolute"/>: <c>GetAbsolute(GetRelative(a)) == a</c>.</summary>
|
||||
public IntPtr GetRelative(IntPtr absolute)
|
||||
{
|
||||
return (IntPtr)((nint)absolute - (nint)ImageBase);
|
||||
}
|
||||
|
||||
// ── 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 byte[] StructureToByteArray<T>(T value, int size) where T : struct
|
||||
{
|
||||
byte[] bytes = new byte[size];
|
||||
GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(value, pin.AddrOfPinnedObject(), false);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
return bytes;
|
||||
}
|
||||
|
||||
private static void StructureToByteArray<T>(T value, Span<byte> destination, int size) where T : struct
|
||||
{
|
||||
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++)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Access rights that open a process object.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ProcessAccess : uint
|
||||
{
|
||||
/// <summary>The right to terminate the process with TerminateProcess.</summary>
|
||||
Terminate = 0x0001,
|
||||
/// <summary>The right to create a thread in the process.</summary>
|
||||
CreateThread = 0x0002,
|
||||
/// <summary>The right to operate on the address space of the process.</summary>
|
||||
VmOperation = 0x0008,
|
||||
/// <summary>The right to read memory with ReadProcessMemory.</summary>
|
||||
VmRead = 0x0010,
|
||||
/// <summary>The right to write memory with WriteProcessMemory.</summary>
|
||||
VmWrite = 0x0020,
|
||||
/// <summary>The right to duplicate a handle with DuplicateHandle.</summary>
|
||||
DupHandle = 0x0040,
|
||||
/// <summary>The right to set information about the process.</summary>
|
||||
SetInformation = 0x0200,
|
||||
/// <summary>The right to read information about the process, such as the exit code.</summary>
|
||||
QueryInformation = 0x0400,
|
||||
/// <summary>The right to suspend or resume the process.</summary>
|
||||
SuspendResume = 0x0800,
|
||||
/// <summary>The right to read a limited set of information about the process.</summary>
|
||||
QueryLimitedInformation = 0x1000,
|
||||
/// <summary>The right to use the process object for synchronization.</summary>
|
||||
Synchronize = 0x00100000,
|
||||
|
||||
/// <summary>All access rights for a process object.</summary>
|
||||
AllAccess = 0x001F0000 | Synchronize | 0xFFFF,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that control how VirtualAllocEx allocates memory.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MemoryAllocationType : uint
|
||||
{
|
||||
/// <summary>Commit physical storage for the reserved pages. The pages start as zero.</summary>
|
||||
Commit = 0x00001000,
|
||||
/// <summary>Reserve a range of address space without physical storage.</summary>
|
||||
Reserve = 0x00002000,
|
||||
/// <summary>Reset the data in the range to indicate that it is no longer of interest.</summary>
|
||||
Reset = 0x00080000,
|
||||
/// <summary>Allocate memory at the highest possible address.</summary>
|
||||
TopDown = 0x00100000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that protect a block of memory.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MemoryProtectionType : uint
|
||||
{
|
||||
/// <summary>No access to the committed pages.</summary>
|
||||
NoAccess = 0x01,
|
||||
/// <summary>Read access to the committed pages.</summary>
|
||||
ReadOnly = 0x02,
|
||||
/// <summary>Read and write access to the committed pages.</summary>
|
||||
ReadWrite = 0x04,
|
||||
/// <summary>Copy-on-write access to the committed pages.</summary>
|
||||
WriteCopy = 0x08,
|
||||
/// <summary>Execute access to the committed pages.</summary>
|
||||
Execute = 0x10,
|
||||
/// <summary>Execute and read access to the committed pages.</summary>
|
||||
ExecuteRead = 0x20,
|
||||
/// <summary>Execute, read, and write access to the committed pages.</summary>
|
||||
ExecuteReadWrite = 0x40,
|
||||
/// <summary>Execute and copy-on-write access to the committed pages.</summary>
|
||||
ExecuteWriteCopy = 0x80,
|
||||
/// <summary>The pages in the range become guard pages.</summary>
|
||||
Guard = 0x100,
|
||||
/// <summary>The system does not cache the committed pages.</summary>
|
||||
NoCache = 0x200,
|
||||
/// <summary>The system uses write-combined access for the pages.</summary>
|
||||
WriteCombine = 0x400,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that control how VirtualFreeEx frees memory.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MemoryFreeType : uint
|
||||
{
|
||||
/// <summary>Decommit the committed pages. The address range stays reserved.</summary>
|
||||
Decommit = 0x4000,
|
||||
/// <summary>Release the range of pages. The size must be zero.</summary>
|
||||
Release = 0x8000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that set the initial state of a new thread.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ThreadCreationFlags : uint
|
||||
{
|
||||
/// <summary>The thread runs immediately after creation.</summary>
|
||||
RunImmediately = 0,
|
||||
/// <summary>The thread starts in a suspended state. Call ResumeThread to start it.</summary>
|
||||
CreateSuspended = 0x00000004,
|
||||
/// <summary>The stack-size parameter sets the reserve size of the stack.</summary>
|
||||
StackSizeParamIsAReservation = 0x00010000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flags that select the registers that the thread-context functions read or write.
|
||||
/// There are separate constants for 32-bit (x86/WOW64) and 64-bit (AMD64) contexts.
|
||||
/// </summary>
|
||||
public static class ContextFlags
|
||||
{
|
||||
/// <summary>Architecture identifier for x86 contexts.</summary>
|
||||
public const uint X86 = 0x00010000;
|
||||
/// <summary>Architecture identifier for AMD64 contexts.</summary>
|
||||
public const uint Amd64 = 0x00100000;
|
||||
|
||||
/// <summary>x86: SS:SP, CS:IP, FLAGS, and BP.</summary>
|
||||
public const uint X86Control = X86 | 0x01;
|
||||
/// <summary>x86: AX, BX, CX, DX, SI, and DI.</summary>
|
||||
public const uint X86Integer = X86 | 0x02;
|
||||
/// <summary>x86: DS, ES, FS, and GS.</summary>
|
||||
public const uint X86Segments = X86 | 0x04;
|
||||
/// <summary>x86: control, integer, and segment registers.</summary>
|
||||
public const uint X86Full = X86Control | X86Integer | X86Segments;
|
||||
|
||||
/// <summary>AMD64: SegSs, Rsp, SegCs, Rip, and EFlags.</summary>
|
||||
public const uint Amd64Control = Amd64 | 0x01;
|
||||
/// <summary>AMD64: Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi, and R8 to R15.</summary>
|
||||
public const uint Amd64Integer = Amd64 | 0x02;
|
||||
/// <summary>AMD64: SegDs, SegEs, SegFs, and SegGs.</summary>
|
||||
public const uint Amd64Segments = Amd64 | 0x04;
|
||||
/// <summary>AMD64: control, integer, and segment registers.</summary>
|
||||
public const uint Amd64Full = Amd64Control | Amd64Integer | Amd64Segments;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// P/Invoke declarations for the Win32 process, memory, thread, and module
|
||||
/// APIs that WhiteMagic uses. Every declaration uses <see cref="LibraryImportAttribute"/>
|
||||
/// (source-generated interop). SetLastError is enabled on all calls that the
|
||||
/// Win32 API documents as setting a thread-local last-error value.
|
||||
/// </summary>
|
||||
internal static partial class NativeMethods
|
||||
{
|
||||
// ── Process ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Opens an existing process and returns a handle to it.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial SafeMemoryHandle OpenProcess(
|
||||
ProcessAccess desiredAccess,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool inheritHandle,
|
||||
int processId);
|
||||
|
||||
/// <summary>Closes an open object handle.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool CloseHandle(IntPtr handle);
|
||||
|
||||
// ── Memory ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reads memory from a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool ReadProcessMemory(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr baseAddress,
|
||||
Span<byte> buffer,
|
||||
int size,
|
||||
out nint bytesRead);
|
||||
|
||||
/// <summary>Writes memory to a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool WriteProcessMemory(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr baseAddress,
|
||||
ReadOnlySpan<byte> buffer,
|
||||
int size,
|
||||
out nint bytesWritten);
|
||||
|
||||
/// <summary>Reserves or commits a region of memory in a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial IntPtr VirtualAllocEx(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr address,
|
||||
nint size,
|
||||
MemoryAllocationType allocationType,
|
||||
MemoryProtectionType protect);
|
||||
|
||||
/// <summary>Changes the protection on a committed region of memory.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool VirtualProtectEx(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr address,
|
||||
nint size,
|
||||
MemoryProtectionType newProtect,
|
||||
out MemoryProtectionType oldProtect);
|
||||
|
||||
/// <summary>Releases or decommits a region of memory in a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool VirtualFreeEx(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr address,
|
||||
nint size,
|
||||
MemoryFreeType freeType);
|
||||
|
||||
// ── Threading ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Creates a thread that runs in the virtual address space of a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial SafeMemoryHandle CreateRemoteThread(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr threadAttributes,
|
||||
nint stackSize,
|
||||
IntPtr startAddress,
|
||||
IntPtr parameter,
|
||||
ThreadCreationFlags creationFlags,
|
||||
out int threadId);
|
||||
|
||||
/// <summary>Sets a 64-bit thread context (AMD64).</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context64 context);
|
||||
|
||||
/// <summary>Gets a 64-bit thread context (AMD64).</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context64 context);
|
||||
|
||||
/// <summary>Sets a 32-bit (WOW64) thread context.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Wow64SetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context32 context);
|
||||
|
||||
/// <summary>Gets a 32-bit (WOW64) thread context.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Wow64GetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context32 context);
|
||||
|
||||
// ── Modules ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Loads a module into the calling process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryW")]
|
||||
internal static partial IntPtr LoadLibrary(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
|
||||
|
||||
/// <summary>Returns the address of a function or variable from a loaded module.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial IntPtr GetProcAddress(
|
||||
IntPtr hModule,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string lpProcName);
|
||||
|
||||
/// <summary>Waits until a thread exits and retrieves its exit code.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial int WaitForSingleObject(
|
||||
SafeMemoryHandle handle,
|
||||
uint milliseconds);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// The x87 and MMX state inside a 32-bit thread context.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct FloatingSaveArea32
|
||||
{
|
||||
/// <summary>The x87 FPU control word.</summary>
|
||||
public uint ControlWord;
|
||||
/// <summary>The x87 FPU status word.</summary>
|
||||
public uint StatusWord;
|
||||
/// <summary>The x87 FPU tag word.</summary>
|
||||
public uint TagWord;
|
||||
/// <summary>The offset of the instruction that caused the last FPU exception.</summary>
|
||||
public uint ErrorOffset;
|
||||
/// <summary>The selector of the instruction that caused the last FPU exception.</summary>
|
||||
public uint ErrorSelector;
|
||||
/// <summary>The offset of the operand that caused the last FPU exception.</summary>
|
||||
public uint DataOffset;
|
||||
/// <summary>The selector of the operand that caused the last FPU exception.</summary>
|
||||
public uint DataSelector;
|
||||
/// <summary>The 80-byte register area.</summary>
|
||||
public fixed byte RegisterArea[80];
|
||||
/// <summary>The CR0 numeric-processor-extension state.</summary>
|
||||
public uint Cr0NpxState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 32-bit (x86/WOW64) thread context. Use it with
|
||||
/// <c>Wow64GetThreadContext</c> and <c>Wow64SetThreadContext</c> to inspect a 32-bit thread.
|
||||
/// The total size is 716 bytes.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct Context32
|
||||
{
|
||||
/// <summary>Selects which parts of the context are valid. See <see cref="ContextFlags"/>.</summary>
|
||||
public uint ContextFlags;
|
||||
|
||||
/// <summary>Debug register 0.</summary>
|
||||
public uint Dr0;
|
||||
/// <summary>Debug register 1.</summary>
|
||||
public uint Dr1;
|
||||
/// <summary>Debug register 2.</summary>
|
||||
public uint Dr2;
|
||||
/// <summary>Debug register 3.</summary>
|
||||
public uint Dr3;
|
||||
/// <summary>Debug register 6.</summary>
|
||||
public uint Dr6;
|
||||
/// <summary>Debug register 7.</summary>
|
||||
public uint Dr7;
|
||||
|
||||
/// <summary>The floating-point state.</summary>
|
||||
public FloatingSaveArea32 FloatSave;
|
||||
|
||||
/// <summary>The GS segment.</summary>
|
||||
public uint SegGs;
|
||||
/// <summary>The FS segment.</summary>
|
||||
public uint SegFs;
|
||||
/// <summary>The ES segment.</summary>
|
||||
public uint SegEs;
|
||||
/// <summary>The DS segment.</summary>
|
||||
public uint SegDs;
|
||||
|
||||
/// <summary>The EDI register.</summary>
|
||||
public uint Edi;
|
||||
/// <summary>The ESI register.</summary>
|
||||
public uint Esi;
|
||||
/// <summary>The EBX register.</summary>
|
||||
public uint Ebx;
|
||||
/// <summary>The EDX register.</summary>
|
||||
public uint Edx;
|
||||
/// <summary>The ECX register.</summary>
|
||||
public uint Ecx;
|
||||
/// <summary>The EAX register.</summary>
|
||||
public uint Eax;
|
||||
|
||||
/// <summary>The base (frame) pointer.</summary>
|
||||
public uint Ebp;
|
||||
/// <summary>The instruction pointer.</summary>
|
||||
public uint Eip;
|
||||
/// <summary>The CS segment.</summary>
|
||||
public uint SegCs;
|
||||
/// <summary>The flags register.</summary>
|
||||
public uint EFlags;
|
||||
/// <summary>The stack pointer.</summary>
|
||||
public uint Esp;
|
||||
/// <summary>The SS segment.</summary>
|
||||
public uint SegSs;
|
||||
|
||||
/// <summary>The extended (processor-specific) registers. The size is 512 bytes.</summary>
|
||||
public fixed byte ExtendedRegisters[512];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 64-bit (AMD64) thread context. Use it with the native
|
||||
/// <c>GetThreadContext</c> and <c>SetThreadContext</c> from a 64-bit process.
|
||||
/// The structure needs 16-byte alignment. The total size is 1232 bytes.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 16)]
|
||||
public unsafe struct Context64
|
||||
{
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P1Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P2Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P3Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P4Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P5Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P6Home;
|
||||
|
||||
/// <summary>Selects which parts of the context are valid. See <see cref="ContextFlags"/>.</summary>
|
||||
public uint ContextFlags;
|
||||
/// <summary>The MXCSR register.</summary>
|
||||
public uint MxCsr;
|
||||
|
||||
/// <summary>The CS segment.</summary>
|
||||
public ushort SegCs;
|
||||
/// <summary>The DS segment.</summary>
|
||||
public ushort SegDs;
|
||||
/// <summary>The ES segment.</summary>
|
||||
public ushort SegEs;
|
||||
/// <summary>The FS segment.</summary>
|
||||
public ushort SegFs;
|
||||
/// <summary>The GS segment.</summary>
|
||||
public ushort SegGs;
|
||||
/// <summary>The SS segment.</summary>
|
||||
public ushort SegSs;
|
||||
|
||||
/// <summary>The flags register.</summary>
|
||||
public uint EFlags;
|
||||
|
||||
/// <summary>Debug register 0.</summary>
|
||||
public ulong Dr0;
|
||||
/// <summary>Debug register 1.</summary>
|
||||
public ulong Dr1;
|
||||
/// <summary>Debug register 2.</summary>
|
||||
public ulong Dr2;
|
||||
/// <summary>Debug register 3.</summary>
|
||||
public ulong Dr3;
|
||||
/// <summary>Debug register 6.</summary>
|
||||
public ulong Dr6;
|
||||
/// <summary>Debug register 7.</summary>
|
||||
public ulong Dr7;
|
||||
|
||||
/// <summary>The RAX register.</summary>
|
||||
public ulong Rax;
|
||||
/// <summary>The RCX register.</summary>
|
||||
public ulong Rcx;
|
||||
/// <summary>The RDX register.</summary>
|
||||
public ulong Rdx;
|
||||
/// <summary>The RBX register.</summary>
|
||||
public ulong Rbx;
|
||||
/// <summary>The stack pointer.</summary>
|
||||
public ulong Rsp;
|
||||
/// <summary>The base (frame) pointer.</summary>
|
||||
public ulong Rbp;
|
||||
/// <summary>The RSI register.</summary>
|
||||
public ulong Rsi;
|
||||
/// <summary>The RDI register.</summary>
|
||||
public ulong Rdi;
|
||||
/// <summary>The R8 register.</summary>
|
||||
public ulong R8;
|
||||
/// <summary>The R9 register.</summary>
|
||||
public ulong R9;
|
||||
/// <summary>The R10 register.</summary>
|
||||
public ulong R10;
|
||||
/// <summary>The R11 register.</summary>
|
||||
public ulong R11;
|
||||
/// <summary>The R12 register.</summary>
|
||||
public ulong R12;
|
||||
/// <summary>The R13 register.</summary>
|
||||
public ulong R13;
|
||||
/// <summary>The R14 register.</summary>
|
||||
public ulong R14;
|
||||
/// <summary>The R15 register.</summary>
|
||||
public ulong R15;
|
||||
|
||||
/// <summary>The instruction pointer.</summary>
|
||||
public ulong Rip;
|
||||
|
||||
/// <summary>The XMM save area. The size is 512 bytes.</summary>
|
||||
public fixed byte FltSave[512];
|
||||
|
||||
/// <summary>The vector registers (26 entries of 16 bytes, stored as 52 entries of 8 bytes).</summary>
|
||||
public fixed ulong VectorRegister[52];
|
||||
|
||||
/// <summary>The vector control register.</summary>
|
||||
public ulong VectorControl;
|
||||
|
||||
/// <summary>The debug-control MSR.</summary>
|
||||
public ulong DebugControl;
|
||||
/// <summary>The target RIP of the last branch.</summary>
|
||||
public ulong LastBranchToRip;
|
||||
/// <summary>The source RIP of the last branch.</summary>
|
||||
public ulong LastBranchFromRip;
|
||||
/// <summary>The target RIP of the last exception.</summary>
|
||||
public ulong LastExceptionToRip;
|
||||
/// <summary>The source RIP of the last exception.</summary>
|
||||
public ulong LastExceptionFromRip;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// A Win32 handle (process, thread, or snapshot) with a managed lifetime.
|
||||
/// The handle closes with <c>CloseHandle</c>, even after an exception or a thread abort.
|
||||
/// </summary>
|
||||
/// <remarks>The pattern comes from MemorySharp's SafeMemoryHandle.</remarks>
|
||||
public sealed class SafeMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes an empty handle. The interop marshaller uses this constructor for a
|
||||
/// handle that a system call returns (for example, <see cref="NativeMethods.OpenProcess"/>).
|
||||
/// </summary>
|
||||
public SafeMemoryHandle() : base(true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a raw handle and takes ownership of the handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle to own.</param>
|
||||
public SafeMemoryHandle(IntPtr handle) : base(true)
|
||||
{
|
||||
SetHandle(handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
return NativeMethods.CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,8 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="WhiteMagicTest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for relative/absolute addressing in <see cref="MemoryBase"/>.
|
||||
/// GetAbsolute(relative) = ImageBase + relative.
|
||||
/// GetRelative(absolute) = absolute - ImageBase (inverse of GetAbsolute).
|
||||
/// </summary>
|
||||
public class AddressingTests
|
||||
{
|
||||
private static ExternalReader OpenSelf()
|
||||
{
|
||||
return new ExternalReader(
|
||||
Process.GetCurrentProcess(),
|
||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAbsolute_resolves_relative_offset()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
IntPtr imageBase = reader.ImageBase;
|
||||
IntPtr result = reader.GetAbsolute((IntPtr)0x1000);
|
||||
Assert.Equal(imageBase + 0x1000, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
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);
|
||||
Assert.Equal((IntPtr)((nint)absolute - (nint)imageBase), relative);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAbsolute_and_GetRelative_are_inverses()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
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();
|
||||
// DOS header 'MZ' at the image base
|
||||
byte firstByte = reader.Read<byte>(IntPtr.Zero, isRelative: true);
|
||||
Assert.Equal(0x4D, firstByte);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_with_isRelative_true_resolves_correctly()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr absolute = pin.AddrOfPinnedObject();
|
||||
IntPtr relative = reader.GetRelative(absolute);
|
||||
|
||||
Assert.True(reader.Write(relative, 42, isRelative: true));
|
||||
Assert.Equal(42, reader.Read<int>(absolute));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadBytes_with_isRelative_true_resolves_correctly()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] data = reader.ReadBytes(IntPtr.Zero, 2, isRelative: true);
|
||||
Assert.Equal(0x4D, data[0]);
|
||||
Assert.Equal(0x5A, data[1]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="InProcessReader"/> — direct pointer dereference against
|
||||
/// the own process. Verifies the shared <see cref="MemoryBase"/> API works for
|
||||
/// both external and in-process readers.
|
||||
/// </summary>
|
||||
public class InProcessReaderTests
|
||||
{
|
||||
private static InProcessReader CreateReader()
|
||||
{
|
||||
return new InProcessReader();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ImageBase_is_nonzero()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
Assert.NotEqual(IntPtr.Zero, reader.ImageBase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_int_reads_known_value_from_own_memory()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
int expected = 0x12345678;
|
||||
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
int result = reader.Read<int>(addr);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_int_writes_and_reads_back()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, unchecked((int)0xCAFEBABE)));
|
||||
Assert.Equal(unchecked((int)0xCAFEBABE), reader.Read<int>(addr));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_bytes_reads_known_bytes()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
byte[] expected = [0x0A, 0x0B, 0x0C, 0x0D];
|
||||
GCHandle pin = GCHandle.Alloc(expected, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
byte[] result = reader.ReadBytes(addr, 4);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_bytes_writes_and_reads_back()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
byte[] slot = new byte[4];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
byte[] expected = [0xDE, 0xAD, 0xBE, 0xEF];
|
||||
|
||||
int written = reader.WriteBytes(addr, expected);
|
||||
Assert.Equal(4, written);
|
||||
|
||||
byte[] result = reader.ReadBytes(addr, 4);
|
||||
Assert.Equal(expected, result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_struct_via_InProcessReader()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
var slot = new TestStruct { X = 10, Y = 20 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
var result = reader.Read<TestStruct>(addr);
|
||||
Assert.Equal(10, result.X);
|
||||
Assert.Equal(20, result.Y);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Write_struct_via_InProcessReader()
|
||||
{
|
||||
using var reader = CreateReader();
|
||||
|
||||
var slot = new TestStruct { X = 1, Y = 2 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.Write(addr, new TestStruct { X = 99, Y = 88 }));
|
||||
var result = reader.Read<TestStruct>(addr);
|
||||
Assert.Equal(99, result.X);
|
||||
Assert.Equal(88, result.Y);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_disposes_handle()
|
||||
{
|
||||
var reader = CreateReader();
|
||||
Assert.False(reader.Handle.IsClosed);
|
||||
reader.Dispose();
|
||||
Assert.True(reader.Handle.IsClosed);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="MarshalCache{T}"/>: blittable size, marshal-required flag,
|
||||
/// IsIntPtr, and computed-once behavior.
|
||||
/// </summary>
|
||||
public class MarshalCacheTests
|
||||
{
|
||||
[Fact]
|
||||
public void Size_for_int_is_4()
|
||||
{
|
||||
Assert.Equal(4, MarshalCache<int>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_byte_is_1()
|
||||
{
|
||||
Assert.Equal(1, MarshalCache<byte>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_IntPtr_matches_native_pointer_size()
|
||||
{
|
||||
Assert.Equal(IntPtr.Size, MarshalCache<IntPtr>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_bool_is_1()
|
||||
{
|
||||
Assert.Equal(1, MarshalCache<bool>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_enum_matches_underlying_type()
|
||||
{
|
||||
Assert.Equal(4, MarshalCache<DayOfWeek>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Size_for_blittable_struct_is_accurate()
|
||||
{
|
||||
Assert.Equal(8, MarshalCache<BlittableStruct>.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeRequiresMarshal_is_false_for_blittable_types()
|
||||
{
|
||||
Assert.False(MarshalCache<int>.TypeRequiresMarshal);
|
||||
Assert.False(MarshalCache<long>.TypeRequiresMarshal);
|
||||
Assert.False(MarshalCache<BlittableStruct>.TypeRequiresMarshal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TypeRequiresMarshal_is_true_for_types_with_MarshalAs_field()
|
||||
{
|
||||
Assert.True(MarshalCache<MarshalAsStruct>.TypeRequiresMarshal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsIntPtr_is_true_for_IntPtr()
|
||||
{
|
||||
Assert.True(MarshalCache<IntPtr>.IsIntPtr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsIntPtr_is_false_for_non_IntPtr_types()
|
||||
{
|
||||
Assert.False(MarshalCache<int>.IsIntPtr);
|
||||
Assert.False(MarshalCache<long>.IsIntPtr);
|
||||
Assert.False(MarshalCache<BlittableStruct>.IsIntPtr);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void All_properties_are_computed_once_and_cached()
|
||||
{
|
||||
int size1 = MarshalCache<int>.Size;
|
||||
bool marshal1 = MarshalCache<int>.TypeRequiresMarshal;
|
||||
bool intPtr1 = MarshalCache<int>.IsIntPtr;
|
||||
|
||||
int size2 = MarshalCache<int>.Size;
|
||||
bool marshal2 = MarshalCache<int>.TypeRequiresMarshal;
|
||||
bool intPtr2 = MarshalCache<int>.IsIntPtr;
|
||||
|
||||
Assert.Equal(size1, size2);
|
||||
Assert.Equal(marshal1, marshal2);
|
||||
Assert.Equal(intPtr1, intPtr2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SizeU_matches_Size_as_uint()
|
||||
{
|
||||
Assert.Equal((uint)MarshalCache<int>.Size, MarshalCache<int>.SizeU);
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BlittableStruct
|
||||
{
|
||||
public int X;
|
||||
public int Y;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MarshalAsStruct
|
||||
{
|
||||
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 16)]
|
||||
public byte[] Data;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
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();
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
|
||||
// ── Graceful failure on invalid addresses ───────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Read_int_on_invalid_address_returns_default()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.Equal(0, reader.Read<int>(IntPtr.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_struct_on_invalid_address_returns_default()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
var result = reader.Read<TestStruct>(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<int>(IntPtr.Zero, 10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Read_bytes_on_invalid_address_returns_empty()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
Assert.Empty(reader.ReadBytes(IntPtr.Zero, 10));
|
||||
}
|
||||
}
|
||||
|
||||
/// <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})";
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests that exercise the P/Invoke surface against the current
|
||||
/// process. They prove the marshalling signatures are correct end-to-end.
|
||||
/// </summary>
|
||||
public class NativeSurfaceTests
|
||||
{
|
||||
private static SafeMemoryHandle OpenSelf(ProcessAccess access)
|
||||
{
|
||||
SafeMemoryHandle handle = NativeMethods.OpenProcess(access, false, Environment.ProcessId);
|
||||
Assert.False(handle.IsInvalid, $"OpenProcess failed: {Marshal.GetLastPInvokeError()}");
|
||||
return handle;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenProcess_on_self_returns_valid_handle_and_closes_on_dispose()
|
||||
{
|
||||
SafeMemoryHandle handle = OpenSelf(ProcessAccess.QueryInformation);
|
||||
Assert.False(handle.IsClosed);
|
||||
handle.Dispose();
|
||||
Assert.True(handle.IsClosed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadProcessMemory_reads_a_known_value_from_own_memory()
|
||||
{
|
||||
int value = 0x1BADB002;
|
||||
GCHandle pin = GCHandle.Alloc(value, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmRead | ProcessAccess.QueryInformation);
|
||||
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
||||
|
||||
bool ok = NativeMethods.ReadProcessMemory(
|
||||
handle, pin.AddrOfPinnedObject(), buffer, buffer.Length, out nint read);
|
||||
|
||||
Assert.True(ok, $"ReadProcessMemory failed: {Marshal.GetLastPInvokeError()}");
|
||||
Assert.Equal(sizeof(int), (int)read);
|
||||
Assert.Equal(value, BitConverter.ToInt32(buffer));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteProcessMemory_writes_a_value_into_own_memory()
|
||||
{
|
||||
int slot = 0;
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
using SafeMemoryHandle handle = OpenSelf(
|
||||
ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
ReadOnlySpan<byte> payload = BitConverter.GetBytes(0x5EED);
|
||||
|
||||
bool ok = NativeMethods.WriteProcessMemory(
|
||||
handle, pin.AddrOfPinnedObject(), payload, payload.Length, out nint written);
|
||||
|
||||
Assert.True(ok, $"WriteProcessMemory failed: {Marshal.GetLastPInvokeError()}");
|
||||
Assert.Equal(payload.Length, (int)written);
|
||||
Assert.Equal(0x5EED, Marshal.ReadInt32(pin.AddrOfPinnedObject()));
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void VirtualAllocEx_commits_then_protects_then_frees()
|
||||
{
|
||||
using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
|
||||
IntPtr region = NativeMethods.VirtualAllocEx(
|
||||
handle, IntPtr.Zero, 0x1000,
|
||||
MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
|
||||
MemoryProtectionType.ReadWrite);
|
||||
Assert.NotEqual(IntPtr.Zero, region);
|
||||
|
||||
bool protect = NativeMethods.VirtualProtectEx(
|
||||
handle, region, 0x1000, MemoryProtectionType.ExecuteReadWrite, out MemoryProtectionType old);
|
||||
Assert.True(protect, $"VirtualProtectEx failed: {Marshal.GetLastPInvokeError()}");
|
||||
Assert.Equal(MemoryProtectionType.ReadWrite, old);
|
||||
|
||||
bool free = NativeMethods.VirtualFreeEx(handle, region, 0, MemoryFreeType.Release);
|
||||
Assert.True(free, $"VirtualFreeEx failed: {Marshal.GetLastPInvokeError()}");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LoadLibrary_then_GetProcAddress_resolves_an_export()
|
||||
{
|
||||
IntPtr module = NativeMethods.LoadLibrary("kernel32.dll");
|
||||
Assert.NotEqual(IntPtr.Zero, module);
|
||||
|
||||
IntPtr proc = NativeMethods.GetProcAddress(module, "CloseHandle");
|
||||
Assert.NotEqual(IntPtr.Zero, proc);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(typeof(Context32), 716)]
|
||||
[InlineData(typeof(Context64), 1232)]
|
||||
public void Thread_context_struct_has_the_exact_native_size(Type contextType, int expectedSize)
|
||||
{
|
||||
Assert.Equal(expectedSize, Marshal.SizeOf(contextType));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="MemoryBase.ReadString"/> and <see cref="MemoryBase.WriteString"/>
|
||||
/// with encoding, null-terminator stop, and max-length behavior.
|
||||
/// </summary>
|
||||
public class StringReadWriteTests
|
||||
{
|
||||
private static ExternalReader OpenSelf()
|
||||
{
|
||||
return new ExternalReader(
|
||||
Process.GetCurrentProcess(),
|
||||
ProcessAccess.VmRead | ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_ascii_then_ReadString_round_trips()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[64];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.WriteString(addr, "hello", Encoding.ASCII));
|
||||
string result = reader.ReadString(addr, Encoding.ASCII);
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_utf8_then_ReadString_round_trips()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[64];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.WriteString(addr, "héllo wörld", Encoding.UTF8));
|
||||
string result = reader.ReadString(addr, Encoding.UTF8);
|
||||
Assert.Equal("héllo wörld", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_unicode_then_ReadString_round_trips()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[128];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
Assert.True(reader.WriteString(addr, "Hello\u00A9\u00AE\u20AC", Encoding.Unicode));
|
||||
string result = reader.ReadString(addr, Encoding.Unicode);
|
||||
Assert.Equal("Hello\u00A9\u00AE\u20AC", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadString_stops_at_null_terminator()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = Encoding.ASCII.GetBytes("hello\0world");
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 64);
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadString_respects_max_length()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = Encoding.ASCII.GetBytes("hello world this is a test");
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 5);
|
||||
Assert.Equal("hello", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_appends_null_terminator_automatically()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[32];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
|
||||
// Write without terminator
|
||||
Assert.True(reader.WriteString(addr, "test", Encoding.ASCII));
|
||||
|
||||
// The written bytes should end with \0
|
||||
byte[] read = reader.ReadBytes(addr, 8);
|
||||
Assert.Equal((byte)'t', read[0]);
|
||||
Assert.Equal((byte)'e', read[1]);
|
||||
Assert.Equal((byte)'s', read[2]);
|
||||
Assert.Equal((byte)'t', read[3]);
|
||||
Assert.Equal(0, read[4]); // null terminator
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReadString_empty_buffer_returns_empty_string()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[1] { 0 };
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
string result = reader.ReadString(addr, Encoding.ASCII, maxLength: 1);
|
||||
Assert.Equal("", result);
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WriteString_empty_string_writes_only_null()
|
||||
{
|
||||
using var reader = OpenSelf();
|
||||
byte[] slot = new byte[8];
|
||||
GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned);
|
||||
try
|
||||
{
|
||||
IntPtr addr = pin.AddrOfPinnedObject();
|
||||
|
||||
// Write a marker first
|
||||
reader.WriteBytes(addr, [0xAB, 0xCD, 0xEF, 0x00]);
|
||||
// Now overwrite with empty string
|
||||
Assert.True(reader.WriteString(addr, "", Encoding.ASCII));
|
||||
|
||||
byte[] read = reader.ReadBytes(addr, 4);
|
||||
Assert.Equal(0, read[0]); // null
|
||||
}
|
||||
finally
|
||||
{
|
||||
pin.Free();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
|
||||
**Goals:**
|
||||
- Single modern (.NET 8, nullable, `Span<byte>`, `SafeHandle`) library that is bitness-agnostic (x86 + x64).
|
||||
- A **three-tier execution model** whose default path for game-state calls is crash-safe (runs on the target's own thread), while `CreateRemoteThread` remains available for thread-agnostic payloads.
|
||||
- Dual memory access: out-of-process (RPM/WPM) and in-process (direct deref) behind one abstract `MemoryBase`, with `MarshalCache<T>` for allocation-free typed IO.
|
||||
- Dual memory access: out-of-process (RPM/WPM) and in-process (RPM-on-self-handle, see D1 revision) behind one abstract `MemoryBase`, with `MarshalCache<T>` for allocation-free typed IO.
|
||||
- Reversible function hooking (`DetourManager`) and byte patching (`PatchManager`) with auto-restore on dispose.
|
||||
- Replace FASM with an `IAssembler` seam: hand-emitted convention stubs by default, optional Iced backend for arbitrary assembly. Zero native dependency in the default configuration.
|
||||
- Port DLL injection (CreateThread + thread-hijack, x86/x64) and pattern scanning + cache from current BlackMagic.
|
||||
@@ -36,9 +36,9 @@ WhiteMagic is a **new, additive** .NET 8 library that unifies the four. It reuse
|
||||
|
||||
`MemoryBase` defines abstract `ReadBytes`/`WriteBytes`/`Read<T>`/`Write<T>`, relative/absolute addressing, and hosts the `PatchManager`. Two concrete readers:
|
||||
- `ExternalReader : MemoryBase` — `ReadProcessMemory`/`WriteProcessMemory` over a `SafeMemoryHandle`. Owns allocation, injection, and the remote-thread + main-thread executors.
|
||||
- `InProcessReader : MemoryBase` — `unsafe` direct pointer deref; owns the `DetourManager` and `InProcessInvoker`.
|
||||
- `InProcessReader : MemoryBase` — reads the current process via `ReadProcessMemory`/`WriteProcessMemory` on a self-handle; owns the `DetourManager` and `InProcessInvoker`. **(Revised from `unsafe` direct deref during Phase 2: .NET cannot catch `AccessViolationException`, so a bad deref kills the host with no soft-failure path; RPM-on-self fails soft. The in-process speed win moves to the delegate-call/detour paths, not the reader. See `specs/memory-access`.)**
|
||||
|
||||
**Why**: GreyMagic proved this abstraction lets the same higher-level code (pattern scan, patch, high-level API) run in either mode. External is the primary path for a bot host; in-process is the fast/crash-free path once injected.
|
||||
**Why**: GreyMagic proved this abstraction lets the same higher-level code (pattern scan, patch, high-level API) run in either mode. External is the primary path for a bot host; in-process becomes valuable once injected — not for faster reads (both readers use RPM/WPM, see the D1 revision) but for the delegate-call and detour paths it unlocks (`InProcessInvoker`, `DetourManager`).
|
||||
|
||||
**Alternatives considered**: single external-only class (current BlackMagic) — rejected: forecloses the in-process delegate path, which is the cleanest crash-free execution. MemorySharp's factory-per-concern model (`Assembly`, `Threads`, `Windows` factories) — adopted selectively for the high-level surface, but the read/write core stays on `MemoryBase` for GreyMagic-style polymorphism.
|
||||
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
### Requirement: Abstract memory base with two readers
|
||||
|
||||
WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read<T>`/`Write<T>`, array read/write, and string read/write, with two concrete implementations: `ExternalReader` (out-of-process via ReadProcessMemory/WriteProcessMemory) and `InProcessReader` (in-process via direct pointer dereference).
|
||||
WhiteMagic SHALL expose an abstract `MemoryBase` type defining `ReadBytes`, `WriteBytes`, generic `Read<T>`/`Write<T>`, array read/write, and string read/write, with two concrete implementations: `ExternalReader` (out-of-process via ReadProcessMemory/WriteProcessMemory) and `InProcessReader` (in-process, reading the current process through ReadProcessMemory/WriteProcessMemory on a self-handle).
|
||||
|
||||
> **Deviation from design D1.** D1 originally specified `InProcessReader` as `unsafe` direct pointer dereference (the "fast/crash-free" path). Implementation revised it to `ReadProcessMemory`/`WriteProcessMemory` on a handle to the current process, because .NET (Core) cannot catch `AccessViolationException` (`HandleProcessCorruptedStateExceptions` is removed), so a raw deref of a bad address terminates the host process with no soft-failure path. RPM on a self-handle fails soft (returns empty) like `ExternalReader`. The in-process performance win therefore moves to the delegate-call and detour paths (`InProcessInvoker`, `DetourManager`), not the reader.
|
||||
|
||||
#### Scenario: in-process read fails soft on an invalid address
|
||||
- **WHEN** an `InProcessReader` reads an unmapped or protected address
|
||||
- **THEN** it MUST return empty/`default` rather than crash the host process
|
||||
|
||||
#### Scenario: external read round-trip
|
||||
- **WHEN** an `ExternalReader` opens a target process and writes a value with `Write<int>(addr, 0x1234)` then reads it back with `Read<int>(addr)`
|
||||
|
||||
@@ -3,20 +3,21 @@
|
||||
- [x] 1.1 Create `WhiteMagic/WhiteMagic.csproj` targeting `net8.0-windows`, `AllowUnsafeBlocks=true`, nullable enabled, `TreatWarningsAsErrors`, `Platforms=x86;x64;AnyCPU`
|
||||
- [x] 1.2 Create `WhiteMagicTest/WhiteMagicTest.csproj` (xUnit, `net8.0-windows`) referencing `WhiteMagic`
|
||||
- [x] 1.3 Create `WhiteMagic.slnx` (SDK 10 default solution format) and add both projects. (Built on SDK 10; `net8.0-windows` targeting pack auto-restored.)
|
||||
- [ ] 1.4 Add `WhiteMagic/Native/` P/Invoke surface (`LibraryImport`): OpenProcess, Read/WriteProcessMemory, VirtualAllocEx/FreeEx/ProtectEx, CreateRemoteThread, Wow64Get/SetThreadContext, Get/SetThreadContext, LoadLibrary, GetProcAddress; add `SafeMemoryHandle`
|
||||
- [x] 1.4 Add `WhiteMagic/Native/` P/Invoke surface (`LibraryImport`): OpenProcess, Read/WriteProcessMemory, VirtualAllocEx/FreeEx/ProtectEx, CreateRemoteThread, Wow64Get/SetThreadContext, Get/SetThreadContext, LoadLibrary, GetProcAddress; add `SafeMemoryHandle`
|
||||
- [x] 1.5 Verify empty projects build: `dotnet build WhiteMagic.slnx` — zero errors, zero warnings
|
||||
|
||||
## 2. Core Memory Access (spec: memory-access)
|
||||
|
||||
- [ ] 2.1 Add tests for `MarshalCache<T>`: blittable size, marshal-required flag, IsIntPtr, computed-once behavior
|
||||
- [ ] 2.2 Implement `WhiteMagic/MarshalCache.cs` to pass 2.1
|
||||
- [ ] 2.3 Add tests for `MemoryBase` abstract contract + `ExternalReader` round-trip (`Read<T>`/`Write<T>`, arrays) using the current process as target
|
||||
- [ ] 2.4 Implement `WhiteMagic/MemoryBase.cs` (abstract) and `WhiteMagic/ExternalReader.cs` to pass 2.3
|
||||
- [ ] 2.5 Add tests for string read/write with encoding, null-terminator stop, and max length
|
||||
- [ ] 2.6 Implement `ReadString`/`WriteString` on `MemoryBase` to pass 2.5
|
||||
- [ ] 2.7 Add tests for relative/absolute addressing (`GetAbsolute`/`GetRelative`, `isRelative` flag)
|
||||
- [ ] 2.8 Implement addressing helpers to pass 2.7
|
||||
- [ ] 2.9 Add tests + `unsafe` implementation for `InProcessReader` (direct deref against own process); verify shared `MemoryBase` API works for both readers
|
||||
- [x] 2.1 Add tests for `MarshalCache<T>`: blittable size, marshal-required flag, IsIntPtr, computed-once behavior
|
||||
- [x] 2.2 Implement `WhiteMagic/MarshalCache.cs` to pass 2.1
|
||||
- [x] 2.3 Add tests for `MemoryBase` abstract contract + `ExternalReader` round-trip (`Read<T>`/`Write<T>`, arrays) using the current process as target
|
||||
- [x] 2.4 Implement `WhiteMagic/MemoryBase.cs` (abstract) and `WhiteMagic/ExternalReader.cs` to pass 2.3
|
||||
- [x] 2.5 Add tests for string read/write with encoding, null-terminator stop, and max length
|
||||
- [x] 2.6 Implement `ReadString`/`WriteString` on `MemoryBase` to pass 2.5
|
||||
- [x] 2.7 Add tests for relative/absolute addressing (`GetAbsolute`/`GetRelative`, `isRelative` flag)
|
||||
- [x] 2.8 Implement addressing helpers to pass 2.7
|
||||
- [x] 2.9 Add tests + implementation for `InProcessReader` (RPM/WPM on a self-handle — see D1 deviation note; direct deref rejected because .NET cannot catch `AccessViolationException`); verify shared `MemoryBase` API works for both readers
|
||||
- [ ] 2.10 Follow-up (found in review): `ReadString` scans for the null terminator byte-by-byte, so for UTF-16/UTF-32 it can match a **misaligned** multi-byte null across a char boundary (e.g. `"A"`+U+4200 = `41 00 00 42` matches `{00,00}` at offset 1) and can miss a terminator split across the 64-byte chunk boundary. Harmless for ASCII/UTF-8 (the WoW case). Fix: align the scan to the encoding's code-unit width and carry the last `(nullLen-1)` bytes across chunks. Add a UTF-16 test.
|
||||
|
||||
## 3. Managed Assembler (spec: managed-assembler)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user