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:
@@ -1,5 +1,6 @@
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using WhiteMagic.Native;
|
||||
|
||||
namespace WhiteMagic.Discovery;
|
||||
@@ -103,6 +104,138 @@ public sealed class PeHeaderParser
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an exported function's absolute address by name, following export
|
||||
/// forwarders (e.g. <c>kernel32!HeapAlloc</c> → <c>NTDLL.RtlAllocateHeap</c>) into
|
||||
/// other modules loaded in the same target process.
|
||||
/// </summary>
|
||||
/// <param name="functionName">The exported symbol name (case-sensitive, as stored
|
||||
/// in the export name table).</param>
|
||||
/// <returns>The absolute address of the export in the target process.</returns>
|
||||
/// <exception cref="InvalidOperationException">The export is not present.</exception>
|
||||
/// <exception cref="NotSupportedException">The export forwards to an ordinal or to a
|
||||
/// module (such as an API set) that is not resolvable from the target's module list.</exception>
|
||||
/// <exception cref="InvalidDataException">The PE export data is malformed.</exception>
|
||||
public IntPtr GetExportAddress(string functionName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(functionName);
|
||||
return ResolveExport(functionName, 0);
|
||||
}
|
||||
|
||||
// Maximum forwarder hops before giving up, to bound pathological chains.
|
||||
private const int MaxForwarderDepth = 16;
|
||||
|
||||
private IntPtr ResolveExport(string functionName, int depth)
|
||||
{
|
||||
if (depth > MaxForwarderDepth)
|
||||
throw new InvalidDataException($"Export forwarder chain for '{functionName}' is too deep.");
|
||||
|
||||
var (optionalHeader, _) = ParseOptionalHeader();
|
||||
if (optionalHeader is null || optionalHeader.Length < 2)
|
||||
throw new InvalidDataException("Optional header unavailable.");
|
||||
|
||||
// 0x10b = PE32 (32-bit), 0x20b = PE32+ (64-bit). Data directories start at a
|
||||
// different offset in each: 96 for PE32, 112 for PE32+. The export table is
|
||||
// directory index 0, so its 8-byte entry sits at that offset.
|
||||
ushort magic = BitConverter.ToUInt16(optionalHeader, 0);
|
||||
bool pe32Plus = magic == 0x20b;
|
||||
int exportDirOffset = pe32Plus ? 112 : 96;
|
||||
if (optionalHeader.Length < exportDirOffset + 8)
|
||||
throw new InvalidDataException("Optional header does not contain the export data directory.");
|
||||
|
||||
uint exportRva = BitConverter.ToUInt32(optionalHeader, exportDirOffset);
|
||||
uint exportSize = BitConverter.ToUInt32(optionalHeader, exportDirOffset + 4);
|
||||
if (exportRva == 0 || exportSize == 0)
|
||||
throw new InvalidOperationException("Module has no export table.");
|
||||
|
||||
// IMAGE_EXPORT_DIRECTORY is 40 bytes.
|
||||
byte[] dir = _memory.ReadBytes(_baseAddress + (nint)exportRva, 40);
|
||||
if (dir.Length < 40)
|
||||
throw new InvalidDataException("Failed to read the export directory.");
|
||||
|
||||
uint numberOfFunctions = BitConverter.ToUInt32(dir, 20);
|
||||
uint numberOfNames = BitConverter.ToUInt32(dir, 24);
|
||||
uint addressOfFunctions = BitConverter.ToUInt32(dir, 28);
|
||||
uint addressOfNames = BitConverter.ToUInt32(dir, 32);
|
||||
uint addressOfNameOrdinals = BitConverter.ToUInt32(dir, 36);
|
||||
|
||||
// Guard against corrupt counts before allocating arrays sized from them.
|
||||
if (numberOfNames > 0x10000 || numberOfFunctions > 0x10000)
|
||||
throw new InvalidDataException("Export table entry count is out of range.");
|
||||
|
||||
if (numberOfNames == 0)
|
||||
throw new InvalidOperationException($"Export '{functionName}' not found (module exports no names).");
|
||||
|
||||
byte[] nameRvas = _memory.ReadBytes(_baseAddress + (nint)addressOfNames, checked((int)(numberOfNames * 4)));
|
||||
byte[] nameOrdinals = _memory.ReadBytes(_baseAddress + (nint)addressOfNameOrdinals, checked((int)(numberOfNames * 2)));
|
||||
if (nameRvas.Length < numberOfNames * 4 || nameOrdinals.Length < numberOfNames * 2)
|
||||
throw new InvalidDataException("Failed to read the export name tables.");
|
||||
|
||||
int nameIndex = -1;
|
||||
for (int i = 0; i < numberOfNames; i++)
|
||||
{
|
||||
uint nameRva = BitConverter.ToUInt32(nameRvas, i * 4);
|
||||
string name = _memory.ReadString(_baseAddress + (nint)nameRva, Encoding.ASCII, 512);
|
||||
if (string.Equals(name, functionName, StringComparison.Ordinal))
|
||||
{
|
||||
nameIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (nameIndex < 0)
|
||||
throw new InvalidOperationException($"Export '{functionName}' not found in module.");
|
||||
|
||||
ushort ordinal = BitConverter.ToUInt16(nameOrdinals, nameIndex * 2);
|
||||
if (ordinal >= numberOfFunctions)
|
||||
throw new InvalidDataException("Export name ordinal is out of range.");
|
||||
|
||||
byte[] funcRvaBytes = _memory.ReadBytes(
|
||||
_baseAddress + (nint)(addressOfFunctions + (uint)ordinal * 4u), 4);
|
||||
if (funcRvaBytes.Length < 4)
|
||||
throw new InvalidDataException("Failed to read the export address table entry.");
|
||||
|
||||
uint funcRva = BitConverter.ToUInt32(funcRvaBytes, 0);
|
||||
if (funcRva == 0)
|
||||
throw new InvalidOperationException($"Export '{functionName}' has no address.");
|
||||
|
||||
// A function RVA that lands inside the export directory region is not code but a
|
||||
// null-terminated "Module.Function" forwarder string.
|
||||
if (funcRva >= exportRva && funcRva < exportRva + exportSize)
|
||||
{
|
||||
string forwarder = _memory.ReadString(_baseAddress + (nint)funcRva, Encoding.ASCII, 512);
|
||||
return ResolveForwarder(forwarder, depth);
|
||||
}
|
||||
|
||||
return _baseAddress + (nint)funcRva;
|
||||
}
|
||||
|
||||
private IntPtr ResolveForwarder(string forwarder, int depth)
|
||||
{
|
||||
int dot = forwarder.LastIndexOf('.');
|
||||
if (dot <= 0 || dot >= forwarder.Length - 1)
|
||||
throw new InvalidDataException($"Malformed export forwarder string '{forwarder}'.");
|
||||
|
||||
string moduleName = forwarder[..dot];
|
||||
string target = forwarder[(dot + 1)..];
|
||||
|
||||
if (target.StartsWith('#'))
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Ordinal export forwarders are not supported (forwarder '{forwarder}').");
|
||||
}
|
||||
|
||||
IntPtr targetBase = RemoteModule.ResolveBase(_memory.ProcessId, moduleName);
|
||||
if (targetBase == IntPtr.Zero)
|
||||
{
|
||||
throw new NotSupportedException(
|
||||
$"Export forwarder target module '{moduleName}' is not loaded in the target " +
|
||||
$"process, or is an unresolvable API set (forwarder '{forwarder}').");
|
||||
}
|
||||
|
||||
return new PeHeaderParser(_memory, targetBase).ResolveExport(target, depth + 1);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses the DOS header, PE signature, and optional header.
|
||||
/// </summary>
|
||||
|
||||
@@ -54,6 +54,10 @@ public sealed class Magic : IDisposable
|
||||
/// <summary>Returns a <see cref="RemotePointer"/> at <paramref name="address"/>.</summary>
|
||||
public RemotePointer this[IntPtr address] => new RemotePointer(Memory, address);
|
||||
|
||||
/// <summary>Returns the loaded <see cref="RemoteModule"/> named <paramref name="moduleName"/>
|
||||
/// (e.g. <c>magic["user32"]["MessageBoxA"]</c>).</summary>
|
||||
public RemoteModule this[string moduleName] => new RemoteModule(this, moduleName);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using System.Threading.Tasks;
|
||||
using WhiteMagic.Assembly;
|
||||
using WhiteMagic.Execution;
|
||||
|
||||
namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// An exported function resolved in the target process, obtained via
|
||||
/// <c>magic["module"]["function"]</c>. Executes through one of the session's execution
|
||||
/// strategies.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The default <see cref="Execute{T}"/> path uses the always-available
|
||||
/// <see cref="RemoteThreadExecutor"/> (<c>CreateRemoteThread</c>), which is safe for
|
||||
/// thread-agnostic exports. For a call that touches single-threaded target state, obtain
|
||||
/// the <see cref="Address"/> and route it through a <see cref="MainThreadPump"/>, or use
|
||||
/// <see cref="CreateDelegate{TDelegate}"/> when running in-process.
|
||||
/// </remarks>
|
||||
public sealed class RemoteFunction
|
||||
{
|
||||
private readonly Magic _magic;
|
||||
|
||||
/// <summary>The export name this function was resolved from.</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>The absolute address of the function in the target process.</summary>
|
||||
public IntPtr Address { get; }
|
||||
|
||||
internal RemoteFunction(Magic magic, string name, IntPtr address)
|
||||
{
|
||||
_magic = magic;
|
||||
Name = name;
|
||||
Address = address;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Calls the function via a remote thread and returns its result cast to
|
||||
/// <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <param name="convention">The calling convention (ignored on x64 targets).</param>
|
||||
/// <param name="args">Arguments to pass; primitives, pointers, enums, strings and
|
||||
/// structs are supported.</param>
|
||||
public T Execute<T>(CallConvention convention, params object?[] args)
|
||||
{
|
||||
return _magic.RemoteThread.Execute<T>(Address, convention, args);
|
||||
}
|
||||
|
||||
/// <summary>Asynchronous variant of <see cref="Execute{T}"/>.</summary>
|
||||
public Task<T> ExecuteAsync<T>(CallConvention convention, params object?[] args)
|
||||
{
|
||||
return _magic.RemoteThread.ExecuteAsync<T>(Address, convention, args);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a managed delegate bound to this function for the in-process scenario.
|
||||
/// Only valid when the session was opened in-process.
|
||||
/// </summary>
|
||||
public TDelegate CreateDelegate<TDelegate>() where TDelegate : Delegate
|
||||
{
|
||||
return new InProcessInvoker(_magic.Memory).CreateFunction<TDelegate>(Address);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
using WhiteMagic.Discovery;
|
||||
using Process = System.Diagnostics.Process;
|
||||
using ProcessModule = System.Diagnostics.ProcessModule;
|
||||
|
||||
namespace WhiteMagic;
|
||||
|
||||
/// <summary>
|
||||
/// A module (loaded DLL/EXE image) in the target process, obtained by indexing the
|
||||
/// facade with a module name (e.g. <c>magic["user32"]</c>). Exposes the module's base
|
||||
/// address and resolves exported functions by name.
|
||||
/// </summary>
|
||||
public sealed class RemoteModule
|
||||
{
|
||||
private readonly Magic _magic;
|
||||
|
||||
/// <summary>The module's file name as reported by the OS (e.g. <c>user32.dll</c>).</summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>The module's load address in the target process.</summary>
|
||||
public IntPtr BaseAddress { get; }
|
||||
|
||||
internal RemoteModule(Magic magic, string moduleName)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(magic);
|
||||
ArgumentException.ThrowIfNullOrEmpty(moduleName);
|
||||
|
||||
_magic = magic;
|
||||
|
||||
(string name, IntPtr baseAddress) = FindModule(magic.Memory.ProcessId, moduleName);
|
||||
Name = name;
|
||||
BaseAddress = baseAddress;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves an exported function by name and returns a <see cref="RemoteFunction"/>
|
||||
/// bound to its address. Export forwarders are followed.
|
||||
/// </summary>
|
||||
public RemoteFunction this[string functionName]
|
||||
{
|
||||
get
|
||||
{
|
||||
IntPtr address = GetExportAddress(functionName);
|
||||
return new RemoteFunction(_magic, functionName, address);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Resolves the absolute address of an exported function by name.</summary>
|
||||
public IntPtr GetExportAddress(string functionName)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrEmpty(functionName);
|
||||
var parser = new PeHeaderParser(_magic.Memory, BaseAddress);
|
||||
return parser.GetExportAddress(functionName);
|
||||
}
|
||||
|
||||
private static (string Name, IntPtr BaseAddress) FindModule(int processId, string moduleName)
|
||||
{
|
||||
using Process process = Process.GetProcessById(processId);
|
||||
foreach (ProcessModule module in process.Modules)
|
||||
{
|
||||
if (NameMatches(module.ModuleName, moduleName))
|
||||
return (module.ModuleName, module.BaseAddress);
|
||||
}
|
||||
|
||||
throw new DllNotFoundException(
|
||||
$"Module '{moduleName}' is not loaded in process {processId}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves a module's base address by name within a target process, returning
|
||||
/// <see cref="IntPtr.Zero"/> if it is not loaded. Used by export-forwarder resolution.
|
||||
/// </summary>
|
||||
internal static IntPtr ResolveBase(int processId, string moduleName)
|
||||
{
|
||||
using Process process = Process.GetProcessById(processId);
|
||||
foreach (ProcessModule module in process.Modules)
|
||||
{
|
||||
if (NameMatches(module.ModuleName, moduleName))
|
||||
return module.BaseAddress;
|
||||
}
|
||||
|
||||
return IntPtr.Zero;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Matches a loaded module's file name against a requested name, tolerating a missing
|
||||
/// or present <c>.dll</c> extension and ignoring case (e.g. <c>KERNEL32</c> matches
|
||||
/// <c>kernel32.dll</c>).
|
||||
/// </summary>
|
||||
private static bool NameMatches(string actual, string requested)
|
||||
{
|
||||
if (string.Equals(actual, requested, StringComparison.OrdinalIgnoreCase))
|
||||
return true;
|
||||
|
||||
string actualNoExt = Path.GetFileNameWithoutExtension(actual);
|
||||
string requestedNoExt = requested.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)
|
||||
? requested[..^4]
|
||||
: requested;
|
||||
|
||||
return string.Equals(actualNoExt, requestedNoExt, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@
|
||||
- [x] 3.5 Add tests for stdcall (no caller cleanup), thiscall (ecx = this), fastcall (ecx/edx) x86 stubs
|
||||
- [x] 3.6 Implement x86 stdcall/thiscall/fastcall stubs to pass 3.5
|
||||
- [x] 3.7 Add tests for x64 stub argument-register placement and call
|
||||
- [x] 3.8 Implement x64 stub to pass 3.7. **Deviation (review):** `BuildCallStub` takes `nuint[]` (was `uint[]`). x64 stub is Microsoft-x64-ABI compliant: allocates 32-byte shadow space, keeps 16-byte stack alignment at the inner `call` (frame `K ≡ 8 (mod 16)`, `K ≥ 0x20 + 8·stackArgs`), loads RCX/RDX/R8/R9 with full 64-bit `imm64` (no >4 GiB pointer truncation), and writes stack args above the shadow window (no return-address clobber). x86 rejects args > `uint.MaxValue`. Argument count bounded by `MaxArguments` (256) to keep frame arithmetic overflow-free. **Byte-level tests only — a live-execution test (5-arg + SSE callee via `CreateRemoteThread`) is still needed to prove the ABI at runtime.**
|
||||
- [x] 3.8 Implement x64 stub to pass 3.7. **Deviation (review):** `BuildCallStub` takes `nuint[]` (was `uint[]`). x64 stub is Microsoft-x64-ABI compliant: allocates 32-byte shadow space, keeps 16-byte stack alignment at the inner `call` (frame `K ≡ 8 (mod 16)`, `K ≥ 0x20 + 8·stackArgs`), loads RCX/RDX/R8/R9 with full 64-bit `imm64` (no >4 GiB pointer truncation), and writes stack args above the shadow window (no return-address clobber). x86 rejects args > `uint.MaxValue`. Argument count bounded by `MaxArguments` (256) to keep frame arithmetic overflow-free. **Runtime ABI now proven:** live-execution tests via `CreateRemoteThread` cover 5-arg register+stack delivery (`Execute_sums_register_and_stack_arguments`), 16-byte entry alignment arithmetically (`Execute_delivers_16byte_aligned_stack_to_callee`), and a hardware-alignment-sensitive SSE callee (`Execute_runs_sse_callee_with_five_args`: aligned `movaps` that #GPs unless the stub delivers a 16-byte-aligned stack, combined with a 5th stack arg).
|
||||
- [x] 3.9 Confirm no FASM/`ManagedFasm` reference exists in `WhiteMagic` output (assert via a test that scans loaded references)
|
||||
|
||||
## 4. Crash-Safe Execution Slice (spec: remote-execution, function-hooking)
|
||||
@@ -66,7 +66,7 @@
|
||||
## 7. High-Level Ergonomics (spec: high-level-api)
|
||||
|
||||
- [x] 7.1 Add tests + implement `RemotePointer` indexer (`sharp[addr].Read/Write/Execute` relative to base)
|
||||
- [ ] 7.2 Add tests + implement `RemoteModule`/`RemoteFunction` (`sharp["mod"]["fn"]`) resolving export addresses and executing via a chosen strategy
|
||||
- [x] 7.2 Add tests + implement `RemoteModule`/`RemoteFunction` (`sharp["mod"]["fn"]`) resolving export addresses and executing via a chosen strategy. **Deviation:** export resolution added to `PeHeaderParser.GetExportAddress` (PE32/PE32+ export directory walk) and **follows export forwarders** (e.g. `kernel32!HeapAlloc` → `NTDLL.RtlAllocateHeap`) into other loaded modules; ordinal forwarders and unresolvable API-set targets throw `NotSupportedException`. `RemoteModule` resolves the base via `Process.Modules` (name match tolerant of `.dll`/case). `RemoteFunction.Execute<T>` defaults to the always-available `RemoteThreadExecutor`; `Address` is exposed for pump routing and `CreateDelegate<T>` for the in-process tier. Tests cross-check resolved addresses against the OS `GetProcAddress` (direct export + forwarder) and execute `kernel32!GetCurrentProcessId` end-to-end.
|
||||
- [x] 7.3 Add tests + implement `ManagedPeb`/`ManagedTeb` field reads
|
||||
- [x] 7.4 Add tests + implement `WindowFactory`/`RemoteWindow` (enumerate, move/resize/title/activate/flash, query by class)
|
||||
- [x] 7.5 Add tests + implement keyboard/mouse simulation (PostMessage + SendInput) to a target window
|
||||
|
||||
Reference in New Issue
Block a user