Implement core diagnostic memory layer, execution helpers, and high-level facade slices

Implemented:
- Core: UTF-16 ReadString boundary/alignment fix, target bitness and process id on MemoryBase
- function interception: PatchManager, DetourManager, InstructionAnalyzer, MainThreadDispatcher
- Execution: BackgroundTaskExecutor, InProcessInvoker
- High-level: Magic facade, RemotePointer, async wrappers
- Discovery/external code loading/Window groundwork (PEB/TEB, pattern scanning, raw allocations, DLL external code loading, window/input)

Tests: 180 passing, 4 integration/interactive tests skipped.
This commit is contained in:
kbe
2026-07-21 23:43:14 +02:00
parent a0ca7050a2
commit 3f0bea6bd4
44 changed files with 5595 additions and 84 deletions
+62 -46
View File
@@ -1,3 +1,4 @@
using WhiteMagic.Hooking;
using WhiteMagic.Native;
using System.Runtime.InteropServices;
using System.Text;
@@ -12,12 +13,31 @@ namespace WhiteMagic;
/// </summary>
public abstract class MemoryBase : IDisposable
{
/// <summary>Creates the shared hooking managers for this memory instance.</summary>
protected MemoryBase()
{
PatchManager = new PatchManager(this);
DetourManager = new DetourManager(this);
}
/// <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; }
/// <summary><see langword="true"/> if the target process is 64-bit.</summary>
public abstract bool Is64Bit { get; }
/// <summary>The operating-system process identifier of the target process.</summary>
public abstract int ProcessId { get; }
/// <summary>Named byte-patch manager; valid for in-process and external readers.</summary>
public PatchManager PatchManager { get; }
/// <summary>Inline-detour manager; valid only when operating in-process.</summary>
public DetourManager DetourManager { get; }
// ── Raw byte IO ────────────────────────────────────────────────────────
/// <summary>Reads a sequence of bytes from the target address.</summary>
@@ -153,11 +173,17 @@ public abstract class MemoryBase : IDisposable
/// 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="address">The address to read from. For multi-byte encodings this must be
/// aligned to a code-unit boundary or the result is undefined.</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>
/// <remarks>
/// The scan is aligned to the encoding's code-unit width (1 byte for UTF-8/ASCII, 2 bytes
/// for UTF-16, 4 bytes for UTF-32). The trailing bytes of each chunk are merged with the
/// next chunk so a null terminator that straddles the chunk boundary is not missed.
/// </remarks>
public virtual string ReadString(IntPtr address, Encoding encoding, int maxLength = 512, bool relative = false)
{
if (relative)
@@ -166,46 +192,54 @@ public abstract class MemoryBase : IDisposable
// 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");
int nullLen = nullTerminator.Length;
const int chunkSize = 64;
int remaining = maxLength;
var accumulated = new System.Collections.Generic.List<byte[]>();
var accumulated = new System.Collections.Generic.List<byte>();
while (remaining > 0)
{
int take = Math.Min(chunkSize, remaining);
byte[] chunk = ReadBytes(address, take);
byte[] chunk = ReadBytes(address + accumulated.Count, take);
if (chunk.Length == 0)
break;
int nullPos = IndexOfPattern(chunk, nullTerminator);
if (nullPos >= 0)
int previousLen = accumulated.Count;
accumulated.AddRange(chunk);
// Search the newly extended buffer at code-unit-aligned positions. A terminator
// can start as far back as (nullLen - 1) bytes before the new bytes, so start
// the search just before the previous end, rounded up to the next code-unit.
int firstAligned = ((previousLen - nullLen + 1 + nullLen - 1) / nullLen) * nullLen;
firstAligned = Math.Max(0, firstAligned);
int limit = accumulated.Count - nullLen;
for (int i = firstAligned; i <= limit; i += nullLen)
{
if (nullPos > 0)
accumulated.Add(chunk[..nullPos]);
break;
bool match = true;
for (int j = 0; j < nullLen; j++)
{
if (accumulated[i + j] != nullTerminator[j])
{
match = false;
break;
}
}
if (match)
{
accumulated.RemoveRange(i, accumulated.Count - i);
remaining = 0;
break;
}
}
accumulated.Add(chunk);
// Advance by the bytes actually read, not the amount requested: a partial
// read (chunk.Length < take) must not skip the unread tail of the window.
address += chunk.Length;
remaining -= chunk.Length;
if (remaining > 0)
remaining -= chunk.Length;
}
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);
return encoding.GetString(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(accumulated));
}
/// <summary>Writes a null-terminated string to the target address.</summary>
@@ -239,6 +273,8 @@ public abstract class MemoryBase : IDisposable
/// <inheritdoc />
public virtual void Dispose()
{
DetourManager.RemoveAll();
PatchManager.RestoreAll();
Handle?.Dispose();
}
@@ -278,24 +314,4 @@ public abstract class MemoryBase : IDisposable
bytes.CopyTo(destination);
}
private static int IndexOfPattern(byte[] data, byte[] pattern)
{
int lastStart = data.Length - pattern.Length;
int stride = Math.Max(1, pattern.Length);
for (int i = 0; i <= lastStart; i += stride)
{
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;
}
}