From 0ddd812829142bc73de58e2774e8d696dfcbaa4e Mon Sep 17 00:00:00 2001 From: Kevin Bataille Date: Wed, 22 Jul 2026 09:22:29 +0200 Subject: [PATCH] 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) --- WhiteMagic/Discovery/PeHeaderParser.cs | 14 +++++++++- WhiteMagic/RemoteFunction.cs | 13 +++++++++- .../Execution/RemoteThreadExecutorTests.cs | 5 ++-- WhiteMagicTest/ModuleFunctionTests.cs | 26 +++++++++++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/WhiteMagic/Discovery/PeHeaderParser.cs b/WhiteMagic/Discovery/PeHeaderParser.cs index d92b914..76b5751 100644 --- a/WhiteMagic/Discovery/PeHeaderParser.cs +++ b/WhiteMagic/Discovery/PeHeaderParser.cs @@ -112,6 +112,15 @@ public sealed class PeHeaderParser /// The exported symbol name (case-sensitive, as stored /// in the export name table). /// The absolute address of the export in the target process. + /// + /// Forwarders are resolved by locating the target module in the process's loaded-module + /// list. API-set forwarders (virtual api-ms-win-* / ext-ms-* names) are NOT + /// supported: those are not real loaded modules, so resolution through the module list is + /// impossible without parsing the API-set schema — such a forwarder throws + /// . On modern Windows many system-DLL exports forward + /// through API sets; resolve those via the OS loader (GetProcAddress) instead. + /// Ordinal forwarders (Module.#N) are likewise unsupported. + /// /// The export is not present. /// 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. @@ -212,7 +221,10 @@ public sealed class PeHeaderParser private IntPtr ResolveForwarder(string forwarder, int depth) { - int dot = forwarder.LastIndexOf('.'); + // A forwarder is "Module.Function"; the module name carries no extension, so the + // FIRST dot is the boundary. Splitting on the last dot would misparse export names + // that themselves contain a dot (e.g. some C++/managed exports). + int dot = forwarder.IndexOf('.'); if (dot <= 0 || dot >= forwarder.Length - 1) throw new InvalidDataException($"Malformed export forwarder string '{forwarder}'."); diff --git a/WhiteMagic/RemoteFunction.cs b/WhiteMagic/RemoteFunction.cs index e353063..342a8ec 100644 --- a/WhiteMagic/RemoteFunction.cs +++ b/WhiteMagic/RemoteFunction.cs @@ -53,10 +53,21 @@ public sealed class RemoteFunction /// /// Creates a managed delegate bound to this function for the in-process scenario. - /// Only valid when the session was opened in-process. /// + /// The session is not in-process. The + /// resolved lives in the target process; a delegate to it would + /// access-violate when invoked from the host, so this is rejected for external sessions. + /// Use (remote thread) for external targets. public TDelegate CreateDelegate() where TDelegate : Delegate { + if (_magic.Memory is not InProcessReader) + { + throw new InvalidOperationException( + "CreateDelegate is only valid for an in-process session (Magic.OpenInProcess). " + + "The function address is not mapped into the host process for an external target; " + + "use Execute to call it via a remote thread."); + } + return new InProcessInvoker(_magic.Memory).CreateFunction(Address); } } diff --git a/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs b/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs index 161c8fa..adb1f1c 100644 --- a/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs +++ b/WhiteMagicTest/Execution/RemoteThreadExecutorTests.cs @@ -38,8 +38,9 @@ public sealed class RemoteThreadExecutorTests // 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]) + // sub rsp, 24 ; entry rsp ≡ 8 (mod 16) -> rsp ≡ 0 (16-aligned), giving a + // ; 16-byte aligned scratch at [rsp..rsp+16) below the saved + // ; return address ([rsp+24]) so the store leaves it intact // movaps [rsp], xmm0 ; aligned 16-byte store — faults unless rsp is 16-aligned // add rsp, 24 ; restore // mov eax, ecx diff --git a/WhiteMagicTest/ModuleFunctionTests.cs b/WhiteMagicTest/ModuleFunctionTests.cs index 68cff17..c962efb 100644 --- a/WhiteMagicTest/ModuleFunctionTests.cs +++ b/WhiteMagicTest/ModuleFunctionTests.cs @@ -79,6 +79,32 @@ public class ModuleFunctionTests Assert.Throws(() => 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(() => fn.CreateDelegate()); + } + + [Fact] + public void CreateDelegate_invokes_function_in_process() + { + Load("kernel32.dll"); + + using var magic = Magic.OpenInProcess(); + var getPid = magic["kernel32"]["GetCurrentProcessId"].CreateDelegate(); + + Assert.Equal((uint)Process.GetCurrentProcess().Id, getPid()); + } + [Fact] public void Resolved_function_executes_via_remote_thread() {