mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
feat: switch provider addressing from RVA to absolute, add pointer expansion tests
This commit is contained in:
@@ -133,19 +133,18 @@ private slots:
|
||||
// ──────────────────────────────────────────────────
|
||||
|
||||
void testVTableDisasm_composedAddress() {
|
||||
// Memory layout (provider-relative, i.e. offset from baseAddress):
|
||||
// Memory layout (absolute addresses, baseAddress = 0):
|
||||
//
|
||||
// [0x0000] Root "Obj" struct
|
||||
// +0x00: Pointer64 __vptr => points to 0xBASE+0x100 (vtable)
|
||||
// +0x00: Pointer64 __vptr => points to 0x100 (vtable)
|
||||
//
|
||||
// [0x0100] VTable (expanded via pointer deref)
|
||||
// +0x00: func ptr 0 => value 0xBASE+0x200 (func0 code)
|
||||
// +0x08: func ptr 1 => value 0xBASE+0x300 (func1 code)
|
||||
// +0x00: func ptr 0 => value 0x200 (func0 code)
|
||||
// +0x08: func ptr 1 => value 0x300 (func1 code)
|
||||
//
|
||||
// [0x0200] func0 code: push rbp; ret
|
||||
// [0x0300] func1 code: xor eax, eax; ret
|
||||
//
|
||||
const uint64_t kBase = 0x7FF600000000ULL;
|
||||
|
||||
// Build a 4KB buffer
|
||||
QByteArray mem(4096, '\0');
|
||||
@@ -153,12 +152,12 @@ private slots:
|
||||
memcpy(mem.data() + off, &val, 8);
|
||||
};
|
||||
|
||||
// Root object at offset 0: __vptr points to vtable at kBase + 0x100
|
||||
w64(0x00, kBase + 0x100);
|
||||
// Root object at offset 0: __vptr points to vtable at 0x100
|
||||
w64(0x00, 0x100);
|
||||
|
||||
// VTable at offset 0x100: two function pointers
|
||||
w64(0x100, kBase + 0x200); // slot 0 -> func0
|
||||
w64(0x108, kBase + 0x300); // slot 1 -> func1
|
||||
w64(0x100, 0x200); // slot 0 -> func0
|
||||
w64(0x108, 0x300); // slot 1 -> func1
|
||||
|
||||
// func0 at offset 0x200: push rbp; ret
|
||||
mem[0x200] = '\x55';
|
||||
@@ -173,7 +172,7 @@ private slots:
|
||||
|
||||
// Build node tree
|
||||
NodeTree tree;
|
||||
tree.baseAddress = kBase;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
// Root struct "Obj"
|
||||
Node root;
|
||||
@@ -227,8 +226,8 @@ private slots:
|
||||
for (int i = 0; i < result.meta.size(); i++) {
|
||||
const LineMeta& lm = result.meta[i];
|
||||
if (lm.nodeKind == NodeKind::FuncPtr64 && lm.lineKind == LineKind::Field) {
|
||||
// Only include the pointer-expanded ones (near vtable at kBase+0x100)
|
||||
if (lm.offsetAddr >= kBase + 0x100 && lm.offsetAddr < kBase + 0x200) {
|
||||
// Only include the pointer-expanded ones (near vtable at 0x100)
|
||||
if (lm.offsetAddr >= 0x100 && lm.offsetAddr < 0x200) {
|
||||
int nodeIdx = lm.nodeIdx;
|
||||
funcPtrs.append({i, lm.offsetAddr, lm.nodeKind,
|
||||
nodeIdx >= 0 ? tree.nodes[nodeIdx].name : QString()});
|
||||
@@ -239,29 +238,29 @@ private slots:
|
||||
QCOMPARE(funcPtrs.size(), 2);
|
||||
|
||||
// Verify composed addresses point to the vtable, NOT to the root struct
|
||||
// func0 should be at kBase + 0x100 (vtable + 0)
|
||||
QCOMPARE(funcPtrs[0].offsetAddr, kBase + 0x100);
|
||||
// func1 should be at kBase + 0x108 (vtable + 8)
|
||||
QCOMPARE(funcPtrs[1].offsetAddr, kBase + 0x108);
|
||||
// func0 should be at 0x100 (vtable + 0)
|
||||
QCOMPARE(funcPtrs[0].offsetAddr, (uint64_t)0x100);
|
||||
// func1 should be at 0x108 (vtable + 8)
|
||||
QCOMPARE(funcPtrs[1].offsetAddr, (uint64_t)0x108);
|
||||
|
||||
// Now simulate what the hover code should do:
|
||||
// Read the function pointer VALUE from the correct provider address
|
||||
for (const auto& fp : funcPtrs) {
|
||||
// Provider-relative address = offsetAddr - baseAddress
|
||||
uint64_t provAddr = fp.offsetAddr - kBase;
|
||||
// Provider reads at absolute address directly
|
||||
uint64_t provAddr = fp.offsetAddr;
|
||||
|
||||
// Read the pointer value (the function address)
|
||||
uint64_t ptrVal = prov.readU64(provAddr);
|
||||
|
||||
// Verify we got the right pointer values
|
||||
if (fp.name == "func0") {
|
||||
QCOMPARE(ptrVal, kBase + 0x200);
|
||||
QCOMPARE(ptrVal, (uint64_t)0x200);
|
||||
} else {
|
||||
QCOMPARE(ptrVal, kBase + 0x300);
|
||||
QCOMPARE(ptrVal, (uint64_t)0x300);
|
||||
}
|
||||
|
||||
// Convert pointer value to provider-relative for reading code bytes
|
||||
uint64_t codeProvAddr = ptrVal - kBase;
|
||||
// Read code bytes at the pointer target (absolute address)
|
||||
uint64_t codeProvAddr = ptrVal;
|
||||
QByteArray codeBytes = prov.readBytes(codeProvAddr, 128);
|
||||
|
||||
// Disassemble and verify
|
||||
@@ -275,14 +274,14 @@ private slots:
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("push rbp"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
// Verify address in output matches the real function address
|
||||
QVERIFY2(lines[0].startsWith("00007ff600000200"),
|
||||
QVERIFY2(lines[0].contains("200"),
|
||||
qPrintable("func0 addr wrong: " + lines[0]));
|
||||
} else {
|
||||
// Should decode: xor eax, eax; ret
|
||||
QVERIFY2(lines.size() >= 2, qPrintable(QString("Expected >= 2 lines for func1, got %1: %2").arg(lines.size()).arg(asm_)));
|
||||
QCOMPARE(mnemonic(lines[0]), QStringLiteral("xor eax, eax"));
|
||||
QCOMPARE(mnemonic(lines[1]), QStringLiteral("ret"));
|
||||
QVERIFY2(lines[0].startsWith("00007ff600000300"),
|
||||
QVERIFY2(lines[0].contains("300"),
|
||||
qPrintable("func1 addr wrong: " + lines[0]));
|
||||
}
|
||||
}
|
||||
@@ -292,26 +291,25 @@ private slots:
|
||||
// inside the ROOT struct, not the vtable.
|
||||
uint64_t wrongVal0 = prov.readU64(0); // node.offset=0: reads __vptr value
|
||||
uint64_t wrongVal1 = prov.readU64(8); // node.offset=8: reads garbage after __vptr
|
||||
// wrongVal0 = kBase + 0x100 (the vptr itself, NOT a function address)
|
||||
QCOMPARE(wrongVal0, kBase + 0x100);
|
||||
// wrongVal0 = 0x100 (the vptr itself, NOT a function address)
|
||||
QCOMPARE(wrongVal0, (uint64_t)0x100);
|
||||
// This is the vtable address, not a function — disassembling it would be wrong
|
||||
QVERIFY2(wrongVal0 != kBase + 0x200,
|
||||
QVERIFY2(wrongVal0 != (uint64_t)0x200,
|
||||
"node.offset reads the vptr, not the function pointer");
|
||||
QVERIFY2(wrongVal1 != kBase + 0x300,
|
||||
QVERIFY2(wrongVal1 != (uint64_t)0x300,
|
||||
"node.offset=8 reads past vptr, not the second function pointer");
|
||||
}
|
||||
|
||||
void testVTableDisasm_wrongAddressGivesWrongCode() {
|
||||
// Demonstrate that using node.offset instead of composed address
|
||||
// gives completely wrong disassembly results
|
||||
const uint64_t kBase = 0x10000;
|
||||
QByteArray mem(1024, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) { memcpy(mem.data()+off, &val, 8); };
|
||||
|
||||
// Root at 0: vptr -> 0x80
|
||||
w64(0x00, kBase + 0x80);
|
||||
w64(0x00, (uint64_t)0x80);
|
||||
// VTable at 0x80: one func ptr -> 0x100
|
||||
w64(0x80, kBase + 0x100);
|
||||
w64(0x80, (uint64_t)0x100);
|
||||
// Code at 0x100: sub rsp, 0x28; nop; ret
|
||||
mem[0x100] = '\x48'; mem[0x101] = '\x83'; mem[0x102] = '\xec';
|
||||
mem[0x103] = '\x28'; mem[0x104] = '\x90'; mem[0x105] = '\xc3';
|
||||
@@ -320,15 +318,15 @@ private slots:
|
||||
|
||||
// WRONG: read from node.offset=0 (root's vptr value, not the func ptr)
|
||||
uint64_t wrongPtrVal = prov.readU64(0);
|
||||
QCOMPARE(wrongPtrVal, kBase + 0x80); // This is the vtable addr, not a function!
|
||||
QCOMPARE(wrongPtrVal, (uint64_t)0x80); // This is the vtable addr, not a function!
|
||||
|
||||
// RIGHT: read from composed address (vtable + 0)
|
||||
uint64_t rightPtrVal = prov.readU64(0x80);
|
||||
QCOMPARE(rightPtrVal, kBase + 0x100); // This IS the function address
|
||||
QCOMPARE(rightPtrVal, (uint64_t)0x100); // This IS the function address
|
||||
|
||||
// Disassemble the RIGHT target
|
||||
QByteArray rightCode = prov.readBytes(0x100, 128);
|
||||
QString rightAsm = disassemble(rightCode, kBase + 0x100, 64, 128);
|
||||
QString rightAsm = disassemble(rightCode, 0x100, 64, 128);
|
||||
QStringList rightLines = rightAsm.split('\n');
|
||||
QVERIFY(rightLines.size() >= 3);
|
||||
QCOMPARE(mnemonic(rightLines[0]), QStringLiteral("sub rsp, 0x28"));
|
||||
@@ -337,7 +335,7 @@ private slots:
|
||||
|
||||
// Disassemble the WRONG target (vtable data, not code!)
|
||||
QByteArray wrongCode = prov.readBytes(0x80, 128);
|
||||
QString wrongAsm = disassemble(wrongCode, kBase + 0x80, 64, 128);
|
||||
QString wrongAsm = disassemble(wrongCode, 0x80, 64, 128);
|
||||
// The wrong bytes are the vtable entries (pointer values),
|
||||
// which decode as garbage instructions, not sub/nop/ret
|
||||
QVERIFY2(!wrongAsm.contains("sub rsp"),
|
||||
@@ -348,9 +346,9 @@ private slots:
|
||||
// Full simulation of the hover flow as implemented in editor.cpp:
|
||||
//
|
||||
// 1. Compose the tree to get LineMeta with correct offsetAddr
|
||||
// 2. For each FuncPtr64 line, read pointer value from snapshot/provider
|
||||
// using lm.offsetAddr - baseAddress (composed address)
|
||||
// 3. Read code bytes from the REAL provider using ptrVal - baseAddress
|
||||
// 2. For each FuncPtr64 line, read pointer value from provider
|
||||
// using lm.offsetAddr (absolute address)
|
||||
// 3. Read code bytes from the REAL provider using ptrVal directly
|
||||
// (the real provider can read any process address; snapshot cannot)
|
||||
// 4. Disassemble the code bytes
|
||||
//
|
||||
@@ -358,28 +356,25 @@ private slots:
|
||||
// the snapshot), step 3 reads from arbitrary code addresses (needs
|
||||
// the real provider, not snapshot).
|
||||
|
||||
const uint64_t kBase = 0x7FF600000000ULL;
|
||||
QByteArray mem(8192, '\0');
|
||||
auto w64 = [&](int off, uint64_t val) {
|
||||
memcpy(mem.data() + off, &val, 8);
|
||||
};
|
||||
|
||||
// Layout:
|
||||
// [0x000] Root struct: __vptr -> vtable at kBase + 0x100
|
||||
// [0x100] VTable: func0 -> kBase + 0x1000, func1 -> kBase + 0x1800
|
||||
// [0x000] Root struct: __vptr -> vtable at 0x100
|
||||
// [0x100] VTable: func0 -> 0x1000, func1 -> 0x1800
|
||||
// [0x1000] func0 code: push rbp; mov rbp, rsp; sub rsp, 0x20; ret
|
||||
// [0x1800] func1 code: xor eax, eax; ret
|
||||
w64(0x000, kBase + 0x100); // __vptr
|
||||
w64(0x100, kBase + 0x1000); // vtable[0]
|
||||
w64(0x108, kBase + 0x1800); // vtable[1]
|
||||
w64(0x000, (uint64_t)0x100); // __vptr
|
||||
w64(0x100, (uint64_t)0x1000); // vtable[0]
|
||||
w64(0x108, (uint64_t)0x1800); // vtable[1]
|
||||
// func0 code
|
||||
memcpy(mem.data() + 0x1000, "\x55\x48\x89\xe5\x48\x83\xec\x20\xc3", 9);
|
||||
// func1 code
|
||||
memcpy(mem.data() + 0x1800, "\x31\xc0\xc3", 3);
|
||||
|
||||
// This provider represents the real process memory.
|
||||
// In production, this is the ProcessMemoryProvider that reads via
|
||||
// ReadProcessMemory at m_base + addr.
|
||||
BufferProvider realProv(mem);
|
||||
|
||||
// Build a snapshot that only contains tree-data pages (like the
|
||||
@@ -392,7 +387,7 @@ private slots:
|
||||
|
||||
// Build node tree
|
||||
NodeTree tree;
|
||||
tree.baseAddress = kBase;
|
||||
tree.baseAddress = 0;
|
||||
|
||||
Node root; root.kind = NodeKind::Struct; root.name = "Obj";
|
||||
root.parentId = 0; root.offset = 0;
|
||||
@@ -423,11 +418,11 @@ private slots:
|
||||
const LineMeta& lm = result.meta[i];
|
||||
if (lm.nodeKind != NodeKind::FuncPtr64 || lm.lineKind != LineKind::Field)
|
||||
continue;
|
||||
if (lm.offsetAddr < kBase + 0x100 || lm.offsetAddr >= kBase + 0x200)
|
||||
if (lm.offsetAddr < 0x100 || lm.offsetAddr >= 0x200)
|
||||
continue; // skip standalone VTable definition entries
|
||||
|
||||
// --- Hover step 1: read pointer value from snapshot ---
|
||||
uint64_t provAddr = lm.offsetAddr - tree.baseAddress;
|
||||
uint64_t provAddr = lm.offsetAddr;
|
||||
// The snapshot has this data (vtable pages are in it)
|
||||
QVERIFY2(snapProv.isReadable(provAddr, 8),
|
||||
qPrintable(QString("Snapshot should have vtable page at %1")
|
||||
@@ -437,7 +432,7 @@ private slots:
|
||||
|
||||
// --- Hover step 2: read code from REAL provider ---
|
||||
// The snapshot does NOT have the code pages:
|
||||
uint64_t codeAddr = ptrVal - tree.baseAddress;
|
||||
uint64_t codeAddr = ptrVal;
|
||||
QVERIFY2(!snapProv.isReadable(codeAddr, 1),
|
||||
"Snapshot should NOT have function code pages");
|
||||
// But the real provider does:
|
||||
|
||||
Reference in New Issue
Block a user