![]()
In version 1.2.0, SindriKit decoupled syscall invocation to support indirect syscalls. By jumping to a legitimate syscall; ret instruction inside ntdll.dll, payloads evaded user-mode API hooking. However, it’s far from enough.
Modern EDRs leverage ETW telemetry and thread suspension to inspect the call stack during kernel transitions. Even if the instruction pointer (RIP) originates from a legitimate NTDLL address, the return address on the stack points into unverified payload memory.
SindriKit 1.3.0 closes this gap by introducing native Call Stack Spoofing into the execution pipeline.
Source, architecture docs, and PoCs: SindriKit on GitHub.
Conceptually, spoofing a return address is simple: overwrite the top of the stack with the address of a legitimate function (e.g., kernel32.dll!SleepEx) before invoking the syscall. But EDRs don’t just look at the raw bytes on the stack. They use Microsoft’s RtlVirtualUnwind API to reconstruct the call chain.
When attempting to spoof a call stack, devs run into three issues:
![]()
Fat Frames diagram for extra clarity
SindriKit 1.3.0 introduces snd_syscall_find_spoof_scan.
Instead of picking a random function to spoof, SindriKit manually parses the Exception Directory (.pdata) of the natively loaded kernel32.dll. It iterates through the RUNTIME_FUNCTION array and parses the underlying UNWIND_INFO structures to calculate the exact stack allocation of every function.
The engine hunts for a “Fat Frame”, a legitimate function that allocates at least 120 bytes of stack space. Why 120 bytes? Because we need enough shadow space to hide our syscall arguments and our trampoline gadget without overflowing into other frames.
If the payload spoofs the exact same kernel32.dll function for every single syscall, the telemetry becomes highly deterministic and suspicious. To combat this, snd_syscall_find_spoof_scan incorporates a randomizer that calculates entropy using the SSN hash, a static counter, and the ASLR-dependent memory addresses of the module and the execution stack. It uses this entropy to randomly skip valid Fat Frames, ensuring the spoofed stack trace constantly shifts during execution.
Once a randomized Fat Frame is chosen, the engine scans the function body for a 0xC3 (RET) instruction. This address becomes our Trampoline Gadget, and the calculated frame size is passed directly to our MASM stub.
In this implementation, we must satisfy two entirely different execution models simultaneously: the physical CPU and the EDR’s virtual unwinder:
+------------------------------------------------------+ <-- [RSP+0]
| Trampoline Gadget (RET inside Fat Frame) |
+------------------------------------------------------+ <-- [RSP+8]
| True Return Address (SyscallCleanup) |
+------------------------------------------------------+
| ... Syscall Arguments (Arg 5+) ... |
+------------------------------------------------------+ <-- [RSP + 8 + Fat Frame Size]
| Caller's Original Return Address |
+------------------------------------------------------+
And here is the exact assembly logic that builds it:
; Build the Top-of-Stack Exposure JMP-Trampoline
; 1. The syscall gadget returns to [RSP]
; 2. The Trampoline Gadget executes RET, popping [RSP+8] into RIP
mov [rsp+0], r8 ; r8 = Trampoline Gadget (pSpoofAddr)
lea rax, SyscallCleanup
mov [rsp+8], rax ; The CPU will ultimately return here
; 3. The EDR's RtlVirtualUnwind simulates unwinding the Fat Frame
; It adds the spoof_frame_size to the Virtual RSP and reads the return address.
; We place the caller's original return address exactly where the EDR expects it.
mov rax, [rbp+16] ; Caller's original return address
; Because SindriKit compiled implants run from disk-backed executable memory,
; leaving the original caller's address ([rbp+16]) provides a perfectly legitimate
; baseline anchor for the thread trace
mov [rsp + r12 + 8], rax ; r12 = spoof_frame_size
When the kernel suspends the thread and the EDR inspects it:
The unwinder parses a perfectly intact call chain.
On x86 architectures, SindriKit falls back to resolving BaseThreadInitThunk and scanning forward/backward for a simple RET gadget.
This is a necessary compromise due to architectural differences. Unlike x64, the x86 architecture does not use .pdata exception directories. Instead, it relies on Structured Exception Handling (SEH) chained via the FS:0 register, and traditional frame pointer (EBP) chains.
Without .pdata, it is impossible to dynamically calculate the frame size of an arbitrary function without shipping a full x86 disassembler/emulator inside the payload. Because payload size and evasion are top priorities, we rely on known functions for x86 spoofing while leveraging the full power of dynamic frame discovery on x64.
By completely decoupling resolution, invocation, and now spoofing, SindriKit allows operators to trivially integrate advanced evasion techniques into their tools. The C-based engines handle the complex PE parsing and geometric math, while the MASM stubs execute the precise physical stack manipulations.