SMAP is Pre-Disarmed: How a Stack Pivot That Shouldn't Work Revealed a Kernel-Wide Design Compromise

While developing a kernel exploit chain against the HackSys Extreme Vulnerable Driver (HEVD), I encountered a weird contradiction. (article here)

I was abusing an arbitrary write primitive to overwrite HalDispatchTable+0x8 and redirect execution through a stack pivot. The pivot gadget was a 32-bit partial register move:

mov esp, ebx ; ret

This instruction zeroes the upper 32 bits of RSP and lands execution in a fake stack I had constructed in low user-mode memory (a VirtualAlloc below the 4GB boundary). From there, a ROP chain disabled SMEP via mov cr4, rcx, jumped to a token-theft payload, and finally recovered the original kernel stack.

The exploit worked. I got SYSTEM. The VM stayed stable long enough to pop a shell.

But something was wrong here. SMAP (Supervisor Mode Access Prevention) was supposed to crash my exploit chain! I ran the exe fully expecting a BSOD. CR4 bit 21 was set. The fake stack was in user-mode memory. The ROP chain was executing in Ring 0, reading gadget addresses from a user-mode page.

Why didn’t SMAP kill the pivot the moment the kernel tried to read the first ROP gadget from my user-mode fake stack?

I had three hypotheses:

  1. SMAP wasn’t actually armed on my test system (unlikely, I checked CR4).
  2. SMAP was being explicitly disabled somewhere in the syscall path before reaching the driver.
  3. SMAP was armed, but the AC (Alignment Check) bit in RFLAGS was set to 1, which overrides SMAP.

I designed three experiments to find out.

Note: This experiment was conducted on a Windows 11 VM (build 26200.8328).

Experiment 1: Does the Syscall Path Leave AC Set?

SMAP enforcement is conditional. The CPU checks both CR4[21] (SMAP enable) and RFLAGS[18] (AC bit). If AC=1, SMAP is suppressed. The SYSCALL instruction masks RFLAGS using IA32_FMASK on entry to the kernel. My first question was simple: does IA32_FMASK clear AC, or does AC remain unmasked?

I checked the mask in WinDbg:

kd> rdmsr 0xC0000084
msr[c0000084] = 00000000`00004700

0x4700 covers bits 8, 9, 10, and 14 (TF, IF, DF, NT). Bit 18 (0x40000) is not set. So SYSCALL does not clear AC via the hardware mask.

But that only tells me what the hardware doesn’t do. Maybe KiSystemCall64 clears it in software? Or maybe it sets it? I needed to know whether AC arrives in kernel mode because it survived from user mode, or because the kernel actively enables it.

Phase A: AC Survives the Transition

First, I built a minimal test driver that read RFLAGS at the very first instruction of its IRP_MJ_DEVICE_CONTROL handler. The user-mode client explicitly set AC=1 before calling DeviceIoControl:

// User mode
__writeeflags(__readeflags() | 0x40000ULL);
DeviceIoControl(hDevice, IOCTL_TEST, ...);

In the driver:

unsigned __int64 current_rflags = __readeflags();
if (current_rflags & 0x40000)
    DbgPrint("[!] SMAP BYPASSED: AC bit survived the journey!\n");
else
    DbgPrint("[*] SMAP should work here.\n");

AC was set. The IOCTL dispatch path entered the driver with RFLAGS.AC=1.

This proved that AC survives the syscall transition, SYSCALL and the kernel entry path do not clear it.

Phase B: The Kernel Actively Sets AC

What if the user-mode client does the opposite: explicitly clears AC before the syscall?

I modified the client to force AC=0:

// User mode
__writeeflags(__readeflags() & ~0x40000ULL);
DeviceIoControl(hDevice, IOCTL_TEST, ...);

The driver still saw AC=1.

This was the critical result. If AC had merely “survived” the transition, clearing it in user mode should have resulted in AC=0 in the kernel. Instead, AC arrived set regardless of the user-mode state. The kernel entry path, somewhere between SYSCALL and the driver dispatch routine, is actively setting AC=1.

By the time my driver code ran, SMAP was already neutered. Not by accident. By design.

Experiment 2: Does AC=1 Actually Suppress SMAP Faults?

Observing that AC=1 is present is one thing. Proving that it genuinely allows dereferencing user-mode pointers without a #PF is another. I needed to verify that AC=1 in Ring 0 actually suppresses SMAP.

I built a second test driver that:

  1. Verified CR4[21] = 1 (SMAP armed)
  2. Verified RFLAGS[18] = 1 (AC set)
  3. Dereferenced a user-mode pointer passed in from the caller
unsigned __int64 cr4_val = __readcr4();
unsigned __int64 rflags_val = __readeflags();

int smap_bit = (cr4_val >> 21) & 1;
int ac_bit   = (rflags_val >> 18) & 1;

volatile char testRead = *(volatile char*)UserModePointer;

The usermode client passed a pointer to a stack-allocated string and never touched AC. Still no crash.

This confirmed that AC=1 indeed disables SMAP protection.

Experiment 3: Is This a VM Artifact?

Hypervisors sometimes play games with RFLAGS. I needed to rule out the possibility that QEMU/KVM was silently suppressing SMAP faults regardless of AC state.

I modified the driver to forcefully clear AC and then attempt the same dereference:

// Force AC to 0
__writeeflags(__readeflags() & ~0x40000ULL);

// Same dereference
volatile char testRead = *(volatile char*)UserModePointer;

Immediate system crash. Clearing AC re-armed SMAP, and the user-mode dereference faulted exactly as the architecture promises.

SMAP is real. It works. But on the normal IOCTL dispatch path, it arrives already disarmed.

The Architectural Root Cause

The three experiments proved that SMAP is not bypassed by an attacker trick. It is pre-bypassed by the operating system itself. The normal syscall entry path on Windows 11 delivers kernel code an execution context where AC=1 is already set.

This means any kernel-mode attacker who gains execution through a standard syscall path (whether via a vulnerable driver, a signed driver with dangerous IOCTLs, or any other mechanism) inherits a kernel context where SMAP is muted.

Digging Deeper: The MSRC Knew This in 2020

After reaching this conclusion, I went looking for prior art. I found two critical pieces of research that validate everything I had just proven experimentally and explain the mechanism behind it.

The stac in KiSystemCall64

Reverse-engineering work by hammertux on the Windows syscall entry path confirms that KiSystemCall64 explicitly executes a stac instruction early in the kernel transition. My experimental observation in Experiment 1 (that AC=1 is present in the driver dispatch routine even when explicitly cleared in user mode) is explained by this deliberate architectural choice.

The MSRC Feasibility Study

I also found a Microsoft Security Response Center paper from July 2020 that validates the broader design compromise and explains why the situation is architecturally inevitable.

“Evaluating the feasibility of enabling SMAP for the Windows kernel” — Joe Bialek and Saar Amar (MSRC)

The paper is a feasibility study on why SMAP cannot be enabled for the general Windows kernel. Their findings map directly onto my experiments:

1. SMAP is a Breaking Change

The Windows kernel was not built with SMAP in mind. Every system call, every driver, every kernel component assumes it can touch user-mode memory freely. MSRC estimated ~2,900 locations in the kernel touch user-mode memory during a normal boot, across 994 unique functions. Retrofitting all of them with stac/clac pairs is a massive engineering effort.

2. The Instrumentation Problem

The only viable path MSRC identified was compiler-level instrumentation: automatically injecting clac (clear AC, disable user-mode access) at function entry points and stac (set AC, enable user-mode access) before any user-mode access. But this creates a performance nightmare.

Their microbenchmarks showed ~23% regression on system call paths when SMAP instrumentation was applied. File system benchmarks showed regressions of 20–40% depending on the workload. These numbers are far too high to ship.

3. The Secure Kernel Exception

MSRC noted that SMAP is viable for the Secure Kernel (VTL1) because Microsoft controls 100% of that code. For the general-purpose Windows kernel, the compatibility surface is too large.

4. The Accessor Model

The paper proposed a “Copyin/Copyout” accessor model, wrapping all user-mode memory access inside explicit RtlCopyFromUser / RtlCopyToUser functions that manage stac/clac internally. This is the model Linux has used for decades. Windows has not adopted it kernel-wide.

The implication is stark: The Windows kernel is architecturally committed to keeping user-mode access enabled by default. SMAP is present in the silicon, but the OS design neutralizes it.

Conclusion

My stack pivot worked because SMAP on Windows 11 is not designed to stop me. The AC bit arrives set in the kernel by default.

If you are a defender: do not rely on SMAP as a meaningful barrier against kernel-mode attackers. Audit your drivers, restrict driver loading, and enable HVCI.

If you are an attacker: Don’t even bother, the door is already open.

*