Blog based on https://github.com/notsnakesilent/AMDStackGuard source code.
Stack spoofing has become the standard workaround in the modern gaming hacking scene. By manipulating the call stack, cheats can make their malicious calls appear to originate from legitimate modules such as “kernel32.dll” or “discord_game_sdk.dll,” completely bypassing traditional return address checks.
For years, anti-cheats relied on APIs such as “RtlCaptureStackBackTrace” to check call chains. The problem with this? These APIs read user mode memory. Since cheaters have full control over user mode memory (ring 3), they can simply create a fake stack for the API while executing code on a hidden stack.
This cat-and-mouse game changes when we shift our trust to the kernel trap frame.
How this works?
Step 1: Understanding the Trap Frame
When a thread in user mode executes a system call (syscall) or triggers an interrupt, the CPU performs a ring transition from ring 3 to ring 0.
During this transition, the Windows kernel saves the entire CPU state on the kernel stack. This saved state is called KTRAP_FRAME.
Importantly, this structure contains the UserRsp (stack pointer) and UserRip (instruction pointer) at the exact moment of the transition. Since this data resides in kernel memory, the user mode cheat cannot fake it without also having a kernel exploit.
It represents the absolute truth of the execution flow.
Step 2: Locating the frame using heuristics
In modern versions, Windows does not export a simple API to retrieve the “trap frame” for the current thread (APIs such as PsGetThreadTrapFrame are often not exported or are unreliable).
Instead, AMDStackGuard uses stack heuristics. Since the trap frame is pushed onto the kernel stack immediately upon entry, we can calculate its position by determining the base of the kernel stack and subtracting the frame size.
In the driver, we calculate the candidate address:
PKTRAP_FRAME GetTrapFrameFromStack() {
PVOID StackBase = IoGetInitialStack();
// The trap frame is usually located at the top of the stack minus its size
ULONG_PTR Candidate = (ULONG_PTR)StackBase - sizeof(KTRAP_FRAME);
// Heuristic check: Does it look like a valid user context?
if (TrapFrame->Rip < 0x7FFFFFFFFFFF && TrapFrame->Rsp < 0x7FFFFFFFFFFF) {
return (PKTRAP_FRAME)Candidate;
}
return NULL;
}
This allows us to restore the frame without having to rely on operating system symbols, which can change between Windows updates.
Step 3: Extract the “source of truth”
Once we have the pointer to the trap frame, we ignore everything the application pretends to do in user mode. We extract the UserRsp directly from the stored hardware context.
This UserRsp points to the real stack used by the CPU, not the fake stack created by the spoofing routine.
if (trapFrame != NULL) {
// BINGO! We have the hardware truth.
// Even if the cheat spoofed the CONTEXT record, this value remains correct.
targetRsp = (PVOID)trapFrame->Rsp;
DbgPrint("[-] SECURE: TrapFrame found via Stack Heuristic.\n");
}
Step 4: Secure memory introspection
Now that we have the actual stack pointer, we need to read the return address stored there to see where the call actually came from.
Since UserRsp points to user mode memory, direct access from ring 0 could cause a BSOD if the page is swapped out or invalid. We use ProbeForRead within an exception handler to do this safely.
__try {
ProbeForRead(targetRsp, sizeof(PVOID), 1);
// Read the return address from the REAL stack
PVOID realReturnAddress = *(PVOID*)(targetRsp);
// Validate it
ValidateReturnAddress(realReturnAddress);
}
__except (EXCEPTION_EXECUTE_HANDLER) {
DbgPrint("[-] Failed to read user stack.\n");
}
This ensures that the driver remains stable even if the cheater attempts to crash the system by passing invalid pointers.
Detection Logic
With trap frame data, identifying a fake stack becomes a logical comparison. We look for specific anomalies that are mathematically impossible during legitimate execution:
- Stack pivoting: The
RSPreported by the thread context (via standard APIs) differs drastically fromTrapFrame->UserRsp. This means that the cheat switched stacks shortly before the syscall. - Canonical violations: The return address recovered from the stack contains invalid upper bits (common in ROP chains).
- Module boundary violations: The return address points to memory that is executable but does not belong to any loaded DLL (indicating manual mapping or shellcode).
Advanced checking: Module boundaries
A common technique for mappers is to hide their code in allocated memory that is not backed by a file on the hard disk (private commit).
By comparing UserRip from the trap frame with the list of loaded modules (PsLoadedModuleList), we can immediately detect anomalies.
// Pseudo-code logic for verification
BOOLEAN IsAddressInValidModule(PVOID Address) {
for (PLDR_DATA_TABLE_ENTRY Entry : KernelModuleList) {
if (Address >= Entry->DllBase && Address < Entry->End) {
return TRUE; // Address belongs to a signed module
}
}
return FALSE; // Address is floating shellcode
}
If the trap frame indicates that the command pointer is in “no man's land” (unsecured memory), it doesn't matter how clean the fake call stack looks—the thread is guilty.
Verification output
If the system is working, it bypasses the forgery and recognizes the real command pointer. In the test, we can see that the driver successfully detects the discrepancy between the user's statements and the evidence provided by the trap frame:
[-] SECURE: TrapFrame found. Real RSP: 000000465A4FF298
[-] MISMATCH: [Stack Return] = 0x7FF... (injected code) vs. [Expected Module] = 0x7FF... (Kernel32.dll)
Although the cheat attempts to “clean up” the stack, the kernel frame reveals the inconsistency.
Limitations & the next step
While trap frames provide a snapshot of the state at the exact moment of the syscall, skilled attackers can use ROP chains (Return-Oriented Programming) to construct a fake stack that looks valid even to the kernel, and fall back to legitimate memory just before the transition.
This brings us to the core of this research: hardware-assisted validation.
Trap frames provide us with a static snapshot. To combat perfect ROP chains, we need the execution history.
The role of AMD IBS (Instruction Based Sampling)
Modern AMD processors have performance counters that are capable of capturing branch targets (jumps and calls) directly in the hardware.
By reading model-specific registers (MSRs) such as “IbsBrTarget,” we can see where the CPU actually jumped from, completely ignoring the stack.
// Future implementation concept
ULONG64 HardwareBranchTarget = __readmsr(MSR_AMD_IBS_BR_TARGET);
if (HardwareBranchTarget != StackReturnAddress) {
// The CPU says we jumped from A.
// The Stack says we returned from B.
// FLAG: ROP Chain detected.
}
This approach moves the "Root of Trust" from the OS Kernel (which can be deceived by ROP) to the Silicon itself.
Conclusion
Stack spoofing is entirely based on the assumption that security tools trust user-mode memory. By anchoring validation in the kernel trap frame, tools such as AMDStackGuard refute this assumption.
While not a panacea, this method requires kernel privileges and stack offset maintenance, and represents a significant advance in integrity monitoring. It forces cheat developers to move from simple stack manipulations to far more complex (and unstable) kernel-level bypass techniques.
For researchers and anti-cheat developers, the message is clear: Trust the CPU, not the memory.
