diff --git a/WhiteMagic/Memory/MemoryRegion.cs b/WhiteMagic/Memory/MemoryRegion.cs
new file mode 100644
index 0000000..b7bf50b
--- /dev/null
+++ b/WhiteMagic/Memory/MemoryRegion.cs
@@ -0,0 +1,75 @@
+using System;
+using WhiteMagic.Native;
+
+namespace WhiteMagic.Memory;
+
+///
+/// An immutable snapshot of a memory region as reported by VirtualQueryEx.
+///
+public readonly record struct MemoryRegion
+{
+ /// The base address of the region of pages.
+ public IntPtr BaseAddress { get; }
+
+ /// The size of the region, in bytes.
+ public nuint Size { get; }
+
+ /// The access protection of the pages in the region.
+ public MemoryProtectionType Protection { get; }
+
+ /// The state of the pages in the region.
+ public MemoryState State { get; }
+
+ /// The type of pages in the region.
+ public MemoryType Type { get; }
+
+ /// The base address of a range of pages allocated by VirtualAllocEx.
+ public IntPtr AllocationBase { get; }
+
+ /// The memory protection option when the region was initially allocated.
+ public MemoryProtectionType AllocationProtect { get; }
+
+ ///
+ /// Initializes a new from explicit values.
+ ///
+ public MemoryRegion(
+ IntPtr baseAddress,
+ nuint size,
+ MemoryProtectionType protection,
+ MemoryState state,
+ MemoryType type,
+ IntPtr allocationBase,
+ MemoryProtectionType allocationProtect)
+ {
+ BaseAddress = baseAddress;
+ Size = size;
+ Protection = protection;
+ State = state;
+ Type = type;
+ AllocationBase = allocationBase;
+ AllocationProtect = allocationProtect;
+ }
+
+ ///
+ /// Initializes a new from a raw MEMORY_BASIC_INFORMATION.
+ ///
+ internal MemoryRegion(MemoryBasicInformation info)
+ {
+ BaseAddress = info.BaseAddress;
+ Size = info.RegionSize;
+ AllocationBase = info.AllocationBase;
+ AllocationProtect = (MemoryProtectionType)info.AllocationProtect;
+ Protection = (MemoryProtectionType)info.Protect;
+ State = (MemoryState)info.State;
+ Type = (MemoryType)info.Type;
+ }
+
+ ///
+ /// Returns if is inside the region,
+ /// defined as [BaseAddress, BaseAddress + Size).
+ ///
+ public bool Contains(IntPtr address)
+ {
+ return (nuint)(address - BaseAddress) < Size;
+ }
+}
diff --git a/WhiteMagic/Memory/ProtectionScope.cs b/WhiteMagic/Memory/ProtectionScope.cs
new file mode 100644
index 0000000..655769c
--- /dev/null
+++ b/WhiteMagic/Memory/ProtectionScope.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Runtime.InteropServices;
+using WhiteMagic.Native;
+
+namespace WhiteMagic.Memory;
+
+///
+/// A scope that temporarily changes page protection via VirtualProtectEx and
+/// restores the original protection when disposed, including when the guarded body throws.
+///
+public sealed class ProtectionScope : IDisposable
+{
+ private readonly MemoryBase _memory;
+ private readonly IntPtr _address;
+ private readonly nint _size;
+ private readonly MemoryProtectionType _originalProtection;
+ private bool _disposed;
+
+ ///
+ /// Creates a new protection scope, applying to the
+ /// specified range immediately.
+ ///
+ internal ProtectionScope(MemoryBase memory, IntPtr address, nint size, MemoryProtectionType newProtection)
+ {
+ _memory = memory ?? throw new ArgumentNullException(nameof(memory));
+
+ if (address == IntPtr.Zero)
+ throw new ArgumentException("Address cannot be zero.", nameof(address));
+
+ if (size <= 0)
+ throw new ArgumentOutOfRangeException(nameof(size), "Size must be positive.");
+
+ _address = address;
+ _size = size;
+
+ if (!NativeMethods.VirtualProtectEx(
+ memory.Handle,
+ address,
+ size,
+ newProtection,
+ out _originalProtection))
+ {
+ int error = Marshal.GetLastPInvokeError();
+ throw new InvalidOperationException(
+ $"VirtualProtectEx failed to change protection: error {error}.");
+ }
+ }
+
+ /// Restores the original page protection if it has not already been restored.
+ public void Dispose()
+ {
+ if (!_disposed)
+ {
+ _disposed = true;
+ NativeMethods.VirtualProtectEx(_memory.Handle, _address, _size, _originalProtection, out _);
+ }
+ }
+}
diff --git a/WhiteMagic/MemoryBase.cs b/WhiteMagic/MemoryBase.cs
index f7c864f..d53b8a4 100644
--- a/WhiteMagic/MemoryBase.cs
+++ b/WhiteMagic/MemoryBase.cs
@@ -1,5 +1,7 @@
using WhiteMagic.Hooking;
+using WhiteMagic.Memory;
using WhiteMagic.Native;
+using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
@@ -268,6 +270,58 @@ public abstract class MemoryBase : IDisposable
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);
+ address = info.BaseAddress + (nint)info.RegionSize;
+ }
+ }
+
+ ///
+ /// 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 ──────────────────────────────────────────────────────────
///
diff --git a/WhiteMagic/Native/NativeEnums.cs b/WhiteMagic/Native/NativeEnums.cs
index 1d690b7..c12aa16 100644
--- a/WhiteMagic/Native/NativeEnums.cs
+++ b/WhiteMagic/Native/NativeEnums.cs
@@ -156,3 +156,61 @@ public static class ContextFlags
/// AMD64: control, integer, and segment registers.
public const uint Amd64Full = Amd64Control | Amd64Integer | Amd64Segments;
}
+
+///
+/// Values that describe the state of memory pages returned by VirtualQueryEx.
+///
+public enum MemoryState : uint
+{
+ /// Indicates committed pages for which physical storage has been allocated.
+ Commit = 0x1000,
+
+ /// Indicates reserved pages where a range of the virtual address space is reserved without any physical storage being allocated.
+ Reserve = 0x2000,
+
+ /// Indicates free pages not accessible to the calling process and available to be allocated.
+ Free = 0x10000,
+}
+
+///
+/// Values that describe the type of memory pages returned by VirtualQueryEx.
+///
+public enum MemoryType : uint
+{
+ /// Indicates that the memory pages within the region are private.
+ Private = 0x20000,
+
+ /// Indicates that the memory pages within the region are mapped into the view of a section.
+ Mapped = 0x40000,
+
+ /// Indicates that the memory pages within the region are mapped into the view of an image section.
+ Image = 0x1000000,
+}
+
+///
+/// Flags used by CreateToolhelp32Snapshot to specify the portions of the system to include in the snapshot.
+///
+[Flags]
+public enum SnapshotFlags : uint
+{
+ /// Enumerate the heap list.
+ HeapList = 0x00000001,
+
+ /// Enumerate the process list.
+ Process = 0x00000002,
+
+ /// Enumerate the thread list.
+ Thread = 0x00000004,
+
+ /// Enumerate the module list.
+ Module = 0x00000008,
+
+ /// Enumerate the 32-bit module list for the specified process.
+ Module32 = 0x00000010,
+
+ /// Include all processes and threads in the system.
+ All = 0x0000001F,
+
+ /// Indicate that the snapshot handle is to be inheritable.
+ Inherit = 0x80000000,
+}
diff --git a/WhiteMagic/Native/NativeMethods.cs b/WhiteMagic/Native/NativeMethods.cs
index ab51b08..bbd64a2 100644
--- a/WhiteMagic/Native/NativeMethods.cs
+++ b/WhiteMagic/Native/NativeMethods.cs
@@ -173,4 +173,46 @@ internal static partial class NativeMethods
SafeMemoryHandle handle,
uint milliseconds);
+ // ── Memory query ───────────────────────────────────────────────────────
+
+ /// Retrieves information about a range of pages in the virtual address space of a specified process.
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ internal static partial nuint VirtualQueryEx(
+ SafeMemoryHandle process,
+ IntPtr address,
+ out MemoryBasicInformation buffer,
+ nuint length);
+
+ // ── Thread enumeration ─────────────────────────────────────────────────
+
+ /// Takes a snapshot of the specified processes, as well as the heaps, modules, and threads used by these processes.
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ internal static partial SafeMemoryHandle CreateToolhelp32Snapshot(
+ SnapshotFlags dwFlags,
+ int th32ProcessID);
+
+ /// Retrieves information about the first thread of any process encountered in a system snapshot.
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool Thread32First(
+ SafeMemoryHandle hSnapshot,
+ ref ThreadEntry32 lpte);
+
+ /// Retrieves information about the next thread of any process encountered in a system snapshot.
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool Thread32Next(
+ SafeMemoryHandle hSnapshot,
+ ref ThreadEntry32 lpte);
+
+ /// Retrieves timing information for the specified thread.
+ [LibraryImport("kernel32.dll", SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static partial bool GetThreadTimes(
+ SafeMemoryHandle thread,
+ out long creationTime,
+ out long exitTime,
+ out long kernelTime,
+ out long userTime);
+
}
diff --git a/WhiteMagic/Native/NativeStructures.cs b/WhiteMagic/Native/NativeStructures.cs
index 1395367..93808db 100644
--- a/WhiteMagic/Native/NativeStructures.cs
+++ b/WhiteMagic/Native/NativeStructures.cs
@@ -205,3 +205,61 @@ public unsafe struct Context64
/// The source RIP of the last exception.
public ulong LastExceptionFromRip;
}
+
+///
+/// Layout matches MEMORY_BASIC_INFORMATION. Uses pointer-sized fields so the
+/// structure is 28 bytes on x86 and 48 bytes on x64, matching the layout the OS expects
+/// from a caller of those bitnesses.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct MemoryBasicInformation
+{
+ /// A pointer to the base address of the region of pages.
+ public nint BaseAddress;
+
+ /// A pointer to the base address of a range of pages allocated by the VirtualAllocEx function.
+ public nint AllocationBase;
+
+ /// The memory protection option when the region was initially allocated.
+ public uint AllocationProtect;
+
+ /// The size of the region beginning at the base address, in bytes.
+ public nuint RegionSize;
+
+ /// The state of the pages in the region.
+ public uint State;
+
+ /// The access protection of the pages in the region.
+ public uint Protect;
+
+ /// The type of pages in the region.
+ public uint Type;
+}
+
+///
+/// Layout matches THREADENTRY32 used by Thread32First/Thread32Next.
+///
+[StructLayout(LayoutKind.Sequential)]
+internal struct ThreadEntry32
+{
+ /// The size of the structure, in bytes.
+ public uint dwSize;
+
+ /// This member is no longer used and is always zero.
+ public uint cntUsage;
+
+ /// The thread identifier.
+ public uint th32ThreadID;
+
+ /// The identifier of the process that owns the thread.
+ public uint th32OwnerProcessID;
+
+ /// The kernel base priority level assigned to the thread.
+ public int tpBasePri;
+
+ /// This member is no longer used.
+ public int tpDeltaPri;
+
+ /// This member is reserved.
+ public uint dwFlags;
+}
diff --git a/WhiteMagicTest/Memory/MemoryRegionTests.cs b/WhiteMagicTest/Memory/MemoryRegionTests.cs
new file mode 100644
index 0000000..a19c38c
--- /dev/null
+++ b/WhiteMagicTest/Memory/MemoryRegionTests.cs
@@ -0,0 +1,159 @@
+using System;
+using System.Linq;
+using System.Runtime.InteropServices;
+using WhiteMagic;
+using WhiteMagic.Memory;
+using WhiteMagic.Native;
+using Xunit;
+
+namespace WhiteMagicTest.Memory;
+
+///
+/// Tests for memory-region query, enumeration and scoped protection (tasks 1.2, 1.4, 1.6, 1.8).
+///
+public sealed class MemoryRegionTests
+{
+ [Fact]
+ public void Contains_returns_true_for_addresses_inside_half_open_range()
+ {
+ var region = new MemoryRegion(
+ new IntPtr(0x10000),
+ 0x1000,
+ MemoryProtectionType.ReadWrite,
+ MemoryState.Commit,
+ MemoryType.Private,
+ new IntPtr(0x10000),
+ MemoryProtectionType.ReadWrite);
+
+ Assert.True(region.Contains(new IntPtr(0x10000)));
+ Assert.True(region.Contains(new IntPtr(0x10FFF)));
+ Assert.False(region.Contains(new IntPtr(0x11000)));
+ Assert.False(region.Contains(new IntPtr(0x0FFF)));
+ }
+
+ [Fact]
+ public void QueryRegion_returns_region_containing_committed_address()
+ {
+ using var reader = new InProcessReader();
+ nint pageSize = Environment.SystemPageSize;
+
+ IntPtr block = NativeMethods.VirtualAllocEx(
+ reader.Handle,
+ IntPtr.Zero,
+ pageSize,
+ MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
+ MemoryProtectionType.ReadWrite);
+
+ Assert.NotEqual(IntPtr.Zero, block);
+
+ try
+ {
+ MemoryRegion region = reader.QueryRegion(block);
+
+ Assert.Equal(block, region.BaseAddress);
+ Assert.True(region.Contains(block));
+ Assert.True(region.Contains(block + (int)pageSize - 1));
+ Assert.Equal(MemoryState.Commit, region.State);
+ Assert.Equal(MemoryType.Private, region.Type);
+ Assert.Equal(MemoryProtectionType.ReadWrite, region.Protection);
+ Assert.Equal(MemoryProtectionType.ReadWrite, region.AllocationProtect);
+ Assert.Equal(block, region.AllocationBase);
+ }
+ finally
+ {
+ NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
+ }
+ }
+
+ [Fact]
+ public void EnumerateRegions_yields_ascending_non_overlapping_regions()
+ {
+ using var reader = new InProcessReader();
+
+ MemoryRegion[] regions = reader.EnumerateRegions().Take(5).ToArray();
+ Assert.True(regions.Length > 0);
+
+ for (int i = 1; i < regions.Length; i++)
+ {
+ Assert.True(
+ (nuint)regions[i].BaseAddress >=
+ (nuint)regions[i - 1].BaseAddress + regions[i - 1].Size);
+ }
+ }
+
+ [Fact]
+ public void EnumerateRegions_is_lazy_and_stops_early()
+ {
+ using var reader = new InProcessReader();
+
+ // Taking a single item must not force a full address-space walk.
+ MemoryRegion first = reader.EnumerateRegions().First();
+ Assert.True(first.Size > 0);
+ }
+
+ [Fact]
+ public void ChangeProtection_applies_new_protection_inside_scope_and_restores_on_dispose()
+ {
+ using var reader = new InProcessReader();
+ nint pageSize = Environment.SystemPageSize;
+
+ IntPtr block = NativeMethods.VirtualAllocEx(
+ reader.Handle,
+ IntPtr.Zero,
+ pageSize,
+ MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
+ MemoryProtectionType.ReadWrite);
+
+ Assert.NotEqual(IntPtr.Zero, block);
+
+ try
+ {
+ Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
+
+ using (reader.ChangeProtection(block, pageSize, MemoryProtectionType.ExecuteReadWrite))
+ {
+ Assert.Equal(MemoryProtectionType.ExecuteReadWrite, reader.QueryRegion(block).Protection);
+ }
+
+ Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
+ }
+ finally
+ {
+ NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
+ }
+ }
+
+ [Fact]
+ public void ChangeProtection_restores_original_protection_when_body_throws()
+ {
+ using var reader = new InProcessReader();
+ nint pageSize = Environment.SystemPageSize;
+
+ IntPtr block = NativeMethods.VirtualAllocEx(
+ reader.Handle,
+ IntPtr.Zero,
+ pageSize,
+ MemoryAllocationType.Commit | MemoryAllocationType.Reserve,
+ MemoryProtectionType.ReadWrite);
+
+ Assert.NotEqual(IntPtr.Zero, block);
+
+ try
+ {
+ Assert.Throws(new Action(() =>
+ {
+ using (reader.ChangeProtection(block, pageSize, MemoryProtectionType.ExecuteReadWrite))
+ {
+ Assert.Equal(MemoryProtectionType.ExecuteReadWrite, reader.QueryRegion(block).Protection);
+ throw new InvalidOperationException("Intentional failure inside scope.");
+ }
+ }));
+
+ Assert.Equal(MemoryProtectionType.ReadWrite, reader.QueryRegion(block).Protection);
+ }
+ finally
+ {
+ NativeMethods.VirtualFreeEx(reader.Handle, block, 0, MemoryFreeType.Release);
+ }
+ }
+}