Call Stack Spoofing: Defeating EDR Stack Telemetry
Advanced techniques for spoofing thread call stacks to evade EDR behavioral detection - covering return address spoofing, synthetic frames, and thread stack manipulation.
Why Call Stacks Matter to EDR
When your implant calls NtAllocateVirtualMemory via indirect syscalls, the syscall itself might bypass userland hooks. But modern EDR products don’t just hook - they inspect the call stack of the calling thread via kernel callbacks or ETW stack walking.
A legitimate call to VirtualAlloc from kernel32.dll has a predictable call stack:
ntdll!NtAllocateVirtualMemory
kernelbase!VirtualAlloc
kernel32!VirtualAllocStub
application!SomeFunction
application!main
ntdll!RtlUserThreadStart
But when your implant calls the same syscall, the stack shows:
ntdll!NtAllocateVirtualMemory
<UNKNOWN MODULE> ← Your implant (unbacked memory)
<UNKNOWN MODULE> ← More implant code
ntdll!RtlUserThreadStart
The presence of return addresses pointing to unbacked memory (not backed by a file on disk) is a strong indicator of malicious in-memory execution. This is what call stack spoofing defeats.
Technique 1: Return Address Spoofing
Concept
Before making a sensitive API call, overwrite the return address on the stack to point to a legitimate module (like kernel32.dll), make the call, then restore the original return address.
Implementation
; Save real return address
pop rax ; Pop real return address into RAX
mov [saved_ret], rax ; Save it
; Push fake return address (points into kernel32)
mov rcx, [kernel32_gadget] ; Address of 'ret' instruction in kernel32.dll
push rcx ; Fake return address on stack
; Make the API call
; When EDR walks the stack, it sees kernel32 as the caller
call target_api
; The 'ret' in kernel32 will return to... nowhere useful
; So we need to handle the return ourselves
The Problem
Simple return address spoofing breaks the return flow. When the API returns, it rets to your fake address (a ret gadget in kernel32), which then rets to whatever is next on the stack - likely crashing.
Solution: Frame Spoofing with Desync
Use a more sophisticated approach where you construct a full fake frame and use a trampoline to redirect execution after the API call returns:
// 1. Save current RSP and RBP
// 2. Set up a fake stack frame that looks legitimate
// 3. Make the API call from this fake frame
// 4. API returns to your trampoline gadget
// 5. Trampoline restores the real RSP/RBP
// 6. Execution continues normally
Technique 2: Synthetic Stack Frames
Concept
Instead of just spoofing one return address, construct an entire fake call stack that mimics a legitimate execution path. The stack looks like a normal chain of kernel32 → kernelbase → ntdll calls.
Implementation with SilentMoonwalk
The SilentMoonwalk technique (by KlezVirus) constructs synthetic frames by:
- Finding ROP gadgets in legitimate modules (
add rsp, X; retsequences) - Building a fake frame chain where each return address points to a gadget that adjusts RSP and returns to the next fake frame
- The final gadget returns to your real code
// Desired fake call stack:
// kernel32!BaseThreadInitThunk+0x14
// ntdll!RtlUserThreadStart+0x21
// Find the right gadgets
PVOID k32_gadget = FindGadget("kernel32.dll", "add rsp, 0x38; ret");
PVOID ntdll_gadget = FindGadget("ntdll.dll", "add rsp, 0x28; ret");
// Construct frames on the stack
*(PVOID*)(fake_stack + 0x00) = api_to_call;
*(PVOID*)(fake_stack + 0x38) = k32_gadget; // After API returns
*(PVOID*)(fake_stack + 0x70) = ntdll_gadget; // After k32 gadget
*(PVOID*)(fake_stack + 0x98) = real_return; // Back to our code
Challenges
- Gadget addresses change across Windows versions
- Stack alignment must be maintained (16-byte on x64)
- Some EDRs validate frame pointer chains (RBP linkage)
- Fake frames must have plausible local variable space
Technique 3: Thread Stack Spoofing (Sleep-Time)
Concept
During sleep, spoof the entire thread’s call stack. When EDR scans sleeping threads (Hunt-Sleeping-Beacons), it sees a normal-looking stack instead of your implant.
CallStackMasker Approach
- Before sleep, capture the current thread context
- Overwrite the stack with a legitimate-looking chain:
ntdll!NtWaitForSingleObject ntdll!RtlpTpWaitCallback ntdll!TppWorkerThread kernel32!BaseThreadInitThunk ntdll!RtlUserThreadStart - Enter sleep
- After waking, restore the real stack
Combined with Sleep Obfuscation
The most effective approach combines stack spoofing with memory encryption:
1. Encrypt implant memory (Ekko/Foliage)
2. Spoof the call stack (CallStackMasker)
3. Sleep
→ EDR scans: memory is encrypted, stack looks normal
4. Wake → Restore stack → Decrypt memory → Execute
Technique 4: Hardware Breakpoint-Based Spoofing
Concept
Use hardware breakpoints and a Vectored Exception Handler (VEH) to intercept execution at the point of the API call and modify the stack in the exception handler - before EDR can inspect it.
Flow
// 1. Set hardware breakpoint on NtAllocateVirtualMemory
SetHardwareBreakpoint(0, NtAllocateVirtualMemory, DR_EXECUTE);
// 2. Register VEH
AddVectoredExceptionHandler(1, SpoofHandler);
// 3. When the API is called, breakpoint fires → VEH executes
LONG SpoofHandler(EXCEPTION_POINTERS* ex) {
if (ex->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) {
// Modify the stack to look legitimate
SpoofCallStack(ex->ContextRecord);
// Continue execution
return EXCEPTION_CONTINUE_EXECUTION;
}
}
Advantages
- No inline assembly required
- Works with indirect syscalls
- Can be applied selectively to specific API calls
- Hardware breakpoints don’t modify code memory
Detection and Counter-Detection
How EDR Detects Spoofing
- Frame pointer validation - Check if RBP chains are consistent
- Return address validation - Verify each return address is preceded by a
callinstruction - Stack size analysis - Fake frames often have unusual gaps
- Module backing - Check if return addresses are in legitimate, file-backed modules
Counter-Detection
- Use real call instructions - Ensure each fake return address is preceded by a valid
callin the module - Maintain RBP chains - Link frame pointers correctly
- Match frame sizes - Use the real function’s expected frame size
- Vary your spoofing - Don’t always use the same fake chain
Comparison
| Technique | Complexity | Stealth | Covers Sleep | Performance |
|---|---|---|---|---|
| Return Address Spoof | Low | Medium | No | Minimal |
| Synthetic Frames | High | High | No | Low |
| Thread Stack Spoof | High | Very High | Yes | Moderate |
| HW Breakpoint Based | Medium | High | No | Minimal |
| Combined (all) | Very High | Maximum | Yes | Moderate |
Call stack telemetry is the next frontier in the EDR arms race. Indirect syscalls bypass hooks, sleep obfuscation defeats memory scanning, and stack spoofing completes the trifecta by defeating behavioral analysis. Master all three for modern red team operations.