using WhiteMagic.Hooking; using WhiteMagic.Memory; using WhiteMagic.Native; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; namespace WhiteMagic; /// /// Abstract base for all memory-access readers and writers. Provides typed /// /, array IO, string IO, and /// relative/absolute addressing. Subclasses implement the concrete /// and methods. /// public abstract class MemoryBase : IDisposable { /// Creates the shared hooking managers for this memory instance. protected MemoryBase() { PatchManager = new PatchManager(this); DetourManager = new DetourManager(this); } /// The base address of the target process's main module. public abstract IntPtr ImageBase { get; } /// The native handle to the target process. public abstract SafeMemoryHandle Handle { get; } /// if the target process is 64-bit. public abstract bool Is64Bit { get; } /// The operating-system process identifier of the target process. public abstract int ProcessId { get; } /// Named byte-patch manager; valid for in-process and external readers. public PatchManager PatchManager { get; } /// Inline-detour manager; valid only when operating in-process. public DetourManager DetourManager { get; } // ── Raw byte IO ──────────────────────────────────────────────────────── /// Reads a sequence of bytes from the target address. public abstract byte[] ReadBytes(IntPtr address, int count, bool isRelative = false); /// Writes a sequence of bytes to the target address. /// The number of bytes written. public abstract int WriteBytes(IntPtr address, ReadOnlySpan bytes, bool isRelative = false); // ── Typed IO ─────────────────────────────────────────────────────────── /// Reads a value of type from the target address. /// The value, or default(T) when the read fails or returns fewer bytes than /// . public T Read(IntPtr address, bool isRelative = false) where T : struct { if (isRelative) address = GetAbsolute(address); int size = MarshalCache.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.Size; byte[] raw = ReadBytes(address, size); if (raw.Length < size) return default; if (MarshalCache.TypeRequiresMarshal) return MarshalByteArrayToStructure(raw); return MemoryMarshal.Read(raw.AsSpan()); } /// Writes a value of type to the target address. /// if all bytes were written. public bool Write(IntPtr address, T value, bool isRelative = false) where T : struct { if (isRelative) address = GetAbsolute(address); int size = MarshalCache.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.Size; byte[] raw; if (MarshalCache.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; } /// Reads an array of values of type from the target address. /// An array of at most elements. May be shorter when the read /// returns fewer bytes than expected. public T[] Read(IntPtr address, int count, bool isRelative = false) where T : struct { ArgumentOutOfRangeException.ThrowIfNegative(count); if (isRelative) address = GetAbsolute(address); int elementSize = MarshalCache.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.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.TypeRequiresMarshal) { GCHandle pin = GCHandle.Alloc(raw, GCHandleType.Pinned); try { IntPtr basePtr = pin.AddrOfPinnedObject(); for (int i = 0; i < actualCount; i++) result[i] = Marshal.PtrToStructure(basePtr + (i * elementSize)); } finally { pin.Free(); } } else { ReadOnlySpan span = raw; for (int i = 0; i < actualCount; i++) result[i] = MemoryMarshal.Read(span.Slice(i * elementSize, elementSize)); } return result; } /// Writes an array of values of type to the target address. /// if all bytes were written. public bool Write(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.TypeRequiresMarshal ? MarshalCache.MarshalSize : MarshalCache.Size; long total = (long)elementSize * values.Length; ArgumentOutOfRangeException.ThrowIfGreaterThan(total, int.MaxValue, nameof(values)); int totalSize = (int)total; byte[] raw = new byte[totalSize]; Span span = raw; for (int i = 0; i < values.Length; i++) { Span slice = span.Slice(i * elementSize, elementSize); if (MarshalCache.TypeRequiresMarshal) StructureToByteArray(values[i], slice, elementSize); else MemoryMarshal.Write(slice, in values[i]); } int written = WriteBytes(address, raw, false); return written == totalSize; } // ── String IO ────────────────────────────────────────────────────────── /// 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). /// The address to read from. For multi-byte encodings this must be /// aligned to a code-unit boundary or the result is undefined. /// The text encoding. /// The maximum number of bytes to read. /// If , is relative /// to . /// /// 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. /// 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(); 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)); } /// Writes a null-terminated string to the target address. 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 ───────────────────────────────────────────────────────── /// Converts a relative offset to an absolute address relative to . public IntPtr GetAbsolute(IntPtr relative) { return ImageBase + (nint)relative; } /// Converts an absolute address to a relative offset from . /// This is the inverse of : GetAbsolute(GetRelative(a)) == a. public IntPtr GetRelative(IntPtr absolute) { return (IntPtr)((nint)absolute - (nint)ImageBase); } // ── Memory region query ──────────────────────────────────────────────── /// /// Queries the memory region that contains in the target /// process using VirtualQueryEx. /// /// An immutable snapshot of the region. /// The query fails. public MemoryRegion QueryRegion(IntPtr address) { nuint bufferSize = (nuint)Marshal.SizeOf(); nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize); if (result == 0) { int error = Marshal.GetLastPInvokeError(); throw new InvalidOperationException($"VirtualQueryEx failed for address 0x{address:X}: error {error}."); } return new MemoryRegion(info); } /// /// Enumerates the memory regions of the target process from the lowest address upward. /// The walk is lazy; callers can stop early without walking the entire address space. /// public IEnumerable EnumerateRegions() { IntPtr address = IntPtr.Zero; nuint bufferSize = (nuint)Marshal.SizeOf(); while (true) { nuint result = NativeMethods.VirtualQueryEx(Handle, address, out MemoryBasicInformation info, bufferSize); if (result == 0) yield break; yield return new MemoryRegion(info); IntPtr next = info.BaseAddress + (nint)info.RegionSize; if (next.ToInt64() <= address.ToInt64()) yield break; address = next; } } /// /// Changes the page protection on a region of memory and returns a disposable scope /// that restores the original protection on dispose, including when an exception escapes /// the guarded body. /// public ProtectionScope ChangeProtection(IntPtr address, nint size, MemoryProtectionType protection) { return new ProtectionScope(this, address, size, protection); } // ── Lifecycle ────────────────────────────────────────────────────────── /// public virtual void Dispose() { DetourManager.RemoveAll(); PatchManager.RestoreAll(); Handle?.Dispose(); } // ── Private helpers ──────────────────────────────────────────────────── private static T MarshalByteArrayToStructure(byte[] bytes) where T : struct { GCHandle pin = GCHandle.Alloc(bytes, GCHandleType.Pinned); try { return Marshal.PtrToStructure(pin.AddrOfPinnedObject()); } finally { pin.Free(); } } private static byte[] StructureToByteArray(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 value, Span destination, int size) where T : struct { byte[] bytes = StructureToByteArray(value, size); bytes.CopyTo(destination); } }