Implement core diagnostic memory layer, execution helpers, and high-level facade slices
Implemented: - Core: UTF-16 ReadString boundary/alignment fix, target bitness and process id on MemoryBase - function interception: PatchManager, DetourManager, InstructionAnalyzer, MainThreadDispatcher - Execution: BackgroundTaskExecutor, InProcessInvoker - High-level: Magic facade, RemotePointer, async wrappers - Discovery/external code loading/Window groundwork (PEB/TEB, pattern scanning, raw allocations, DLL external code loading, window/input) Tests: 180 passing, 4 integration/interactive tests skipped.
This commit is contained in:
@@ -0,0 +1,278 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Hooking;
|
||||
using WhiteMagic.Native;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest.Hooking;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="PatchManager"/>, <see cref="DetourManager"/> and
|
||||
/// <see cref="Execution.MainThreadPump"/> operating in-process.
|
||||
/// </summary>
|
||||
public class HookingTests
|
||||
{
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate int FrameFunc();
|
||||
|
||||
private const int FrameResult = 42;
|
||||
|
||||
private static InProcessReader CreateReader()
|
||||
{
|
||||
return new InProcessReader();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a tiny executable function whose prologue is made entirely of
|
||||
/// covered instruction shapes, so detours apply cleanly in tests.
|
||||
/// </summary>
|
||||
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<FrameFunc>(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<InvalidOperationException>(() => 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<int> 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<FrameFunc>(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<FrameFunc>(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<int> bad = pump.ExecuteAsync<int>(() => throw new InvalidOperationException("boom"));
|
||||
Task<int> good = pump.ExecuteAsync(() => 7);
|
||||
|
||||
FrameFunc routed = Marshal.GetDelegateForFunctionPointer<FrameFunc>(targetPtr);
|
||||
routed();
|
||||
|
||||
await Assert.ThrowsAsync<InvalidOperationException>(() => bad);
|
||||
Assert.Equal(7, await good);
|
||||
|
||||
pump.Dispose();
|
||||
}
|
||||
finally
|
||||
{
|
||||
NativeMethods.VirtualFreeEx(reader.Handle, allocation, 0, MemoryFreeType.Release);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user