Some documentation was broken. I refactored it to be declarative now the project build correctly and produce XML documentation.
318 lines
13 KiB
C#
318 lines
13 KiB
C#
using WhiteMagic.Hooking;
|
|
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}(nint, bool)"/>/<see cref="Write{T}(nint, T, bool)"/>, 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>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>
|
|
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>.TypeRequiresMarshal ? MarshalCache<T>.MarshalSize : 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>.TypeRequiresMarshal ? MarshalCache<T>.MarshalSize : 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
|
|
{
|
|
ArgumentOutOfRangeException.ThrowIfNegative(count);
|
|
|
|
if (isRelative)
|
|
address = GetAbsolute(address);
|
|
|
|
int elementSize = MarshalCache<T>.TypeRequiresMarshal ? MarshalCache<T>.MarshalSize : MarshalCache<T>.Size;
|
|
long totalSize = (long)elementSize * count;
|
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(totalSize, int.MaxValue, nameof(count));
|
|
|
|
byte[] raw = ReadBytes(address, (int)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>.TypeRequiresMarshal ? MarshalCache<T>.MarshalSize : MarshalCache<T>.Size;
|
|
long total = (long)elementSize * values.Length;
|
|
ArgumentOutOfRangeException.ThrowIfGreaterThan(total, int.MaxValue, nameof(values));
|
|
int totalSize = (int)total;
|
|
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. 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)
|
|
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");
|
|
int nullLen = nullTerminator.Length;
|
|
|
|
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 + accumulated.Count, take);
|
|
if (chunk.Length == 0)
|
|
break;
|
|
|
|
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 - (previousLen % nullLen);
|
|
if (firstAligned < 0) firstAligned = 0;
|
|
|
|
int limit = accumulated.Count - nullLen;
|
|
for (int i = firstAligned; i <= limit; i += nullLen)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
|
|
if (remaining > 0)
|
|
remaining -= chunk.Length;
|
|
}
|
|
|
|
return encoding.GetString(System.Runtime.InteropServices.CollectionsMarshal.AsSpan(accumulated));
|
|
}
|
|
|
|
/// <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()
|
|
{
|
|
DetourManager.RemoveAll();
|
|
PatchManager.RestoreAll();
|
|
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);
|
|
}
|
|
|
|
}
|