mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
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:
@@ -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 '['
|
||||
|
||||
@@ -16,6 +16,11 @@ struct AddressParserCallbacks {
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveModule;
|
||||
std::function<uint64_t(uint64_t addr, bool* ok)> readPointer;
|
||||
std::function<uint64_t(const QString& name, bool* ok)> resolveIdentifier;
|
||||
|
||||
// Kernel paging functions (optional — only wired when kernel provider active)
|
||||
std::function<uint64_t(uint32_t pid, uint64_t va, bool* ok)> vtop;
|
||||
std::function<uint64_t(uint32_t pid, bool* ok)> cr3;
|
||||
std::function<uint64_t(uint64_t physAddr, bool* ok)> physRead;
|
||||
};
|
||||
|
||||
class AddressParser {
|
||||
|
||||
@@ -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<const Provider&>(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<int>& 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);
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include <QFileDialog>
|
||||
#include <QMessageBox>
|
||||
#include <QSettings>
|
||||
#include <QRegularExpression>
|
||||
#include <QtConcurrent/QtConcurrentRun>
|
||||
#include <limits>
|
||||
|
||||
@@ -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<QWidget*>(parent()),
|
||||
QStringLiteral("Physical Address"), msg);
|
||||
} else {
|
||||
QMessageBox::warning(
|
||||
qobject_cast<QWidget*>(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<QWidget*>(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<QWidget*>(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<QWidget*>(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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<QPair<QString, int64_t>> 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) ──
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
#include "core.h"
|
||||
#include "providerregistry.h"
|
||||
#include "themes/theme.h"
|
||||
#include <QWidget>
|
||||
#include <QSet>
|
||||
@@ -12,11 +13,6 @@ class QsciLexerCPP;
|
||||
|
||||
namespace rcx {
|
||||
|
||||
struct SavedSourceDisplay {
|
||||
QString text;
|
||||
bool active = false;
|
||||
};
|
||||
|
||||
class RcxEditor : public QWidget {
|
||||
Q_OBJECT
|
||||
public:
|
||||
|
||||
@@ -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
|
||||
|
||||
145
src/main.cpp
145
src/main.cpp
@@ -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() {
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
#include "providerregistry.h"
|
||||
#include <QDebug>
|
||||
#include <QMenu>
|
||||
#include <QIcon>
|
||||
#include <QHash>
|
||||
|
||||
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<SavedSourceDisplay>& savedSources)
|
||||
{
|
||||
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")},
|
||||
};
|
||||
|
||||
// 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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SavedSourceDisplay>& savedSources = {});
|
||||
|
||||
private:
|
||||
ProviderRegistry() = default;
|
||||
QList<ProviderInfo> m_providers;
|
||||
|
||||
@@ -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<ThreadInfo> 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<uint64_t> 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; }
|
||||
|
||||
Reference in New Issue
Block a user