Files
whitemagic/WhiteMagicTest/ModuleFunctionTests.cs
T
kbeandClaude Opus 4.8 0ddd812829 Address review: forwarder split, CreateDelegate guard, API-set docs
- PeHeaderParser: split export forwarders on the FIRST dot (IndexOf), not the
  last. A forwarder is "Module.Function" and the module name has no extension, so
  the last-dot split misparsed export names that themselves contain a dot.
- PeHeaderParser: document that API-set (api-ms-win-*/ext-ms-*) and ordinal
  forwarders are unsupported and should be resolved via the OS loader.
- RemoteFunction.CreateDelegate now throws InvalidOperationException unless the
  session is in-process; an external target's address is not host-mapped and a
  delegate to it would access-violate on invocation. Tests cover both paths.
- Reword the SSE-payload comment: the 16-byte scratch sits below the saved
  return address, which the aligned store leaves intact (it never overwrote it).

Tests: 223 passing, 4 skipped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 09:22:29 +02:00

122 lines
3.9 KiB
C#

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"]);
}
private delegate uint GetCurrentProcessIdDelegate();
[Fact]
public void CreateDelegate_throws_for_external_session()
{
Load("kernel32.dll");
// External reader (even to self): the address is not treated as host-mapped, so a
// delegate to it is rejected rather than handed back to AV on invocation.
using var magic = Magic.Open(Process.GetCurrentProcess());
RemoteFunction fn = magic["kernel32"]["GetCurrentProcessId"];
Assert.Throws<InvalidOperationException>(() => fn.CreateDelegate<GetCurrentProcessIdDelegate>());
}
[Fact]
public void CreateDelegate_invokes_function_in_process()
{
Load("kernel32.dll");
using var magic = Magic.OpenInProcess();
var getPid = magic["kernel32"]["GetCurrentProcessId"].CreateDelegate<GetCurrentProcessIdDelegate>();
Assert.Equal((uint)Process.GetCurrentProcess().Id, getPid());
}
[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);
}
}