mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
WinDbg plugin: load dbgeng.dll dynamically from Debugging Tools directory instead of static linking (system dbgeng.dll lacks remote DebugConnect). Copy tools dbghelp.dll next to exe so it loads before System32 version. Add COM init on DbgEng thread, browse for tools dir, styled dialog. Editor: derive darker background via theme.background.darker(115) for visual depth between chrome and editor surfaces. UI: global scrollbar styling, workspace accent bar 1px, pane tab font from editor settings, workspace dock default width 128px.
813 lines
26 KiB
C++
813 lines
26 KiB
C++
#include <QTest>
|
|
#include <QSignalSpy>
|
|
#include <QByteArray>
|
|
#include <QProcess>
|
|
#include <QThread>
|
|
#include <QtConcurrent>
|
|
#include <QFuture>
|
|
#include <cstring>
|
|
#include <atomic>
|
|
#include <thread>
|
|
#include <chrono>
|
|
|
|
#include "providers/provider.h"
|
|
#include "scanner.h"
|
|
#include "../plugins/WinDbgMemory/WinDbgMemoryPlugin.h"
|
|
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#include <tlhelp32.h>
|
|
#include <initguid.h>
|
|
#include <dbgeng.h>
|
|
#endif
|
|
|
|
using namespace rcx;
|
|
|
|
static const char* CDB_PATH = "C:\\Program Files (x86)\\Windows Kits\\10\\Debuggers\\x64\\cdb.exe";
|
|
static const int DBG_PORT = 5056;
|
|
|
|
class TestWinDbgProvider : public QObject {
|
|
Q_OBJECT
|
|
|
|
private:
|
|
QProcess* m_cdbProcess = nullptr;
|
|
uint32_t m_notepadPid = 0;
|
|
bool m_weSpawnedNotepad = false;
|
|
QString m_connString;
|
|
|
|
static uint32_t findProcess(const wchar_t* name)
|
|
{
|
|
#ifdef _WIN32
|
|
HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
|
if (snap == INVALID_HANDLE_VALUE) return 0;
|
|
PROCESSENTRY32W entry;
|
|
entry.dwSize = sizeof(entry);
|
|
uint32_t pid = 0;
|
|
if (Process32FirstW(snap, &entry)) {
|
|
do {
|
|
if (_wcsicmp(entry.szExeFile, name) == 0) {
|
|
pid = entry.th32ProcessID;
|
|
break;
|
|
}
|
|
} while (Process32NextW(snap, &entry));
|
|
}
|
|
CloseHandle(snap);
|
|
return pid;
|
|
#else
|
|
Q_UNUSED(name); return 0;
|
|
#endif
|
|
}
|
|
|
|
static uint32_t launchNotepad()
|
|
{
|
|
#ifdef _WIN32
|
|
STARTUPINFOW si{};
|
|
si.cb = sizeof(si);
|
|
PROCESS_INFORMATION pi{};
|
|
if (CreateProcessW(L"C:\\Windows\\notepad.exe", nullptr, nullptr, nullptr,
|
|
FALSE, 0, nullptr, nullptr, &si, &pi)) {
|
|
WaitForInputIdle(pi.hProcess, 3000);
|
|
CloseHandle(pi.hThread);
|
|
CloseHandle(pi.hProcess);
|
|
return pi.dwProcessId;
|
|
}
|
|
return 0;
|
|
#else
|
|
return 0;
|
|
#endif
|
|
}
|
|
|
|
static void terminateProcess(uint32_t pid)
|
|
{
|
|
#ifdef _WIN32
|
|
HANDLE h = OpenProcess(PROCESS_TERMINATE, FALSE, pid);
|
|
if (h) { TerminateProcess(h, 0); CloseHandle(h); }
|
|
#else
|
|
Q_UNUSED(pid);
|
|
#endif
|
|
}
|
|
|
|
private slots:
|
|
|
|
// ── Fixture ──
|
|
|
|
/// Try a quick DebugConnect to see if the port is already serving.
|
|
/// Runs in a detached thread with a timeout because DebugConnect can
|
|
/// hang indefinitely with WinDbg Preview servers.
|
|
static bool canConnect(const QString& connStr, int timeoutMs = 8000)
|
|
{
|
|
#ifdef _WIN32
|
|
QByteArray utf8 = connStr.toUtf8();
|
|
std::atomic<int> state{0}; // 0=pending, 1=connected, -1=failed
|
|
std::thread t([&state, utf8]() {
|
|
CoInitializeEx(NULL, COINIT_MULTITHREADED);
|
|
IDebugClient* probe = nullptr;
|
|
HRESULT hr = DebugConnect(utf8.constData(), IID_IDebugClient, (void**)&probe);
|
|
if (SUCCEEDED(hr) && probe) {
|
|
probe->EndSession(DEBUG_END_DISCONNECT);
|
|
probe->Release();
|
|
state.store(1);
|
|
} else {
|
|
state.store(-1);
|
|
}
|
|
CoUninitialize();
|
|
});
|
|
t.detach(); // Don't block on join — DebugConnect may hang forever
|
|
|
|
auto deadline = std::chrono::steady_clock::now()
|
|
+ std::chrono::milliseconds(timeoutMs);
|
|
while (state.load() == 0) {
|
|
if (std::chrono::steady_clock::now() >= deadline) {
|
|
qDebug() << "canConnect: DebugConnect timed out after" << timeoutMs << "ms";
|
|
return false;
|
|
}
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
}
|
|
return state.load() == 1;
|
|
#else
|
|
Q_UNUSED(connStr); Q_UNUSED(timeoutMs);
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void initTestCase()
|
|
{
|
|
m_connString = QString("tcp:Port=%1,Server=127.0.0.1").arg(DBG_PORT);
|
|
|
|
// If a debug server is already listening (e.g. WinDbg with .server),
|
|
// skip launching our own cdb.exe.
|
|
if (canConnect(m_connString)) {
|
|
qDebug() << "Debug server already running on port" << DBG_PORT << "— using it";
|
|
return;
|
|
}
|
|
|
|
// No server running — try to launch cdb ourselves.
|
|
// If cdb isn't available, user-mode tests will be skipped but
|
|
// kernel/dump tests can still run via WINDBG_KERNEL_CONN.
|
|
m_notepadPid = findProcess(L"notepad.exe");
|
|
if (m_notepadPid == 0) {
|
|
m_notepadPid = launchNotepad();
|
|
m_weSpawnedNotepad = true;
|
|
}
|
|
if (m_notepadPid == 0) {
|
|
qDebug() << "No notepad.exe and could not launch — user-mode tests will skip";
|
|
return;
|
|
}
|
|
qDebug() << "Using notepad.exe PID:" << m_notepadPid;
|
|
|
|
m_cdbProcess = new QProcess(this);
|
|
QStringList args;
|
|
args << "-server" << QString("tcp:port=%1").arg(DBG_PORT)
|
|
<< "-pv"
|
|
<< "-p" << QString::number(m_notepadPid);
|
|
|
|
m_cdbProcess->setProgram(CDB_PATH);
|
|
m_cdbProcess->setArguments(args);
|
|
m_cdbProcess->start();
|
|
|
|
if (!m_cdbProcess->waitForStarted(5000)) {
|
|
qDebug() << "Failed to start cdb.exe — user-mode tests will skip";
|
|
delete m_cdbProcess;
|
|
m_cdbProcess = nullptr;
|
|
return;
|
|
}
|
|
QThread::sleep(3);
|
|
|
|
qDebug() << "cdb.exe debug server started on port" << DBG_PORT;
|
|
}
|
|
|
|
void cleanupTestCase()
|
|
{
|
|
if (m_cdbProcess) {
|
|
m_cdbProcess->write("q\n");
|
|
if (!m_cdbProcess->waitForFinished(5000))
|
|
m_cdbProcess->kill();
|
|
delete m_cdbProcess;
|
|
m_cdbProcess = nullptr;
|
|
}
|
|
|
|
if (m_weSpawnedNotepad && m_notepadPid)
|
|
terminateProcess(m_notepadPid);
|
|
}
|
|
|
|
// ── Plugin metadata ──
|
|
|
|
void plugin_name()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QCOMPARE(plugin.Name(), std::string("WinDbg Memory"));
|
|
}
|
|
|
|
void plugin_version()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QCOMPARE(plugin.Version(), std::string("2.0.0"));
|
|
}
|
|
|
|
void plugin_canHandle_tcp()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QVERIFY(plugin.canHandle("tcp:Port=5056,Server=localhost"));
|
|
QVERIFY(plugin.canHandle("TCP:Port=1234,Server=10.0.0.1"));
|
|
}
|
|
|
|
void plugin_canHandle_npipe()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QVERIFY(plugin.canHandle("npipe:Pipe=test,Server=localhost"));
|
|
}
|
|
|
|
void plugin_canHandle_pid()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QVERIFY(plugin.canHandle("pid:1234"));
|
|
}
|
|
|
|
void plugin_canHandle_dump()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QVERIFY(plugin.canHandle("dump:C:/test.dmp"));
|
|
}
|
|
|
|
void plugin_canHandle_invalid()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QVERIFY(!plugin.canHandle(""));
|
|
QVERIFY(!plugin.canHandle("1234"));
|
|
QVERIFY(!plugin.canHandle("file:///test.bin"));
|
|
}
|
|
|
|
// ── Connection failure ──
|
|
|
|
void provider_connect_badPort()
|
|
{
|
|
WinDbgMemoryProvider prov("tcp:Port=59999,Server=localhost");
|
|
QVERIFY(!prov.isValid());
|
|
QCOMPARE(prov.size(), 0);
|
|
}
|
|
|
|
void provider_connect_badPipe()
|
|
{
|
|
WinDbgMemoryProvider prov("npipe:Pipe=nonexistent_reclass_test_pipe,Server=localhost");
|
|
QVERIFY(!prov.isValid());
|
|
QCOMPARE(prov.size(), 0);
|
|
}
|
|
|
|
void plugin_createProvider_badConnection()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QString error;
|
|
auto prov = plugin.createProvider("tcp:Port=59999,Server=localhost", &error);
|
|
QVERIFY(prov == nullptr);
|
|
QVERIFY(!error.isEmpty());
|
|
}
|
|
|
|
// ── Connect and read (main thread) ──
|
|
|
|
void provider_connect_valid()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY2(prov.isValid(), "Should connect to cdb debug server");
|
|
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
|
QVERIFY(prov.size() > 0);
|
|
}
|
|
|
|
void provider_name()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
QVERIFY(!prov.name().isEmpty());
|
|
qDebug() << "Provider name:" << prov.name();
|
|
}
|
|
|
|
void provider_isLive()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
QVERIFY(prov.isLive());
|
|
}
|
|
|
|
void provider_baseAddress()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
// WinDbg provider no longer auto-selects a module base — it returns 0
|
|
// so the controller doesn't override the user's chosen base address.
|
|
QCOMPARE(prov.base(), (uint64_t)0);
|
|
}
|
|
|
|
// ── Read: MZ header on main thread ──
|
|
|
|
void provider_read_mz_mainThread()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
uint8_t buf[2] = {};
|
|
bool ok = prov.read(0, buf, 2);
|
|
QVERIFY2(ok, "Failed to read from debug session (main thread)");
|
|
QCOMPARE(buf[0], (uint8_t)'M');
|
|
QCOMPARE(buf[1], (uint8_t)'Z');
|
|
}
|
|
|
|
// ── Read: MZ header from a background thread (the actual failure case) ──
|
|
|
|
void provider_read_mz_backgroundThread()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
// Simulate what the controller's refresh does:
|
|
// read from a QtConcurrent worker thread.
|
|
QFuture<QByteArray> future = QtConcurrent::run([&prov]() -> QByteArray {
|
|
return prov.readBytes(0, 128);
|
|
});
|
|
future.waitForFinished();
|
|
QByteArray data = future.result();
|
|
|
|
QCOMPARE(data.size(), 128);
|
|
QCOMPARE((uint8_t)data[0], (uint8_t)'M');
|
|
QCOMPARE((uint8_t)data[1], (uint8_t)'Z');
|
|
}
|
|
|
|
// ── Read: bulk data from background thread ──
|
|
|
|
void provider_read_4k_backgroundThread()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
QFuture<QByteArray> future = QtConcurrent::run([&prov]() -> QByteArray {
|
|
return prov.readBytes(0, 4096);
|
|
});
|
|
future.waitForFinished();
|
|
QByteArray data = future.result();
|
|
|
|
QCOMPARE(data.size(), 4096);
|
|
QCOMPARE((uint8_t)data[0], (uint8_t)'M');
|
|
QCOMPARE((uint8_t)data[1], (uint8_t)'Z');
|
|
|
|
// Verify it's not all zeros (the old failure mode)
|
|
bool allZero = true;
|
|
for (int i = 0; i < data.size(); ++i) {
|
|
if (data[i] != '\0') { allZero = false; break; }
|
|
}
|
|
QVERIFY2(!allZero, "Data is all zeros — background thread read failed");
|
|
}
|
|
|
|
// ── Multiple sequential background reads (simulates refresh timer) ──
|
|
|
|
void provider_read_multipleRefreshes()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
for (int i = 0; i < 5; ++i) {
|
|
QFuture<QByteArray> future = QtConcurrent::run([&prov]() -> QByteArray {
|
|
return prov.readBytes(0, 128);
|
|
});
|
|
future.waitForFinished();
|
|
QByteArray data = future.result();
|
|
QCOMPARE(data.size(), 128);
|
|
QCOMPARE((uint8_t)data[0], (uint8_t)'M');
|
|
QCOMPARE((uint8_t)data[1], (uint8_t)'Z');
|
|
}
|
|
}
|
|
|
|
// ── Read helpers ──
|
|
|
|
void provider_readU16()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
QCOMPARE(prov.readU16(0), (uint16_t)0x5A4D); // "MZ" little-endian
|
|
}
|
|
|
|
void provider_read_peSignature()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
uint32_t peOffset = prov.readU32(0x3C);
|
|
QVERIFY2(peOffset > 0 && peOffset < 0x1000, "PE offset should be reasonable");
|
|
|
|
uint8_t sig[4] = {};
|
|
bool ok = prov.read(peOffset, sig, 4);
|
|
QVERIFY(ok);
|
|
QCOMPARE(sig[0], (uint8_t)'P');
|
|
QCOMPARE(sig[1], (uint8_t)'E');
|
|
QCOMPARE(sig[2], (uint8_t)0);
|
|
QCOMPARE(sig[3], (uint8_t)0);
|
|
}
|
|
|
|
// ── Edge cases ──
|
|
|
|
void provider_read_zeroLength()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
uint8_t buf = 0xFF;
|
|
QVERIFY(!prov.read(0, &buf, 0));
|
|
}
|
|
|
|
void provider_read_negativeLength()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
uint8_t buf = 0xFF;
|
|
QVERIFY(!prov.read(0, &buf, -1));
|
|
}
|
|
|
|
// ── getSymbol ──
|
|
|
|
void provider_getSymbol()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
QString sym = prov.getSymbol(0);
|
|
qDebug() << "Symbol at base+0:" << sym;
|
|
// Should not crash; may or may not resolve
|
|
}
|
|
|
|
void provider_getSymbol_backgroundThread()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
QFuture<QString> future = QtConcurrent::run([&prov]() -> QString {
|
|
return prov.getSymbol(0);
|
|
});
|
|
future.waitForFinished();
|
|
// Should not crash from background thread
|
|
qDebug() << "Symbol (bg thread):" << future.result();
|
|
}
|
|
|
|
// ── createProvider full flow ──
|
|
|
|
void plugin_createProvider_valid()
|
|
{
|
|
WinDbgMemoryPlugin plugin;
|
|
QString error;
|
|
auto prov = plugin.createProvider(m_connString, &error);
|
|
QVERIFY2(prov != nullptr, qPrintable("createProvider failed: " + error));
|
|
QVERIFY(prov->isValid());
|
|
|
|
uint8_t mz[2] = {};
|
|
QVERIFY(prov->read(0, mz, 2));
|
|
QCOMPARE(mz[0], (uint8_t)'M');
|
|
QCOMPARE(mz[1], (uint8_t)'Z');
|
|
}
|
|
|
|
// ── Multiple concurrent connections ──
|
|
|
|
void provider_multipleConcurrent()
|
|
{
|
|
WinDbgMemoryProvider prov1(m_connString);
|
|
WinDbgMemoryProvider prov2(m_connString);
|
|
|
|
QVERIFY(prov1.isValid());
|
|
QVERIFY(prov2.isValid());
|
|
|
|
QCOMPARE(prov1.readU16(0), (uint16_t)0x5A4D);
|
|
QCOMPARE(prov2.readU16(0), (uint16_t)0x5A4D);
|
|
}
|
|
|
|
// ── Factory ──
|
|
|
|
void factory_createPlugin()
|
|
{
|
|
IPlugin* raw = CreatePlugin();
|
|
QVERIFY(raw != nullptr);
|
|
QCOMPARE(raw->Type(), IPlugin::ProviderPlugin);
|
|
QCOMPARE(raw->Name(), std::string("WinDbg Memory"));
|
|
delete raw;
|
|
}
|
|
|
|
// ── enumerateRegions ──
|
|
|
|
void provider_enumerateRegions()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
auto regions = prov.enumerateRegions();
|
|
qDebug() << "enumerateRegions returned" << regions.size() << "regions";
|
|
QVERIFY2(!regions.isEmpty(), "Should return at least one memory region");
|
|
|
|
// Every region should have sane values
|
|
for (const auto& r : regions) {
|
|
QVERIFY(r.size > 0);
|
|
QVERIFY(r.readable);
|
|
}
|
|
}
|
|
|
|
void provider_enumerateRegions_hasModuleNames()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
auto regions = prov.enumerateRegions();
|
|
QVERIFY(!regions.isEmpty());
|
|
|
|
// At least one region should have a module name
|
|
bool hasModule = false;
|
|
for (const auto& r : regions) {
|
|
if (!r.moduleName.isEmpty()) {
|
|
hasModule = true;
|
|
qDebug() << "Region base=0x" + QString::number(r.base, 16)
|
|
<< "size=" << r.size
|
|
<< "module=" << r.moduleName
|
|
<< "r/w/x:" << r.readable << r.writable << r.executable;
|
|
break;
|
|
}
|
|
}
|
|
QVERIFY2(hasModule, "At least one region should have a module name");
|
|
}
|
|
|
|
void provider_enumerateRegions_hasExecutable()
|
|
{
|
|
WinDbgMemoryProvider prov(m_connString);
|
|
QVERIFY(prov.isValid());
|
|
|
|
auto regions = prov.enumerateRegions();
|
|
QVERIFY(!regions.isEmpty());
|
|
|
|
bool hasExec = false;
|
|
for (const auto& r : regions) {
|
|
if (r.executable) { hasExec = true; break; }
|
|
}
|
|
QVERIFY2(hasExec, "Should have at least one executable region (code)");
|
|
}
|
|
|
|
// ── Scanner integration ──
|
|
|
|
void scanner_signature_mz()
|
|
{
|
|
// Scan for the MZ header — should find at least one match
|
|
auto prov = std::make_shared<WinDbgMemoryProvider>(m_connString);
|
|
QVERIFY(prov->isValid());
|
|
|
|
auto regions = prov->enumerateRegions();
|
|
QVERIFY2(!regions.isEmpty(), "Need regions for scan");
|
|
|
|
rcx::ScanRequest req;
|
|
QString err;
|
|
QVERIFY(rcx::parseSignature("4D 5A", req.pattern, req.mask, &err));
|
|
req.alignment = 1;
|
|
req.maxResults = 100;
|
|
|
|
rcx::ScanEngine engine;
|
|
QSignalSpy spy(&engine, &rcx::ScanEngine::finished);
|
|
|
|
engine.start(prov, req);
|
|
QVERIFY(spy.wait(30000));
|
|
|
|
auto results = spy.at(0).at(0).value<QVector<rcx::ScanResult>>();
|
|
qDebug() << "MZ scan found" << results.size() << "results";
|
|
QVERIFY2(!results.isEmpty(), "Should find at least one MZ header");
|
|
|
|
// Verify the first result is actually 'MZ'
|
|
uint8_t buf[2] = {};
|
|
prov->read(results[0].address, buf, 2);
|
|
QCOMPARE(buf[0], (uint8_t)'M');
|
|
QCOMPARE(buf[1], (uint8_t)'Z');
|
|
}
|
|
|
|
void scanner_value_int32()
|
|
{
|
|
// Read a known 4-byte value from offset 0x3C (PE offset) then scan for it.
|
|
// This only works for user-mode targets where address 0 is the main module.
|
|
auto prov = std::make_shared<WinDbgMemoryProvider>(m_connString);
|
|
QVERIFY(prov->isValid());
|
|
|
|
auto regions = prov->enumerateRegions();
|
|
QVERIFY2(!regions.isEmpty(), "Need regions for scan");
|
|
|
|
uint32_t peOffset = prov->readU32(0x3C);
|
|
if (peOffset == 0 || peOffset >= 0x1000)
|
|
QSKIP("Address 0 not readable (kernel session) — value scan test requires user-mode target");
|
|
|
|
rcx::ScanRequest req;
|
|
QString err;
|
|
QVERIFY(rcx::serializeValue(rcx::ValueType::UInt32,
|
|
QString::number(peOffset), req.pattern, req.mask, &err));
|
|
req.alignment = 4;
|
|
req.maxResults = 100;
|
|
|
|
rcx::ScanEngine engine;
|
|
QSignalSpy spy(&engine, &rcx::ScanEngine::finished);
|
|
|
|
engine.start(prov, req);
|
|
QVERIFY(spy.wait(30000));
|
|
|
|
auto results = spy.at(0).at(0).value<QVector<rcx::ScanResult>>();
|
|
qDebug() << "Value scan for" << peOffset << "found" << results.size() << "results";
|
|
QVERIFY2(!results.isEmpty(), "Should find the PE offset value somewhere");
|
|
}
|
|
|
|
// ── Kernel/dump session tests ──
|
|
// Set WINDBG_KERNEL_CONN to a target string:
|
|
// "dump:F:/path/to/file.dmp" — open dump directly
|
|
// "tcp:Port=5056,Server=localhost" — connect to debug server
|
|
// Set WINDBG_KERNEL_ADDR to a readable hex address (e.g. kernel base).
|
|
|
|
static QString kernelTarget()
|
|
{
|
|
return qEnvironmentVariable("WINDBG_KERNEL_CONN", "");
|
|
}
|
|
|
|
void provider_kernel_connect()
|
|
{
|
|
QString target = kernelTarget();
|
|
if (target.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_CONN (e.g. dump:F:/file.dmp)");
|
|
|
|
WinDbgMemoryProvider prov(target);
|
|
QVERIFY2(prov.isValid(),
|
|
qPrintable("Should connect to " + target));
|
|
QCOMPARE(prov.kind(), QStringLiteral("WinDbg"));
|
|
|
|
qDebug() << "Kernel provider name:" << prov.name();
|
|
qDebug() << "Kernel provider base:" << QString("0x%1").arg(prov.base(), 0, 16);
|
|
qDebug() << "Kernel provider isLive:" << prov.isLive();
|
|
}
|
|
|
|
void provider_kernel_read_base()
|
|
{
|
|
QString target = kernelTarget();
|
|
if (target.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_CONN");
|
|
|
|
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
|
if (addrStr.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
|
|
|
WinDbgMemoryProvider prov(target);
|
|
QVERIFY2(prov.isValid(),
|
|
qPrintable("Failed to connect to " + target));
|
|
|
|
bool ok = false;
|
|
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
|
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
|
|
|
uint8_t buf[16] = {};
|
|
ok = prov.read(addr, buf, 16);
|
|
QVERIFY2(ok, "Should read from kernel address");
|
|
|
|
bool allZero = true;
|
|
for (int i = 0; i < 16; ++i) {
|
|
if (buf[i] != 0) { allZero = false; break; }
|
|
}
|
|
QVERIFY2(!allZero, "Kernel read returned all zeros");
|
|
|
|
qDebug() << "Read 16 bytes at" << QString("0x%1").arg(addr, 0, 16)
|
|
<< "first 4:" << QString("%1 %2 %3 %4")
|
|
.arg(buf[0], 2, 16, QChar('0'))
|
|
.arg(buf[1], 2, 16, QChar('0'))
|
|
.arg(buf[2], 2, 16, QChar('0'))
|
|
.arg(buf[3], 2, 16, QChar('0'));
|
|
}
|
|
|
|
void provider_kernel_read_high_address()
|
|
{
|
|
QString target = kernelTarget();
|
|
if (target.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_CONN");
|
|
|
|
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
|
uint64_t addr = 0;
|
|
if (!addrStr.isEmpty()) {
|
|
bool ok = false;
|
|
addr = addrStr.toULongLong(&ok, 16);
|
|
if (!ok) addr = 0;
|
|
}
|
|
|
|
WinDbgMemoryProvider prov(target);
|
|
QVERIFY2(prov.isValid(),
|
|
qPrintable("Failed to connect to " + target));
|
|
|
|
if (addr == 0) addr = prov.base();
|
|
if (addr == 0)
|
|
QSKIP("No kernel address available (set WINDBG_KERNEL_ADDR)");
|
|
|
|
uint8_t buf[64] = {};
|
|
bool ok = prov.read(addr, buf, 64);
|
|
QVERIFY2(ok, qPrintable(QString("Should read kernel addr 0x%1")
|
|
.arg(addr, 0, 16)));
|
|
|
|
bool allZero = true;
|
|
for (int i = 0; i < 64; ++i) {
|
|
if (buf[i] != 0) { allZero = false; break; }
|
|
}
|
|
QVERIFY2(!allZero, "Kernel high-address read returned all zeros");
|
|
|
|
qDebug() << "Read 64 bytes at" << QString("0x%1").arg(addr, 0, 16)
|
|
<< "first 8:" << QString("%1 %2 %3 %4 %5 %6 %7 %8")
|
|
.arg(buf[0], 2, 16, QChar('0'))
|
|
.arg(buf[1], 2, 16, QChar('0'))
|
|
.arg(buf[2], 2, 16, QChar('0'))
|
|
.arg(buf[3], 2, 16, QChar('0'))
|
|
.arg(buf[4], 2, 16, QChar('0'))
|
|
.arg(buf[5], 2, 16, QChar('0'))
|
|
.arg(buf[6], 2, 16, QChar('0'))
|
|
.arg(buf[7], 2, 16, QChar('0'));
|
|
}
|
|
|
|
void provider_kernel_enumerateRegions()
|
|
{
|
|
QString target = kernelTarget();
|
|
if (target.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_CONN");
|
|
|
|
WinDbgMemoryProvider prov(target);
|
|
QVERIFY2(prov.isValid(),
|
|
qPrintable("Failed to connect to " + target));
|
|
|
|
auto regions = prov.enumerateRegions();
|
|
qDebug() << "Kernel enumerateRegions returned" << regions.size() << "regions";
|
|
QVERIFY2(!regions.isEmpty(), "Should return kernel memory regions");
|
|
|
|
// Log first few regions
|
|
int logged = 0;
|
|
for (const auto& r : regions) {
|
|
if (logged++ >= 10) break;
|
|
qDebug() << " base=0x" + QString::number(r.base, 16)
|
|
<< "size=" << r.size
|
|
<< "module=" << r.moduleName
|
|
<< "r/w/x:" << r.readable << r.writable << r.executable;
|
|
}
|
|
}
|
|
|
|
void provider_kernel_scan_signature()
|
|
{
|
|
QString target = kernelTarget();
|
|
if (target.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_CONN");
|
|
|
|
auto prov = std::make_shared<WinDbgMemoryProvider>(target);
|
|
QVERIFY2(prov->isValid(),
|
|
qPrintable("Failed to connect to " + target));
|
|
|
|
auto regions = prov->enumerateRegions();
|
|
if (regions.isEmpty())
|
|
QSKIP("No regions enumerated — QueryVirtual may not be supported for this target");
|
|
|
|
// Scan for MZ header in executable regions
|
|
rcx::ScanRequest req;
|
|
QString err;
|
|
QVERIFY(rcx::parseSignature("4D 5A 90 00", req.pattern, req.mask, &err));
|
|
req.alignment = 1;
|
|
req.filterExecutable = true;
|
|
req.maxResults = 50;
|
|
|
|
rcx::ScanEngine engine;
|
|
QSignalSpy spy(&engine, &rcx::ScanEngine::finished);
|
|
|
|
engine.start(prov, req);
|
|
QVERIFY(spy.wait(60000));
|
|
|
|
auto results = spy.at(0).at(0).value<QVector<rcx::ScanResult>>();
|
|
qDebug() << "Kernel MZ scan (exec only) found" << results.size() << "results";
|
|
for (const auto& r : results)
|
|
qDebug() << " 0x" + QString::number(r.address, 16) << r.regionModule;
|
|
|
|
QVERIFY2(!results.isEmpty(), "Should find MZ headers in kernel modules");
|
|
}
|
|
|
|
void provider_kernel_read_backgroundThread()
|
|
{
|
|
QString target = kernelTarget();
|
|
if (target.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_CONN");
|
|
|
|
QString addrStr = qEnvironmentVariable("WINDBG_KERNEL_ADDR", "");
|
|
if (addrStr.isEmpty())
|
|
QSKIP("Set WINDBG_KERNEL_ADDR to a readable kernel address");
|
|
|
|
bool ok = false;
|
|
uint64_t addr = addrStr.toULongLong(&ok, 16);
|
|
QVERIFY2(ok && addr != 0, "WINDBG_KERNEL_ADDR must be a valid hex address");
|
|
|
|
WinDbgMemoryProvider prov(target);
|
|
QVERIFY2(prov.isValid(),
|
|
qPrintable("Failed to connect to " + target));
|
|
|
|
// Simulate the controller's async refresh pattern
|
|
QFuture<QByteArray> future = QtConcurrent::run([&prov, addr]() -> QByteArray {
|
|
return prov.readBytes(addr, 4096);
|
|
});
|
|
future.waitForFinished();
|
|
QByteArray data = future.result();
|
|
|
|
QCOMPARE(data.size(), 4096);
|
|
bool allZero = true;
|
|
for (int i = 0; i < data.size(); ++i) {
|
|
if (data[i] != '\0') { allZero = false; break; }
|
|
}
|
|
QVERIFY2(!allZero, "Kernel background read returned all zeros");
|
|
}
|
|
};
|
|
|
|
QTEST_MAIN(TestWinDbgProvider)
|
|
#include "test_windbg_provider.moc"
|