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.
279 lines
9.0 KiB
C#
279 lines
9.0 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|