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>
This commit is contained in:
kbe
2026-07-22 09:22:29 +02:00
co-authored by Claude Opus 4.8
parent 30d5a9a5ec
commit 0ddd812829
4 changed files with 54 additions and 4 deletions
+13 -1
View File
@@ -112,6 +112,15 @@ public sealed class PeHeaderParser
/// <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>
/// <remarks>
/// Forwarders are resolved by locating the target module in the process's loaded-module
/// list. API-set forwarders (virtual <c>api-ms-win-*</c> / <c>ext-ms-*</c> 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
/// <see cref="NotSupportedException"/>. On modern Windows many system-DLL exports forward
/// through API sets; resolve those via the OS loader (<c>GetProcAddress</c>) instead.
/// Ordinal forwarders (<c>Module.#N</c>) are likewise unsupported.
/// </remarks>
/// <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>
@@ -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}'.");
+12 -1
View File
@@ -53,10 +53,21 @@ public sealed class RemoteFunction
/// <summary>
/// Creates a managed delegate bound to this function for the in-process scenario.
/// Only valid when the session was opened in-process.
/// </summary>
/// <exception cref="InvalidOperationException">The session is not in-process. The
/// resolved <see cref="Address"/> 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 <see cref="Execute{T}"/> (remote thread) for external targets.</exception>
public TDelegate CreateDelegate<TDelegate>() 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<T> to call it via a remote thread.");
}
return new InProcessInvoker(_magic.Memory).CreateFunction<TDelegate>(Address);
}
}
@@ -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
+26
View File
@@ -79,6 +79,32 @@ public class ModuleFunctionTests
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()
{