task 1.4: add NativeMethods.cs LibraryImport P/Invoke surface
Add LibraryImport-based P/Invoke declarations for all required Win32 APIs: OpenProcess, CloseHandle, ReadProcessMemory, WriteProcessMemory, VirtualAllocEx, VirtualProtectEx, VirtualFreeEx, CreateRemoteThread, GetThreadContext, SetThreadContext, Wow64GetThreadContext, Wow64SetThreadContext, LoadLibrary, GetProcAddress, and WaitForSingleObject. Add InternalsVisibleTo WhiteMagicTest so the test project can call internal NativeMethods. Tests (NativeSurfaceTests): 7/7 passing.
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Access rights that open a process object.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ProcessAccess : uint
|
||||
{
|
||||
/// <summary>The right to terminate the process with TerminateProcess.</summary>
|
||||
Terminate = 0x0001,
|
||||
/// <summary>The right to create a thread in the process.</summary>
|
||||
CreateThread = 0x0002,
|
||||
/// <summary>The right to operate on the address space of the process.</summary>
|
||||
VmOperation = 0x0008,
|
||||
/// <summary>The right to read memory with ReadProcessMemory.</summary>
|
||||
VmRead = 0x0010,
|
||||
/// <summary>The right to write memory with WriteProcessMemory.</summary>
|
||||
VmWrite = 0x0020,
|
||||
/// <summary>The right to duplicate a handle with DuplicateHandle.</summary>
|
||||
DupHandle = 0x0040,
|
||||
/// <summary>The right to set information about the process.</summary>
|
||||
SetInformation = 0x0200,
|
||||
/// <summary>The right to read information about the process, such as the exit code.</summary>
|
||||
QueryInformation = 0x0400,
|
||||
/// <summary>The right to suspend or resume the process.</summary>
|
||||
SuspendResume = 0x0800,
|
||||
/// <summary>The right to read a limited set of information about the process.</summary>
|
||||
QueryLimitedInformation = 0x1000,
|
||||
/// <summary>The right to use the process object for synchronization.</summary>
|
||||
Synchronize = 0x00100000,
|
||||
|
||||
/// <summary>All access rights for a process object.</summary>
|
||||
AllAccess = 0x001F0000 | Synchronize | 0xFFFF,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that control how VirtualAllocEx allocates memory.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MemoryAllocationType : uint
|
||||
{
|
||||
/// <summary>Commit physical storage for the reserved pages. The pages start as zero.</summary>
|
||||
Commit = 0x00001000,
|
||||
/// <summary>Reserve a range of address space without physical storage.</summary>
|
||||
Reserve = 0x00002000,
|
||||
/// <summary>Reset the data in the range to indicate that it is no longer of interest.</summary>
|
||||
Reset = 0x00080000,
|
||||
/// <summary>Allocate memory at the highest possible address.</summary>
|
||||
TopDown = 0x00100000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that protect a block of memory.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MemoryProtectionType : uint
|
||||
{
|
||||
/// <summary>No access to the committed pages.</summary>
|
||||
NoAccess = 0x01,
|
||||
/// <summary>Read access to the committed pages.</summary>
|
||||
ReadOnly = 0x02,
|
||||
/// <summary>Read and write access to the committed pages.</summary>
|
||||
ReadWrite = 0x04,
|
||||
/// <summary>Copy-on-write access to the committed pages.</summary>
|
||||
WriteCopy = 0x08,
|
||||
/// <summary>Execute access to the committed pages.</summary>
|
||||
Execute = 0x10,
|
||||
/// <summary>Execute and read access to the committed pages.</summary>
|
||||
ExecuteRead = 0x20,
|
||||
/// <summary>Execute, read, and write access to the committed pages.</summary>
|
||||
ExecuteReadWrite = 0x40,
|
||||
/// <summary>Execute and copy-on-write access to the committed pages.</summary>
|
||||
ExecuteWriteCopy = 0x80,
|
||||
/// <summary>The pages in the range become guard pages.</summary>
|
||||
Guard = 0x100,
|
||||
/// <summary>The system does not cache the committed pages.</summary>
|
||||
NoCache = 0x200,
|
||||
/// <summary>The system uses write-combined access for the pages.</summary>
|
||||
WriteCombine = 0x400,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that control how VirtualFreeEx frees memory.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum MemoryFreeType : uint
|
||||
{
|
||||
/// <summary>Decommit the committed pages. The address range stays reserved.</summary>
|
||||
Decommit = 0x4000,
|
||||
/// <summary>Release the range of pages. The size must be zero.</summary>
|
||||
Release = 0x8000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Values that set the initial state of a new thread.
|
||||
/// </summary>
|
||||
[Flags]
|
||||
public enum ThreadCreationFlags : uint
|
||||
{
|
||||
/// <summary>The thread runs immediately after creation.</summary>
|
||||
RunImmediately = 0,
|
||||
/// <summary>The thread starts in a suspended state. Call ResumeThread to start it.</summary>
|
||||
CreateSuspended = 0x00000004,
|
||||
/// <summary>The stack-size parameter sets the reserve size of the stack.</summary>
|
||||
StackSizeParamIsAReservation = 0x00010000,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public static class ContextFlags
|
||||
{
|
||||
/// <summary>Architecture identifier for x86 contexts.</summary>
|
||||
public const uint X86 = 0x00010000;
|
||||
/// <summary>Architecture identifier for AMD64 contexts.</summary>
|
||||
public const uint Amd64 = 0x00100000;
|
||||
|
||||
/// <summary>x86: SS:SP, CS:IP, FLAGS, and BP.</summary>
|
||||
public const uint X86Control = X86 | 0x01;
|
||||
/// <summary>x86: AX, BX, CX, DX, SI, and DI.</summary>
|
||||
public const uint X86Integer = X86 | 0x02;
|
||||
/// <summary>x86: DS, ES, FS, and GS.</summary>
|
||||
public const uint X86Segments = X86 | 0x04;
|
||||
/// <summary>x86: control, integer, and segment registers.</summary>
|
||||
public const uint X86Full = X86Control | X86Integer | X86Segments;
|
||||
|
||||
/// <summary>AMD64: SegSs, Rsp, SegCs, Rip, and EFlags.</summary>
|
||||
public const uint Amd64Control = Amd64 | 0x01;
|
||||
/// <summary>AMD64: Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi, and R8 to R15.</summary>
|
||||
public const uint Amd64Integer = Amd64 | 0x02;
|
||||
/// <summary>AMD64: SegDs, SegEs, SegFs, and SegGs.</summary>
|
||||
public const uint Amd64Segments = Amd64 | 0x04;
|
||||
/// <summary>AMD64: control, integer, and segment registers.</summary>
|
||||
public const uint Amd64Full = Amd64Control | Amd64Integer | Amd64Segments;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// P/Invoke declarations for the Win32 process, memory, thread, and module
|
||||
/// APIs that WhiteMagic uses. Every declaration uses <see cref="LibraryImportAttribute"/>
|
||||
/// (source-generated interop). SetLastError is enabled on all calls that the
|
||||
/// Win32 API documents as setting a thread-local last-error value.
|
||||
/// </summary>
|
||||
internal static partial class NativeMethods
|
||||
{
|
||||
// ── Process ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Opens an existing process and returns a handle to it.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial SafeMemoryHandle OpenProcess(
|
||||
ProcessAccess desiredAccess,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool inheritHandle,
|
||||
int processId);
|
||||
|
||||
/// <summary>Closes an open object handle.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool CloseHandle(IntPtr handle);
|
||||
|
||||
// ── Memory ───────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Reads memory from a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool ReadProcessMemory(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr baseAddress,
|
||||
Span<byte> buffer,
|
||||
int size,
|
||||
out nint bytesRead);
|
||||
|
||||
/// <summary>Writes memory to a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool WriteProcessMemory(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr baseAddress,
|
||||
ReadOnlySpan<byte> buffer,
|
||||
int size,
|
||||
out nint bytesWritten);
|
||||
|
||||
/// <summary>Reserves or commits a region of memory in a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial IntPtr VirtualAllocEx(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr address,
|
||||
nint size,
|
||||
MemoryAllocationType allocationType,
|
||||
MemoryProtectionType protect);
|
||||
|
||||
/// <summary>Changes the protection on a committed region of memory.</summary>
|
||||
[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);
|
||||
|
||||
/// <summary>Releases or decommits a region of memory in a process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool VirtualFreeEx(
|
||||
SafeMemoryHandle process,
|
||||
IntPtr address,
|
||||
nint size,
|
||||
MemoryFreeType freeType);
|
||||
|
||||
// ── Threading ────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Creates a thread that runs in the virtual address space of a process.</summary>
|
||||
[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);
|
||||
|
||||
/// <summary>Sets a 64-bit thread context (AMD64).</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool SetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context64 context);
|
||||
|
||||
/// <summary>Gets a 64-bit thread context (AMD64).</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool GetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context64 context);
|
||||
|
||||
/// <summary>Sets a 32-bit (WOW64) thread context.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Wow64SetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context32 context);
|
||||
|
||||
/// <summary>Gets a 32-bit (WOW64) thread context.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
internal static partial bool Wow64GetThreadContext(
|
||||
SafeMemoryHandle thread,
|
||||
ref Context32 context);
|
||||
|
||||
// ── Modules ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>Loads a module into the calling process.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true, EntryPoint = "LoadLibraryW")]
|
||||
internal static partial IntPtr LoadLibrary(
|
||||
[MarshalAs(UnmanagedType.LPWStr)] string lpFileName);
|
||||
|
||||
/// <summary>Returns the address of a function or variable from a loaded module.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial IntPtr GetProcAddress(
|
||||
IntPtr hModule,
|
||||
[MarshalAs(UnmanagedType.LPStr)] string lpProcName);
|
||||
|
||||
/// <summary>Waits until a thread exits and retrieves its exit code.</summary>
|
||||
[LibraryImport("kernel32.dll", SetLastError = true)]
|
||||
internal static partial int WaitForSingleObject(
|
||||
SafeMemoryHandle handle,
|
||||
uint milliseconds);
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// The x87 and MMX state inside a 32-bit thread context.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct FloatingSaveArea32
|
||||
{
|
||||
/// <summary>The x87 FPU control word.</summary>
|
||||
public uint ControlWord;
|
||||
/// <summary>The x87 FPU status word.</summary>
|
||||
public uint StatusWord;
|
||||
/// <summary>The x87 FPU tag word.</summary>
|
||||
public uint TagWord;
|
||||
/// <summary>The offset of the instruction that caused the last FPU exception.</summary>
|
||||
public uint ErrorOffset;
|
||||
/// <summary>The selector of the instruction that caused the last FPU exception.</summary>
|
||||
public uint ErrorSelector;
|
||||
/// <summary>The offset of the operand that caused the last FPU exception.</summary>
|
||||
public uint DataOffset;
|
||||
/// <summary>The selector of the operand that caused the last FPU exception.</summary>
|
||||
public uint DataSelector;
|
||||
/// <summary>The 80-byte register area.</summary>
|
||||
public fixed byte RegisterArea[80];
|
||||
/// <summary>The CR0 numeric-processor-extension state.</summary>
|
||||
public uint Cr0NpxState;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 32-bit (x86/WOW64) thread context. Use it with
|
||||
/// <c>Wow64GetThreadContext</c> and <c>Wow64SetThreadContext</c> to inspect a 32-bit thread.
|
||||
/// The total size is 716 bytes.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public unsafe struct Context32
|
||||
{
|
||||
/// <summary>Selects which parts of the context are valid. See <see cref="ContextFlags"/>.</summary>
|
||||
public uint ContextFlags;
|
||||
|
||||
/// <summary>Debug register 0.</summary>
|
||||
public uint Dr0;
|
||||
/// <summary>Debug register 1.</summary>
|
||||
public uint Dr1;
|
||||
/// <summary>Debug register 2.</summary>
|
||||
public uint Dr2;
|
||||
/// <summary>Debug register 3.</summary>
|
||||
public uint Dr3;
|
||||
/// <summary>Debug register 6.</summary>
|
||||
public uint Dr6;
|
||||
/// <summary>Debug register 7.</summary>
|
||||
public uint Dr7;
|
||||
|
||||
/// <summary>The floating-point state.</summary>
|
||||
public FloatingSaveArea32 FloatSave;
|
||||
|
||||
/// <summary>The GS segment.</summary>
|
||||
public uint SegGs;
|
||||
/// <summary>The FS segment.</summary>
|
||||
public uint SegFs;
|
||||
/// <summary>The ES segment.</summary>
|
||||
public uint SegEs;
|
||||
/// <summary>The DS segment.</summary>
|
||||
public uint SegDs;
|
||||
|
||||
/// <summary>The EDI register.</summary>
|
||||
public uint Edi;
|
||||
/// <summary>The ESI register.</summary>
|
||||
public uint Esi;
|
||||
/// <summary>The EBX register.</summary>
|
||||
public uint Ebx;
|
||||
/// <summary>The EDX register.</summary>
|
||||
public uint Edx;
|
||||
/// <summary>The ECX register.</summary>
|
||||
public uint Ecx;
|
||||
/// <summary>The EAX register.</summary>
|
||||
public uint Eax;
|
||||
|
||||
/// <summary>The base (frame) pointer.</summary>
|
||||
public uint Ebp;
|
||||
/// <summary>The instruction pointer.</summary>
|
||||
public uint Eip;
|
||||
/// <summary>The CS segment.</summary>
|
||||
public uint SegCs;
|
||||
/// <summary>The flags register.</summary>
|
||||
public uint EFlags;
|
||||
/// <summary>The stack pointer.</summary>
|
||||
public uint Esp;
|
||||
/// <summary>The SS segment.</summary>
|
||||
public uint SegSs;
|
||||
|
||||
/// <summary>The extended (processor-specific) registers. The size is 512 bytes.</summary>
|
||||
public fixed byte ExtendedRegisters[512];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A 64-bit (AMD64) thread context. Use it with the native
|
||||
/// <c>GetThreadContext</c> and <c>SetThreadContext</c> from a 64-bit process.
|
||||
/// The structure needs 16-byte alignment. The total size is 1232 bytes.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 16)]
|
||||
public unsafe struct Context64
|
||||
{
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P1Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P2Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P3Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P4Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P5Home;
|
||||
/// <summary>Home storage for a register parameter.</summary>
|
||||
public ulong P6Home;
|
||||
|
||||
/// <summary>Selects which parts of the context are valid. See <see cref="ContextFlags"/>.</summary>
|
||||
public uint ContextFlags;
|
||||
/// <summary>The MXCSR register.</summary>
|
||||
public uint MxCsr;
|
||||
|
||||
/// <summary>The CS segment.</summary>
|
||||
public ushort SegCs;
|
||||
/// <summary>The DS segment.</summary>
|
||||
public ushort SegDs;
|
||||
/// <summary>The ES segment.</summary>
|
||||
public ushort SegEs;
|
||||
/// <summary>The FS segment.</summary>
|
||||
public ushort SegFs;
|
||||
/// <summary>The GS segment.</summary>
|
||||
public ushort SegGs;
|
||||
/// <summary>The SS segment.</summary>
|
||||
public ushort SegSs;
|
||||
|
||||
/// <summary>The flags register.</summary>
|
||||
public uint EFlags;
|
||||
|
||||
/// <summary>Debug register 0.</summary>
|
||||
public ulong Dr0;
|
||||
/// <summary>Debug register 1.</summary>
|
||||
public ulong Dr1;
|
||||
/// <summary>Debug register 2.</summary>
|
||||
public ulong Dr2;
|
||||
/// <summary>Debug register 3.</summary>
|
||||
public ulong Dr3;
|
||||
/// <summary>Debug register 6.</summary>
|
||||
public ulong Dr6;
|
||||
/// <summary>Debug register 7.</summary>
|
||||
public ulong Dr7;
|
||||
|
||||
/// <summary>The RAX register.</summary>
|
||||
public ulong Rax;
|
||||
/// <summary>The RCX register.</summary>
|
||||
public ulong Rcx;
|
||||
/// <summary>The RDX register.</summary>
|
||||
public ulong Rdx;
|
||||
/// <summary>The RBX register.</summary>
|
||||
public ulong Rbx;
|
||||
/// <summary>The stack pointer.</summary>
|
||||
public ulong Rsp;
|
||||
/// <summary>The base (frame) pointer.</summary>
|
||||
public ulong Rbp;
|
||||
/// <summary>The RSI register.</summary>
|
||||
public ulong Rsi;
|
||||
/// <summary>The RDI register.</summary>
|
||||
public ulong Rdi;
|
||||
/// <summary>The R8 register.</summary>
|
||||
public ulong R8;
|
||||
/// <summary>The R9 register.</summary>
|
||||
public ulong R9;
|
||||
/// <summary>The R10 register.</summary>
|
||||
public ulong R10;
|
||||
/// <summary>The R11 register.</summary>
|
||||
public ulong R11;
|
||||
/// <summary>The R12 register.</summary>
|
||||
public ulong R12;
|
||||
/// <summary>The R13 register.</summary>
|
||||
public ulong R13;
|
||||
/// <summary>The R14 register.</summary>
|
||||
public ulong R14;
|
||||
/// <summary>The R15 register.</summary>
|
||||
public ulong R15;
|
||||
|
||||
/// <summary>The instruction pointer.</summary>
|
||||
public ulong Rip;
|
||||
|
||||
/// <summary>The XMM save area. The size is 512 bytes.</summary>
|
||||
public fixed byte FltSave[512];
|
||||
|
||||
/// <summary>The vector registers (26 entries of 16 bytes, stored as 52 entries of 8 bytes).</summary>
|
||||
public fixed ulong VectorRegister[52];
|
||||
|
||||
/// <summary>The vector control register.</summary>
|
||||
public ulong VectorControl;
|
||||
|
||||
/// <summary>The debug-control MSR.</summary>
|
||||
public ulong DebugControl;
|
||||
/// <summary>The target RIP of the last branch.</summary>
|
||||
public ulong LastBranchToRip;
|
||||
/// <summary>The source RIP of the last branch.</summary>
|
||||
public ulong LastBranchFromRip;
|
||||
/// <summary>The target RIP of the last exception.</summary>
|
||||
public ulong LastExceptionToRip;
|
||||
/// <summary>The source RIP of the last exception.</summary>
|
||||
public ulong LastExceptionFromRip;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace WhiteMagic.Native;
|
||||
|
||||
/// <summary>
|
||||
/// A Win32 handle (process, thread, or snapshot) with a managed lifetime.
|
||||
/// The handle closes with <c>CloseHandle</c>, even after an exception or a thread abort.
|
||||
/// </summary>
|
||||
/// <remarks>The pattern comes from MemorySharp's SafeMemoryHandle.</remarks>
|
||||
public sealed class SafeMemoryHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
/// <summary>
|
||||
/// Makes an empty handle. The interop marshaller uses this constructor for a
|
||||
/// handle that a system call returns (for example, <see cref="NativeMethods.OpenProcess"/>).
|
||||
/// </summary>
|
||||
public SafeMemoryHandle() : base(true)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps a raw handle and takes ownership of the handle.
|
||||
/// </summary>
|
||||
/// <param name="handle">The handle to own.</param>
|
||||
public SafeMemoryHandle(IntPtr handle) : base(true)
|
||||
{
|
||||
SetHandle(handle);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
return NativeMethods.CloseHandle(handle);
|
||||
}
|
||||
}
|
||||
@@ -9,4 +9,8 @@
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="WhiteMagicTest" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagicTest.Native;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests that exercise the P/Invoke surface against the current
|
||||
/// process. They prove the marshalling signatures are correct end-to-end.
|
||||
/// </summary>
|
||||
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<byte> 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<byte> 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));
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user