- Implement complete cross-process injection engine via snd_inj_ctx_t - Track explicit remote_entry_point for safe thread hijacking and PE execution - Refactor standalone syscall resolvers into a unified pipeline (scan/sort) - Perfect status code system by purging generic fallbacks in favor of highly-specific, domain-aware error codes - Fix minor pointer validation and parsing bugs in the reflective loader
3.4 KiB
Dynamic Invocation FFI
The Foreign Function Interface (FFI) layer invokes dynamically resolved function pointers at runtime using a generic argument array. PoCs use it for named exports (-e/-a) whose signatures are unknown at compile time. DllMain and EXE entry use typed calls inside the loader engine — see Loader integration.
Public API: snd_ffi_execute in include/sindri/primitives/ffi.h
Dispatch: src/primitives/execution/ffi/ffi_invoke.c
Bridges: src/primitives/execution/ffi/asm/
Why not cast in C?
Calling an arbitrary pointer in C requires casting to a typed function signature. MSVC emits C4152 (non-standard function pointer conversion) when casting between incompatible function pointer types.
SindriKit avoids this at the call site:
- Resolve the export to
FARPROC/UINT_PTR. - Cast through
(PVOID)(UINT_PTR)proc— an integer conversion, not a function-pointer conversion. - Pass to
snd_ffi_execute, which type-puns inside the MASM stub where the CPU simplyCALLs the raw address.
PoC pattern (from pocs/loader_winapi/main.c):
FARPROC dynamic_proc = NULL;
status = snd_ldr_pe_get_proc_address(&ctx, export_name, &dynamic_proc);
PVOID fn_ptr = (PVOID)(UINT_PTR)dynamic_proc;
UINT_PTR retval = snd_ffi_execute(fn_ptr, call_argc, call_argc > 0 ? call_args : NULL);
Validation (ffi_invoke.c)
Before entering assembly:
| Check | Action |
|---|---|
pFunctionAddress == NULL |
Return 0 |
dwArgCount > 0 && pArgs == NULL |
Return 0 |
| Otherwise | Route to arch-specific bridge |
There is no arity or type validation — mismatched arguments corrupt the stack or registers.
x64 bridge (snd_ffi_bridge_x64)
File: ffi/asm/ffi_x64.asm
Follows the Microsoft x64 calling convention:
- Registers: Arguments 1–4 load into
RCX,RDX,R8,R9frompArgs[0..3]. - Stack: Arguments 5+ spill above the mandatory 32-byte shadow space.
- Alignment: Dynamic frame allocation keeps
RSP16-byte aligned beforeCALL. - Preservation: Non-volatile registers saved in the prologue and restored on return.
The bridge computes frame size as ALIGN_UP(32 + max(0, count-4)*8, 16).
x86 bridge (snd_ffi_bridge_x86)
File: ffi/asm/ffi_x86.asm
- Push arguments right-to-left (reverse iteration over
pArgs). CALLthe target.- Restore
ESPto its pre-push value (lea esp, [ebp-12]).
This supports both __cdecl (caller cleans) and __stdcall (callee cleans) targets without knowing the convention in advance — the bridge always resets the stack pointer after return.
Loader integration
| Stage | Mechanism |
|---|---|
| EXE entry | Typed function pointer in snd_ldr_pe_execute_image (chain.c) |
DLL DllMain |
Typed snd_dll_entry_proc_t call in snd_ldr_pe_execute_image |
| TLS callbacks | Direct PIMAGE_TLS_CALLBACK invocations in snd_ldr_pe_execute_tls_callbacks |
Named export (-e) |
PoC resolves with snd_ldr_pe_get_proc_address, then snd_ffi_execute |
See Loaders techniques for the full reflective pipeline.
Limitations
- No floating-point or struct returns beyond what fits in
UINT_PTR/ integer registers. - No varargs — pass a fixed count matching the target.
- x86 vs x64 — build architecture selects the bridge; there is no cross-bitness FFI (use Heaven's Gate for WoW64 → x64).