After trying out the Stack Buffer Overflow, the next step is moving from Control Flow Hijacking to Data-Only manipulation.
The Arbitrary Overwrite (or Write-What-Where) vulnerability is one powerful bug you can find in a driver. Instead of blindly overflowing a buffer and corrupting the stack, you tell the driver exactly what data to write and where.
__int64 __fastcall TriggerArbitraryWrite(WriteWhatWhere *UserWriteWhatWhere)
{
_QWORD *What; // rbx
_QWORD *Where; // rdi
// Validates that the struct itself is readable in user-mode
ProbeForRead(UserWriteWhatWhere, 0x10u, 1u);
What = UserWriteWhatWhere->What;
Where = UserWriteWhatWhere->Where;
*Where = *What;
return 0;
}
The driver expects a pointer to a struct containing two 8-byte values: What and Where.
Because the driver operates in Ring 0, the instruction *Where = *What; executes with the highest possible privileges. If you provide a kernel memory address for Where, the driver will overwrite it with your What value.
Before diving into the data-only token swapping, I wanted to try and exploit the write primitive on its own without an informational memory leak. To do this, I need to overwrite a function pointer that the kernel will predictably execute. In this case, I went with the classic nt!HalDispatchTable.
Inside this table, the pointer at offset 0x8 is used by the HalQuerySystemInformation function. I can trigger the kernel to execute this pointer on command by calling the user-mode API NtQueryIntervalProfile.
Since I’m executing a Write-Only attack and cannot dynamically leak the kernel base address from memory, I’ll temporarily “cheat” by using WinDbg. I can grab the live kernel base address (K_BASE), calculate the static offset to the table (HAL_DISPATCH_TABLE_RVA), and hardcode the absolute address into a C exploit.
Supervisor Mode Execution Prevention (SMEP) actively monitors Ring 0 execution. If the kernel instruction pointer (RIP) ever transitions to a user-mode memory page, the CPU hardware catches it and triggers an immediate Blue Screen of Death.
Before executing the shellcode, I must disable SMEP by flipping the 20th bit in the CR4 control register.
The initial plan: Because an Arbitrary write doesn’t destroy the surrounding stack like a buffer overflow, I don’t have to do everything at once via a massive ROP chain. Instead, I can abuse the vulnerability sequentially, triggering NtQueryIntervalProfile multiple times to stage the execution.
The initial strategy was built around a straightforward hypothesis: leverage the x64 fastcall calling convention to directly manipulate CPU control registers.
The target was NtQueryIntervalProfile, which accepts two primary arguments. The plan was to hijack HalDispatchTable + 0x8 and point it directly to a primitive mov cr4, rcx ; ret gadget.
// Overwriting the HAL Dispatch Table hook
www.What = (PVOID)&qGadget_movcr4_rcx_ret;
www.Where = (PVOID)(K_BASE + HAL_DISPATCH_TABLE_RVA + 0x8);
// ...
// Triggering the execution flow
unsigned int cr4_payload = CR4_VALUE_NO_SMEP;
PULONG Discard;
pNtQueryIntervalProfile(cr4_payload, Discard);
Because fastcall dictates that the first argument is passed via the RCX register, the assumption was that the desired CR4 bitmask would still reside in RCX by the time the kernel jumped to the gadget. If successful, the gadget would load the payload into CR4, disabling SMEP.
This approach immediately ran into the realities of kernel call stacks. NtQueryIntervalProfile is not a direct line to the HAL; it is a heavy system call handler that executes an entire call chain before reaching the dispatch table.
Specifically, execution flows through the system call architecture down into nt!KeQueryIntervalProfile. By the time the execution path finally invokes the function pointer at HalDispatchTable + 0x8, RCX value already changed multiple times. Because the gadget required strict control over RCX, this direct register attack was a dead end.

Shifting focus, I looked at the second parameter of NtQueryIntervalProfile: a pointer to a ULONG interval, passed via the RDX register.
NTSTATUS NtQueryIntervalProfile(
IN KPROFILE_SOURCE ProfileSource,
OUT PULONG Interval // Passed via RDX
);
Attempting to pass the raw CR4 directly as a value in this parameter caused an early return, since obviously the value would be invalid as a pointer. To bypass this validation, I had to pass a legitimate, fully mapped pointer pointing to a special value (0xDEADBEEF, classic).
while RDX value changed, the user-mode pointer had survived the internal function transitions and was preserved inside the register RBX at the exact moment the HAL hook was executed.

While now I had a register (RBX) holding a pointer to the data, I lacked the necessary architectural bridge to exploit it.
To disable SMEP from this state, I would need an indirect dereference gadget capable of loading memory into a control register, something resembling:
mov rax, [rbx]
mov cr4, rax
ret
The available gadget pool in ntoskrnl.exe only provided direct register-to-register moves for CR4 modification (primarily mov cr4, rcx or mov cr4, rax). No realistic gadget existed that could dereference RBX and conveniently drop that value into a control register.
This forced a fundamental paradigm shift: instead of using the user-mode pointer as a data reference, I needed to treat it as a fake stack framework. It was time to transition to a stack pivot followed by a structured ROP chain.
The most direct way to achieve a stack pivot when controlling the RBX register is a 64-bit gadget such as mov rsp, rbx ; ret. However, a gadget scan of ntoskrnl.exe yielded zero usable variants of this instruction.
Instead, I found an alternative partial match:
mov esp, ebx ; ret
While functional, this instruction introduced a slight architectural constraint due to how the x86-64 architecture handles subregister operations. When a 64-bit processor executes an instruction that writes to a 32-bit register (like ESP or EBX), the CPU automatically zeros out the upper 32 bits of the corresponding 64-bit register (RSP or RBX) to prevent residual data pollution.
Consequently, executing mov esp, ebx meant that the upper 32 bits of RSP would become 0x00000000. This dictated a non-negotiable rule for the exploit payload: The fake stack had to reside entirely within a 32-bit address space (the lower 4GB of virtual memory).
To satisfy this hardware requirement, the allocation routine utilizing VirtualAlloc was modified to explicitly request a base address within the 32-bit boundary. Once a safe, low-memory region was mapped, the artificial stack was constructed.
// Structuring the fake stack payload within the 32-bit allocation
fakeStack[0] = qGadget_poprcx_ret; // Gadget 1: Pop the next value into RCX
fakeStack[1] = cr4_off; // The desired CR4 bitmask (SMEP/SMAP disabled)
fakeStack[2] = qGadget_movcr4_rcx_ret; // Gadget 2: Overwrite CR4 with the value in RCX
fakeStack[3] = (ULONG64)Shellcode; // Destination: The user-mode privilege escalation code
This allowed the token-stealing loop to locate the SYSTEM process structure and copy its security token over the exploit process’s token.
But, the exploit was now executing on a completely artificial stack frame. The original, legitimate kernel stack pointer along with the return pointer required to safely exit the system call and hand control back to the operating system was gone. Well not completely.
Even though I hijacked the RSP register, the Windows kernel still tracks the active thread’s true stack limits. It does this using the Processor Control Region (PCR) and the active _KTHREAD structure.
In Kernel Mode, the GS segment register points to the KPCR. Inside it (specifically inside the nested KPRCB structure), the kernel maintains a pointer to the currently running thread’s _KTHREAD structure at a fixed offset of 0x188.
Inside _KTHREAD, the operating system records the hard physical limits of the thread’s stack. At offset +0x28 (the InitialStack field), the kernel stores the anchor address of the stack’s initialization point (known in !thread as Stack Init).
To rebuild the original stack context before returning, I needed to append a prologue to the shellcode buffer:
// 1. mov rax, qword ptr gs:[188h] (Get KTHREAD)
AppendToBuffer("\x65\x48\x8b\x04\x25\x88\x01\x00\x00", 9);
// 2. mov rbx, qword ptr [rax+28h] ; RBX = Stack Init
AppendToBuffer("\x48\x8B\x58\x28", 4);
// 3. sub rbx, 228h (228h value calculated using windbg)
AppendToBuffer("\x48\x81\xEB\x28\x02\x00\x00", 7);
// 4. mov rsp, rbx
AppendToBuffer("\x48\x89\xDC", 3);
To make this recovery routine work, I have to calculate the precise distance (the delta) between the Stack Init address and the RSP register at the moment the hijacked function is hit.
I can find this by inspecting a live thread in WinDbg right before the pivot occurs:
1: kd> r rsp
rsp=ffffa70049ef2a48
This tells me that the current stack pointer when the system call hits nt!SymCryptScsTableLoad128Xmm+0x167 is exactly:
Target RSP = 0xffffa70049ef2a48
Next, dump the current thread structures using !thread:
1: kd> !thread
THREAD ffffd208f7f2c080 ...
Stack Init ffffa70049ef2c70 Current ffffa70049ef2450
Base ffffa70049ef3000 Limit ffffa70049eed000 Call 0000000000000000
From this output, extract the structural anchor:
Stack Init = 0xffffa70049ef2c70
To calculate the exact byte difference, subtract the desired RSP target from the Stack Init value:
Delta = 0xffffa70049ef2c70 - 0xffffa70049ef2a48 = 0x228
The math reveals that the target RSP sits exactly 0x228 bytes below Stack Init. By writing sub rbx, 0x228 into the shellcode, I could dynamically land precisely on the correct stack address every single time the exploit is executed.

It is worth noting, however, that this exploit is technically a bit of a “one-shot” construct. In a real-world scenario, leaving the system in this un-sanitized state is a ticking time bomb. In fact, without proper remediation, the target VM literally didn’t survive a minute before a critical bugcheck occured.
Restoring the CR4 register to its original, secure state is straightforward. I can append a cleanup routine directly into the shellcode to hand execution back over to the control-register gadget:
mov rcx, cr4Original ; Load the original secure CR4 bitmask
mov rax, qGadget_movcr4_rcx_ret ; Address of the CR4 modification gadget
jmp rax ; Re-arm SMEP/SMAP
Leaving the hook sitting dirty at HalDispatchTable + 0x8 means the very next time any process on the system attempts to query an interval profile, the kernel will corrupt the stack.
To fix this, I had to locate the static Relative Virtual Address (RVA) for the nt!HaliQuerySystemInformation function (either dynamically or using windbg). By combining this RVA with the kernel base, I dynamically reconstructed the exact pointer that originally occupied that slot. From there, I simply executed the write primitive to clean up the table:
// Reconstruct the original pointer and heal the dispatch table
UINT64 originalHal = kBase + HALI_QUERY_SYSTEM_INFO_RVA;
WriteAndCall(originalHal, target, hDevice, NULL);
With the kernel structures cleanly sanitized and hardware protections locked securely back into place, I achieved a perfectly stable system and an active, SYSTEM shell.