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

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

View File

@@ -58,6 +58,7 @@
#include <windowsx.h>
#include <dwmapi.h>
#include <dbghelp.h>
#include <shellapi.h>
#include <cstdio>
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<QDockWidget> dockGuard = dock;
@@ -4633,63 +4704,19 @@ void MainWindow::populateSourceMenu() {
m_sourceMenu->clear();
auto* ctrl = activeController();
// Icon map for known provider identifiers
static const QHash<QString, QString> 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<decltype(slot)>(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<SavedSourceDisplay> 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() {