diff --git a/WhiteMagic/Native/NativeEnums.cs b/WhiteMagic/Native/NativeEnums.cs new file mode 100644 index 0000000..47f15fd --- /dev/null +++ b/WhiteMagic/Native/NativeEnums.cs @@ -0,0 +1,136 @@ +namespace WhiteMagic.Native; + +/// +/// Access rights that open a process object. +/// +[Flags] +public enum ProcessAccess : uint +{ + /// The right to terminate the process with TerminateProcess. + Terminate = 0x0001, + /// The right to create a thread in the process. + CreateThread = 0x0002, + /// The right to operate on the address space of the process. + VmOperation = 0x0008, + /// The right to read memory with ReadProcessMemory. + VmRead = 0x0010, + /// The right to write memory with WriteProcessMemory. + VmWrite = 0x0020, + /// The right to duplicate a handle with DuplicateHandle. + DupHandle = 0x0040, + /// The right to set information about the process. + SetInformation = 0x0200, + /// The right to read information about the process, such as the exit code. + QueryInformation = 0x0400, + /// The right to suspend or resume the process. + SuspendResume = 0x0800, + /// The right to read a limited set of information about the process. + QueryLimitedInformation = 0x1000, + /// The right to use the process object for synchronization. + Synchronize = 0x00100000, + + /// All access rights for a process object. + AllAccess = 0x001F0000 | Synchronize | 0xFFFF, +} + +/// +/// Values that control how VirtualAllocEx allocates memory. +/// +[Flags] +public enum MemoryAllocationType : uint +{ + /// Commit physical storage for the reserved pages. The pages start as zero. + Commit = 0x00001000, + /// Reserve a range of address space without physical storage. + Reserve = 0x00002000, + /// Reset the data in the range to indicate that it is no longer of interest. + Reset = 0x00080000, + /// Allocate memory at the highest possible address. + TopDown = 0x00100000, +} + +/// +/// Values that protect a block of memory. +/// +[Flags] +public enum MemoryProtectionType : uint +{ + /// No access to the committed pages. + NoAccess = 0x01, + /// Read access to the committed pages. + ReadOnly = 0x02, + /// Read and write access to the committed pages. + ReadWrite = 0x04, + /// Copy-on-write access to the committed pages. + WriteCopy = 0x08, + /// Execute access to the committed pages. + Execute = 0x10, + /// Execute and read access to the committed pages. + ExecuteRead = 0x20, + /// Execute, read, and write access to the committed pages. + ExecuteReadWrite = 0x40, + /// Execute and copy-on-write access to the committed pages. + ExecuteWriteCopy = 0x80, + /// The pages in the range become guard pages. + Guard = 0x100, + /// The system does not cache the committed pages. + NoCache = 0x200, + /// The system uses write-combined access for the pages. + WriteCombine = 0x400, +} + +/// +/// Values that control how VirtualFreeEx frees memory. +/// +[Flags] +public enum MemoryFreeType : uint +{ + /// Decommit the committed pages. The address range stays reserved. + Decommit = 0x4000, + /// Release the range of pages. The size must be zero. + Release = 0x8000, +} + +/// +/// Values that set the initial state of a new thread. +/// +[Flags] +public enum ThreadCreationFlags : uint +{ + /// The thread runs immediately after creation. + RunImmediately = 0, + /// The thread starts in a suspended state. Call ResumeThread to start it. + CreateSuspended = 0x00000004, + /// The stack-size parameter sets the reserve size of the stack. + StackSizeParamIsAReservation = 0x00010000, +} + +/// +/// Flags that select the registers that the thread-context functions read or write. +/// There are separate constants for 32-bit (x86/WOW64) and 64-bit (AMD64) contexts. +/// +public static class ContextFlags +{ + /// Architecture identifier for x86 contexts. + public const uint X86 = 0x00010000; + /// Architecture identifier for AMD64 contexts. + public const uint Amd64 = 0x00100000; + + /// x86: SS:SP, CS:IP, FLAGS, and BP. + public const uint X86Control = X86 | 0x01; + /// x86: AX, BX, CX, DX, SI, and DI. + public const uint X86Integer = X86 | 0x02; + /// x86: DS, ES, FS, and GS. + public const uint X86Segments = X86 | 0x04; + /// x86: control, integer, and segment registers. + public const uint X86Full = X86Control | X86Integer | X86Segments; + + /// AMD64: SegSs, Rsp, SegCs, Rip, and EFlags. + public const uint Amd64Control = Amd64 | 0x01; + /// AMD64: Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi, and R8 to R15. + public const uint Amd64Integer = Amd64 | 0x02; + /// AMD64: SegDs, SegEs, SegFs, and SegGs. + public const uint Amd64Segments = Amd64 | 0x04; + /// AMD64: control, integer, and segment registers. + public const uint Amd64Full = Amd64Control | Amd64Integer | Amd64Segments; +} diff --git a/WhiteMagic/Native/NativeMethods.cs b/WhiteMagic/Native/NativeMethods.cs new file mode 100644 index 0000000..bdc07e2 --- /dev/null +++ b/WhiteMagic/Native/NativeMethods.cs @@ -0,0 +1,136 @@ +using System.Runtime.InteropServices; + +namespace WhiteMagic.Native; + +/// +/// P/Invoke declarations for the Win32 process, memory, thread, and module +/// APIs that WhiteMagic uses. Every declaration uses +/// (source-generated interop). SetLastError is enabled on all calls that the +/// Win32 API documents as setting a thread-local last-error value. +/// +internal static partial class NativeMethods +{ + // ── Process ────────────────────────────────────────────────────────────── + + /// Opens an existing process and returns a handle to it. + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial SafeMemoryHandle OpenProcess( + ProcessAccess desiredAccess, + [MarshalAs(UnmanagedType.Bool)] bool inheritHandle, + int processId); + + /// Closes an open object handle. + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool CloseHandle(IntPtr handle); + + // ── Memory ─────────────────────────────────────────────────────────────── + + /// Reads memory from a process. + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool ReadProcessMemory( + SafeMemoryHandle process, + IntPtr baseAddress, + Span buffer, + int size, + out nint bytesRead); + + /// Writes memory to a process. + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool WriteProcessMemory( + SafeMemoryHandle process, + IntPtr baseAddress, + ReadOnlySpan buffer, + int size, + out nint bytesWritten); + + /// Reserves or commits a region of memory in a process. + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial IntPtr VirtualAllocEx( + SafeMemoryHandle process, + IntPtr address, + nint size, + MemoryAllocationType allocationType, + MemoryProtectionType protect); + + /// Changes the protection on a committed region of memory. + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool VirtualProtectEx( + SafeMemoryHandle process, + IntPtr address, + nint size, + MemoryProtectionType newProtect, + out MemoryProtectionType oldProtect); + + /// Releases or decommits a region of memory in a process. + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool VirtualFreeEx( + SafeMemoryHandle process, + IntPtr address, + nint size, + MemoryFreeType freeType); + + // ── Threading ──────────────────────────────────────────────────────────── + + /// Creates a thread that runs in the virtual address space of a process. + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial SafeMemoryHandle CreateRemoteThread( + SafeMemoryHandle process, + IntPtr threadAttributes, + nint stackSize, + IntPtr startAddress, + IntPtr parameter, + ThreadCreationFlags creationFlags, + out int threadId); + + /// Sets a 64-bit thread context (AMD64). + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetThreadContext( + SafeMemoryHandle thread, + ref Context64 context); + + /// Gets a 64-bit thread context (AMD64). + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool GetThreadContext( + SafeMemoryHandle thread, + ref Context64 context); + + /// Sets a 32-bit (WOW64) thread context. + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool Wow64SetThreadContext( + SafeMemoryHandle thread, + ref Context32 context); + + /// Gets a 32-bit (WOW64) thread context. + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool Wow64GetThreadContext( + SafeMemoryHandle thread, + ref Context32 context); + + // ── Modules ────────────────────────────────────────────────────────────── + + /// Loads a module into the calling process. + [LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryW")] + internal static partial IntPtr LoadLibrary( + [MarshalAs(UnmanagedType.LPWStr)] string lpFileName); + + /// Returns the address of a function or variable from a loaded module. + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial IntPtr GetProcAddress( + IntPtr hModule, + [MarshalAs(UnmanagedType.LPStr)] string lpProcName); + + /// Waits until a thread exits and retrieves its exit code. + [LibraryImport("kernel32.dll", SetLastError = true)] + internal static partial int WaitForSingleObject( + SafeMemoryHandle handle, + uint milliseconds); +} diff --git a/WhiteMagic/Native/NativeStructures.cs b/WhiteMagic/Native/NativeStructures.cs new file mode 100644 index 0000000..5fef352 --- /dev/null +++ b/WhiteMagic/Native/NativeStructures.cs @@ -0,0 +1,207 @@ +using System.Runtime.InteropServices; + +namespace WhiteMagic.Native; + +/// +/// The x87 and MMX state inside a 32-bit thread context. +/// +[StructLayout(LayoutKind.Sequential)] +public unsafe struct FloatingSaveArea32 +{ + /// The x87 FPU control word. + public uint ControlWord; + /// The x87 FPU status word. + public uint StatusWord; + /// The x87 FPU tag word. + public uint TagWord; + /// The offset of the instruction that caused the last FPU exception. + public uint ErrorOffset; + /// The selector of the instruction that caused the last FPU exception. + public uint ErrorSelector; + /// The offset of the operand that caused the last FPU exception. + public uint DataOffset; + /// The selector of the operand that caused the last FPU exception. + public uint DataSelector; + /// The 80-byte register area. + public fixed byte RegisterArea[80]; + /// The CR0 numeric-processor-extension state. + public uint Cr0NpxState; +} + +/// +/// A 32-bit (x86/WOW64) thread context. Use it with +/// Wow64GetThreadContext and Wow64SetThreadContext to inspect a 32-bit thread. +/// The total size is 716 bytes. +/// +[StructLayout(LayoutKind.Sequential)] +public unsafe struct Context32 +{ + /// Selects which parts of the context are valid. See . + public uint ContextFlags; + + /// Debug register 0. + public uint Dr0; + /// Debug register 1. + public uint Dr1; + /// Debug register 2. + public uint Dr2; + /// Debug register 3. + public uint Dr3; + /// Debug register 6. + public uint Dr6; + /// Debug register 7. + public uint Dr7; + + /// The floating-point state. + public FloatingSaveArea32 FloatSave; + + /// The GS segment. + public uint SegGs; + /// The FS segment. + public uint SegFs; + /// The ES segment. + public uint SegEs; + /// The DS segment. + public uint SegDs; + + /// The EDI register. + public uint Edi; + /// The ESI register. + public uint Esi; + /// The EBX register. + public uint Ebx; + /// The EDX register. + public uint Edx; + /// The ECX register. + public uint Ecx; + /// The EAX register. + public uint Eax; + + /// The base (frame) pointer. + public uint Ebp; + /// The instruction pointer. + public uint Eip; + /// The CS segment. + public uint SegCs; + /// The flags register. + public uint EFlags; + /// The stack pointer. + public uint Esp; + /// The SS segment. + public uint SegSs; + + /// The extended (processor-specific) registers. The size is 512 bytes. + public fixed byte ExtendedRegisters[512]; +} + +/// +/// A 64-bit (AMD64) thread context. Use it with the native +/// GetThreadContext and SetThreadContext from a 64-bit process. +/// The structure needs 16-byte alignment. The total size is 1232 bytes. +/// +[StructLayout(LayoutKind.Sequential, Pack = 16)] +public unsafe struct Context64 +{ + /// Home storage for a register parameter. + public ulong P1Home; + /// Home storage for a register parameter. + public ulong P2Home; + /// Home storage for a register parameter. + public ulong P3Home; + /// Home storage for a register parameter. + public ulong P4Home; + /// Home storage for a register parameter. + public ulong P5Home; + /// Home storage for a register parameter. + public ulong P6Home; + + /// Selects which parts of the context are valid. See . + public uint ContextFlags; + /// The MXCSR register. + public uint MxCsr; + + /// The CS segment. + public ushort SegCs; + /// The DS segment. + public ushort SegDs; + /// The ES segment. + public ushort SegEs; + /// The FS segment. + public ushort SegFs; + /// The GS segment. + public ushort SegGs; + /// The SS segment. + public ushort SegSs; + + /// The flags register. + public uint EFlags; + + /// Debug register 0. + public ulong Dr0; + /// Debug register 1. + public ulong Dr1; + /// Debug register 2. + public ulong Dr2; + /// Debug register 3. + public ulong Dr3; + /// Debug register 6. + public ulong Dr6; + /// Debug register 7. + public ulong Dr7; + + /// The RAX register. + public ulong Rax; + /// The RCX register. + public ulong Rcx; + /// The RDX register. + public ulong Rdx; + /// The RBX register. + public ulong Rbx; + /// The stack pointer. + public ulong Rsp; + /// The base (frame) pointer. + public ulong Rbp; + /// The RSI register. + public ulong Rsi; + /// The RDI register. + public ulong Rdi; + /// The R8 register. + public ulong R8; + /// The R9 register. + public ulong R9; + /// The R10 register. + public ulong R10; + /// The R11 register. + public ulong R11; + /// The R12 register. + public ulong R12; + /// The R13 register. + public ulong R13; + /// The R14 register. + public ulong R14; + /// The R15 register. + public ulong R15; + + /// The instruction pointer. + public ulong Rip; + + /// The XMM save area. The size is 512 bytes. + public fixed byte FltSave[512]; + + /// The vector registers (26 entries of 16 bytes, stored as 52 entries of 8 bytes). + public fixed ulong VectorRegister[52]; + + /// The vector control register. + public ulong VectorControl; + + /// The debug-control MSR. + public ulong DebugControl; + /// The target RIP of the last branch. + public ulong LastBranchToRip; + /// The source RIP of the last branch. + public ulong LastBranchFromRip; + /// The target RIP of the last exception. + public ulong LastExceptionToRip; + /// The source RIP of the last exception. + public ulong LastExceptionFromRip; +} diff --git a/WhiteMagic/Native/SafeMemoryHandle.cs b/WhiteMagic/Native/SafeMemoryHandle.cs new file mode 100644 index 0000000..000c79f --- /dev/null +++ b/WhiteMagic/Native/SafeMemoryHandle.cs @@ -0,0 +1,34 @@ +using Microsoft.Win32.SafeHandles; + +namespace WhiteMagic.Native; + +/// +/// A Win32 handle (process, thread, or snapshot) with a managed lifetime. +/// The handle closes with CloseHandle, even after an exception or a thread abort. +/// +/// The pattern comes from MemorySharp's SafeMemoryHandle. +public sealed class SafeMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid +{ + /// + /// Makes an empty handle. The interop marshaller uses this constructor for a + /// handle that a system call returns (for example, ). + /// + public SafeMemoryHandle() : base(true) + { + } + + /// + /// Wraps a raw handle and takes ownership of the handle. + /// + /// The handle to own. + public SafeMemoryHandle(IntPtr handle) : base(true) + { + SetHandle(handle); + } + + /// + protected override bool ReleaseHandle() + { + return NativeMethods.CloseHandle(handle); + } +} diff --git a/WhiteMagic/WhiteMagic.csproj b/WhiteMagic/WhiteMagic.csproj index 2b1816a..edf34ce 100644 --- a/WhiteMagic/WhiteMagic.csproj +++ b/WhiteMagic/WhiteMagic.csproj @@ -9,4 +9,8 @@ true + + + + diff --git a/WhiteMagicTest/Native/NativeSurfaceTests.cs b/WhiteMagicTest/Native/NativeSurfaceTests.cs new file mode 100644 index 0000000..786b48e --- /dev/null +++ b/WhiteMagicTest/Native/NativeSurfaceTests.cs @@ -0,0 +1,112 @@ +using System.Runtime.InteropServices; +using WhiteMagic.Native; + +namespace WhiteMagicTest.Native; + +/// +/// Integration tests that exercise the P/Invoke surface against the current +/// process. They prove the marshalling signatures are correct end-to-end. +/// +public class NativeSurfaceTests +{ + private static SafeMemoryHandle OpenSelf(ProcessAccess access) + { + SafeMemoryHandle handle = NativeMethods.OpenProcess(access, false, Environment.ProcessId); + Assert.False(handle.IsInvalid, $"OpenProcess failed: {Marshal.GetLastPInvokeError()}"); + return handle; + } + + [Fact] + public void OpenProcess_on_self_returns_valid_handle_and_closes_on_dispose() + { + SafeMemoryHandle handle = OpenSelf(ProcessAccess.QueryInformation); + Assert.False(handle.IsClosed); + handle.Dispose(); + Assert.True(handle.IsClosed); + } + + [Fact] + public void ReadProcessMemory_reads_a_known_value_from_own_memory() + { + int value = 0x1BADB002; + GCHandle pin = GCHandle.Alloc(value, GCHandleType.Pinned); + try + { + using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmRead | ProcessAccess.QueryInformation); + Span buffer = stackalloc byte[sizeof(int)]; + + bool ok = NativeMethods.ReadProcessMemory( + handle, pin.AddrOfPinnedObject(), buffer, buffer.Length, out nint read); + + Assert.True(ok, $"ReadProcessMemory failed: {Marshal.GetLastPInvokeError()}"); + Assert.Equal(sizeof(int), (int)read); + Assert.Equal(value, BitConverter.ToInt32(buffer)); + } + finally + { + pin.Free(); + } + } + + [Fact] + public void WriteProcessMemory_writes_a_value_into_own_memory() + { + int slot = 0; + GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); + try + { + using SafeMemoryHandle handle = OpenSelf( + ProcessAccess.VmWrite | ProcessAccess.VmOperation | ProcessAccess.QueryInformation); + ReadOnlySpan payload = BitConverter.GetBytes(0x5EED); + + bool ok = NativeMethods.WriteProcessMemory( + handle, pin.AddrOfPinnedObject(), payload, payload.Length, out nint written); + + Assert.True(ok, $"WriteProcessMemory failed: {Marshal.GetLastPInvokeError()}"); + Assert.Equal(payload.Length, (int)written); + Assert.Equal(0x5EED, Marshal.ReadInt32(pin.AddrOfPinnedObject())); + } + finally + { + pin.Free(); + } + } + + [Fact] + public void VirtualAllocEx_commits_then_protects_then_frees() + { + using SafeMemoryHandle handle = OpenSelf(ProcessAccess.VmOperation | ProcessAccess.QueryInformation); + + IntPtr region = NativeMethods.VirtualAllocEx( + handle, IntPtr.Zero, 0x1000, + MemoryAllocationType.Commit | MemoryAllocationType.Reserve, + MemoryProtectionType.ReadWrite); + Assert.NotEqual(IntPtr.Zero, region); + + bool protect = NativeMethods.VirtualProtectEx( + handle, region, 0x1000, MemoryProtectionType.ExecuteReadWrite, out MemoryProtectionType old); + Assert.True(protect, $"VirtualProtectEx failed: {Marshal.GetLastPInvokeError()}"); + Assert.Equal(MemoryProtectionType.ReadWrite, old); + + bool free = NativeMethods.VirtualFreeEx(handle, region, 0, MemoryFreeType.Release); + Assert.True(free, $"VirtualFreeEx failed: {Marshal.GetLastPInvokeError()}"); + } + + [Fact] + public void LoadLibrary_then_GetProcAddress_resolves_an_export() + { + IntPtr module = NativeMethods.LoadLibrary("kernel32.dll"); + Assert.NotEqual(IntPtr.Zero, module); + + IntPtr proc = NativeMethods.GetProcAddress(module, "CloseHandle"); + Assert.NotEqual(IntPtr.Zero, proc); + } + + [Theory] + [InlineData(typeof(Context32), 716)] + [InlineData(typeof(Context64), 1232)] + public void Thread_context_struct_has_the_exact_native_size(Type contextType, int expectedSize) + { + Assert.Equal(expectedSize, Marshal.SizeOf(contextType)); + } +} diff --git a/openspec/changes/whitemagic-foundation/tasks.md b/openspec/changes/whitemagic-foundation/tasks.md index 10b5fb5..848a454 100644 --- a/openspec/changes/whitemagic-foundation/tasks.md +++ b/openspec/changes/whitemagic-foundation/tasks.md @@ -3,7 +3,7 @@ - [x] 1.1 Create `WhiteMagic/WhiteMagic.csproj` targeting `net8.0-windows`, `AllowUnsafeBlocks=true`, nullable enabled, `TreatWarningsAsErrors`, `Platforms=x86;x64;AnyCPU` - [x] 1.2 Create `WhiteMagicTest/WhiteMagicTest.csproj` (xUnit, `net8.0-windows`) referencing `WhiteMagic` - [x] 1.3 Create `WhiteMagic.slnx` (SDK 10 default solution format) and add both projects. (Built on SDK 10; `net8.0-windows` targeting pack auto-restored.) -- [ ] 1.4 Add `WhiteMagic/Native/` P/Invoke surface (`LibraryImport`): OpenProcess, Read/WriteProcessMemory, VirtualAllocEx/FreeEx/ProtectEx, CreateRemoteThread, Wow64Get/SetThreadContext, Get/SetThreadContext, LoadLibrary, GetProcAddress; add `SafeMemoryHandle` +- [x] 1.4 Add `WhiteMagic/Native/` P/Invoke surface (`LibraryImport`): OpenProcess, Read/WriteProcessMemory, VirtualAllocEx/FreeEx/ProtectEx, CreateRemoteThread, Wow64Get/SetThreadContext, Get/SetThreadContext, LoadLibrary, GetProcAddress; add `SafeMemoryHandle` - [x] 1.5 Verify empty projects build: `dotnet build WhiteMagic.slnx` — zero errors, zero warnings ## 2. Core Memory Access (spec: memory-access)