Implement RemoteModule/RemoteFunction and prove x64 ABI at runtime
Task 7.2: export resolution + module/function facade. - PeHeaderParser.GetExportAddress walks the PE32/PE32+ export directory and follows export forwarders (e.g. kernel32!HeapAlloc -> NTDLL.RtlAllocateHeap) into other loaded modules; ordinal and unresolvable API-set forwarders throw NotSupportedException. - RemoteModule resolves a module base via Process.Modules (name match tolerant of .dll/case); RemoteFunction executes via RemoteThreadExecutor by default, exposes Address for pump routing and CreateDelegate<T> for in-process. - Magic gains a string indexer: magic["user32"]["MessageBoxA"]. Task 3.8: add the missing live-execution ABI test - an SSE callee whose aligned movaps #GPs unless the stub delivers a 16-byte-aligned stack, combined with a 5th stack argument. Runtime-proves shadow space, alignment, and arg placement. Tests: 214 passing, 4 skipped. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -35,6 +35,32 @@ public sealed class RemoteThreadExecutorTests
|
||||
0xC3
|
||||
];
|
||||
|
||||
// Five-arg callee that also executes an alignment-sensitive SSE instruction, proving
|
||||
// the stub delivers a 16-byte-aligned stack the CPU actually accepts (movaps #GPs on a
|
||||
// misaligned address) alongside correct register+stack argument placement.
|
||||
// sub rsp, 24 ; entry rsp ≡ 8 (mod 16) -> rsp ≡ 0 (16-aligned), 16-byte
|
||||
// ; scratch at [rsp..rsp+16) that clears the return slot ([rsp+24])
|
||||
// movaps [rsp], xmm0 ; aligned 16-byte store — faults unless rsp is 16-aligned
|
||||
// add rsp, 24 ; restore
|
||||
// mov eax, ecx
|
||||
// add eax, edx
|
||||
// add eax, r8d
|
||||
// add eax, r9d
|
||||
// add eax, [rsp+0x28] ; 5th arg above the shadow space
|
||||
// ret
|
||||
private static readonly byte[] SseAlignedSumPayload =
|
||||
[
|
||||
0x48, 0x83, 0xEC, 0x18,
|
||||
0x0F, 0x29, 0x04, 0x24,
|
||||
0x48, 0x83, 0xC4, 0x18,
|
||||
0x89, 0xC8,
|
||||
0x01, 0xD0,
|
||||
0x44, 0x01, 0xC0,
|
||||
0x44, 0x01, 0xC8,
|
||||
0x03, 0x84, 0x24, 0x28, 0x00, 0x00, 0x00,
|
||||
0xC3
|
||||
];
|
||||
|
||||
// xor eax, eax
|
||||
// cmp byte ptr [rcx+rax], 0
|
||||
// je done
|
||||
@@ -110,6 +136,21 @@ public sealed class RemoteThreadExecutorTests
|
||||
Assert.Equal(0, misalign);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_runs_sse_callee_with_five_args()
|
||||
{
|
||||
if (!Environment.Is64BitProcess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Correct result (150) requires BOTH the 5th arg reaching [rsp+0x28] AND the
|
||||
// aligned movaps not faulting. A broken frame size/alignment either mis-sums or
|
||||
// #GPs in the callee.
|
||||
int result = RunPayload(SseAlignedSumPayload, CallConvention.Cdecl, 10, 20, 30, 40, 50);
|
||||
Assert.Equal(150, result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Execute_marshals_string_as_utf8_pointer()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using WhiteMagic;
|
||||
using WhiteMagic.Assembly;
|
||||
using WhiteMagic.Native;
|
||||
using Xunit;
|
||||
|
||||
namespace WhiteMagicTest;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for <see cref="RemoteModule"/> / <see cref="RemoteFunction"/> resolution and
|
||||
/// execution through the <see cref="Magic"/> facade (task 7.2).
|
||||
/// </summary>
|
||||
public class ModuleFunctionTests
|
||||
{
|
||||
// Ensure a module is loaded in this process before resolving it.
|
||||
private static IntPtr Load(string module)
|
||||
{
|
||||
IntPtr handle = NativeMethods.LoadLibrary(module);
|
||||
Assert.NotEqual(IntPtr.Zero, handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Module_indexer_resolves_base_address()
|
||||
{
|
||||
IntPtr handle = Load("kernel32.dll");
|
||||
|
||||
using var magic = Magic.OpenInProcess();
|
||||
RemoteModule module = magic["kernel32"];
|
||||
|
||||
// The module handle returned by LoadLibrary is the module's base address.
|
||||
Assert.Equal(handle, module.BaseAddress);
|
||||
Assert.Equal("KERNEL32.DLL", module.Name, ignoreCase: true);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Function_indexer_resolves_direct_export()
|
||||
{
|
||||
IntPtr handle = Load("user32.dll");
|
||||
IntPtr expected = NativeMethods.GetProcAddress(handle, "MessageBoxA");
|
||||
Assert.NotEqual(IntPtr.Zero, expected);
|
||||
|
||||
using var magic = Magic.OpenInProcess();
|
||||
RemoteFunction fn = magic["user32"]["MessageBoxA"];
|
||||
|
||||
Assert.Equal(expected, fn.Address);
|
||||
Assert.Equal("MessageBoxA", fn.Name);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Function_indexer_follows_export_forwarder()
|
||||
{
|
||||
// kernel32!HeapAlloc is a classic forwarder to NTDLL.RtlAllocateHeap. Whatever the
|
||||
// OS loader resolves it to, our parser must reach the same final address.
|
||||
IntPtr handle = Load("kernel32.dll");
|
||||
IntPtr expected = NativeMethods.GetProcAddress(handle, "HeapAlloc");
|
||||
Assert.NotEqual(IntPtr.Zero, expected);
|
||||
|
||||
using var magic = Magic.OpenInProcess();
|
||||
RemoteFunction fn = magic["kernel32"]["HeapAlloc"];
|
||||
|
||||
Assert.Equal(expected, fn.Address);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Module_indexer_throws_for_unloaded_module()
|
||||
{
|
||||
using var magic = Magic.OpenInProcess();
|
||||
Assert.Throws<DllNotFoundException>(() => magic["definitely-not-loaded-xyz.dll"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Function_indexer_throws_for_unknown_export()
|
||||
{
|
||||
Load("kernel32.dll");
|
||||
|
||||
using var magic = Magic.OpenInProcess();
|
||||
Assert.Throws<InvalidOperationException>(() => magic["kernel32"]["NoSuchExport_ZZZ"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Resolved_function_executes_via_remote_thread()
|
||||
{
|
||||
Load("kernel32.dll");
|
||||
|
||||
using var magic = Magic.OpenInProcess();
|
||||
RemoteFunction getPid = magic["kernel32"]["GetCurrentProcessId"];
|
||||
|
||||
// GetCurrentProcessId takes no args and is thread-agnostic; a remote thread in our
|
||||
// own process must report our PID.
|
||||
uint pid = getPid.Execute<uint>(CallConvention.Stdcall);
|
||||
Assert.Equal((uint)Process.GetCurrentProcess().Id, pid);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user