using System; using System.Runtime.InteropServices; using System.Threading.Tasks; using WhiteMagic; using WhiteMagic.Hooking; using WhiteMagic.Native; using Xunit; namespace WhiteMagicTest.Hooking; /// /// Tests for , and /// operating in-process. /// public class HookingTests { [UnmanagedFunctionPointer(CallingConvention.Cdecl)] private delegate int FrameFunc(); private const int FrameResult = 42; private static InProcessReader CreateReader() { return new InProcessReader(); } /// /// Allocates a tiny executable function whose prologue is made entirely of /// covered instruction shapes, so detours apply cleanly in tests. /// private static IntPtr AllocateFrameStub(MemoryBase reader, out IntPtr allocationBase) { // x64: push rbp; push rdi; push rsi; push rbx; sub rsp, 0x28; sub rsp, 0x12345678; // mov eax, 42; add rsp, 0x12345678; add rsp, 0x28; pop rbx; pop rsi; pop rdi; pop rbp; ret byte[] code = [ 0x55, // push rbp 0x57, // push rdi 0x56, // push rsi 0x53, // push rbx 0x48, 0x83, 0xEC, 0x28, // sub rsp, 0x28 0x48, 0x81, 0xEC, 0x78, 0x56, 0x34, 0x12, // sub rsp, 0x12345678 0xB8, 0x2A, 0x00, 0x00, 0x00, // mov eax, 42 0x48, 0x81, 0xC4, 0x78, 0x56, 0x34, 0x12, // add rsp, 0x12345678 0x48, 0x83, 0xC4, 0x28, // add rsp, 0x28 0x5B, // pop rbx 0x5E, // pop rsi 0x5F, // pop rdi 0x5D, // pop rbp 0xC3 // ret ]; allocationBase = NativeMethods.VirtualAllocEx( reader.Handle, IntPtr.Zero, code.Length, MemoryAllocationType.Commit | MemoryAllocationType.Reserve, MemoryProtectionType.ExecuteReadWrite); Assert.NotEqual(IntPtr.Zero, allocationBase); reader.WriteBytes(allocationBase, code); return allocationBase; } [Fact] public void Patch_apply_writes_bytes_and_remove_restores_original() { using var reader = CreateReader(); byte[] slot = new byte[8]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); byte[] original = reader.ReadBytes(addr, 4); byte[] patchBytes = [0x90, 0x90, 0x90, 0x90]; Patch patch = reader.PatchManager.Create("nop", addr, patchBytes); Assert.False(patch.IsApplied); patch.Apply(); Assert.True(patch.IsApplied); Assert.Equal(patchBytes, reader.ReadBytes(addr, 4)); patch.Remove(); Assert.False(patch.IsApplied); Assert.Equal(original, reader.ReadBytes(addr, 4)); } finally { pin.Free(); } } [Fact] public void Detour_apply_redirects_callOriginal_remove_restores() { using var reader = CreateReader(); IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation); try { int hookCalls = 0; Detour? detour = null; FrameFunc hook = () => { hookCalls++; return (int?)detour?.CallOriginal() ?? 0; }; detour = reader.DetourManager.Create("frame", targetPtr, hook); detour.Apply(); FrameFunc routed = Marshal.GetDelegateForFunctionPointer(targetPtr); int result = routed(); Assert.True(hookCalls > 0); Assert.Equal(FrameResult, result); detour.Remove(); hookCalls = 0; result = routed(); Assert.Equal(0, hookCalls); Assert.Equal(FrameResult, result); GC.KeepAlive(hook); } finally { NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release); } } [Fact] public void Detour_named_lookup_returns_existing_detour() { using var reader = CreateReader(); IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation); try { Detour detour = reader.DetourManager.Create("lookup", targetPtr, (FrameFunc)(() => FrameResult)); Assert.Same(detour, reader.DetourManager["lookup"]); } finally { NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release); } } [Fact] public void Detour_aligned_prologue_applies_and_unknown_prologue_rejects() { using var reader = CreateReader(); // A normal JIT-compiled function has a covered prologue shape. IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr goodAllocation); try { Detour good = reader.DetourManager.Create("good", targetPtr, (FrameFunc)(() => FrameResult)); good.Apply(); good.Remove(); } finally { NativeMethods.VirtualFreeEx(reader.Handle, goodAllocation, 0, MemoryFreeType.Release); } // Allocate a small executable region whose first instruction is outside the // covered set. The decoder must refuse to splice it. IntPtr code = NativeMethods.VirtualAllocEx( reader.Handle, IntPtr.Zero, 32, MemoryAllocationType.Commit | MemoryAllocationType.Reserve, MemoryProtectionType.ExecuteReadWrite); Assert.NotEqual(IntPtr.Zero, code); try { // 0x0F 0x05 = syscall (not covered), followed by padding and a ret. byte[] unknown = [0x0F, 0x05, 0xC3, 0xCC, 0xCC, 0xCC, 0xCC, 0xCC]; reader.WriteBytes(code, unknown); Detour bad = reader.DetourManager.Create("bad", code, (FrameFunc)(() => FrameResult)); Assert.Throws(() => bad.Apply()); } finally { NativeMethods.VirtualFreeEx(reader.Handle, code, 0, MemoryFreeType.Release); } } [Fact] public void Dispose_restores_active_patches_and_detours() { InProcessReader reader = CreateReader(); byte[] slot = new byte[8]; GCHandle pin = GCHandle.Alloc(slot, GCHandleType.Pinned); try { IntPtr addr = pin.AddrOfPinnedObject(); byte[] original = reader.ReadBytes(addr, 2); Patch patch = reader.PatchManager.Create("dispose-patch", addr, [0x90, 0x90]); patch.Apply(); reader.Dispose(); // Verify with a fresh reader; the original handle was closed by Dispose. using var verify = CreateReader(); Assert.Equal(original, verify.ReadBytes(addr, 2)); } finally { pin.Free(); } } [Fact] public async Task MainThreadPump_drains_work_on_frame_call_and_uninstalls_on_dispose() { using var reader = CreateReader(); IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation); try { var pump = new WhiteMagic.Execution.MainThreadPump(reader.DetourManager, targetPtr); pump.Install(); Task work = pump.ExecuteAsync(() => 123); // Drive the frame function manually. The detoured frame runs the pump hook on // this thread, drains the work queue, then calls the original frame function. FrameFunc routed = Marshal.GetDelegateForFunctionPointer(targetPtr); int frameResult = routed(); Assert.Equal(FrameResult, frameResult); Assert.Equal(123, await work); pump.Dispose(); // After uninstall, calling the frame function should behave like the original. routed = Marshal.GetDelegateForFunctionPointer(targetPtr); Assert.Equal(FrameResult, routed()); } finally { NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release); } } [Fact] public async Task MainThreadPump_exception_survives_and_does_not_kill_pump() { using var reader = CreateReader(); IntPtr targetPtr = AllocateFrameStub(reader, out IntPtr allocation); try { var pump = new WhiteMagic.Execution.MainThreadPump(reader.DetourManager, targetPtr); pump.Install(); Task bad = pump.ExecuteAsync(() => throw new InvalidOperationException("boom")); Task good = pump.ExecuteAsync(() => 7); FrameFunc routed = Marshal.GetDelegateForFunctionPointer(targetPtr); routed(); await Assert.ThrowsAsync(() => bad); Assert.Equal(7, await good); pump.Dispose(); } finally { NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release); } } }