feat: kernel memory plugin + unified source menu + driver improvements

- KernelMemory plugin: kernel-mode process/physical memory R/W via IOCTL driver
- rcxdrv.sys: MmCopyMemory for reads, MDL mapping with correct cache types
  (MmCached for RAM, MmNonCached for MMIO only — fixes cache corruption BSOD)
- Driver reconnect: ensureDriverLoaded tries device handle first, no auto
  stop+delete cycle. Manual unload closes handle only, service stays running.
- Unified source menu: ProviderRegistry::populateSourceMenu() shared by both
  main window Data Source menu and RcxEditor inline picker (icons + dll names)
- IProviderPlugin::populatePluginMenu() for conditional plugin actions
  (e.g. "Unload Kernel Driver" only when loaded)
- Physical memory mode removed from selectTarget (access via context menu only)
- requestOpenProviderTab sets base address from provider after template load
- Address parser: vtop(), cr3(), physRead() callbacks for kernel paging expressions
This commit is contained in:
IChooseYou
2026-03-13 14:46:22 -06:00
committed by IChooseYou
parent 7f7bbdcc45
commit b08736245b
22 changed files with 2671 additions and 120 deletions

View File

@@ -0,0 +1,56 @@
@echo off
setlocal
set MSVC=C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Tools\MSVC\14.39.33519
set WDK=C:\Program Files (x86)\Windows Kits\10
set WDKVER=10.0.22621.0
set CL_EXE=%MSVC%\bin\Hostx64\x64\cl.exe
set LINK_EXE=%MSVC%\bin\Hostx64\x64\link.exe
set SRCDIR=%~dp0
set OUTDIR=%SRCDIR%build
if not exist "%OUTDIR%" mkdir "%OUTDIR%"
echo === Compiling rcxdrv.c ===
"%CL_EXE%" /nologo /c /Zi /W4 /WX- /O2 /GS- ^
/D "NDEBUG" /D "_AMD64_" /D "AMD64" /D "_WIN64" /D "KERNEL" ^
/D "NTDDI_VERSION=0x0A000000" ^
/I "%WDK%\Include\%WDKVER%\km" ^
/I "%WDK%\Include\%WDKVER%\km\crt" ^
/I "%WDK%\Include\%WDKVER%\shared" ^
/kernel ^
/Fo"%OUTDIR%\rcxdrv.obj" ^
"%SRCDIR%rcxdrv.c"
if errorlevel 1 goto :fail
echo === Linking rcxdrv.sys ===
"%LINK_EXE%" /nologo ^
/OUT:"%OUTDIR%\rcxdrv.sys" ^
/DRIVER:WDM ^
/SUBSYSTEM:NATIVE ^
/ENTRY:DriverEntry ^
/MACHINE:X64 ^
/NODEFAULTLIB ^
/RELEASE ^
/MERGE:.rdata=.text ^
/INTEGRITYCHECK ^
/PDBALTPATH:rcxdrv.pdb ^
/PDB:"%OUTDIR%\rcxdrv.pdb" ^
"%OUTDIR%\rcxdrv.obj" ^
"%WDK%\Lib\%WDKVER%\km\x64\ntoskrnl.lib" ^
"%WDK%\Lib\%WDKVER%\km\x64\hal.lib" ^
"%WDK%\Lib\%WDKVER%\km\x64\BufferOverflowK.lib" ^
"%MSVC%\lib\x64\libcmt.lib"
if errorlevel 1 goto :fail
echo.
echo === SUCCESS ===
echo Output: %OUTDIR%\rcxdrv.sys
goto :eof
:fail
echo.
echo === BUILD FAILED ===
exit /b 1

View File

@@ -0,0 +1,808 @@
/*
* rcxdrv.c -- Minimal kernel-mode memory driver for Reclass.
*
* Provides: virtual memory R/W (per-process), physical memory R/W,
* region/PEB/module/TEB query, CR3 read, virtual-to-physical translation.
*
* Safety: all inputs validated, SEH around privileged instructions,
* MmCopyVirtualMemory for cross-process reads (no attach deadlock),
* METHOD_BUFFERED (no raw user pointers).
*/
#include <ntifs.h>
#include "../rcx_drv_protocol.h"
/* ── Undocumented but stable kernel exports (Vista+) ────────────── */
NTSTATUS NTAPI MmCopyVirtualMemory(
PEPROCESS SourceProcess, PVOID SourceAddress,
PEPROCESS TargetProcess, PVOID TargetAddress,
SIZE_T BufferSize, KPROCESSOR_MODE PreviousMode,
PSIZE_T ReturnSize);
PPEB NTAPI PsGetProcessPeb(PEPROCESS Process);
PVOID NTAPI PsGetProcessWow64Process(PEPROCESS Process);
PVOID NTAPI PsGetThreadTeb(PETHREAD Thread);
/*
* PsGetNextProcessThread is undocumented (not in any .lib).
* We resolve it dynamically via MmGetSystemRoutineAddress.
*/
typedef PETHREAD (NTAPI *PsGetNextProcessThread_t)(PEPROCESS Process, PETHREAD Thread);
static PsGetNextProcessThread_t g_PsGetNextProcessThread = NULL;
/* ── Manual structure definitions (kernel-mode) ─────────────────── */
/* These are partially opaque in WDK headers; define just the offsets we need. */
typedef struct _MEMORY_BASIC_INFORMATION_KM {
PVOID BaseAddress;
PVOID AllocationBase;
ULONG AllocationProtect;
SIZE_T RegionSize;
ULONG State;
ULONG Protect;
ULONG Type;
} MEMORY_BASIC_INFORMATION_KM;
#define MEM_COMMIT_KM 0x1000
/* PEB.Ldr minimal definition for module enumeration */
typedef struct _PEB_LDR_DATA_KM {
UCHAR Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InLoadOrderModuleList;
} PEB_LDR_DATA_KM;
/* PEB minimal: only need Ldr at offset 0x18 (x64) */
typedef struct _PEB_KM {
UCHAR Reserved1[2];
UCHAR BeingDebugged;
UCHAR Reserved2[0x15];
PEB_LDR_DATA_KM* Ldr; /* offset 0x18 on x64 */
} PEB_KM;
/* LDR_DATA_TABLE_ENTRY minimal for walking InLoadOrderModuleList */
typedef struct _LDR_DATA_TABLE_ENTRY_KM {
LIST_ENTRY InLoadOrderLinks; /* offset 0x00 */
LIST_ENTRY InMemoryOrderLinks; /* offset 0x10 */
LIST_ENTRY InInitializationOrderLinks; /* offset 0x20 */
PVOID DllBase; /* offset 0x30 */
PVOID EntryPoint; /* offset 0x38 */
ULONG SizeOfImage; /* offset 0x40 */
ULONG _pad;
UNICODE_STRING FullDllName; /* offset 0x48 */
UNICODE_STRING BaseDllName; /* offset 0x58 */
} LDR_DATA_TABLE_ENTRY_KM;
/* ── Forward declarations ────────────────────────────────────────── */
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp);
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp);
DRIVER_UNLOAD DriverUnload;
/* ZwCurrentProcess() macro for ZwQueryVirtualMemory */
#ifndef ZwCurrentProcess
#define ZwCurrentProcess() ((HANDLE)(LONG_PTR)-1)
#endif
/* ── Helpers ─────────────────────────────────────────────────────── */
#define VALIDATE_INPUT(irp, stk, T) \
do { \
if ((stk)->Parameters.DeviceIoControl.InputBufferLength < sizeof(T)) { \
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
(irp)->IoStatus.Information = 0; \
IoCompleteRequest((irp), IO_NO_INCREMENT); \
return STATUS_BUFFER_TOO_SMALL; \
} \
} while (0)
#define VALIDATE_OUTPUT(irp, stk, minSize) \
do { \
if ((stk)->Parameters.DeviceIoControl.OutputBufferLength < (ULONG)(minSize)) { \
(irp)->IoStatus.Status = STATUS_BUFFER_TOO_SMALL; \
(irp)->IoStatus.Information = 0; \
IoCompleteRequest((irp), IO_NO_INCREMENT); \
return STATUS_BUFFER_TOO_SMALL; \
} \
} while (0)
static NTSTATUS LookupProcess(ULONG pid, PEPROCESS* proc)
{
return PsLookupProcessByProcessId((HANDLE)(ULONG_PTR)pid, proc);
}
/* ── Safe physical mapping (MDL-based, avoids MmMapIoSpace BSOD) ── */
/*
* MmMapIoSpace/MmUnmapIoSpace BSODs (bugcheck 0x50 in
* MiClearMappingAndDereferenceIoSpace) when used on RAM-backed physical
* addresses. MDL-based mapping is safe for both RAM and MMIO.
*
* CRITICAL: cacheType must match the existing kernel mapping of the page.
* Use MmCached for RAM pages (already mapped cached by the kernel).
* Use MmNonCached ONLY for MMIO/device registers.
* Mismatched cache attributes (e.g. MmNonCached on RAM) cause silent
* kernel memory corruption via CPU cache coherency conflicts.
*/
typedef struct { PMDL mdl; PVOID base; } PHYS_MAP_CTX;
static PVOID MapPhysical(uint64_t physAddr, SIZE_T size,
MEMORY_CACHING_TYPE cacheType, PHYS_MAP_CTX* ctx)
{
ctx->mdl = NULL;
ctx->base = NULL;
ULONG_PTR pageOff = (ULONG_PTR)(physAddr & (PAGE_SIZE - 1));
SIZE_T totalSize = pageOff + size;
ULONG pages = (ULONG)((totalSize + PAGE_SIZE - 1) / PAGE_SIZE);
PMDL mdl = IoAllocateMdl(NULL, (ULONG)totalSize, FALSE, FALSE, NULL);
if (!mdl) return NULL;
PPFN_NUMBER pfn = MmGetMdlPfnArray(mdl);
PFN_NUMBER startPfn = (PFN_NUMBER)(physAddr / PAGE_SIZE);
for (ULONG i = 0; i < pages; i++)
pfn[i] = startPfn + i;
mdl->MdlFlags |= MDL_PAGES_LOCKED;
__try {
ctx->base = MmMapLockedPagesSpecifyCache(
mdl, KernelMode, cacheType, NULL, FALSE, NormalPagePriority);
} __except (EXCEPTION_EXECUTE_HANDLER) {
IoFreeMdl(mdl);
return NULL;
}
if (!ctx->base) { IoFreeMdl(mdl); return NULL; }
ctx->mdl = mdl;
return (PUCHAR)ctx->base + pageOff;
}
static void UnmapPhysical(PHYS_MAP_CTX* ctx)
{
if (ctx->base) MmUnmapLockedPages(ctx->base, ctx->mdl);
if (ctx->mdl) IoFreeMdl(ctx->mdl);
ctx->base = NULL;
ctx->mdl = NULL;
}
/* ── Virtual memory read ─────────────────────────────────────────── */
static NTSTATUS HandleReadMemory(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvReadRequest);
struct RcxDrvReadRequest* req = (struct RcxDrvReadRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
return STATUS_INVALID_PARAMETER;
VALIDATE_OUTPUT(irp, stk, req->length);
/* Save request fields before MmCopyVirtualMemory overwrites SystemBuffer.
* METHOD_BUFFERED aliases input and output to the same buffer, so the
* copy destination (SystemBuffer) clobbers req->* fields. */
ULONG pid = req->pid;
uint64_t address = req->address;
ULONG length = req->length;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(pid, &proc);
if (!NT_SUCCESS(st)) return st;
SIZE_T bytesRead = 0;
st = MmCopyVirtualMemory(
proc, (PVOID)address,
PsGetCurrentProcess(), irp->AssociatedIrp.SystemBuffer,
(SIZE_T)length, KernelMode, &bytesRead);
ObDereferenceObject(proc);
/* Partial reads: zero remainder, report success */
if (st == STATUS_PARTIAL_COPY) {
RtlZeroMemory((PUCHAR)irp->AssociatedIrp.SystemBuffer + bytesRead,
length - bytesRead);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
irp->IoStatus.Information = NT_SUCCESS(st) ? length : 0;
return st;
}
/* ── Virtual memory write ────────────────────────────────────────── */
static NTSTATUS HandleWriteMemory(PIRP irp, PIO_STACK_LOCATION stk)
{
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
if (inputLen < sizeof(struct RcxDrvWriteRequest))
return STATUS_BUFFER_TOO_SMALL;
struct RcxDrvWriteRequest* req = (struct RcxDrvWriteRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_VIRTUAL)
return STATUS_INVALID_PARAMETER;
if (inputLen < sizeof(struct RcxDrvWriteRequest) + req->length)
return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
PUCHAR data = (PUCHAR)req + sizeof(struct RcxDrvWriteRequest);
SIZE_T bytesWritten = 0;
st = MmCopyVirtualMemory(
PsGetCurrentProcess(), data,
proc, (PVOID)req->address,
(SIZE_T)req->length, KernelMode, &bytesWritten);
ObDereferenceObject(proc);
irp->IoStatus.Information = 0;
return st;
}
/* ── Physical memory read ────────────────────────────────────────── */
static NTSTATUS HandleReadPhys(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvPhysReadRequest);
struct RcxDrvPhysReadRequest* req = (struct RcxDrvPhysReadRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
return STATUS_INVALID_PARAMETER;
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
return STATUS_INVALID_PARAMETER;
VALIDATE_OUTPUT(irp, stk, req->length);
/* Save request fields before SystemBuffer is overwritten (METHOD_BUFFERED
* aliases input and output to the same buffer). */
uint64_t physAddress = req->physAddress;
ULONG length = req->length;
ULONG width = req->width;
PUCHAR dst = (PUCHAR)irp->AssociatedIrp.SystemBuffer;
if (width == 0) {
/* Byte copy -- use MmCopyMemory (safe for both RAM and MMIO) */
MM_COPY_ADDRESS srcAddr;
srcAddr.PhysicalAddress.QuadPart = (LONGLONG)physAddress;
SIZE_T bytesCopied = 0;
NTSTATUS st = MmCopyMemory(dst, srcAddr, (SIZE_T)length,
MM_COPY_MEMORY_PHYSICAL, &bytesCopied);
if (!NT_SUCCESS(st)) return st;
if (bytesCopied < length)
RtlZeroMemory(dst + bytesCopied, length - bytesCopied);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
/* Width-aware MMIO reads -- map via MDL (safe for all physical addresses).
* Use MmNonCached: width>0 implies MMIO register access where uncached
* semantics are required for correct device interaction. */
PHYS_MAP_CTX mapCtx;
PUCHAR src = (PUCHAR)MapPhysical(physAddress, (SIZE_T)length, MmNonCached, &mapCtx);
if (!src) return STATUS_UNSUCCESSFUL;
__try {
ULONG off = 0;
while (off + width <= length) {
if (width == 1)
dst[off] = READ_REGISTER_UCHAR(&src[off]);
else if (width == 2)
*(USHORT*)(dst + off) = READ_REGISTER_USHORT((PUSHORT)(src + off));
else
*(ULONG*)(dst + off) = READ_REGISTER_ULONG((PULONG)(src + off));
off += width;
}
if (off < length)
RtlZeroMemory(dst + off, length - off);
} __except (EXCEPTION_EXECUTE_HANDLER) {
UnmapPhysical(&mapCtx);
return STATUS_UNSUCCESSFUL;
}
UnmapPhysical(&mapCtx);
irp->IoStatus.Information = length;
return STATUS_SUCCESS;
}
/* ── Physical memory write ───────────────────────────────────────── */
static NTSTATUS HandleWritePhys(PIRP irp, PIO_STACK_LOCATION stk)
{
ULONG inputLen = stk->Parameters.DeviceIoControl.InputBufferLength;
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest))
return STATUS_BUFFER_TOO_SMALL;
struct RcxDrvPhysWriteRequest* req = (struct RcxDrvPhysWriteRequest*)irp->AssociatedIrp.SystemBuffer;
if (req->length == 0 || req->length > RCX_DRV_MAX_PHYSICAL)
return STATUS_INVALID_PARAMETER;
if (req->width != 0 && req->width != 1 && req->width != 2 && req->width != 4)
return STATUS_INVALID_PARAMETER;
if (inputLen < sizeof(struct RcxDrvPhysWriteRequest) + req->length)
return STATUS_BUFFER_TOO_SMALL;
PUCHAR src = (PUCHAR)req + sizeof(struct RcxDrvPhysWriteRequest);
/* Map via MDL (safe for both RAM and MMIO).
* width==0 → RAM byte write (MmCached to avoid cache attribute conflict).
* width>0 → MMIO register write (MmNonCached for correct device semantics). */
MEMORY_CACHING_TYPE ct = (req->width == 0) ? MmCached : MmNonCached;
PHYS_MAP_CTX mapCtx;
PUCHAR dst = (PUCHAR)MapPhysical(req->physAddress, (SIZE_T)req->length, ct, &mapCtx);
if (!dst) return STATUS_UNSUCCESSFUL;
__try {
if (req->width == 0) {
RtlCopyMemory(dst, src, req->length);
} else {
ULONG off = 0;
while (off + req->width <= req->length) {
if (req->width == 1)
WRITE_REGISTER_UCHAR(&dst[off], src[off]);
else if (req->width == 2)
WRITE_REGISTER_USHORT((PUSHORT)(dst + off), *(USHORT*)(src + off));
else
WRITE_REGISTER_ULONG((PULONG)(dst + off), *(ULONG*)(src + off));
off += req->width;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
UnmapPhysical(&mapCtx);
return STATUS_UNSUCCESSFUL;
}
UnmapPhysical(&mapCtx);
irp->IoStatus.Information = 0;
return STATUS_SUCCESS;
}
/* ── Ping ────────────────────────────────────────────────────────── */
static NTSTATUS HandlePing(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvPingResponse));
struct RcxDrvPingResponse* rsp = (struct RcxDrvPingResponse*)irp->AssociatedIrp.SystemBuffer;
rsp->version = RCX_DRV_VERSION;
rsp->driverBuild = __LINE__;
irp->IoStatus.Information = sizeof(struct RcxDrvPingResponse);
return STATUS_SUCCESS;
}
/* ── Query PEB ───────────────────────────────────────────────────── */
static NTSTATUS HandleQueryPeb(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryPebRequest);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvQueryPebResponse));
struct RcxDrvQueryPebRequest* req = (struct RcxDrvQueryPebRequest*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvQueryPebResponse* rsp = (struct RcxDrvQueryPebResponse*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
rsp->pebAddress = (uint64_t)(ULONG_PTR)PsGetProcessPeb(proc);
rsp->pointerSize = 8;
rsp->_pad = 0;
/* Detect WoW64 (32-bit process on 64-bit OS) */
PVOID wow64 = PsGetProcessWow64Process(proc);
if (wow64) {
rsp->pebAddress = (uint64_t)(ULONG_PTR)wow64;
rsp->pointerSize = 4;
}
ObDereferenceObject(proc);
irp->IoStatus.Information = sizeof(struct RcxDrvQueryPebResponse);
return STATUS_SUCCESS;
}
/* ── Query Regions ───────────────────────────────────────────────── */
static NTSTATUS HandleQueryRegions(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryRegionsRequest);
struct RcxDrvQueryRegionsRequest* req = (struct RcxDrvQueryRegionsRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvRegionEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Attach to target process to query its address space.
* IOCTLs arrive at PASSIVE_LEVEL; KeStackAttachProcess requires <= APC_LEVEL.
* ZwQueryVirtualMemory with ZwCurrentProcess() while attached queries the
* attached process's address space (correct). */
KAPC_STATE apcState;
KeStackAttachProcess(proc, &apcState);
struct RcxDrvRegionEntry* entries = (struct RcxDrvRegionEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
PVOID addr = NULL;
MEMORY_BASIC_INFORMATION_KM mbi;
while (count < maxEntries) {
SIZE_T retLen = 0;
st = ZwQueryVirtualMemory(ZwCurrentProcess(), addr, 0 /*MemoryBasicInformation*/,
&mbi, sizeof(mbi), &retLen);
if (!NT_SUCCESS(st)) break;
if (mbi.State == MEM_COMMIT_KM) {
entries[count].base = (uint64_t)(ULONG_PTR)mbi.BaseAddress;
entries[count].size = (uint64_t)mbi.RegionSize;
entries[count].protect = mbi.Protect;
entries[count].state = mbi.State;
count++;
}
ULONG_PTR next = (ULONG_PTR)mbi.BaseAddress + mbi.RegionSize;
if (next <= (ULONG_PTR)addr) break; /* overflow */
addr = (PVOID)next;
}
KeUnstackDetachProcess(&apcState);
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvRegionEntry);
return STATUS_SUCCESS;
}
/* ── Query Modules ───────────────────────────────────────────────── */
static NTSTATUS HandleQueryModules(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryModulesRequest);
struct RcxDrvQueryModulesRequest* req = (struct RcxDrvQueryModulesRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvModuleEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Attach to target process to read PEB->Ldr */
KAPC_STATE apcState;
KeStackAttachProcess(proc, &apcState);
struct RcxDrvModuleEntry* entries = (struct RcxDrvModuleEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
__try {
/* Read PEB address */
PEB_KM* peb = (PEB_KM*)PsGetProcessPeb(proc);
if (!peb) goto done;
ProbeForRead(peb, sizeof(PEB_KM), 1);
/* PEB->Ldr at offset 0x18 (x64) */
PEB_LDR_DATA_KM* ldr = peb->Ldr;
if (!ldr) goto done;
ProbeForRead(ldr, sizeof(PEB_LDR_DATA_KM), 1);
/* Walk InLoadOrderModuleList */
LIST_ENTRY* head = &ldr->InLoadOrderModuleList;
LIST_ENTRY* cur = head->Flink;
while (cur != head && count < maxEntries) {
LDR_DATA_TABLE_ENTRY_KM* entry = CONTAINING_RECORD(cur, LDR_DATA_TABLE_ENTRY_KM, InLoadOrderLinks);
entries[count].base = (uint64_t)(ULONG_PTR)entry->DllBase;
entries[count].size = (uint64_t)entry->SizeOfImage;
/* Copy wide-char name (truncate to 259 chars + null) */
USHORT nameLen = entry->BaseDllName.Length / sizeof(WCHAR);
if (nameLen > 259) nameLen = 259;
if (entry->BaseDllName.Buffer) {
RtlCopyMemory(entries[count].name, entry->BaseDllName.Buffer,
nameLen * sizeof(uint16_t));
}
entries[count].name[nameLen] = 0;
count++;
cur = cur->Flink;
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
/* Partial results are fine */
}
done:
KeUnstackDetachProcess(&apcState);
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvModuleEntry);
return STATUS_SUCCESS;
}
/* ── Query TEBs ──────────────────────────────────────────────────── */
/*
* Walk the target process's thread list to collect TEB addresses.
* Uses PsGetNextProcessThread (undocumented but stable since Vista).
*/
static NTSTATUS HandleQueryTebs(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvQueryTebsRequest);
struct RcxDrvQueryTebsRequest* req = (struct RcxDrvQueryTebsRequest*)irp->AssociatedIrp.SystemBuffer;
ULONG outputLen = stk->Parameters.DeviceIoControl.OutputBufferLength;
ULONG maxEntries = outputLen / sizeof(struct RcxDrvTebEntry);
if (maxEntries == 0) return STATUS_BUFFER_TOO_SMALL;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
struct RcxDrvTebEntry* entries = (struct RcxDrvTebEntry*)irp->AssociatedIrp.SystemBuffer;
ULONG count = 0;
if (!g_PsGetNextProcessThread) {
ObDereferenceObject(proc);
return STATUS_NOT_SUPPORTED;
}
/* PsGetNextProcessThread increments the ref on the returned PETHREAD and
* dereferences the previous one. We must release the last thread if we
* exit the loop early (exception or maxEntries hit). */
{
PETHREAD thread = NULL;
__try {
while ((thread = g_PsGetNextProcessThread(proc, thread)) != NULL) {
if (count >= maxEntries) {
/* Hit limit — release the thread PsGetNextProcessThread just returned */
ObDereferenceObject(thread);
break;
}
PVOID teb = PsGetThreadTeb(thread);
if (teb) {
entries[count].tebAddress = (uint64_t)(ULONG_PTR)teb;
entries[count].threadId = (uint32_t)(ULONG_PTR)PsGetThreadId(thread);
entries[count]._pad = 0;
count++;
}
}
} __except (EXCEPTION_EXECUTE_HANDLER) {
/* Exception mid-iteration: thread holds a referenced PETHREAD — release it */
if (thread)
ObDereferenceObject(thread);
}
}
ObDereferenceObject(proc);
irp->IoStatus.Information = count * sizeof(struct RcxDrvTebEntry);
return STATUS_SUCCESS;
}
/* ── Read CR3 (DirectoryTableBase) ────────────────────────────────── */
/*
* EPROCESS.DirectoryTableBase offset. Stable across Win10/11 x64.
* Verified: 0x028 on 1507-22H2+ (KPROCESS is at offset 0 of EPROCESS).
*/
#define KPROCESS_DIRECTORY_TABLE_BASE 0x028
static NTSTATUS HandleReadCr3(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvReadCr3Request);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvReadCr3Response));
struct RcxDrvReadCr3Request* req = (struct RcxDrvReadCr3Request*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvReadCr3Response* rsp = (struct RcxDrvReadCr3Response*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
__try {
rsp->cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
/* Mask off PCID bits (bits 0-11) to get the PML4 physical address */
rsp->cr3 &= ~0xFFFULL;
rsp->kernelCr3 = rsp->cr3; /* same on non-KPTI; KPTI shadow is not easily accessible */
} __except (EXCEPTION_EXECUTE_HANDLER) {
ObDereferenceObject(proc);
return STATUS_UNSUCCESSFUL;
}
ObDereferenceObject(proc);
irp->IoStatus.Information = sizeof(struct RcxDrvReadCr3Response);
return STATUS_SUCCESS;
}
/* ── Virtual-to-Physical address translation ─────────────────────── */
/* NOTE: This walks the page table non-atomically via 4 sequential physical reads.
* The page table can be modified between reads (e.g., page-out, remap). This is
* an inherent limitation shared by WinDbg's !vtop and similar tools. For a
* debugging/reversing tool this tradeoff is acceptable. */
/* Extract physical frame address from a page table entry (bits 51:12) */
#define PTE_FRAME(pte) ((pte) & 0x000FFFFFFFFFF000ULL)
/* Check Present bit (bit 0) */
#define PTE_PRESENT(pte) ((pte) & 1ULL)
/* Check Page Size bit (bit 7) -- indicates large/huge page */
#define PTE_PS(pte) ((pte) & (1ULL << 7))
static NTSTATUS HandleVtop(PIRP irp, PIO_STACK_LOCATION stk)
{
VALIDATE_INPUT(irp, stk, struct RcxDrvVtopRequest);
VALIDATE_OUTPUT(irp, stk, sizeof(struct RcxDrvVtopResponse));
struct RcxDrvVtopRequest* req = (struct RcxDrvVtopRequest*)irp->AssociatedIrp.SystemBuffer;
struct RcxDrvVtopResponse* rsp = (struct RcxDrvVtopResponse*)irp->AssociatedIrp.SystemBuffer;
PEPROCESS proc = NULL;
NTSTATUS st = LookupProcess(req->pid, &proc);
if (!NT_SUCCESS(st)) return st;
/* Read CR3 */
uint64_t cr3;
__try {
cr3 = *(uint64_t*)((PUCHAR)proc + KPROCESS_DIRECTORY_TABLE_BASE);
cr3 &= ~0xFFFULL;
} __except (EXCEPTION_EXECUTE_HANDLER) {
ObDereferenceObject(proc);
return STATUS_UNSUCCESSFUL;
}
ObDereferenceObject(proc);
uint64_t va = req->virtualAddress;
RtlZeroMemory(rsp, sizeof(*rsp));
/* Extract indices from virtual address:
* [47:39] = PML4 index, [38:30] = PDPT index,
* [29:21] = PD index, [20:12] = PT index,
* [11:0] = page offset */
ULONG pml4Idx = (ULONG)((va >> 39) & 0x1FF);
ULONG pdptIdx = (ULONG)((va >> 30) & 0x1FF);
ULONG pdIdx = (ULONG)((va >> 21) & 0x1FF);
ULONG ptIdx = (ULONG)((va >> 12) & 0x1FF);
MM_COPY_ADDRESS ca;
SIZE_T copied;
uint64_t entry;
/* Level 4: PML4 -- use MmCopyMemory (safe for RAM, unlike MmMapIoSpace) */
ca.PhysicalAddress.QuadPart = (LONGLONG)(cr3 + pml4Idx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pml4e = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
/* Level 3: PDPT */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdptIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pdpte = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
if (PTE_PS(entry)) {
/* 1GB huge page: physical = frame[51:30] | va[29:0] */
rsp->physicalAddress = (entry & 0x000FFFFFC0000000ULL) | (va & 0x3FFFFFFFULL);
rsp->pageSize = 2;
rsp->valid = 1;
goto done;
}
/* Level 2: PD */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + pdIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pde = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
if (PTE_PS(entry)) {
/* 2MB large page: physical = frame[51:21] | va[20:0] */
rsp->physicalAddress = (entry & 0x000FFFFFFFE00000ULL) | (va & 0x1FFFFFULL);
rsp->pageSize = 1;
rsp->valid = 1;
goto done;
}
/* Level 1: PT */
ca.PhysicalAddress.QuadPart = (LONGLONG)(PTE_FRAME(entry) + ptIdx * 8);
st = MmCopyMemory(&entry, ca, 8, MM_COPY_MEMORY_PHYSICAL, &copied);
if (!NT_SUCCESS(st) || copied < 8) return STATUS_UNSUCCESSFUL;
rsp->pte = entry;
if (!PTE_PRESENT(entry)) { rsp->valid = 0; goto done; }
/* 4KB page: physical = frame[51:12] | va[11:0] */
rsp->physicalAddress = PTE_FRAME(entry) | (va & 0xFFFULL);
rsp->pageSize = 0;
rsp->valid = 1;
done:
irp->IoStatus.Information = sizeof(struct RcxDrvVtopResponse);
return STATUS_SUCCESS;
}
/* ── IOCTL dispatch ──────────────────────────────────────────────── */
static NTSTATUS DispatchIoctl(PDEVICE_OBJECT dev, PIRP irp)
{
UNREFERENCED_PARAMETER(dev);
PIO_STACK_LOCATION stk = IoGetCurrentIrpStackLocation(irp);
NTSTATUS st;
switch (stk->Parameters.DeviceIoControl.IoControlCode) {
case IOCTL_RCX_READ_MEMORY: st = HandleReadMemory(irp, stk); break;
case IOCTL_RCX_WRITE_MEMORY: st = HandleWriteMemory(irp, stk); break;
case IOCTL_RCX_QUERY_REGIONS: st = HandleQueryRegions(irp, stk); break;
case IOCTL_RCX_QUERY_PEB: st = HandleQueryPeb(irp, stk); break;
case IOCTL_RCX_QUERY_MODULES: st = HandleQueryModules(irp, stk); break;
case IOCTL_RCX_QUERY_TEBS: st = HandleQueryTebs(irp, stk); break;
case IOCTL_RCX_PING: st = HandlePing(irp, stk); break;
case IOCTL_RCX_READ_PHYS: st = HandleReadPhys(irp, stk); break;
case IOCTL_RCX_WRITE_PHYS: st = HandleWritePhys(irp, stk); break;
case IOCTL_RCX_READ_CR3: st = HandleReadCr3(irp, stk); break;
case IOCTL_RCX_VTOP: st = HandleVtop(irp, stk); break;
default:
st = STATUS_INVALID_DEVICE_REQUEST;
irp->IoStatus.Information = 0;
break;
}
irp->IoStatus.Status = st;
IoCompleteRequest(irp, IO_NO_INCREMENT);
return st;
}
/* ── Create / Close (permit open/close) ──────────────────────────── */
static NTSTATUS DispatchCreateClose(PDEVICE_OBJECT dev, PIRP irp)
{
UNREFERENCED_PARAMETER(dev);
irp->IoStatus.Status = STATUS_SUCCESS;
irp->IoStatus.Information = 0;
IoCompleteRequest(irp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
/* ── Unload ──────────────────────────────────────────────────────── */
void DriverUnload(PDRIVER_OBJECT drv)
{
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
IoDeleteSymbolicLink(&symlink);
if (drv->DeviceObject)
IoDeleteDevice(drv->DeviceObject);
}
/* ── Entry point ─────────────────────────────────────────────────── */
NTSTATUS DriverEntry(PDRIVER_OBJECT drv, PUNICODE_STRING regPath)
{
UNREFERENCED_PARAMETER(regPath);
/* Resolve undocumented APIs */
UNICODE_STRING fnName = RTL_CONSTANT_STRING(L"PsGetNextProcessThread");
g_PsGetNextProcessThread = (PsGetNextProcessThread_t)MmGetSystemRoutineAddress(&fnName);
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\\Device\\RcxDrv");
UNICODE_STRING symlink = RTL_CONSTANT_STRING(L"\\DosDevices\\RcxDrv");
PDEVICE_OBJECT devObj = NULL;
NTSTATUS st = IoCreateDevice(drv, 0, &devName, FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN, FALSE, &devObj);
if (!NT_SUCCESS(st)) return st;
st = IoCreateSymbolicLink(&symlink, &devName);
if (!NT_SUCCESS(st)) {
IoDeleteDevice(devObj);
return st;
}
drv->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose;
drv->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose;
drv->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchIoctl;
drv->DriverUnload = DriverUnload;
devObj->Flags |= DO_BUFFERED_IO;
devObj->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}