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:
+62
-46
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user