From b08736245b70a7a9248bb747c5d598b00394bea3 Mon Sep 17 00:00:00 2001 From: IChooseYou Date: Fri, 13 Mar 2026 14:46:22 -0600 Subject: [PATCH] feat: kernel memory plugin + unified source menu + driver improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- CMakeLists.txt | 12 + plugins/KernelMemory/CMakeLists.txt | 63 ++ plugins/KernelMemory/KernelMemoryPlugin.cpp | 751 +++++++++++++++++ plugins/KernelMemory/KernelMemoryPlugin.h | 142 ++++ plugins/KernelMemory/driver/build_driver.bat | 56 ++ plugins/KernelMemory/driver/rcxdrv.c | 808 +++++++++++++++++++ plugins/KernelMemory/linux/Makefile | 17 + plugins/KernelMemory/linux/rcxkm.c | 132 +++ plugins/KernelMemory/rcx_drv_protocol.h | 189 +++++ src/addressparser.cpp | 71 ++ src/addressparser.h | 5 + src/compose.cpp | 49 +- src/controller.cpp | 164 +++- src/controller.h | 2 + src/core.h | 7 +- src/editor.cpp | 70 +- src/editor.h | 6 +- src/iplugin.h | 10 +- src/main.cpp | 145 ++-- src/providerregistry.cpp | 57 ++ src/providerregistry.h | 15 +- src/providers/provider.h | 20 + 22 files changed, 2671 insertions(+), 120 deletions(-) create mode 100644 plugins/KernelMemory/CMakeLists.txt create mode 100644 plugins/KernelMemory/KernelMemoryPlugin.cpp create mode 100644 plugins/KernelMemory/KernelMemoryPlugin.h create mode 100644 plugins/KernelMemory/driver/build_driver.bat create mode 100644 plugins/KernelMemory/driver/rcxdrv.c create mode 100644 plugins/KernelMemory/linux/Makefile create mode 100644 plugins/KernelMemory/linux/rcxkm.c create mode 100644 plugins/KernelMemory/rcx_drv_protocol.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0723dd6..678bd3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -544,6 +544,17 @@ if(BUILD_TESTING) target_link_libraries(test_windbg_provider PRIVATE ${QT}::Widgets ${QT}::Concurrent ${QT}::Test dbgeng ole32) add_test(NAME test_windbg_provider COMMAND test_windbg_provider) + + add_executable(test_kernel_provider tests/test_kernel_provider.cpp + plugins/KernelMemory/KernelMemoryPlugin.cpp + src/processpicker.cpp src/processpicker.ui + src/scanner.cpp) + target_include_directories(test_kernel_provider PRIVATE + src plugins/KernelMemory) + target_link_libraries(test_kernel_provider PRIVATE + ${QT}::Widgets ${QT}::Concurrent ${QT}::Test + psapi shell32 advapi32 ${_QT_WINEXTRAS}) + add_test(NAME test_kernel_provider COMMAND test_kernel_provider) endif() add_executable(bench_large_class tests/bench_large_class.cpp @@ -587,6 +598,7 @@ if(NOT APPLE) add_subdirectory(plugins/RemoteProcessMemory) endif() if(WIN32) + add_subdirectory(plugins/KernelMemory) add_subdirectory(plugins/WinDbgMemory) add_subdirectory(plugins/RcNetPluginCompatLayer) endif() diff --git a/plugins/KernelMemory/CMakeLists.txt b/plugins/KernelMemory/CMakeLists.txt new file mode 100644 index 0000000..7c9be62 --- /dev/null +++ b/plugins/KernelMemory/CMakeLists.txt @@ -0,0 +1,63 @@ +cmake_minimum_required(VERSION 3.20) +project(KernelMemoryPlugin LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC OFF) # run uic manually to avoid dupbuild with ProcessMemoryPlugin + +# ─── Generate ui_processpicker.h in our own build dir ──────────────── +set(_UI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui") +set(_UI_HDR "${CMAKE_CURRENT_BINARY_DIR}/ui_processpicker.h") + +add_custom_command( + OUTPUT "${_UI_HDR}" + COMMAND ${QT}::uic -o "${_UI_HDR}" "${_UI_SRC}" + DEPENDS "${_UI_SRC}" + COMMENT "UIC processpicker.ui (KernelMemoryPlugin)" + VERBATIM +) + +# ─── Plugin DLL ────────────────────────────────────────────────────── +set(PLUGIN_SOURCES + KernelMemoryPlugin.h + KernelMemoryPlugin.cpp + rcx_drv_protocol.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp + "${_UI_HDR}" +) + +add_library(KernelMemoryPlugin SHARED ${PLUGIN_SOURCES}) + +target_link_libraries(KernelMemoryPlugin PRIVATE + ${QT}::Widgets + ${_QT_WINEXTRAS} +) + +if(WIN32) + target_link_libraries(KernelMemoryPlugin PRIVATE psapi shell32 advapi32) +endif() + +if(UNIX AND NOT APPLE) + target_compile_options(KernelMemoryPlugin PRIVATE -fvisibility=hidden) +endif() + +target_include_directories(KernelMemoryPlugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h +) + +set_target_properties(KernelMemoryPlugin PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) + +install(TARGETS KernelMemoryPlugin + LIBRARY DESTINATION Plugins + RUNTIME DESTINATION Plugins +) diff --git a/plugins/KernelMemory/KernelMemoryPlugin.cpp b/plugins/KernelMemory/KernelMemoryPlugin.cpp new file mode 100644 index 0000000..c6737dd --- /dev/null +++ b/plugins/KernelMemory/KernelMemoryPlugin.cpp @@ -0,0 +1,751 @@ +#include "KernelMemoryPlugin.h" +#include "../../src/processpicker.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#include +#include +#include +#include +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) +#include +#endif +#endif + +// ───────────────────────────────────────────────────────────────────────── +// Helper: DeviceIoControl wrapper +// ───────────────────────────────────────────────────────────────────────── + +#ifdef _WIN32 + +static bool ioctlCall(HANDLE h, DWORD code, + const void* in, DWORD inLen, + void* out, DWORD outLen, + DWORD* bytesReturned = nullptr) +{ + DWORD br = 0; + BOOL ok = DeviceIoControl(h, code, const_cast(in), inLen, + out, outLen, &br, nullptr); + if (bytesReturned) *bytesReturned = br; + return ok != FALSE; +} + +#endif // _WIN32 + +// ───────────────────────────────────────────────────────────────────────── +// KernelProcessProvider +// ───────────────────────────────────────────────────────────────────────── + +KernelProcessProvider::KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName) + : m_driverHandle(driverHandle) + , m_pid(pid) + , m_processName(processName) +{ + if (m_driverHandle) { + queryPeb(); + cacheModules(); + } +} + +bool KernelProcessProvider::read(uint64_t addr, void* buf, int len) const +{ +#ifdef _WIN32 + if (!m_driverHandle || len <= 0) return false; + if (len > RCX_DRV_MAX_VIRTUAL) len = RCX_DRV_MAX_VIRTUAL; + + RcxDrvReadRequest req{}; + req.pid = m_pid; + req.address = addr; + req.length = (uint32_t)len; + + DWORD br = 0; + BOOL ok = DeviceIoControl((HANDLE)m_driverHandle, + IOCTL_RCX_READ_MEMORY, + &req, sizeof(req), + buf, (DWORD)len, &br, nullptr); + // Zero unread portion (partial copy) + if ((int)br < len) + memset((char*)buf + br, 0, len - br); + return ok || br > 0; +#else + Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len); + return false; +#endif +} + +int KernelProcessProvider::size() const +{ + return m_driverHandle ? 0x10000 : 0; +} + +bool KernelProcessProvider::write(uint64_t addr, const void* buf, int len) +{ +#ifdef _WIN32 + if (!m_driverHandle || len <= 0) return false; + if (len > RCX_DRV_MAX_VIRTUAL) return false; + + // Build request: header + inline data + QByteArray packet(sizeof(RcxDrvWriteRequest) + len, Qt::Uninitialized); + auto* req = reinterpret_cast(packet.data()); + req->pid = m_pid; + req->_pad0 = 0; + req->address = addr; + req->length = (uint32_t)len; + req->_pad1 = 0; + memcpy(packet.data() + sizeof(RcxDrvWriteRequest), buf, len); + + return ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_MEMORY, + packet.constData(), (DWORD)packet.size(), + nullptr, 0); +#else + Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len); + return false; +#endif +} + +QString KernelProcessProvider::getSymbol(uint64_t addr) const +{ + for (const auto& mod : m_modules) { + if (addr >= mod.base && addr < mod.base + mod.size) { + uint64_t offset = addr - mod.base; + return QStringLiteral("%1+0x%2") + .arg(mod.name) + .arg(offset, 0, 16, QChar('0')); + } + } + return {}; +} + +uint64_t KernelProcessProvider::symbolToAddress(const QString& name) const +{ + for (const auto& mod : m_modules) { + if (mod.name.compare(name, Qt::CaseInsensitive) == 0) + return mod.base; + } + return 0; +} + +QVector KernelProcessProvider::enumerateRegions() const +{ + QVector regions; +#ifdef _WIN32 + if (!m_driverHandle) return regions; + + RcxDrvQueryRegionsRequest req{}; + req.pid = m_pid; + + // Allocate generous output buffer for region entries + constexpr int kMaxEntries = 8192; + QByteArray outBuf(kMaxEntries * sizeof(RcxDrvRegionEntry), Qt::Uninitialized); + + DWORD br = 0; + if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_REGIONS, + &req, sizeof(req), + outBuf.data(), (DWORD)outBuf.size(), &br)) + return regions; + + int count = (int)(br / sizeof(RcxDrvRegionEntry)); + auto* entries = reinterpret_cast(outBuf.constData()); + + for (int i = 0; i < count; ++i) { + const auto& e = entries[i]; + // Only include committed, accessible regions + if (!(e.state & 0x1000)) continue; // MEM_COMMIT = 0x1000 + uint32_t p = e.protect; + if (p & 0x01) continue; // PAGE_NOACCESS + if (p & 0x100) continue; // PAGE_GUARD + + rcx::MemoryRegion region; + region.base = e.base; + region.size = e.size; + region.readable = true; + region.writable = (p & 0x04) || (p & 0x08) || (p & 0x40) || (p & 0x80); + region.executable = (p & 0x10) || (p & 0x20) || (p & 0x40) || (p & 0x80); + + // Match module name + for (const auto& mod : m_modules) { + if (region.base >= mod.base && region.base < mod.base + mod.size) { + region.moduleName = mod.name; + break; + } + } + + regions.append(region); + } +#endif + return regions; +} + +void KernelProcessProvider::queryPeb() +{ +#ifdef _WIN32 + RcxDrvQueryPebRequest req{}; + req.pid = m_pid; + + RcxDrvQueryPebResponse resp{}; + if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_PEB, + &req, sizeof(req), &resp, sizeof(resp))) { + m_peb = resp.pebAddress; + if (resp.pointerSize == 4) + m_pointerSize = 4; + } +#endif +} + +QVector KernelProcessProvider::tebs() const +{ + QVector result; +#ifdef _WIN32 + if (!m_driverHandle) return result; + + RcxDrvQueryTebsRequest req{}; + req.pid = m_pid; + + constexpr int kMaxThreads = 4096; + QByteArray outBuf(kMaxThreads * sizeof(RcxDrvTebEntry), Qt::Uninitialized); + + DWORD br = 0; + if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_TEBS, + &req, sizeof(req), + outBuf.data(), (DWORD)outBuf.size(), &br)) + return result; + + int count = (int)(br / sizeof(RcxDrvTebEntry)); + auto* entries = reinterpret_cast(outBuf.constData()); + + for (int i = 0; i < count; ++i) + result.append({entries[i].tebAddress, entries[i].threadId}); +#endif + return result; +} + +void KernelProcessProvider::cacheModules() +{ +#ifdef _WIN32 + if (!m_driverHandle) return; + + RcxDrvQueryModulesRequest req{}; + req.pid = m_pid; + + constexpr int kMaxModules = 1024; + QByteArray outBuf(kMaxModules * sizeof(RcxDrvModuleEntry), Qt::Uninitialized); + + DWORD br = 0; + if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_QUERY_MODULES, + &req, sizeof(req), + outBuf.data(), (DWORD)outBuf.size(), &br)) + return; + + int count = (int)(br / sizeof(RcxDrvModuleEntry)); + auto* entries = reinterpret_cast(outBuf.constData()); + + m_modules.reserve(count); + for (int i = 0; i < count; ++i) { + QString modName = QString::fromUtf16(reinterpret_cast(entries[i].name)); + if (i == 0) + m_base = entries[i].base; + + m_modules.append({modName, entries[i].base, entries[i].size}); + } +#endif +} + +// ───────────────────────────────────────────────────────────────────────── +// KernelProcessProvider — paging / address translation +// ───────────────────────────────────────────────────────────────────────── + +uint64_t KernelProcessProvider::getCr3() const +{ +#ifdef _WIN32 + if (m_cr3Cache) return m_cr3Cache; + if (!m_driverHandle) return 0; + + RcxDrvReadCr3Request req{}; + req.pid = m_pid; + + RcxDrvReadCr3Response resp{}; + if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_CR3, + &req, sizeof(req), &resp, sizeof(resp))) { + m_cr3Cache = resp.cr3; + return m_cr3Cache; + } +#endif + return 0; +} + +rcx::VtopResult KernelProcessProvider::translateAddress(uint64_t va) const +{ + rcx::VtopResult result{}; +#ifdef _WIN32 + if (!m_driverHandle) return result; + + RcxDrvVtopRequest req{}; + req.pid = m_pid; + req.virtualAddress = va; + + RcxDrvVtopResponse resp{}; + if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_VTOP, + &req, sizeof(req), &resp, sizeof(resp))) { + result.physical = resp.physicalAddress; + result.pml4e = resp.pml4e; + result.pdpte = resp.pdpte; + result.pde = resp.pde; + result.pte = resp.pte; + result.pageSize = resp.pageSize; + result.valid = resp.valid != 0; + } +#else + Q_UNUSED(va); +#endif + return result; +} + +QVector KernelProcessProvider::readPageTable(uint64_t physAddr, int startIdx, int count) const +{ + QVector entries; +#ifdef _WIN32 + if (!m_driverHandle) return entries; + if (startIdx < 0 || startIdx >= 512) return entries; + if (count <= 0) return entries; + if (startIdx + count > 512) count = 512 - startIdx; + + // Read the full 4KB page table via physical read + int byteOffset = startIdx * 8; + int byteLen = count * 8; + QByteArray buf(byteLen, 0); + + RcxDrvPhysReadRequest req{}; + req.physAddress = physAddr + byteOffset; + req.length = (uint32_t)byteLen; + req.width = 0; // memcpy mode + + DWORD br = 0; + if (ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_READ_PHYS, + &req, sizeof(req), buf.data(), (DWORD)byteLen, &br)) { + entries.resize(count); + memcpy(entries.data(), buf.constData(), byteLen); + } +#else + Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count); +#endif + return entries; +} + +// ───────────────────────────────────────────────────────────────────────── +// KernelPhysProvider +// ───────────────────────────────────────────────────────────────────────── + +KernelPhysProvider::KernelPhysProvider(void* driverHandle, uint64_t baseAddr) + : m_driverHandle(driverHandle) + , m_baseAddr(baseAddr) +{ +} + +bool KernelPhysProvider::read(uint64_t addr, void* buf, int len) const +{ +#ifdef _WIN32 + if (!m_driverHandle || len <= 0) return false; + + // Read in 4KB chunks (driver cap) + int offset = 0; + while (offset < len) { + int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL); + + RcxDrvPhysReadRequest req{}; + req.physAddress = addr + offset; + req.length = (uint32_t)chunk; + req.width = 0; // memcpy mode + + DWORD br = 0; + BOOL ok = DeviceIoControl((HANDLE)m_driverHandle, + IOCTL_RCX_READ_PHYS, + &req, sizeof(req), + (char*)buf + offset, (DWORD)chunk, &br, nullptr); + if (!ok && br == 0) { + memset((char*)buf + offset, 0, len - offset); + return offset > 0; + } + if ((int)br < chunk) + memset((char*)buf + offset + br, 0, chunk - br); + offset += chunk; + } + return true; +#else + Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len); + return false; +#endif +} + +bool KernelPhysProvider::write(uint64_t addr, const void* buf, int len) +{ +#ifdef _WIN32 + if (!m_driverHandle || len <= 0) return false; + + int offset = 0; + while (offset < len) { + int chunk = qMin(len - offset, (int)RCX_DRV_MAX_PHYSICAL); + + QByteArray packet(sizeof(RcxDrvPhysWriteRequest) + chunk, Qt::Uninitialized); + auto* req = reinterpret_cast(packet.data()); + req->physAddress = addr + offset; + req->length = (uint32_t)chunk; + req->width = 0; + memcpy(packet.data() + sizeof(RcxDrvPhysWriteRequest), (const char*)buf + offset, chunk); + + if (!ioctlCall((HANDLE)m_driverHandle, IOCTL_RCX_WRITE_PHYS, + packet.constData(), (DWORD)packet.size(), + nullptr, 0)) + return false; + offset += chunk; + } + return true; +#else + Q_UNUSED(addr); Q_UNUSED(buf); Q_UNUSED(len); + return false; +#endif +} + +// ───────────────────────────────────────────────────────────────────────── +// KernelMemoryPlugin +// ───────────────────────────────────────────────────────────────────────── + +KernelMemoryPlugin::KernelMemoryPlugin() +{ +} + +KernelMemoryPlugin::~KernelMemoryPlugin() +{ + stopDriver(); +} + +QIcon KernelMemoryPlugin::Icon() const +{ + return qApp->style()->standardIcon(QStyle::SP_DriveHDIcon); +} + +bool KernelMemoryPlugin::canHandle(const QString& target) const +{ + return target.startsWith(QStringLiteral("km:")) + || target.startsWith(QStringLiteral("phys:")); +} + +std::unique_ptr KernelMemoryPlugin::createProvider(const QString& target, QString* errorMsg) +{ + if (!ensureDriverLoaded(errorMsg)) + return nullptr; + +#ifdef _WIN32 + if (target.startsWith(QStringLiteral("km:"))) { + // km:{pid}:{name} + QStringList parts = target.mid(3).split(':'); + bool ok = false; + uint32_t pid = parts[0].toUInt(&ok); + if (!ok || pid == 0) { + if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target: ") + target; + return nullptr; + } + QString name = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid); + auto prov = std::make_unique((void*)m_driverHandle, pid, name); + if (!prov->isValid()) { + if (errorMsg) + *errorMsg = QStringLiteral("Failed to read process %1 (PID: %2) via kernel driver.") + .arg(name).arg(pid); + return nullptr; + } + return prov; + } + + if (target.startsWith(QStringLiteral("phys:"))) { + // phys:{baseAddr} + bool ok = false; + uint64_t baseAddr = target.mid(5).toULongLong(&ok, 16); + if (!ok) baseAddr = 0; + return std::make_unique((void*)m_driverHandle, baseAddr); + } + +#endif + + if (errorMsg) *errorMsg = QStringLiteral("Unknown target format: ") + target; + return nullptr; +} + +uint64_t KernelMemoryPlugin::getInitialBaseAddress(const QString& target) const +{ + if (target.startsWith(QStringLiteral("phys:"))) { + bool ok = false; + uint64_t addr = target.mid(5).toULongLong(&ok, 16); + return ok ? addr : 0; + } + // For process mode, the provider discovers base via modules + return 0; +} + +bool KernelMemoryPlugin::selectTarget(QWidget* parent, QString* target) +{ + // Show process picker directly (physical memory is accessed via + // context menu "Browse Page Tables" / "Follow Physical Frame" on an + // attached kernel process). + QVector pluginProcesses = enumerateProcesses(); + QList processes; + for (const auto& pinfo : pluginProcesses) { + ProcessInfo info; + info.pid = pinfo.pid; + info.name = pinfo.name; + info.path = pinfo.path; + info.icon = pinfo.icon; + info.is32Bit = pinfo.is32Bit; + processes.append(info); + } + + ProcessPicker picker(processes, parent); + if (picker.exec() == QDialog::Accepted) { + uint32_t pid = picker.selectedProcessId(); + QString name = picker.selectedProcessName(); + *target = QStringLiteral("km:%1:%2").arg(pid).arg(name); + return true; + } + return false; +} + +QVector KernelMemoryPlugin::enumerateProcesses() +{ + QVector processes; + +#ifdef _WIN32 + HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snapshot == INVALID_HANDLE_VALUE) return processes; + + PROCESSENTRY32W entry; + entry.dwSize = sizeof(entry); + + if (Process32FirstW(snapshot, &entry)) { + do { + PluginProcessInfo info; + info.pid = entry.th32ProcessID; + info.name = QString::fromWCharArray(entry.szExeFile); + + HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID); + if (hProcess) { + wchar_t path[MAX_PATH * 2]; + DWORD pathLen = sizeof(path) / sizeof(wchar_t); + + if (QueryFullProcessImageNameW(hProcess, 0, path, &pathLen)) { + info.path = QString::fromWCharArray(path); + + SHFILEINFOW sfi = {}; + if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) { + if (sfi.hIcon) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + QPixmap pixmap = QPixmap::fromImage(QImage::fromHICON(sfi.hIcon)); +#else + QPixmap pixmap = QtWin::fromHICON(sfi.hIcon); +#endif + info.icon = QIcon(pixmap); + DestroyIcon(sfi.hIcon); + } + } + } + + BOOL isWow64 = FALSE; + if (IsWow64Process(hProcess, &isWow64) && isWow64) + info.is32Bit = true; + + CloseHandle(hProcess); + } + + processes.append(info); + } while (Process32NextW(snapshot, &entry)); + } + + CloseHandle(snapshot); +#endif + + return processes; +} + +void KernelMemoryPlugin::populatePluginMenu(QMenu* menu) +{ + if (!m_driverLoaded) return; + menu->addAction(QStringLiteral("Unload Kernel Driver"), [this]() { unloadDriver(); }); +} + +// ───────────────────────────────────────────────────────────────────────── +// Driver service management +// ───────────────────────────────────────────────────────────────────────── + +QString KernelMemoryPlugin::driverPath() const +{ + // Resolve rcxdrv.sys next to the plugin DLL + QString pluginDir = QCoreApplication::applicationDirPath() + QStringLiteral("/Plugins"); + return pluginDir + QStringLiteral("/rcxdrv.sys"); +} + +bool KernelMemoryPlugin::ensureDriverLoaded(QString* errorMsg) +{ +#ifdef _WIN32 + // Already connected? + if (m_driverLoaded && m_driverHandle != INVALID_HANDLE_VALUE) { + RcxDrvPingResponse ping{}; + if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) + return true; + // Handle went stale — close it and try to reconnect + CloseHandle(m_driverHandle); + m_driverHandle = INVALID_HANDLE_VALUE; + m_driverLoaded = false; + } + + // Show wait cursor (SCM + StartService can take seconds on first load) + struct WaitCursorGuard { + WaitCursorGuard() { QGuiApplication::setOverrideCursor(Qt::WaitCursor); } + ~WaitCursorGuard() { QGuiApplication::restoreOverrideCursor(); } + } waitCursor; + + // Fast path: driver may already be running (previous session, or after disconnect). + // Just try to open the device handle directly. + m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH, + GENERIC_READ | GENERIC_WRITE, + 0, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (m_driverHandle != INVALID_HANDLE_VALUE) { + RcxDrvPingResponse ping{}; + if (ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) { + m_driverLoaded = true; + return true; + } + CloseHandle(m_driverHandle); + m_driverHandle = INVALID_HANDLE_VALUE; + } + + // Slow path: need to install/start the service. + QString sysPath = driverPath(); + if (!QFileInfo::exists(sysPath)) { + if (errorMsg) + *errorMsg = QStringLiteral("Driver not found: %1\n\n" + "Place rcxdrv.sys in the Plugins folder next to the plugin DLL.").arg(sysPath); + return false; + } + + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (!scm) { + if (errorMsg) + *errorMsg = QStringLiteral("Failed to open Service Control Manager.\n" + "Run Reclass as Administrator to load the kernel driver."); + return false; + } + + // Try to open existing service first + SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS); + if (!svc) { + // Service doesn't exist — create it + std::wstring wPath = sysPath.toStdWString(); + svc = CreateServiceW(scm, L"RcxDrv", L"RcxDrv", + SERVICE_ALL_ACCESS, SERVICE_KERNEL_DRIVER, + SERVICE_DEMAND_START, SERVICE_ERROR_NORMAL, + wPath.c_str(), + nullptr, nullptr, nullptr, nullptr, nullptr); + if (!svc) { + DWORD err = GetLastError(); + if (errorMsg) + *errorMsg = QStringLiteral("Failed to create driver service (error %1).\n" + "Ensure test signing is enabled: bcdedit /set testsigning on").arg(err); + CloseServiceHandle(scm); + return false; + } + } + + // Start service (ERROR_SERVICE_ALREADY_RUNNING is fine — means it's already up) + if (!StartServiceW(svc, 0, nullptr)) { + DWORD err = GetLastError(); + if (err != ERROR_SERVICE_ALREADY_RUNNING) { + if (errorMsg) + *errorMsg = QStringLiteral("Failed to start driver (error %1).\n" + "Ensure test signing is enabled and the driver is properly signed.").arg(err); + CloseServiceHandle(svc); + CloseServiceHandle(scm); + return false; + } + } + + // Done with SCM — don't hold handles open + CloseServiceHandle(svc); + CloseServiceHandle(scm); + + // Open device handle + m_driverHandle = CreateFileA(RCX_DRV_USERMODE_PATH, + GENERIC_READ | GENERIC_WRITE, + 0, nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL, nullptr); + if (m_driverHandle == INVALID_HANDLE_VALUE) { + if (errorMsg) + *errorMsg = QStringLiteral("Driver started but could not open device handle.\n" + "Device path: %1").arg(QString::fromLatin1(RCX_DRV_USERMODE_PATH)); + return false; + } + + // Verify with ping + RcxDrvPingResponse ping{}; + if (!ioctlCall(m_driverHandle, IOCTL_RCX_PING, nullptr, 0, &ping, sizeof(ping))) { + if (errorMsg) + *errorMsg = QStringLiteral("Driver opened but ping failed."); + CloseHandle(m_driverHandle); + m_driverHandle = INVALID_HANDLE_VALUE; + return false; + } + + m_driverLoaded = true; + return true; +#else + if (errorMsg) + *errorMsg = QStringLiteral("Kernel driver is only supported on Windows."); + return false; +#endif +} + +void KernelMemoryPlugin::unloadDriver() +{ +#ifdef _WIN32 + // Close device handle only — service stays running so we can reconnect + if (m_driverHandle != INVALID_HANDLE_VALUE) { + CloseHandle(m_driverHandle); + m_driverHandle = INVALID_HANDLE_VALUE; + } + m_driverLoaded = false; +#endif +} + +void KernelMemoryPlugin::stopDriver() +{ +#ifdef _WIN32 + unloadDriver(); + + // Full cleanup: stop + delete the service + SC_HANDLE scm = OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); + if (scm) { + SC_HANDLE svc = OpenServiceW(scm, L"RcxDrv", SERVICE_ALL_ACCESS); + if (svc) { + SERVICE_STATUS ss; + ControlService(svc, SERVICE_CONTROL_STOP, &ss); + DeleteService(svc); + CloseServiceHandle(svc); + } + CloseServiceHandle(scm); + } +#endif +} + +// ───────────────────────────────────────────────────────────────────────── +// Plugin factory +// ───────────────────────────────────────────────────────────────────────── + +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin() +{ + return new KernelMemoryPlugin(); +} diff --git a/plugins/KernelMemory/KernelMemoryPlugin.h b/plugins/KernelMemory/KernelMemoryPlugin.h new file mode 100644 index 0000000..62df6ca --- /dev/null +++ b/plugins/KernelMemory/KernelMemoryPlugin.h @@ -0,0 +1,142 @@ +#pragma once +#include "../../src/iplugin.h" +#include "../../src/core.h" +#include "rcx_drv_protocol.h" + +#include + +#ifdef _WIN32 +#include +#endif + +// ───────────────────────────────────────────────────────────────────────── +// Provider variants +// ───────────────────────────────────────────────────────────────────────── + +/** + * Kernel-mode process memory provider. + * Reads/writes target process virtual memory via IOCTL_RCX_READ/WRITE_MEMORY. + */ +class KernelProcessProvider : public rcx::Provider +{ +public: + KernelProcessProvider(void* driverHandle, uint32_t pid, const QString& processName); + ~KernelProcessProvider() override = default; + + bool read(uint64_t addr, void* buf, int len) const override; + int size() const override; + + bool write(uint64_t addr, const void* buf, int len) override; + bool isWritable() const override { return true; } + QString name() const override { return m_processName; } + QString kind() const override { return QStringLiteral("KernelProcess"); } + QString getSymbol(uint64_t addr) const override; + uint64_t symbolToAddress(const QString& name) const override; + + bool isLive() const override { return true; } + uint64_t base() const override { return m_base; } + int pointerSize() const override { return m_pointerSize; } + QVector enumerateRegions() const override; + bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; } + + uint32_t pid() const { return m_pid; } + uint64_t peb() const override { return m_peb; } + QVector tebs() const override; + + // ── Paging / address translation ── + bool hasKernelPaging() const override { return true; } + uint64_t getCr3() const override; + rcx::VtopResult translateAddress(uint64_t va) const override; + QVector readPageTable(uint64_t physAddr, int startIdx = 0, int count = 512) const override; + void* driverHandle() const { return m_driverHandle; } + +private: + void queryPeb(); + void cacheModules(); + + void* m_driverHandle; + uint32_t m_pid; + QString m_processName; + uint64_t m_base = 0; + int m_pointerSize = 8; + uint64_t m_peb = 0; + mutable uint64_t m_cr3Cache = 0; + + struct ModuleInfo { + QString name; + uint64_t base; + uint64_t size; + }; + QVector m_modules; +}; + +/** + * Kernel-mode physical memory provider. + * Reads/writes raw physical addresses via IOCTL_RCX_READ/WRITE_PHYS. + */ +class KernelPhysProvider : public rcx::Provider +{ +public: + KernelPhysProvider(void* driverHandle, uint64_t baseAddr); + ~KernelPhysProvider() override = default; + + bool read(uint64_t addr, void* buf, int len) const override; + int size() const override { return m_driverHandle ? 0x10000 : 0; } + + bool write(uint64_t addr, const void* buf, int len) override; + bool isWritable() const override { return true; } + QString name() const override { return QStringLiteral("Physical Memory"); } + QString kind() const override { return QStringLiteral("Physical"); } + + bool isLive() const override { return true; } + uint64_t base() const override { return m_baseAddr; } + bool isReadable(uint64_t, int len) const override { return m_driverHandle && len >= 0; } + + void setBaseAddr(uint64_t addr) { m_baseAddr = addr; } + void* driverHandle() const { return m_driverHandle; } + +private: + void* m_driverHandle; + uint64_t m_baseAddr; +}; + +// ───────────────────────────────────────────────────────────────────────── +// Plugin +// ───────────────────────────────────────────────────────────────────────── + +class KernelMemoryPlugin : public IProviderPlugin +{ +public: + KernelMemoryPlugin(); + ~KernelMemoryPlugin() override; + + std::string Name() const override { return "Kernel Memory"; } + std::string Version() const override { return "1.0.0"; } + std::string Author() const override { return "Reclass"; } + std::string Description() const override { return "Read and write memory via kernel driver (IOCTL)"; } + k_ELoadType LoadType() const override { return k_ELoadTypeManual; } + QIcon Icon() const override; + + bool canHandle(const QString& target) const override; + std::unique_ptr createProvider(const QString& target, QString* errorMsg) override; + uint64_t getInitialBaseAddress(const QString& target) const override; + bool selectTarget(QWidget* parent, QString* target) override; + + bool providesProcessList() const override { return true; } + QVector enumerateProcesses() override; + void populatePluginMenu(QMenu* menu) override; + +private: + bool ensureDriverLoaded(QString* errorMsg = nullptr); + void unloadDriver(); // close handle only — service stays running + void stopDriver(); // full cleanup: close handle + stop + delete service + QString driverPath() const; + +#ifdef _WIN32 + HANDLE m_driverHandle = INVALID_HANDLE_VALUE; +#endif + bool m_driverLoaded = false; +}; + +// Plugin export +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin(); diff --git a/plugins/KernelMemory/driver/build_driver.bat b/plugins/KernelMemory/driver/build_driver.bat new file mode 100644 index 0000000..dbd8698 --- /dev/null +++ b/plugins/KernelMemory/driver/build_driver.bat @@ -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 diff --git a/plugins/KernelMemory/driver/rcxdrv.c b/plugins/KernelMemory/driver/rcxdrv.c new file mode 100644 index 0000000..6d4fc0a --- /dev/null +++ b/plugins/KernelMemory/driver/rcxdrv.c @@ -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 +#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; +} diff --git a/plugins/KernelMemory/linux/Makefile b/plugins/KernelMemory/linux/Makefile new file mode 100644 index 0000000..0eaa09d --- /dev/null +++ b/plugins/KernelMemory/linux/Makefile @@ -0,0 +1,17 @@ +obj-m += rcxkm.o + +KDIR ?= /lib/modules/$(shell uname -r)/build + +all: + $(MAKE) -C $(KDIR) M=$(PWD) modules + +clean: + $(MAKE) -C $(KDIR) M=$(PWD) clean + +install: + insmod rcxkm.ko + +uninstall: + rmmod rcxkm + +.PHONY: all clean install uninstall diff --git a/plugins/KernelMemory/linux/rcxkm.c b/plugins/KernelMemory/linux/rcxkm.c new file mode 100644 index 0000000..0e1b8f6 --- /dev/null +++ b/plugins/KernelMemory/linux/rcxkm.c @@ -0,0 +1,132 @@ +/* + * rcxkm.c -- Linux kernel module stub for Reclass kernel memory provider. + * + * Provides /dev/rcxkm char device with ioctl() dispatch using the same + * protocol structs as the Windows driver (rcx_drv_protocol.h). + * + * Build: make -C /lib/modules/$(uname -r)/build M=$(pwd) modules + * + * TODO: implement handlers (currently returns -ENOSYS for all IOCTLs). + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../rcx_drv_protocol.h" + +#define DEVICE_NAME "rcxkm" + +MODULE_LICENSE("GPL"); +MODULE_AUTHOR("Reclass"); +MODULE_DESCRIPTION("Reclass kernel memory provider (stub)"); + +/* ── IOCTL dispatch ─────────────────────────────────────────────────── */ + +static long rcxkm_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) +{ + (void)filp; + (void)arg; + + switch (cmd) { + case IOCTL_RCX_READ_MEMORY: + /* TODO: find_get_pid(pid) -> get_task_struct -> access_process_vm() */ + return -ENOSYS; + + case IOCTL_RCX_WRITE_MEMORY: + /* TODO: access_process_vm() with FOLL_WRITE */ + return -ENOSYS; + + case IOCTL_RCX_QUERY_REGIONS: + /* TODO: walk target mm->mmap via VMA iteration */ + return -ENOSYS; + + case IOCTL_RCX_QUERY_PEB: + /* N/A on Linux (no PEB); could return mm->start_brk or similar */ + return -ENOSYS; + + case IOCTL_RCX_QUERY_MODULES: + /* TODO: walk target /proc/pid/maps or mm VMAs */ + return -ENOSYS; + + case IOCTL_RCX_QUERY_TEBS: + /* N/A on Linux (no TEB) */ + return -ENOSYS; + + case IOCTL_RCX_PING: { + struct RcxDrvPingResponse resp = { + .version = RCX_DRV_VERSION, + .driverBuild = 1, + }; + if (copy_to_user((void __user *)arg, &resp, sizeof(resp))) + return -EFAULT; + return 0; + } + + case IOCTL_RCX_READ_PHYS: + /* TODO: ioremap() + memcpy_fromio() */ + return -ENOSYS; + + case IOCTL_RCX_WRITE_PHYS: + /* TODO: ioremap() + memcpy_toio() */ + return -ENOSYS; + + default: + return -EINVAL; + } +} + +/* ── File operations ────────────────────────────────────────────────── */ + +static int rcxkm_open(struct inode *inode, struct file *filp) +{ + (void)inode; (void)filp; + return 0; +} + +static int rcxkm_release(struct inode *inode, struct file *filp) +{ + (void)inode; (void)filp; + return 0; +} + +static const struct file_operations rcxkm_fops = { + .owner = THIS_MODULE, + .unlocked_ioctl = rcxkm_ioctl, + .open = rcxkm_open, + .release = rcxkm_release, +}; + +static struct miscdevice rcxkm_device = { + .minor = MISC_DYNAMIC_MINOR, + .name = DEVICE_NAME, + .fops = &rcxkm_fops, +}; + +/* ── Module init/exit ───────────────────────────────────────────────── */ + +static int __init rcxkm_init(void) +{ + int ret = misc_register(&rcxkm_device); + if (ret) { + pr_err("rcxkm: failed to register misc device (err=%d)\n", ret); + return ret; + } + pr_info("rcxkm: loaded, device /dev/%s\n", DEVICE_NAME); + return 0; +} + +static void __exit rcxkm_exit(void) +{ + misc_deregister(&rcxkm_device); + pr_info("rcxkm: unloaded\n"); +} + +module_init(rcxkm_init); +module_exit(rcxkm_exit); diff --git a/plugins/KernelMemory/rcx_drv_protocol.h b/plugins/KernelMemory/rcx_drv_protocol.h new file mode 100644 index 0000000..d8d0490 --- /dev/null +++ b/plugins/KernelMemory/rcx_drv_protocol.h @@ -0,0 +1,189 @@ +/* + * RCX Driver Protocol -- shared between kernel driver and usermode plugin. + * No dependencies beyond standard C headers. Pure C, no Windows types. + */ +#pragma once + +#ifdef KERNEL +/* Kernel mode build: avoid stdint.h (not in WDK km/crt) */ +typedef unsigned char uint8_t; +typedef unsigned short uint16_t; +typedef unsigned int uint32_t; +typedef unsigned __int64 uint64_t; +typedef signed __int64 int64_t; +#else +#include +#endif + +/* ── Device / service names ───────────────────────────────────────── */ +#define RCX_DRV_DEVICE_NAME L"\\Device\\RcxDrv" +#define RCX_DRV_SYMLINK_NAME L"\\DosDevices\\RcxDrv" +#define RCX_DRV_USERMODE_PATH "\\\\.\\RcxDrv" +#define RCX_DRV_SERVICE_NAME "RcxDrv" + +/* ── Protocol version ─────────────────────────────────────────────── */ +#define RCX_DRV_VERSION 1 + +/* ── Size limits ──────────────────────────────────────────────────── */ +#define RCX_DRV_MAX_VIRTUAL (1024 * 1024) /* 1 MB per virtual read/write */ +#define RCX_DRV_MAX_PHYSICAL 4096 /* 4 KB per physical read/write */ + +/* ── IOCTL codes ──────────────────────────────────────────────────── */ +/* CTL_CODE(FILE_DEVICE_UNKNOWN=0x22, function, METHOD_BUFFERED=0, FILE_ANY_ACCESS=0) */ + +/* Virtual memory (per-process) */ +#define IOCTL_RCX_READ_MEMORY 0x222000 /* function 0x800 */ +#define IOCTL_RCX_WRITE_MEMORY 0x222004 /* function 0x801 */ +#define IOCTL_RCX_QUERY_REGIONS 0x222008 /* function 0x802 */ +#define IOCTL_RCX_QUERY_PEB 0x22200C /* function 0x803 */ +#define IOCTL_RCX_QUERY_MODULES 0x222010 /* function 0x804 */ +#define IOCTL_RCX_QUERY_TEBS 0x222014 /* function 0x805 */ +#define IOCTL_RCX_PING 0x222018 /* function 0x806 */ + +/* Physical memory (MMIO) */ +#define IOCTL_RCX_READ_PHYS 0x22201C /* function 0x807 */ +#define IOCTL_RCX_WRITE_PHYS 0x222020 /* function 0x808 */ + +/* Paging / address translation */ +#define IOCTL_RCX_READ_CR3 0x222044 /* function 0x811 */ +#define IOCTL_RCX_VTOP 0x222048 /* function 0x812 */ + +/* ── Request / Response structures ────────────────────────────────── */ +/* All structs are naturally aligned. Padding fields are explicit. */ + +/* -- Virtual memory -- */ + +struct RcxDrvReadRequest { + uint32_t pid; + uint32_t _pad0; + uint64_t address; + uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */ + uint32_t _pad1; +}; + +/* Write: input = header + inline data bytes */ +struct RcxDrvWriteRequest { + uint32_t pid; + uint32_t _pad0; + uint64_t address; + uint32_t length; /* max RCX_DRV_MAX_VIRTUAL */ + uint32_t _pad1; + /* uint8_t data[length] follows */ +}; + +/* -- Region enumeration -- */ + +struct RcxDrvQueryRegionsRequest { + uint32_t pid; + uint32_t _pad; +}; + +struct RcxDrvRegionEntry { + uint64_t base; + uint64_t size; + uint32_t protect; /* raw PAGE_* flags */ + uint32_t state; /* MEM_COMMIT etc. */ +}; + +/* -- PEB -- */ + +struct RcxDrvQueryPebRequest { + uint32_t pid; + uint32_t _pad; +}; + +struct RcxDrvQueryPebResponse { + uint64_t pebAddress; + uint32_t pointerSize; /* 4 or 8 */ + uint32_t _pad; +}; + +/* -- Modules -- */ + +struct RcxDrvQueryModulesRequest { + uint32_t pid; + uint32_t _pad; +}; + +struct RcxDrvModuleEntry { + uint64_t base; + uint64_t size; + uint16_t name[260]; /* wide-char, null-terminated */ +}; + +/* -- TEBs -- */ + +struct RcxDrvQueryTebsRequest { + uint32_t pid; + uint32_t _pad; +}; + +struct RcxDrvTebEntry { + uint64_t tebAddress; + uint32_t threadId; + uint32_t _pad; +}; + +/* -- Ping -- */ + +struct RcxDrvPingResponse { + uint32_t version; + uint32_t driverBuild; +}; + +/* -- Physical memory -- */ + +struct RcxDrvPhysReadRequest { + uint64_t physAddress; + uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */ + uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */ +}; + +struct RcxDrvPhysWriteRequest { + uint64_t physAddress; + uint32_t length; /* max RCX_DRV_MAX_PHYSICAL */ + uint32_t width; /* access width: 1, 2, or 4 (0 = memcpy) */ + /* uint8_t data[length] follows */ +}; + +/* -- Paging / address translation -- */ + +struct RcxDrvReadCr3Request { + uint32_t pid; + uint32_t _pad; +}; + +struct RcxDrvReadCr3Response { + uint64_t cr3; /* DirectoryTableBase (PML4 physical address) */ + uint64_t kernelCr3; /* KernelDirectoryTableBase (KPTI shadow) */ +}; + +struct RcxDrvVtopRequest { + uint32_t pid; + uint32_t _pad; + uint64_t virtualAddress; +}; + +struct RcxDrvVtopResponse { + uint64_t physicalAddress; /* final translated physical address (with page offset) */ + uint64_t pml4e; /* raw PML4 entry value */ + uint64_t pdpte; /* raw PDPT entry value */ + uint64_t pde; /* raw PD entry value */ + uint64_t pte; /* raw PT entry value (0 if large/huge page) */ + uint8_t pageSize; /* 0=4KB, 1=2MB, 2=1GB */ + uint8_t valid; /* 1 if translation succeeded, 0 if not present */ + uint8_t _pad2[6]; +}; + +/* ── Compile-time validation ──────────────────────────────────────── */ +#ifdef __cplusplus +static_assert(sizeof(RcxDrvReadRequest) == 24, "ReadRequest layout"); +static_assert(sizeof(RcxDrvWriteRequest) == 24, "WriteRequest layout"); +static_assert(sizeof(RcxDrvRegionEntry) == 24, "RegionEntry layout"); +static_assert(sizeof(RcxDrvModuleEntry) == 536, "ModuleEntry layout"); +static_assert(sizeof(RcxDrvTebEntry) == 16, "TebEntry layout"); +static_assert(sizeof(RcxDrvPingResponse) == 8, "PingResponse layout"); +static_assert(sizeof(RcxDrvReadCr3Response) == 16, "ReadCr3Response layout"); +static_assert(sizeof(RcxDrvVtopRequest) == 16, "VtopRequest layout"); +static_assert(sizeof(RcxDrvVtopResponse) == 48, "VtopResponse layout"); +#endif diff --git a/src/addressparser.cpp b/src/addressparser.cpp index 10cb121..e9a1c45 100644 --- a/src/addressparser.cpp +++ b/src/addressparser.cpp @@ -273,6 +273,7 @@ private: // Identifier or hex literal disambiguation. // Scan [a-zA-Z_][a-zA-Z0-9_]*. If it contains any non-hex char → identifier. // Otherwise → backtrack and parse as hex number. + // If the identifier is followed by '(', try to parse as a built-in function call. bool parseIdentifierOrHex(uint64_t& result) { int start = m_pos; bool hasNonHex = false; @@ -292,6 +293,11 @@ private: return parseHexNumber(result); } + // Check for function call syntax: identifier '(' args ')' + skipSpaces(); + if (peek() == '(') + return parseFunctionCall(token, result); + // It's an identifier — resolve via callback if (!m_callbacks || !m_callbacks->resolveIdentifier) { result = 0; @@ -305,6 +311,71 @@ private: return true; } + // Built-in function call: vtop(pid, va), cr3(pid), phys(addr) + bool parseFunctionCall(const QString& name, uint64_t& result) { + advance(); // skip '(' + + if (name == QStringLiteral("vtop")) { + // vtop(pid, virtualAddress) → physical address + uint64_t pid = 0; + if (!parseBitwiseOr(pid)) return false; + skipSpaces(); + if (peek() != ',') + return fail("vtop() requires 2 arguments: vtop(pid, va)"); + advance(); // skip ',' + uint64_t va = 0; + if (!parseBitwiseOr(va)) return false; + if (!expect(')')) return false; + + if (!m_callbacks || !m_callbacks->vtop) { + result = 0; + return true; + } + bool ok = false; + result = m_callbacks->vtop((uint32_t)pid, va, &ok); + if (!ok) + return fail(QStringLiteral("vtop(0x%1, 0x%2) failed") + .arg(pid, 0, 16).arg(va, 0, 16)); + return true; + } + + if (name == QStringLiteral("cr3")) { + // cr3(pid) → CR3 value + uint64_t pid = 0; + if (!parseBitwiseOr(pid)) return false; + if (!expect(')')) return false; + + if (!m_callbacks || !m_callbacks->cr3) { + result = 0; + return true; + } + bool ok = false; + result = m_callbacks->cr3((uint32_t)pid, &ok); + if (!ok) + return fail(QStringLiteral("cr3(%1) failed").arg(pid)); + return true; + } + + if (name == QStringLiteral("phys")) { + // phys(addr) → read 8 bytes from physical address + uint64_t addr = 0; + if (!parseBitwiseOr(addr)) return false; + if (!expect(')')) return false; + + if (!m_callbacks || !m_callbacks->physRead) { + result = 0; + return true; + } + bool ok = false; + result = m_callbacks->physRead(addr, &ok); + if (!ok) + return fail(QStringLiteral("phys(0x%1) failed").arg(addr, 0, 16)); + return true; + } + + return fail(QStringLiteral("unknown function '%1'").arg(name)); + } + // '[' bitwiseOr ']' — read the pointer value at the computed address bool parseDereference(uint64_t& result) { advance(); // skip '[' diff --git a/src/addressparser.h b/src/addressparser.h index 28683c5..6780e8b 100644 --- a/src/addressparser.h +++ b/src/addressparser.h @@ -16,6 +16,11 @@ struct AddressParserCallbacks { std::function resolveModule; std::function readPointer; std::function resolveIdentifier; + + // Kernel paging functions (optional — only wired when kernel provider active) + std::function vtop; + std::function cr3; + std::function physRead; }; class AddressParser { diff --git a/src/compose.cpp b/src/compose.cpp index b274114..7429018 100644 --- a/src/compose.cpp +++ b/src/compose.cpp @@ -695,6 +695,11 @@ void composeParent(ComposeState& state, const NodeTree& tree, *ok = false; return 0; }; + cbs.resolveModule = [&prov](const QString& name, bool* ok) -> uint64_t { + uint64_t base = prov.symbolToAddress(name); + *ok = (base != 0); + return base; + }; return cbs; }; @@ -827,6 +832,43 @@ void composeParent(ComposeState& state, const NodeTree& tree, } } + // Static pointer: read pointer value at evaluated addr, expand ref struct + if (exprOk && sf.refId != 0 + && (sf.kind == NodeKind::Pointer64 || sf.kind == NodeKind::Pointer32)) { + int psz = sf.byteSize(); + uint64_t ptrVal = 0; + if (prov.isValid() && psz > 0 && prov.isReadable(staticAddr, psz)) { + ptrVal = (sf.kind == NodeKind::Pointer32) + ? (uint64_t)prov.readU32(staticAddr) : prov.readU64(staticAddr); + if (ptrVal == UINT64_MAX || (sf.kind == NodeKind::Pointer32 && ptrVal == 0xFFFFFFFF)) + ptrVal = 0; + } + // Relative pointer (RVA): target = base + value + if (sf.isRelative && ptrVal != 0) + ptrVal += absAddr; + + if (ptrVal != 0) { + uint64_t pBase = ptrVal; + bool ptrReadable = prov.isReadable(pBase, 1); + static NullProvider s_nullProv2; + const Provider& childProv = ptrReadable ? prov : static_cast(s_nullProv2); + if (!ptrReadable) pBase = 0; + + int refIdx = tree.indexOfId(sf.refId); + if (refIdx >= 0) { + const Node& ref = tree.nodes[refIdx]; + if (ref.kind == NodeKind::Struct || ref.kind == NodeKind::Array) { + uint64_t savedPtrBase = state.currentPtrBase; + state.currentPtrBase = pBase; + composeParent(state, tree, childProv, refIdx, + childDepth, pBase, ref.id, + /*isArrayChild=*/true); + state.currentPtrBase = savedPtrBase; + } + } + } + } + // Footer line: "};" { LineMeta flm; @@ -893,6 +935,8 @@ void composeNode(ComposeState& state, const NodeTree& tree, && node.refId != 0) { QString ptrTargetName = resolvePointerTarget(tree, node.refId); QString ptrTypeOverride = fmt::pointerTypeName(node.kind, ptrTargetName); + if (node.isRelative) + ptrTypeOverride += QStringLiteral(" rva"); // Check if this pointer has materialized children (from materializeRefChildren) const QVector& ptrChildren = childIndices(state, node.id); @@ -961,7 +1005,10 @@ void composeNode(ComposeState& state, const NodeTree& tree, } } - // Pointer target address is used directly (absolute) + // Relative pointer (RVA): target = base + value + if (node.isRelative && ptrVal != 0) + ptrVal += base; + uint64_t pBase = ptrVal; bool ptrReadable = (ptrVal != 0) && prov.isReadable(pBase, 1); diff --git a/src/controller.cpp b/src/controller.cpp index 9b0cecc..5a2c231 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -441,13 +442,35 @@ void RcxController::connectEditor(RcxEditor* editor) { *ok = prov->read(addr, &val, ptrSz); return val; }; + // Wire kernel paging callbacks if provider supports it + if (prov->hasKernelPaging()) { + cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t { + Q_UNUSED(pid); + auto r = prov->translateAddress(va); + *ok = r.valid; + return r.physical; + }; + cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t { + Q_UNUSED(pid); + uint64_t cr3 = prov->getCr3(); + *ok = (cr3 != 0); + return cr3; + }; + cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t { + auto entries = prov->readPageTable(physAddr, 0, 1); + *ok = !entries.isEmpty(); + return entries.isEmpty() ? 0 : entries[0]; + }; + } } auto result = AddressParser::evaluate(s, m_doc->tree.pointerSize, &cbs); if (result.ok && result.value != m_doc->tree.baseAddress) { uint64_t oldBase = m_doc->tree.baseAddress; QString oldFormula = m_doc->tree.baseAddressFormula; - // Store formula if input uses module/deref syntax, otherwise clear - QString newFormula = (s.contains('<') || s.contains('[')) ? s : QString(); + // Store formula if input uses module/deref/kernel-function syntax + static const QRegularExpression formulaRx( + QStringLiteral("[<\\[]|\\b(?:vtop|cr3|phys)\\s*\\(")); + QString newFormula = formulaRx.match(s).hasMatch() ? s : QString(); m_doc->undoStack.push(new RcxCommand(this, cmd::ChangeBase{oldBase, result.value, oldFormula, newFormula})); } @@ -2440,6 +2463,103 @@ void RcxController::showContextMenu(RcxEditor* editor, int line, int nodeIdx, QTimer::singleShot(0, editor, &RcxEditor::showFindBar); }); + // ── Kernel paging menu items ── + if (m_doc->provider && m_doc->provider->hasKernelPaging()) { + menu.addSeparator(); + auto* kernelMenu = menu.addMenu(icon("symbol-key.svg"), "Kernel"); + + // Show Physical Address — translate the node's VA to physical + if (hasNode) { + uint64_t nodeAddr = m_doc->tree.baseAddress + + m_doc->tree.computeOffset(nodeIdx); + kernelMenu->addAction("Show Physical Address", [this, nodeAddr, &menu]() { + auto result = m_doc->provider->translateAddress(nodeAddr); + if (result.valid) { + const char* pageSz = result.pageSize == 2 ? "1 GB" + : result.pageSize == 1 ? "2 MB" : "4 KB"; + QString msg = QStringLiteral( + "Virtual: 0x%1\n" + "Physical: 0x%2\n" + "Page Size: %3\n\n" + "PML4E: 0x%4\n" + "PDPTE: 0x%5\n" + "PDE: 0x%6\n" + "PTE: 0x%7") + .arg(nodeAddr, 16, 16, QChar('0')) + .arg(result.physical, 16, 16, QChar('0')) + .arg(pageSz) + .arg(result.pml4e, 16, 16, QChar('0')) + .arg(result.pdpte, 16, 16, QChar('0')) + .arg(result.pde, 16, 16, QChar('0')) + .arg(result.pte, 16, 16, QChar('0')); + QMessageBox::information( + qobject_cast(parent()), + QStringLiteral("Physical Address"), msg); + } else { + QMessageBox::warning( + qobject_cast(parent()), + QStringLiteral("Translation Failed"), + QStringLiteral("Address 0x%1 is not mapped") + .arg(nodeAddr, 16, 16, QChar('0'))); + } + }); + } + + // Browse Page Tables — open PML4 in a new physical tab + kernelMenu->addAction("Browse Page Tables", [this]() { + uint64_t cr3 = m_doc->provider->getCr3(); + if (cr3 == 0) { + QMessageBox::warning(qobject_cast(parent()), + QStringLiteral("Error"), + QStringLiteral("Failed to read CR3")); + return; + } + emit requestOpenProviderTab( + QStringLiteral("kernelmemory"), + QStringLiteral("phys:%1").arg(cr3, 0, 16), + QStringLiteral("PML4 @ 0x%1").arg(cr3, 0, 16)); + }); + + // Follow Physical Frame — on a PTE bitfield, extract PhysAddr and open + if (hasNode) { + const auto& node = m_doc->tree.nodes[nodeIdx]; + if (node.classKeyword == QStringLiteral("bitfield")) { + for (const auto& bf : node.bitfieldMembers) { + if (bf.name == QStringLiteral("PhysAddr")) { + int bitOff = bf.bitOffset; + int bitWid = bf.bitWidth; + uint64_t nodeAddr = m_doc->tree.baseAddress + + m_doc->tree.computeOffset(nodeIdx); + kernelMenu->addAction("Follow Physical Frame", + [this, nodeAddr, bitOff, bitWid]() { + uint64_t pteValue = 0; + if (!m_doc->provider->read(nodeAddr, &pteValue, 8)) { + QMessageBox::warning(qobject_cast(parent()), + QStringLiteral("Error"), + QStringLiteral("Failed to read PTE at 0x%1") + .arg(nodeAddr, 0, 16)); + return; + } + uint64_t mask = (1ULL << bitWid) - 1; + uint64_t frame = ((pteValue >> bitOff) & mask) << bitOff; + if (frame == 0) { + QMessageBox::warning(qobject_cast(parent()), + QStringLiteral("Error"), + QStringLiteral("Physical frame is zero (not present?)")); + return; + } + emit requestOpenProviderTab( + QStringLiteral("kernelmemory"), + QStringLiteral("phys:%1").arg(frame, 0, 16), + QStringLiteral("PT @ 0x%1").arg(frame, 0, 16)); + }); + break; + } + } + } + } + } + emit contextMenuAboutToShow(&menu, line); menu.exec(globalPos); } @@ -3208,6 +3328,26 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt *ok = prov->read(addr, &val, ptrSz); return val; }; + // Wire kernel paging callbacks if provider supports it + if (prov->hasKernelPaging()) { + cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t { + Q_UNUSED(pid); // current provider already targets a specific process + auto r = prov->translateAddress(va); + *ok = r.valid; + return r.physical; + }; + cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t { + Q_UNUSED(pid); + uint64_t cr3 = prov->getCr3(); + *ok = (cr3 != 0); + return cr3; + }; + cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t { + auto entries = prov->readPageTable(physAddr, 0, 1); + *ok = !entries.isEmpty(); + return entries.isEmpty() ? 0 : entries[0]; + }; + } auto result = AddressParser::evaluate(m_doc->tree.baseAddressFormula, ptrSz, &cbs); if (result.ok) m_doc->tree.baseAddress = result.value; @@ -3330,6 +3470,26 @@ void RcxController::selectSource(const QString& text) { *ok = prov->read(addr, &val, ptrSz); return val; }; + // Wire kernel paging callbacks if provider supports it + if (prov->hasKernelPaging()) { + cbs.vtop = [prov](uint32_t pid, uint64_t va, bool* ok) -> uint64_t { + Q_UNUSED(pid); + auto r = prov->translateAddress(va); + *ok = r.valid; + return r.physical; + }; + cbs.cr3 = [prov](uint32_t pid, bool* ok) -> uint64_t { + Q_UNUSED(pid); + uint64_t cr3 = prov->getCr3(); + *ok = (cr3 != 0); + return cr3; + }; + cbs.physRead = [prov](uint64_t physAddr, bool* ok) -> uint64_t { + auto entries = prov->readPageTable(physAddr, 0, 1); + *ok = !entries.isEmpty(); + return entries.isEmpty() ? 0 : entries[0]; + }; + } auto result = AddressParser::evaluate( m_doc->tree.baseAddressFormula, ptrSz, &cbs); if (result.ok) diff --git a/src/controller.h b/src/controller.h index b8576bc..081551a 100644 --- a/src/controller.h +++ b/src/controller.h @@ -163,6 +163,8 @@ signals: void nodeSelected(int nodeIdx); void selectionChanged(int count); void contextMenuAboutToShow(QMenu* menu, int line); + void requestOpenProviderTab(const QString& pluginId, const QString& target, + const QString& title); private: RcxDocument* m_doc; diff --git a/src/core.h b/src/core.h index 09e84e9..ede6657 100644 --- a/src/core.h +++ b/src/core.h @@ -197,6 +197,7 @@ struct Node { int offset = 0; bool isStatic = false; // static field — excluded from struct layout QString offsetExpr; // C/C++ expression → absolute address (static fields only) + bool isRelative = false; // Pointer: target = base + value (RVA) instead of absolute int arrayLen = 1; // Array: element count int strLen = 64; bool collapsed = true; @@ -242,6 +243,8 @@ struct Node { o["isStatic"] = true; if (!offsetExpr.isEmpty()) o["offsetExpr"] = offsetExpr; + if (isRelative) + o["isRelative"] = true; o["arrayLen"] = arrayLen; o["strLen"] = strLen; o["collapsed"] = collapsed; @@ -283,6 +286,7 @@ struct Node { n.offset = o["offset"].toInt(0); n.isStatic = o["isStatic"].toBool(o["isHelper"].toBool(false)); n.offsetExpr = o["offsetExpr"].toString(); + n.isRelative = o["isRelative"].toBool(false); n.arrayLen = qBound(1, o["arrayLen"].toInt(1), 1000000); n.strLen = qBound(1, o["strLen"].toInt(64), 1000000); n.collapsed = o["collapsed"].toBool(true); @@ -677,6 +681,7 @@ namespace cmd { QVector> oldMembers, newMembers; }; struct ChangeOffsetExpr { uint64_t nodeId; QString oldExpr, newExpr; }; struct ToggleStatic { uint64_t nodeId; bool oldVal, newVal; }; + struct ToggleRelative { uint64_t nodeId; bool oldVal, newVal; }; } using Command = std::variant< @@ -684,7 +689,7 @@ using Command = std::variant< cmd::Insert, cmd::Remove, cmd::ChangeBase, cmd::WriteBytes, cmd::ChangeArrayMeta, cmd::ChangePointerRef, cmd::ChangeStructTypeName, cmd::ChangeClassKeyword, cmd::ChangeOffset, cmd::ChangeEnumMembers, - cmd::ChangeOffsetExpr, cmd::ToggleStatic + cmd::ChangeOffsetExpr, cmd::ToggleStatic, cmd::ToggleRelative >; // ── Column spans (for inline editing) ── diff --git a/src/editor.cpp b/src/editor.cpp index b8e5561..b292a0f 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -2377,12 +2377,6 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { int line, col; m_sci->getCursorPosition(&line, &col); int minCol = m_editState.spanStart; - // Don't allow backing into "0x" prefix - if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) { - QString lineText = getLineText(m_sci, m_editState.line); - if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) - minCol = m_editState.spanStart + 2; - } // If there's an active selection, collapse it to the left end (Left only, not Backspace) if (ke->key() == Qt::Key_Left) { int sL, sC, eL, eC; @@ -2410,17 +2404,9 @@ bool RcxEditor::handleEditKey(QKeyEvent* ke) { if (col >= editEndCol()) return true; // block past end return false; } - case Qt::Key_Home: { - int home = m_editState.spanStart; - // Skip "0x" prefix for hex values - if (m_editState.target == EditTarget::Value || m_editState.target == EditTarget::BaseAddress) { - QString lineText = getLineText(m_sci, m_editState.line); - if (lineText.mid(m_editState.spanStart, 2).startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) - home = m_editState.spanStart + 2; - } - m_sci->setCursorPosition(m_editState.line, home); + case Qt::Key_Home: + m_sci->setCursorPosition(m_editState.line, m_editState.spanStart); return true; - } case Qt::Key_End: m_sci->setCursorPosition(m_editState.line, editEndCol()); return true; @@ -2865,21 +2851,21 @@ bool RcxEditor::beginInlineEdit(EditTarget target, int line, int col) { || target == EditTarget::PointerTarget || target == EditTarget::RootClassType); m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELFORE, (long)0, (long)0); - if (!isPicker) - m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, - ThemeManager::instance().current().selection); + if (!isPicker) { + // Subtle tint derived from theme background (neutral, not blue) + const auto& bg = ThemeManager::instance().current().background; + int shift = (bg.lightness() < 128) ? 25 : -25; + QColor tint(qBound(0, bg.red() + shift, 255), + qBound(0, bg.green() + shift, 255), + qBound(0, bg.blue() + shift, 255)); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSELBACK, (long)1, tint); + } // Use correct UTF-8 position conversion (not lineStart + col!) m_editState.posStart = posFromCol(m_sci, line, norm.start); m_editState.posEnd = posFromCol(m_sci, line, norm.end); - // For Value/BaseAddress: skip 0x prefix in selection (select only the number) - long selStart = m_editState.posStart; - if ((target == EditTarget::Value || target == EditTarget::BaseAddress) && - trimmed.startsWith(QStringLiteral("0x"), Qt::CaseInsensitive)) { - selStart = m_editState.posStart + 2; // Skip "0x" - } - m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, selStart, m_editState.posEnd); + m_sci->SendScintilla(QsciScintillaBase::SCI_SETSEL, m_editState.posStart, m_editState.posEnd); // Hex overwrite: place cursor at start, no selection if (m_editState.hexOverwrite) @@ -3062,26 +3048,8 @@ void RcxEditor::showSourcePicker() { int zoom = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_GETZOOM); menuFont.setPointSize(menuFont.pointSize() + zoom); menu.setFont(menuFont); - menu.addAction("File"); - // Add all registered providers from global registry - const auto& providers = ProviderRegistry::instance().providers(); - for (const auto& provider : providers) - menu.addAction(provider.name); - - // Saved sources below separator (with checkmarks) - if (!m_savedSourceDisplay.isEmpty()) { - menu.addSeparator(); - for (int i = 0; i < m_savedSourceDisplay.size(); i++) { - auto* act = menu.addAction(m_savedSourceDisplay[i].text); - act->setCheckable(true); - act->setChecked(m_savedSourceDisplay[i].active); - act->setData(i); - } - menu.addSeparator(); - auto* clearAct = menu.addAction("Clear All"); - clearAct->setData(QStringLiteral("#clear")); - } + ProviderRegistry::populateSourceMenu(&menu, m_savedSourceDisplay); int lineH = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_TEXTHEIGHT, 0); int x = (int)m_sci->SendScintilla(QsciScintillaBase::SCI_POINTXFROMPOSITION, @@ -3095,11 +3063,13 @@ void RcxEditor::showSourcePicker() { const LineMeta* lm = metaForLine(m_editState.line); uint64_t addr = lm ? lm->offsetAddr : 0; auto info = endInlineEdit(); - QString text = sel->text(); - if (sel->data().toString() == QStringLiteral("#clear")) - text = QStringLiteral("#clear"); - else if (sel->data().isValid()) - text = QStringLiteral("#saved:") + QString::number(sel->data().toInt()); + // Route via action data (set by populateSourceMenu) + QString text = sel->data().toString(); + if (text.isEmpty()) { + // Plugin action (e.g. "Unload Driver") — already handled by its own lambda + cancelInlineEdit(); + return; + } emit inlineEditCommitted(info.nodeIdx, info.subLine, info.target, text, addr); } else { cancelInlineEdit(); diff --git a/src/editor.h b/src/editor.h index 7db69b8..d7d9cf2 100644 --- a/src/editor.h +++ b/src/editor.h @@ -1,5 +1,6 @@ #pragma once #include "core.h" +#include "providerregistry.h" #include "themes/theme.h" #include #include @@ -12,11 +13,6 @@ class QsciLexerCPP; namespace rcx { -struct SavedSourceDisplay { - QString text; - bool active = false; -}; - class RcxEditor : public QWidget { Q_OBJECT public: diff --git a/src/iplugin.h b/src/iplugin.h index 49025ef..b5ad687 100644 --- a/src/iplugin.h +++ b/src/iplugin.h @@ -10,8 +10,9 @@ #define RCX_PLUGIN_EXPORT __attribute__((visibility("default"))) #endif -// Forward declaration +// Forward declarations namespace rcx { class Provider; } +class QMenu; /** * Plugin interface for Reclass @@ -129,6 +130,13 @@ public: * @return true if enumerateProcesses() should be called */ virtual bool providesProcessList() const { return false; } + + /** + * Add plugin-specific actions to the source menu (optional). + * Called each time the source menu is shown. Only add items when relevant + * (e.g., "Unload Driver" only when the driver is loaded). + */ + virtual void populatePluginMenu(QMenu*) {} }; // Plugin factory function signature diff --git a/src/main.cpp b/src/main.cpp index b1e74d8..7e63b27 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -58,6 +58,7 @@ #include #include #include +#include #include static void setDarkTitleBar(QWidget* widget) { @@ -552,7 +553,7 @@ void applyMacTitleBarTheme(QWidget* window, const Theme& theme); MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent) { setWindowTitle("Reclass"); - resize(1200, 800); + resize(1080, 720); #ifndef __APPLE__ // Frameless window with system menu (Alt+Space) and min/max/close support. @@ -755,6 +756,52 @@ void MainWindow::createMenus() { file->addSeparator(); Qt5Qt6AddAction(file, "&Close Project", QKeySequence(Qt::CTRL | Qt::Key_W), QIcon(), this, &MainWindow::closeFile); file->addSeparator(); +#ifdef _WIN32 + { + // "Relaunch as Administrator" — hidden when already elevated + bool elevated = false; + HANDLE token = nullptr; + if (OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) { + TOKEN_ELEVATION elev{}; + DWORD sz = sizeof(elev); + if (GetTokenInformation(token, TokenElevation, &elev, sizeof(elev), &sz)) + elevated = (elev.TokenIsElevated != 0); + CloseHandle(token); + } + if (!elevated) { + Qt5Qt6AddAction(file, "Relaunch as &Administrator", + QKeySequence(Qt::CTRL | Qt::SHIFT | Qt::Key_A), + makeIcon(":/vsicons/shield.svg"), this, [this]() { + wchar_t exePath[MAX_PATH]; + GetModuleFileNameW(nullptr, exePath, MAX_PATH); + SHELLEXECUTEINFOW sei{}; + sei.cbSize = sizeof(sei); + sei.lpVerb = L"runas"; + sei.lpFile = exePath; + sei.nShow = SW_SHOWNORMAL; + if (ShellExecuteExW(&sei)) + QCoreApplication::quit(); + // If UAC was cancelled, do nothing + }); + file->addSeparator(); + } + } +#endif + m_sourceMenu = file->addMenu("&Data Source"); + connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu); + connect(m_sourceMenu, &QMenu::triggered, this, [this](QAction* act) { + auto* c = activeController(); + if (!c) return; + QString data = act->data().toString(); + if (data.isEmpty()) return; // plugin actions handle themselves via lambda + if (data == QStringLiteral("#clear")) + c->clearSources(); + else if (data.startsWith(QStringLiteral("#saved:"))) + c->switchSource(data.mid(7).toInt()); + else + c->selectSource(data); + }); + file->addSeparator(); Qt5Qt6AddAction(file, "E&xit", QKeySequence(Qt::Key_Close), makeIcon(":/vsicons/close.svg"), this, &QMainWindow::close); // Edit @@ -785,9 +832,6 @@ void MainWindow::createMenus() { QTimer::singleShot(0, this, [this]() { setupDockTabBars(); }); }); view->addSeparator(); - m_sourceMenu = view->addMenu("&Data Source"); - connect(m_sourceMenu, &QMenu::aboutToShow, this, &MainWindow::populateSourceMenu); - view->addSeparator(); auto* fontMenu = view->addMenu(makeIcon(":/vsicons/text-size.svg"), "&Font"); auto* fontGroup = new QActionGroup(this); fontGroup->setExclusive(true); @@ -1888,6 +1932,33 @@ QDockWidget* MainWindow::createTab(RcxDocument* doc) { menu->addAction("Close Tab", [dock]() { dock->close(); }); }); + // Open a new tab with a plugin-provided provider (e.g. kernel physical memory) + connect(ctrl, &RcxController::requestOpenProviderTab, + this, [this](const QString& pluginId, const QString& target, + const QString& title) { + auto* newDoc = new RcxDocument(this); + QByteArray data(4096, '\0'); + newDoc->loadData(data); + newDoc->tree.baseAddress = 0; + + auto* newDock = createTab(newDoc); + auto it = m_tabs.find(newDock); + if (it != m_tabs.end()) { + it->ctrl->attachViaPlugin(pluginId, target); + // Try to load PageTables.rcx template for physical kernel tabs + QString examplesPath = QCoreApplication::applicationDirPath() + + QStringLiteral("/examples/PageTables.rcx"); + if (QFile::exists(examplesPath)) + newDoc->load(examplesPath); + // Set base address from provider (template has baseAddress=0, + // but we want to start at the target physical address) + if (newDoc->provider) + newDoc->tree.baseAddress = newDoc->provider->base(); + } + newDock->setWindowTitle(title); + rebuildWorkspaceModelNow(); + }); + // Update rendered panes and workspace on document changes and undo/redo // Use QPointer to guard against dock being destroyed before deferred timer fires QPointer dockGuard = dock; @@ -4633,63 +4704,19 @@ void MainWindow::populateSourceMenu() { m_sourceMenu->clear(); auto* ctrl = activeController(); - // Icon map for known provider identifiers - static const QHash s_providerIcons = { - {QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")}, - {QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")}, - {QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")}, - {QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")}, - }; - - auto addSourceAction = [this](const QString& text, const QIcon& icon, auto&& slot) { - auto* act = m_sourceMenu->addAction(icon, text); - act->setIconVisibleInMenu(true); - connect(act, &QAction::triggered, this, std::forward(slot)); - return act; - }; - - addSourceAction(QStringLiteral("File"), - makeIcon(QStringLiteral(":/vsicons/file-binary.svg")), - [this]() { - if (auto* c = activeController()) c->selectSource(QStringLiteral("File")); - }); - - const auto& providers = ProviderRegistry::instance().providers(); - for (const auto& prov : providers) { - QString name = prov.name; - auto it = s_providerIcons.constFind(prov.identifier); - QIcon icon = makeIcon(it != s_providerIcons.constEnd() ? *it - : QStringLiteral(":/vsicons/extensions.svg")); - - QString label = prov.dllFileName.isEmpty() - ? name - : QStringLiteral("%1 (%2)").arg(name, prov.dllFileName); - - addSourceAction(label, icon, [this, name]() { - if (auto* c = activeController()) c->selectSource(name); - }); - } - - if (ctrl && !ctrl->savedSources().isEmpty()) { - m_sourceMenu->addSeparator(); - for (int i = 0; i < ctrl->savedSources().size(); i++) { - const auto& e = ctrl->savedSources()[i]; - auto* act = m_sourceMenu->addAction( - QStringLiteral("%1 '%2'").arg(e.kind, e.displayName)); - act->setCheckable(true); - act->setChecked(i == ctrl->activeSourceIndex()); - connect(act, &QAction::triggered, this, [this, i]() { - if (auto* c = activeController()) c->switchSource(i); - }); + // Build saved sources for the shared menu builder + QVector saved; + if (ctrl) { + const auto& ss = ctrl->savedSources(); + for (int i = 0; i < ss.size(); i++) { + SavedSourceDisplay d; + d.text = QStringLiteral("%1 '%2'").arg(ss[i].kind, ss[i].displayName); + d.active = (i == ctrl->activeSourceIndex()); + saved.append(d); } - m_sourceMenu->addSeparator(); - auto* clearAct = addSourceAction(QStringLiteral("Clear All"), - makeIcon(QStringLiteral(":/vsicons/clear-all.svg")), - [this]() { - if (auto* c = activeController()) c->clearSources(); - }); - Q_UNUSED(clearAct); } + + ProviderRegistry::populateSourceMenu(m_sourceMenu, saved); } void MainWindow::showPluginsDialog() { diff --git a/src/providerregistry.cpp b/src/providerregistry.cpp index 242cc83..edc9f11 100644 --- a/src/providerregistry.cpp +++ b/src/providerregistry.cpp @@ -1,5 +1,8 @@ #include "providerregistry.h" #include +#include +#include +#include ProviderRegistry& ProviderRegistry::instance() { static ProviderRegistry s_instance; @@ -56,3 +59,57 @@ const ProviderRegistry::ProviderInfo* ProviderRegistry::findProvider(const QStri void ProviderRegistry::clear() { m_providers.clear(); } + +void ProviderRegistry::populateSourceMenu(QMenu* menu, + const QVector& savedSources) +{ + static const QHash s_providerIcons = { + {QStringLiteral("processmemory"), QStringLiteral(":/vsicons/server-process.svg")}, + {QStringLiteral("remoteprocessmemory"), QStringLiteral(":/vsicons/remote.svg")}, + {QStringLiteral("windbgmemory"), QStringLiteral(":/vsicons/debug.svg")}, + {QStringLiteral("reclass.netcompatlayer"), QStringLiteral(":/vsicons/plug.svg")}, + }; + + // File source + auto* fileAct = menu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")), + QStringLiteral("File")); + fileAct->setIconVisibleInMenu(true); + fileAct->setData(QStringLiteral("File")); + + // Registered providers + const auto& providers = instance().providers(); + for (const auto& prov : providers) { + auto it = s_providerIcons.constFind(prov.identifier); + QIcon icon(it != s_providerIcons.constEnd() ? *it + : QStringLiteral(":/vsicons/extensions.svg")); + + QString label = prov.dllFileName.isEmpty() + ? prov.name + : QStringLiteral("%1 (%2)").arg(prov.name, prov.dllFileName); + + auto* act = menu->addAction(icon, label); + act->setIconVisibleInMenu(true); + act->setData(prov.name); // routing key for selectSource() + + // Plugin-specific actions (e.g. "Unload Driver" when loaded) + if (prov.plugin) + prov.plugin->populatePluginMenu(menu); + } + + // Saved sources + if (!savedSources.isEmpty()) { + menu->addSeparator(); + for (int i = 0; i < savedSources.size(); i++) { + auto* act = menu->addAction(savedSources[i].text); + act->setCheckable(true); + act->setChecked(savedSources[i].active); + act->setData(QStringLiteral("#saved:%1").arg(i)); + } + menu->addSeparator(); + auto* clearAct = menu->addAction( + QIcon(QStringLiteral(":/vsicons/clear-all.svg")), + QStringLiteral("Clear All")); + clearAct->setIconVisibleInMenu(true); + clearAct->setData(QStringLiteral("#clear")); + } +} diff --git a/src/providerregistry.h b/src/providerregistry.h index 9c13f4b..55b1abb 100644 --- a/src/providerregistry.h +++ b/src/providerregistry.h @@ -7,6 +7,13 @@ // Forward declarations namespace rcx { class Provider; } class QWidget; +class QMenu; + +// Lightweight struct for saved source display in menus +struct SavedSourceDisplay { + QString text; + bool active = false; +}; /** * Global registry for data source providers @@ -56,7 +63,13 @@ public: // Clear all providers void clear(); - + + // Populate a QMenu with source items (File, providers with icons/dll names, + // plugin actions, saved sources). Used by both the main window Data Source + // menu and the RcxEditor inline source picker. + static void populateSourceMenu(QMenu* menu, + const QVector& savedSources = {}); + private: ProviderRegistry() = default; QList m_providers; diff --git a/src/providers/provider.h b/src/providers/provider.h index 67df724..1ba5526 100644 --- a/src/providers/provider.h +++ b/src/providers/provider.h @@ -16,6 +16,13 @@ struct MemoryRegion { QString moduleName; }; +struct VtopResult { + uint64_t physical = 0; + uint64_t pml4e = 0, pdpte = 0, pde = 0, pte = 0; + uint8_t pageSize = 0; // 0=4KB, 1=2MB, 2=1GB + bool valid = false; +}; + class Provider { public: virtual ~Provider() = default; @@ -80,6 +87,19 @@ public: struct ThreadInfo { uint64_t tebAddress; uint32_t threadId; }; virtual QVector tebs() const { return {}; } + // --- Kernel paging capabilities (override in kernel providers) --- + virtual bool hasKernelPaging() const { return false; } + virtual uint64_t getCr3() const { return 0; } + virtual VtopResult translateAddress(uint64_t va) const { + Q_UNUSED(va); return {}; + } + virtual QVector readPageTable(uint64_t physAddr, + int startIdx = 0, + int count = 512) const { + Q_UNUSED(physAddr); Q_UNUSED(startIdx); Q_UNUSED(count); + return {}; + } + // --- Derived convenience (non-virtual, never override) --- bool isValid() const { return size() > 0; }