mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
- Add pointerSize() to Provider base; WoW64/ELF detection in ProcessMemory, WinDbg, and RemoteProcessMemory plugins - Wire pointer size through NodeTree, source/XML imports, C++ generator, controller, compose, address parser, and RPC protocol header - Add is32Bit to PluginProcessInfo and ProcessInfo; show (32-bit) in picker - Scanner rescan now filters results against the current input value - Go-to-address from scanner resets change tracking to prevent false flashing
441 lines
14 KiB
C++
441 lines
14 KiB
C++
#include <QtTest/QTest>
|
|
#include "core.h"
|
|
#include "generator.h"
|
|
#include "imports/import_source.h"
|
|
#include "imports/import_reclass_xml.h"
|
|
#include "providers/provider.h"
|
|
#include "addressparser.h"
|
|
#include "iplugin.h"
|
|
#include "processpicker.h"
|
|
|
|
// Include RPC protocol for header size test
|
|
#include "rcx_rpc_protocol.h"
|
|
|
|
using namespace rcx;
|
|
|
|
// ── Test provider that reports a configurable pointer size ──
|
|
|
|
class TestProvider32 : public Provider {
|
|
public:
|
|
QByteArray m_data;
|
|
int m_ptrSize;
|
|
|
|
TestProvider32(int ptrSize, int dataSize = 256)
|
|
: m_ptrSize(ptrSize), m_data(dataSize, '\0') {}
|
|
|
|
bool read(uint64_t addr, void* buf, int len) const override {
|
|
if ((int)addr + len > m_data.size()) {
|
|
memset(buf, 0, len);
|
|
return false;
|
|
}
|
|
memcpy(buf, m_data.constData() + addr, len);
|
|
return true;
|
|
}
|
|
int size() const override { return m_data.size(); }
|
|
int pointerSize() const override { return m_ptrSize; }
|
|
};
|
|
|
|
class Test32BitSupport : public QObject {
|
|
Q_OBJECT
|
|
|
|
private slots:
|
|
|
|
// ── 1. Provider::pointerSize() default is 8 ──
|
|
|
|
void providerDefaultPointerSize() {
|
|
// NullProvider inherits default
|
|
NullProvider np;
|
|
QCOMPARE(np.pointerSize(), 8);
|
|
}
|
|
|
|
void providerCustomPointerSize() {
|
|
TestProvider32 p32(4);
|
|
QCOMPARE(p32.pointerSize(), 4);
|
|
TestProvider32 p64(8);
|
|
QCOMPARE(p64.pointerSize(), 8);
|
|
}
|
|
|
|
// ── 2. NodeTree pointerSize field ──
|
|
|
|
void nodeTreeDefaultPointerSize() {
|
|
NodeTree tree;
|
|
QCOMPARE(tree.pointerSize, 8);
|
|
}
|
|
|
|
void nodeTreePointerSizeRoundTrip() {
|
|
// 32-bit tree persists to JSON and back
|
|
NodeTree tree;
|
|
tree.pointerSize = 4;
|
|
tree.baseAddress = 0x00400000;
|
|
|
|
Node root;
|
|
root.kind = NodeKind::Struct;
|
|
root.name = "Test";
|
|
root.structTypeName = "Test";
|
|
root.parentId = 0;
|
|
tree.addNode(root);
|
|
|
|
QJsonObject json = tree.toJson();
|
|
QCOMPARE(json["pointerSize"].toInt(), 4);
|
|
|
|
NodeTree restored = NodeTree::fromJson(json);
|
|
QCOMPARE(restored.pointerSize, 4);
|
|
}
|
|
|
|
void nodeTreePointerSizeOmittedForDefault() {
|
|
// 64-bit (default) should not write pointerSize key
|
|
NodeTree tree;
|
|
tree.pointerSize = 8;
|
|
QJsonObject json = tree.toJson();
|
|
QVERIFY(!json.contains("pointerSize"));
|
|
}
|
|
|
|
void nodeTreePointerSizeDefaultOnMissing() {
|
|
// Legacy JSON without pointerSize should default to 8
|
|
QJsonObject json;
|
|
json["baseAddress"] = "400000";
|
|
json["nextId"] = "1";
|
|
json["nodes"] = QJsonArray();
|
|
|
|
NodeTree tree = NodeTree::fromJson(json);
|
|
QCOMPARE(tree.pointerSize, 8);
|
|
}
|
|
|
|
// ── 3. Source import respects pointer size ──
|
|
|
|
void sourceImport64bitDefault() {
|
|
QString src = R"(
|
|
struct Test {
|
|
PVOID ptr; // 0x0
|
|
SIZE_T sz; // 0x8
|
|
};
|
|
)";
|
|
QString error;
|
|
NodeTree tree = importFromSource(src, &error);
|
|
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
|
|
// Default: 64-bit pointers
|
|
bool foundPtr64 = false, foundUInt64 = false;
|
|
for (const auto& n : tree.nodes) {
|
|
if (n.name == "ptr" && n.kind == NodeKind::Pointer64) foundPtr64 = true;
|
|
if (n.name == "sz" && n.kind == NodeKind::UInt64) foundUInt64 = true;
|
|
}
|
|
QVERIFY2(foundPtr64, "PVOID should be Pointer64 in 64-bit mode");
|
|
QVERIFY2(foundUInt64, "SIZE_T should be UInt64 in 64-bit mode");
|
|
}
|
|
|
|
void sourceImport32bit() {
|
|
QString src = R"(
|
|
struct Test {
|
|
PVOID ptr; // 0x0
|
|
SIZE_T sz; // 0x4
|
|
};
|
|
)";
|
|
QString error;
|
|
NodeTree tree = importFromSource(src, &error, 4);
|
|
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
|
|
QCOMPARE(tree.pointerSize, 4);
|
|
|
|
bool foundPtr32 = false, foundUInt32 = false;
|
|
for (const auto& n : tree.nodes) {
|
|
if (n.name == "ptr" && n.kind == NodeKind::Pointer32) foundPtr32 = true;
|
|
if (n.name == "sz" && n.kind == NodeKind::UInt32) foundUInt32 = true;
|
|
}
|
|
QVERIFY2(foundPtr32, "PVOID should be Pointer32 in 32-bit mode");
|
|
QVERIFY2(foundUInt32, "SIZE_T should be UInt32 in 32-bit mode");
|
|
}
|
|
|
|
void sourceImportPointerField32bit() {
|
|
// A generic pointer (void* field) should become Pointer32 in 32-bit mode
|
|
QString src = R"(
|
|
struct Test {
|
|
void* ptr; // 0x0
|
|
int value; // 0x4
|
|
};
|
|
)";
|
|
QString error;
|
|
NodeTree tree = importFromSource(src, &error, 4);
|
|
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
|
|
bool foundPtr32 = false;
|
|
for (const auto& n : tree.nodes) {
|
|
if (n.name == "ptr" && n.kind == NodeKind::Pointer32) foundPtr32 = true;
|
|
}
|
|
QVERIFY2(foundPtr32, "void* should be Pointer32 in 32-bit mode");
|
|
}
|
|
|
|
void sourceImportPointerSizeTypes32bit() {
|
|
// All pointer-size-dependent types should be 32-bit
|
|
QString src = R"(
|
|
struct Test {
|
|
HANDLE h; // 0x0
|
|
ULONG_PTR up; // 0x4
|
|
LONG_PTR lp; // 0x8
|
|
uintptr_t uip; // 0xC
|
|
intptr_t ip; // 0x10
|
|
size_t sz; // 0x14
|
|
LPVOID lv; // 0x18
|
|
PCHAR pc; // 0x1C
|
|
};
|
|
)";
|
|
QString error;
|
|
NodeTree tree = importFromSource(src, &error, 4);
|
|
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
|
|
for (const auto& n : tree.nodes) {
|
|
if (n.parentId == 0) continue; // skip root struct
|
|
int sz = n.byteSize();
|
|
QVERIFY2(sz == 4,
|
|
qPrintable(QString("Field '%1' has size %2, expected 4")
|
|
.arg(n.name).arg(sz)));
|
|
}
|
|
}
|
|
|
|
// ── 4. Generator respects pointer size ──
|
|
|
|
void generatorPointer32NativeVoidStar() {
|
|
// For 32-bit target, untyped Pointer32 should emit void*
|
|
NodeTree tree;
|
|
tree.pointerSize = 4;
|
|
|
|
Node root;
|
|
root.kind = NodeKind::Struct;
|
|
root.name = "Test";
|
|
root.structTypeName = "Test";
|
|
root.parentId = 0;
|
|
int ri = tree.addNode(root);
|
|
uint64_t rootId = tree.nodes[ri].id;
|
|
|
|
Node p;
|
|
p.kind = NodeKind::Pointer32;
|
|
p.name = "ptr";
|
|
p.parentId = rootId;
|
|
p.offset = 0;
|
|
tree.addNode(p);
|
|
|
|
QString result = renderCpp(tree, rootId);
|
|
QVERIFY2(result.contains("void* ptr;"),
|
|
qPrintable("32-bit native Pointer32 should emit void*:\n" + result));
|
|
}
|
|
|
|
void generatorPointer64NativeVoidStar() {
|
|
// For 64-bit target (default), untyped Pointer64 should emit void*
|
|
NodeTree tree;
|
|
tree.pointerSize = 8;
|
|
|
|
Node root;
|
|
root.kind = NodeKind::Struct;
|
|
root.name = "Test";
|
|
root.structTypeName = "Test";
|
|
root.parentId = 0;
|
|
int ri = tree.addNode(root);
|
|
uint64_t rootId = tree.nodes[ri].id;
|
|
|
|
Node p;
|
|
p.kind = NodeKind::Pointer64;
|
|
p.name = "ptr";
|
|
p.parentId = rootId;
|
|
p.offset = 0;
|
|
tree.addNode(p);
|
|
|
|
QString result = renderCpp(tree, rootId);
|
|
QVERIFY2(result.contains("void* ptr;"),
|
|
qPrintable("64-bit native Pointer64 should emit void*:\n" + result));
|
|
}
|
|
|
|
void generatorPointer32CrossSizeInt() {
|
|
// For 64-bit target, Pointer32 should emit uint32_t (cross-size)
|
|
NodeTree tree;
|
|
tree.pointerSize = 8;
|
|
|
|
Node root;
|
|
root.kind = NodeKind::Struct;
|
|
root.name = "Test";
|
|
root.structTypeName = "Test";
|
|
root.parentId = 0;
|
|
int ri = tree.addNode(root);
|
|
uint64_t rootId = tree.nodes[ri].id;
|
|
|
|
Node p;
|
|
p.kind = NodeKind::Pointer32;
|
|
p.name = "ptr32";
|
|
p.parentId = rootId;
|
|
p.offset = 0;
|
|
tree.addNode(p);
|
|
|
|
QString result = renderCpp(tree, rootId);
|
|
QVERIFY2(result.contains("uint32_t ptr32;"),
|
|
qPrintable("Cross-size Pointer32 on 64-bit target should emit uint32_t:\n" + result));
|
|
}
|
|
|
|
void generatorTypedPointerBothSizes() {
|
|
// Typed pointers (with refId) always emit struct X* regardless of size
|
|
NodeTree tree;
|
|
tree.pointerSize = 4;
|
|
|
|
Node target;
|
|
target.kind = NodeKind::Struct;
|
|
target.name = "Target";
|
|
target.structTypeName = "TargetData";
|
|
target.parentId = 0;
|
|
int ti = tree.addNode(target);
|
|
uint64_t targetId = tree.nodes[ti].id;
|
|
|
|
Node main;
|
|
main.kind = NodeKind::Struct;
|
|
main.name = "Main";
|
|
main.structTypeName = "MainStruct";
|
|
main.parentId = 0;
|
|
int mi = tree.addNode(main);
|
|
uint64_t mainId = tree.nodes[mi].id;
|
|
|
|
Node p;
|
|
p.kind = NodeKind::Pointer32;
|
|
p.name = "pTarget";
|
|
p.parentId = mainId;
|
|
p.offset = 0;
|
|
p.refId = targetId;
|
|
tree.addNode(p);
|
|
|
|
QString result = renderCpp(tree, mainId);
|
|
QVERIFY2(result.contains("struct TargetData* pTarget;"),
|
|
qPrintable("Typed Pointer32 should emit struct X*:\n" + result));
|
|
}
|
|
|
|
// ── 5. RPC protocol header has pointerSize field ──
|
|
|
|
void rpcHeaderHasPointerSize() {
|
|
// Verify the field exists and header is still 4096 bytes
|
|
RcxRpcHeader hdr = {};
|
|
hdr.pointerSize = 4;
|
|
QCOMPARE(hdr.pointerSize, (uint32_t)4);
|
|
QCOMPARE((int)sizeof(RcxRpcHeader), RCX_RPC_HEADER_SIZE);
|
|
}
|
|
|
|
// ── 6. PluginProcessInfo has is32Bit field ──
|
|
|
|
void pluginProcessInfoIs32Bit() {
|
|
PluginProcessInfo info;
|
|
QCOMPARE(info.is32Bit, false); // default
|
|
|
|
info.is32Bit = true;
|
|
QCOMPARE(info.is32Bit, true);
|
|
}
|
|
|
|
// ── 7. ProcessInfo has is32Bit field ──
|
|
|
|
void processInfoIs32Bit() {
|
|
ProcessInfo info;
|
|
QCOMPARE(info.is32Bit, false); // default
|
|
|
|
info.is32Bit = true;
|
|
QCOMPARE(info.is32Bit, true);
|
|
}
|
|
|
|
// ── 8. AddressParser readPointer uses correct size ──
|
|
|
|
void addressParserReadPointer32bit() {
|
|
// Create a test provider with a 32-bit pointer at address 0
|
|
TestProvider32 prov(4, 16);
|
|
uint32_t val32 = 0xDEADBEEF;
|
|
memcpy(prov.m_data.data(), &val32, 4);
|
|
// Write garbage in bytes 4-7 to verify we only read 4 bytes
|
|
memset(prov.m_data.data() + 4, 0xFF, 4);
|
|
|
|
AddressParserCallbacks cbs;
|
|
int ptrSz = prov.pointerSize();
|
|
auto* p = &prov;
|
|
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
|
uint64_t val = 0;
|
|
*ok = p->read(addr, &val, ptrSz);
|
|
return val;
|
|
};
|
|
|
|
auto result = AddressParser::evaluate("[0]", ptrSz, &cbs);
|
|
QVERIFY(result.ok);
|
|
QCOMPARE(result.value, (uint64_t)0xDEADBEEF);
|
|
}
|
|
|
|
void addressParserReadPointer64bit() {
|
|
TestProvider32 prov(8, 16);
|
|
uint64_t val64 = 0x0000DEADBEEF1234ULL;
|
|
memcpy(prov.m_data.data(), &val64, 8);
|
|
|
|
AddressParserCallbacks cbs;
|
|
int ptrSz = prov.pointerSize();
|
|
auto* p = &prov;
|
|
cbs.readPointer = [p, ptrSz](uint64_t addr, bool* ok) -> uint64_t {
|
|
uint64_t val = 0;
|
|
*ok = p->read(addr, &val, ptrSz);
|
|
return val;
|
|
};
|
|
|
|
auto result = AddressParser::evaluate("[0]", ptrSz, &cbs);
|
|
QVERIFY(result.ok);
|
|
QCOMPARE(result.value, (uint64_t)0x0000DEADBEEF1234ULL);
|
|
}
|
|
|
|
// ── 9. Source import HANDLE/LPVOID remain 64-bit by default ──
|
|
|
|
void sourceImportBackwardsCompat() {
|
|
QString src = R"(
|
|
struct Test {
|
|
HANDLE h; // 0x0
|
|
LPVOID lv; // 0x8
|
|
};
|
|
)";
|
|
QString error;
|
|
NodeTree tree = importFromSource(src, &error);
|
|
QVERIFY(!tree.nodes.isEmpty());
|
|
|
|
// Default (no pointerSize arg) should be 64-bit
|
|
for (const auto& n : tree.nodes) {
|
|
if (n.name == "h") QCOMPARE(n.kind, NodeKind::Pointer64);
|
|
if (n.name == "lv") QCOMPARE(n.kind, NodeKind::Pointer64);
|
|
}
|
|
}
|
|
|
|
// ── 10. Full round-trip: 32-bit import → generate → verify ──
|
|
|
|
void fullRoundTrip32bit() {
|
|
QString src = R"(
|
|
struct EPROCESS_32 {
|
|
PVOID Pcb; // 0x0
|
|
HANDLE UniqueProcessId; // 0x4
|
|
DWORD ActiveProcessLinks; // 0x8
|
|
};
|
|
)";
|
|
QString error;
|
|
NodeTree tree = importFromSource(src, &error, 4);
|
|
QVERIFY2(!tree.nodes.isEmpty(), qPrintable(error));
|
|
QCOMPARE(tree.pointerSize, 4);
|
|
|
|
// Find the root struct
|
|
uint64_t rootId = 0;
|
|
for (const auto& n : tree.nodes) {
|
|
if (n.parentId == 0 && n.kind == NodeKind::Struct) {
|
|
rootId = n.id;
|
|
break;
|
|
}
|
|
}
|
|
QVERIFY(rootId != 0);
|
|
|
|
// Generate C++ code
|
|
QString code = renderCpp(tree, rootId);
|
|
QVERIFY2(code.contains("void* Pcb;"),
|
|
qPrintable("PVOID in 32-bit should generate void*:\n" + code));
|
|
QVERIFY2(code.contains("void* UniqueProcessId;"),
|
|
qPrintable("HANDLE in 32-bit should generate void*:\n" + code));
|
|
|
|
// Verify JSON persistence
|
|
QJsonObject json = tree.toJson();
|
|
QCOMPARE(json["pointerSize"].toInt(), 4);
|
|
NodeTree restored = NodeTree::fromJson(json);
|
|
QCOMPARE(restored.pointerSize, 4);
|
|
}
|
|
};
|
|
|
|
QTEST_MAIN(Test32BitSupport)
|
|
#include "test_32bit_support.moc"
|