diff --git a/CMakeLists.txt b/CMakeLists.txt index 8078632..006d6cb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -397,6 +397,7 @@ if(BUILD_TESTING) endif() # BUILD_UI_TESTS endif() add_subdirectory(plugins/ProcessMemory) +add_subdirectory(plugins/RemoteProcessMemory) if(WIN32) add_subdirectory(plugins/WinDbgMemory) add_subdirectory(plugins/RcNetPluginCompatLayer) diff --git a/plugins/RemoteProcessMemory/CMakeLists.txt b/plugins/RemoteProcessMemory/CMakeLists.txt new file mode 100644 index 0000000..b563d57 --- /dev/null +++ b/plugins/RemoteProcessMemory/CMakeLists.txt @@ -0,0 +1,124 @@ +cmake_minimum_required(VERSION 3.20) +project(RemoteProcessMemory LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC OFF) # run uic manually to avoid dupbuild with ProcessMemoryPlugin + +# ─── 1. Payload DLL/SO (no Qt, minimal dependencies) ──────────────── + +add_library(rcx_payload SHARED + payload/rcx_payload.cpp + rcx_rpc_protocol.h +) + +set_target_properties(rcx_payload PROPERTIES PREFIX "") # rcx_payload.dll / rcx_payload.so + +target_include_directories(rcx_payload PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +if(WIN32) + target_link_libraries(rcx_payload PRIVATE psapi) +else() + target_link_libraries(rcx_payload PRIVATE pthread rt) + target_compile_options(rcx_payload PRIVATE -fvisibility=hidden) +endif() + +# Output payload to Plugins/ (same dir as plugin DLL, discovered at runtime) +set_target_properties(rcx_payload PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) + +# Install rule: copy both DLLs to install Plugins/ folder +install(TARGETS rcx_payload + LIBRARY DESTINATION Plugins + RUNTIME DESTINATION Plugins +) + +# ─── 2. Plugin DLL (Qt, implements IProviderPlugin) ────────────────── + +# Generate ui_processpicker.h in our own build dir (avoids dupbuild with ProcessMemoryPlugin) +set(_UI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui") +set(_UI_HDR "${CMAKE_CURRENT_BINARY_DIR}/ui_processpicker.h") + +add_custom_command( + OUTPUT "${_UI_HDR}" + COMMAND ${QT}::uic -o "${_UI_HDR}" "${_UI_SRC}" + DEPENDS "${_UI_SRC}" + COMMENT "UIC processpicker.ui (RemoteProcessMemory)" + VERBATIM +) + +set(PLUGIN_SOURCES + RemoteProcessMemoryPlugin.h + RemoteProcessMemoryPlugin.cpp + rcx_rpc_protocol.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp + "${_UI_HDR}" +) + +add_library(RemoteProcessMemoryPlugin SHARED ${PLUGIN_SOURCES}) + +target_link_libraries(RemoteProcessMemoryPlugin PRIVATE + ${QT}::Widgets + ${_QT_WINEXTRAS} +) + +if(WIN32) + target_link_libraries(RemoteProcessMemoryPlugin PRIVATE psapi shell32) +else() + target_link_libraries(RemoteProcessMemoryPlugin PRIVATE rt dl) + target_compile_options(RemoteProcessMemoryPlugin PRIVATE -fvisibility=hidden) +endif() + +target_include_directories(RemoteProcessMemoryPlugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR} # for ui_processpicker.h +) + +set_target_properties(RemoteProcessMemoryPlugin PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) + +install(TARGETS RemoteProcessMemoryPlugin + LIBRARY DESTINATION Plugins + RUNTIME DESTINATION Plugins +) + +# Plugin must be able to find the payload at runtime +add_dependencies(RemoteProcessMemoryPlugin rcx_payload) + +# ─── 3. Test executables (no Qt) ──────────────────────────────────── + +# Host: loads payload in-process, exposes test buffer +add_executable(test_rpc_host tests/test_rpc_host.cpp) +target_include_directories(test_rpc_host PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +if(WIN32) + target_link_libraries(test_rpc_host PRIVATE psapi) +else() + target_link_libraries(test_rpc_host PRIVATE pthread rt dl) +endif() +set_target_properties(test_rpc_host PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) +add_dependencies(test_rpc_host rcx_payload) + +# Client: connects to host, tests + benchmarks +add_executable(test_rpc_client tests/test_rpc_client.cpp) +target_include_directories(test_rpc_client PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) +if(WIN32) + target_link_libraries(test_rpc_client PRIVATE psapi) +else() + target_link_libraries(test_rpc_client PRIVATE pthread rt) +endif() +set_target_properties(test_rpc_client PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) +add_dependencies(test_rpc_client test_rpc_host) diff --git a/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp new file mode 100644 index 0000000..7701ebb --- /dev/null +++ b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.cpp @@ -0,0 +1,931 @@ +#include "RemoteProcessMemoryPlugin.h" +#include "rcx_rpc_protocol.h" +#include "../../src/processpicker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32) +#include +#endif + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +# include +# include +# include +#else +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +# include +#endif + +/* ══════════════════════════════════════════════════════════════════════ + * IPC Client + * ══════════════════════════════════════════════════════════════════════ */ + +struct IpcClient { +#ifdef _WIN32 + HANDLE hShm = nullptr; + HANDLE hReqEvent = nullptr; + HANDLE hRspEvent = nullptr; +#else + int shmFd = -1; + sem_t* reqSem = SEM_FAILED; + sem_t* rspSem = SEM_FAILED; + char shmNameBuf[128] = {}; + char reqNameBuf[128] = {}; + char rspNameBuf[128] = {}; +#endif + void* mappedView = nullptr; + QMutex mutex; + bool connected = false; + + ~IpcClient() { disconnect(); } + + /* ── connect / disconnect ──────────────────────────────────────── */ + + bool connect(uint32_t pid, const QByteArray& nonce, int timeoutMs = 5000) + { + char shmName[128], reqName[128], rspName[128]; + rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce.constData()); + rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce.constData()); + rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce.constData()); + +#ifdef _WIN32 + /* poll for shared memory to appear (payload creating it) */ + auto deadline = GetTickCount64() + (uint64_t)timeoutMs; + while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) { + if (GetTickCount64() >= deadline) return false; + Sleep(10); + } + + mappedView = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE); + if (!mappedView) { CloseHandle(hShm); hShm = nullptr; return false; } + + hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName); + hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName); + if (!hReqEvent || !hRspEvent) { disconnect(); return false; } +#else + strncpy(shmNameBuf, shmName, sizeof(shmNameBuf) - 1); + strncpy(reqNameBuf, reqName, sizeof(reqNameBuf) - 1); + strncpy(rspNameBuf, rspName, sizeof(rspNameBuf) - 1); + + /* poll for shared memory */ + auto start = std::chrono::steady_clock::now(); + while (true) { + shmFd = shm_open(shmName, O_RDWR, 0); + if (shmFd >= 0) break; + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + if (elapsed >= timeoutMs) return false; + usleep(10000); + } + + mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, shmFd, 0); + if (mappedView == MAP_FAILED) { mappedView = nullptr; close(shmFd); shmFd = -1; return false; } + + reqSem = sem_open(reqName, 0); + rspSem = sem_open(rspName, 0); + if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) { disconnect(); return false; } +#endif + + /* wait for payloadReady */ + auto* hdr = static_cast(mappedView); +#ifdef _WIN32 + while (!hdr->payloadReady) { + if (GetTickCount64() >= deadline) { disconnect(); return false; } + Sleep(5); + } +#else + while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) { + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + if (elapsed >= timeoutMs) { disconnect(); return false; } + usleep(5000); + } +#endif + + connected = true; + return true; + } + + void disconnect() + { +#ifdef _WIN32 + if (mappedView) { UnmapViewOfFile(mappedView); mappedView = nullptr; } + if (hShm) { CloseHandle(hShm); hShm = nullptr; } + if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; } + if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; } +#else + if (mappedView) { munmap(mappedView, RCX_RPC_SHM_SIZE); mappedView = nullptr; } + if (shmFd >= 0) { close(shmFd); shmFd = -1; } + if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; } + if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; } +#endif + connected = false; + } + + /* ── low-level RPC round-trip ──────────────────────────────────── */ + + bool signalAndWait(int timeoutMs = 2000) + { +#ifdef _WIN32 + SetEvent(hReqEvent); + return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0; +#else + sem_post(reqSem); + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += timeoutMs / 1000; + ts.tv_nsec += (timeoutMs % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } + return sem_timedwait(rspSem, &ts) == 0; +#endif + } + + /* ── public API ────────────────────────────────────────────────── */ + + bool readSingle(uint64_t addr, void* buf, int len) + { + QMutexLocker lock(&mutex); + if (!connected || len <= 0) return false; + + auto* hdr = static_cast(mappedView); + auto* data = static_cast(mappedView) + RCX_RPC_DATA_OFFSET; + + hdr->command = RPC_CMD_READ_BATCH; + hdr->requestCount = 1; + hdr->status = RCX_RPC_STATUS_OK; + + auto* entry = reinterpret_cast(data); + entry->address = addr; + entry->length = (uint32_t)len; + entry->dataOffset = sizeof(RcxRpcReadEntry); + + if (!signalAndWait()) { connected = false; return false; } + + memcpy(buf, data + entry->dataOffset, len); + return true; + } + + bool writeSingle(uint64_t addr, const void* buf, int len) + { + QMutexLocker lock(&mutex); + if (!connected || len <= 0) return false; + + auto* hdr = static_cast(mappedView); + auto* data = static_cast(mappedView) + RCX_RPC_DATA_OFFSET; + + hdr->command = RPC_CMD_WRITE; + hdr->writeAddress = addr; + hdr->writeLength = (uint32_t)len; + hdr->status = RCX_RPC_STATUS_OK; + + memcpy(data, buf, len); + + if (!signalAndWait()) { connected = false; return false; } + + return hdr->status == RCX_RPC_STATUS_OK; + } + + QVector enumerateModules() + { + QVector result; + QMutexLocker lock(&mutex); + if (!connected) return result; + + auto* hdr = static_cast(mappedView); + auto* data = static_cast(mappedView) + RCX_RPC_DATA_OFFSET; + + hdr->command = RPC_CMD_ENUM_MODULES; + hdr->status = RCX_RPC_STATUS_OK; + + if (!signalAndWait()) { connected = false; return result; } + if (hdr->status != RCX_RPC_STATUS_OK) return result; + + uint32_t count = hdr->responseCount; + result.reserve((int)count); + + for (uint32_t i = 0; i < count; ++i) { + auto* entry = reinterpret_cast( + data + i * sizeof(RcxRpcModuleEntry)); + + QString modName; +#ifdef _WIN32 + modName = QString::fromWCharArray( + reinterpret_cast(data + entry->nameOffset), + (int)(entry->nameLength / sizeof(wchar_t))); +#else + modName = QString::fromUtf8( + reinterpret_cast(data + entry->nameOffset), + (int)entry->nameLength); +#endif + result.append({modName, entry->base, entry->size}); + } + return result; + } + + bool ping() + { + QMutexLocker lock(&mutex); + if (!connected) return false; + + auto* hdr = static_cast(mappedView); + hdr->command = RPC_CMD_PING; + hdr->status = RCX_RPC_STATUS_OK; + + if (!signalAndWait()) { connected = false; return false; } + return true; + } + + void shutdown() + { + QMutexLocker lock(&mutex); + if (!connected) return; + + auto* hdr = static_cast(mappedView); + hdr->command = RPC_CMD_SHUTDOWN; + hdr->status = RCX_RPC_STATUS_OK; + + signalAndWait(500); + connected = false; + } +}; + +/* ══════════════════════════════════════════════════════════════════════ + * RemoteProcessProvider + * ══════════════════════════════════════════════════════════════════════ */ + +RemoteProcessProvider::RemoteProcessProvider( + uint32_t pid, const QString& processName, + std::shared_ptr ipc) + : m_pid(pid) + , m_processName(processName) + , m_connected(ipc && ipc->connected) + , m_base(0) + , m_ipc(std::move(ipc)) +{ + if (m_connected) + cacheModules(); +} + +RemoteProcessProvider::~RemoteProcessProvider() = default; + +bool RemoteProcessProvider::read(uint64_t addr, void* buf, int len) const +{ + if (!m_connected || len <= 0) return false; + bool ok = m_ipc->readSingle(addr, buf, len); + if (!ok) { + memset(buf, 0, (size_t)len); + /* update connectivity flag through mutable ipc */ + const_cast(this)->m_connected = m_ipc->connected; + } + return ok; +} + +int RemoteProcessProvider::size() const +{ + return m_connected ? 0x10000 : 0; +} + +bool RemoteProcessProvider::write(uint64_t addr, const void* buf, int len) +{ + if (!m_connected || len <= 0) return false; + bool ok = m_ipc->writeSingle(addr, buf, len); + if (!ok) m_connected = m_ipc->connected; + return ok; +} + +QString RemoteProcessProvider::getSymbol(uint64_t addr) const +{ + for (const auto& mod : m_modules) { + if (addr >= mod.base && addr < mod.base + mod.size) { + uint64_t off = addr - mod.base; + return QStringLiteral("%1+0x%2") + .arg(mod.name) + .arg(off, 0, 16, QChar('0')); + } + } + return {}; +} + +uint64_t RemoteProcessProvider::symbolToAddress(const QString& n) const +{ + for (const auto& mod : m_modules) { + if (mod.name.compare(n, Qt::CaseInsensitive) == 0) + return mod.base; + } + return 0; +} + +void RemoteProcessProvider::cacheModules() +{ + m_modules = m_ipc->enumerateModules(); + if (!m_modules.isEmpty()) + m_base = m_modules.first().base; +} + +/* ══════════════════════════════════════════════════════════════════════ + * Injection helpers + * ══════════════════════════════════════════════════════════════════════ */ + +namespace { + +/* Resolve payload DLL/SO path next to this plugin DLL/SO */ +static QString payloadPath() +{ +#ifdef _WIN32 + HMODULE hSelf = nullptr; + GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + reinterpret_cast(&payloadPath), &hSelf); + WCHAR buf[MAX_PATH]; + GetModuleFileNameW(hSelf, buf, MAX_PATH); + QFileInfo fi(QString::fromWCharArray(buf)); + return fi.absolutePath() + QStringLiteral("/rcx_payload.dll"); +#else + Dl_info info; + dladdr(reinterpret_cast(&payloadPath), &info); + QFileInfo fi(QString::fromUtf8(info.dli_fname)); + return fi.absolutePath() + QStringLiteral("/rcx_payload.so"); +#endif +} + +/* Create bootstrap shared memory with the nonce */ +static bool createBootstrapShm(uint32_t pid, const QByteArray& nonce) +{ + char bootName[128]; + rcx_rpc_boot_name(bootName, sizeof(bootName), pid); + +#ifdef _WIN32 + HANDLE hBoot = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE, + bootName); + if (!hBoot) return false; + + auto* view = static_cast( + MapViewOfFile(hBoot, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE)); + if (!view) { CloseHandle(hBoot); return false; } + + memset(view, 0, RCX_RPC_BOOT_SIZE); + view->nonceLength = (uint32_t)nonce.size(); + memcpy(view->nonce, nonce.constData(), qMin(nonce.size(), 59)); + + UnmapViewOfFile(view); + /* keep hBoot open until payload reads it (payload unlinks after reading) */ + /* leak intentional: closed when process exits or payload consumes it */ + return true; +#else + int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600); + if (fd < 0) return false; + if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return false; } + + void* view = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, fd, 0); + close(fd); + if (view == MAP_FAILED) return false; + + auto* boot = static_cast(view); + memset(boot, 0, RCX_RPC_BOOT_SIZE); + boot->nonceLength = (uint32_t)nonce.size(); + memcpy(boot->nonce, nonce.constData(), qMin(nonce.size(), 59)); + + munmap(view, RCX_RPC_BOOT_SIZE); + /* payload unlinks after consuming */ + return true; +#endif +} + +#ifdef _WIN32 +/* ── Windows injection: CreateRemoteThread + LoadLibraryA ─────────── */ + +static bool injectPayload(uint32_t pid, QString* errorMsg) +{ + QString path = payloadPath(); + QByteArray pathUtf8 = QDir::toNativeSeparators(path).toLocal8Bit(); + + HANDLE hProc = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid); + if (!hProc) { + if (errorMsg) + *errorMsg = QStringLiteral("OpenProcess failed (error %1).\n" + "Try running as Administrator.") + .arg(GetLastError()); + return false; + } + + /* allocate + write path string in target */ + SIZE_T pathLen = (SIZE_T)(pathUtf8.size() + 1); + void* remotePath = VirtualAllocEx(hProc, nullptr, pathLen, + MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); + if (!remotePath) { + if (errorMsg) *errorMsg = QStringLiteral("VirtualAllocEx failed."); + CloseHandle(hProc); + return false; + } + + WriteProcessMemory(hProc, remotePath, pathUtf8.constData(), pathLen, nullptr); + + /* create remote thread calling LoadLibraryA(path) */ + HMODULE hK32 = GetModuleHandleA("kernel32.dll"); + auto pLoadLib = reinterpret_cast( + GetProcAddress(hK32, "LoadLibraryA")); + + HANDLE hThread = CreateRemoteThread(hProc, nullptr, 0, + pLoadLib, remotePath, 0, nullptr); + if (!hThread) { + if (errorMsg) *errorMsg = QStringLiteral("CreateRemoteThread failed (error %1).") + .arg(GetLastError()); + VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE); + CloseHandle(hProc); + return false; + } + + WaitForSingleObject(hThread, 10000); + + /* check if LoadLibrary returned non-null */ + DWORD exitCode = 0; + GetExitCodeThread(hThread, &exitCode); + CloseHandle(hThread); + + VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE); + CloseHandle(hProc); + + if (exitCode == 0) { + if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n" + "Ensure rcx_payload.dll is in: %1").arg(path); + return false; + } + return true; +} + +#else +/* ── Linux injection: ptrace + dlopen ─────────────────────────────── */ + +static uint64_t findLibBase(pid_t pid, const char* libName) +{ + char mapsPath[64]; + snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid); + FILE* f = fopen(mapsPath, "r"); + if (!f) return 0; + + char line[1024]; + while (fgets(line, sizeof(line), f)) { + if (strstr(line, libName)) { + uint64_t base; + if (sscanf(line, "%lx-", &base) == 1) { + fclose(f); + return base; + } + } + } + fclose(f); + return 0; +} + +static uint64_t findSyscallInsn(pid_t pid) +{ + char mapsPath[64]; + snprintf(mapsPath, sizeof(mapsPath), "/proc/%d/maps", pid); + FILE* f = fopen(mapsPath, "r"); + if (!f) return 0; + + char line[1024]; + while (fgets(line, sizeof(line), f)) { + if (strstr(line, "libc") && strstr(line, "r-xp")) { + uint64_t start, end; + if (sscanf(line, "%lx-%lx", &start, &end) != 2) continue; + fclose(f); + + /* scan for 0F 05 (syscall) */ + char memPath[64]; + snprintf(memPath, sizeof(memPath), "/proc/%d/mem", pid); + int memFd = open(memPath, O_RDONLY); + if (memFd < 0) return 0; + + uint8_t buf[4096]; + for (uint64_t off = start; off < end; off += sizeof(buf)) { + ssize_t n = pread(memFd, buf, sizeof(buf), (off_t)off); + if (n <= 1) break; + for (ssize_t i = 0; i + 1 < n; ++i) { + if (buf[i] == 0x0F && buf[i + 1] == 0x05) { + close(memFd); + return off + (uint64_t)i; + } + } + } + close(memFd); + return 0; + } + } + fclose(f); + return 0; +} + +static bool writeTargetMem(pid_t pid, uint64_t addr, const void* src, size_t len) +{ + const uint8_t* p = static_cast(src); + for (size_t i = 0; i < len; i += sizeof(long)) { + long val = 0; + size_t chunk = (len - i < sizeof(long)) ? (len - i) : sizeof(long); + if (chunk < sizeof(long)) { + errno = 0; + val = ptrace(PTRACE_PEEKDATA, pid, (void*)(addr + i), nullptr); + if (errno) return false; + } + memcpy(&val, p + i, chunk); + if (ptrace(PTRACE_POKEDATA, pid, (void*)(addr + i), (void*)val) < 0) + return false; + } + return true; +} + +static bool injectPayload(uint32_t pid, QString* errorMsg) +{ + QString path = payloadPath(); + QByteArray pathUtf8 = path.toUtf8(); + + if (ptrace(PTRACE_ATTACH, (pid_t)pid, nullptr, nullptr) < 0) { + if (errorMsg) + *errorMsg = QStringLiteral("ptrace attach failed: %1\n" + "Check /proc/sys/kernel/yama/ptrace_scope or run as root.") + .arg(strerror(errno)); + return false; + } + + int status; + waitpid((pid_t)pid, &status, 0); + + /* save registers */ + struct user_regs_struct savedRegs, regs; + ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, &savedRegs); + regs = savedRegs; + + /* find syscall instruction in target's libc */ + uint64_t syscallAddr = findSyscallInsn((pid_t)pid); + if (!syscallAddr) { + ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr); + if (errorMsg) *errorMsg = QStringLiteral("Could not find syscall instruction in target."); + return false; + } + + /* find dlopen in target via libc offset technique */ + void* ourDlopen = dlsym(RTLD_DEFAULT, "dlopen"); + uint64_t ourLibcBase = findLibBase(getpid(), "libc"); + uint64_t targetLibcBase = findLibBase((pid_t)pid, "libc"); + + if (!ourDlopen || !ourLibcBase || !targetLibcBase) { + ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr); + if (errorMsg) *errorMsg = QStringLiteral("Could not resolve dlopen address."); + return false; + } + + uint64_t targetDlopen = targetLibcBase + ((uint64_t)ourDlopen - ourLibcBase); + + /* call mmap in target via syscall: mmap(0, 4096, RWX, MAP_PRIVATE|MAP_ANON, -1, 0) */ + regs.rax = 9; /* __NR_mmap */ + regs.rdi = 0; + regs.rsi = 4096; + regs.rdx = 7; /* PROT_READ|PROT_WRITE|PROT_EXEC */ + regs.r10 = 0x22; /* MAP_PRIVATE|MAP_ANONYMOUS */ + regs.r8 = (uint64_t)-1; + regs.r9 = 0; + regs.rip = syscallAddr; + + ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s); + ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr); + waitpid((pid_t)pid, &status, 0); + + ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s); + uint64_t mmapPage = regs.rax; + + if ((int64_t)mmapPage < 0 || mmapPage == 0) { + ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs); + ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr); + if (errorMsg) *errorMsg = QStringLiteral("mmap in target failed."); + return false; + } + + /* write path string at start of page */ + writeTargetMem((pid_t)pid, mmapPage, pathUtf8.constData(), (size_t)(pathUtf8.size() + 1)); + + /* write shellcode after path: + * mov rdi, pathAddr (48 BF xxxxxxxx) + * mov rsi, 2 (48 BE 02000000 00000000) + * mov rax, dlopenAddr (48 B8 xxxxxxxx) + * call rax (FF D0) + * int3 (CC) + */ + uint64_t pathAddr = mmapPage; + uint64_t codeAddr = mmapPage + ((pathUtf8.size() + 1 + 15) & ~15ULL); + + uint8_t sc[64]; + int len = 0; + /* mov rdi, imm64 */ + sc[len++] = 0x48; sc[len++] = 0xBF; + memcpy(sc + len, &pathAddr, 8); len += 8; + /* mov rsi, 2 (RTLD_NOW) */ + sc[len++] = 0x48; sc[len++] = 0xBE; + uint64_t rtldNow = 2; + memcpy(sc + len, &rtldNow, 8); len += 8; + /* mov rax, dlopen */ + sc[len++] = 0x48; sc[len++] = 0xB8; + memcpy(sc + len, &targetDlopen, 8); len += 8; + /* call rax */ + sc[len++] = 0xFF; sc[len++] = 0xD0; + /* int3 */ + sc[len++] = 0xCC; + + writeTargetMem((pid_t)pid, codeAddr, sc, (size_t)len); + + /* execute shellcode */ + regs = savedRegs; + regs.rip = codeAddr; + regs.rsp = (mmapPage + 4096) & ~0xFULL; + + ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, ®s); + ptrace(PTRACE_CONT, (pid_t)pid, nullptr, nullptr); + waitpid((pid_t)pid, &status, 0); + + bool ok = false; + if (WIFSTOPPED(status) && WSTOPSIG(status) == SIGTRAP) { + ptrace(PTRACE_GETREGS, (pid_t)pid, nullptr, ®s); + ok = (regs.rax != 0); + } + + /* clean up: munmap the page via syscall */ + struct user_regs_struct cleanRegs = savedRegs; + cleanRegs.rax = 11; /* __NR_munmap */ + cleanRegs.rdi = mmapPage; + cleanRegs.rsi = 4096; + cleanRegs.rip = syscallAddr; + ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &cleanRegs); + ptrace(PTRACE_SINGLESTEP, (pid_t)pid, nullptr, nullptr); + waitpid((pid_t)pid, &status, 0); + + /* restore and detach */ + ptrace(PTRACE_SETREGS, (pid_t)pid, nullptr, &savedRegs); + ptrace(PTRACE_DETACH, (pid_t)pid, nullptr, nullptr); + + if (!ok && errorMsg) + *errorMsg = QStringLiteral("dlopen failed in target.\n" + "Ensure payload is at: %1").arg(path); + return ok; +} +#endif /* _WIN32 / linux injection */ + +} /* anonymous namespace */ + +/* ══════════════════════════════════════════════════════════════════════ + * RemoteProcessMemoryPlugin + * ══════════════════════════════════════════════════════════════════════ */ + +RemoteProcessMemoryPlugin::RemoteProcessMemoryPlugin() = default; +RemoteProcessMemoryPlugin::~RemoteProcessMemoryPlugin() = default; + +QIcon RemoteProcessMemoryPlugin::Icon() const +{ + return qApp->style()->standardIcon(QStyle::SP_DriveNetIcon); +} + +bool RemoteProcessMemoryPlugin::canHandle(const QString& target) const +{ + return target.startsWith(QStringLiteral("rpm:")); +} + +std::unique_ptr +RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg) +{ + /* target = "rpm:{pid}:{nonce}:{name}" */ + QStringList parts = target.split(':'); + if (parts.size() < 4 || parts[0] != QStringLiteral("rpm")) { + if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target; + return nullptr; + } + + bool ok; + uint32_t pid = parts[1].toUInt(&ok); + QString nonce = parts[2]; + QString name = parts.mid(3).join(':'); /* name may contain colons */ + + if (!ok || pid == 0) { + if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target."); + return nullptr; + } + + auto ipc = getOrCreateConnection(pid, nonce, errorMsg); + if (!ipc) return nullptr; + + return std::make_unique(pid, name, ipc); +} + +uint64_t RemoteProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const +{ + /* Read imageBase directly from the shared-memory header -- zero IPC cost. + The payload filled it at init from PEB->Ldr (Win) / /proc/self/maps (Linux). */ + QStringList parts = target.split(':'); + if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) + return 0; + + bool ok; + uint32_t pid = parts[1].toUInt(&ok); + if (!ok) return 0; + + QMutexLocker lock(&m_connectionsMutex); + auto it = m_connections.constFind(pid); + if (it == m_connections.constEnd() || !(*it)->connected) + return 0; + + auto* hdr = static_cast((*it)->mappedView); + return hdr->imageBase; +} + +bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target) +{ + /* ── 1. pick a process ── */ + QVector pluginProcs = enumerateProcesses(); + QList procs; + for (const auto& pi : pluginProcs) { + ProcessInfo info; + info.pid = pi.pid; + info.name = pi.name; + info.path = pi.path; + info.icon = pi.icon; + procs.append(info); + } + + ProcessPicker picker(procs, parent); + if (picker.exec() != QDialog::Accepted) return false; + + uint32_t pid = picker.selectedProcessId(); + QString name = picker.selectedProcessName(); + + /* ── 2. ask inject or connect ── */ + QMessageBox box(parent); + box.setWindowTitle(QStringLiteral("Remote Process Memory")); + box.setText(QStringLiteral("Connect to %1 (PID %2)").arg(name).arg(pid)); + box.setInformativeText(QStringLiteral("Choose how to connect to the target:")); + QAbstractButton* injectBtn = box.addButton(QStringLiteral("Inject Payload"), QMessageBox::ActionRole); + QAbstractButton* connectBtn = box.addButton(QStringLiteral("Already Injected"), QMessageBox::ActionRole); + box.addButton(QMessageBox::Cancel); + box.exec(); + + QAbstractButton* clicked = box.clickedButton(); + if (clicked == injectBtn) { + /* generate nonce */ + QString nonce = QUuid::createUuid().toString(QUuid::Id128).left(16); + QByteArray nonceUtf8 = nonce.toUtf8(); + + /* create bootstrap, inject */ + if (!createBootstrapShm(pid, nonceUtf8)) { + QMessageBox::critical(parent, QStringLiteral("Error"), + QStringLiteral("Failed to create bootstrap shared memory.")); + return false; + } + + QString injectErr; + if (!injectPayload(pid, &injectErr)) { + QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr); + return false; + } + + *target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name); + return true; + } + else if (clicked == connectBtn) { + bool ok; + QString nonce = QInputDialog::getText(parent, + QStringLiteral("Connect to Payload"), + QStringLiteral("Enter the payload nonce:"), + QLineEdit::Normal, QString(), &ok); + if (!ok || nonce.isEmpty()) return false; + + *target = QStringLiteral("rpm:%1:%2:%3").arg(pid).arg(nonce, name); + return true; + } + + return false; +} + +QVector RemoteProcessMemoryPlugin::enumerateProcesses() +{ + QVector procs; + +#ifdef _WIN32 + HANDLE snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); + if (snap == INVALID_HANDLE_VALUE) return procs; + + PROCESSENTRY32W entry; + entry.dwSize = sizeof(entry); + + if (Process32FirstW(snap, &entry)) { + do { + PluginProcessInfo info; + info.pid = entry.th32ProcessID; + info.name = QString::fromWCharArray(entry.szExeFile); + + HANDLE hProc = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, + FALSE, entry.th32ProcessID); + if (hProc) { + wchar_t path[MAX_PATH * 2]; + DWORD pathLen = sizeof(path) / sizeof(wchar_t); + if (QueryFullProcessImageNameW(hProc, 0, path, &pathLen)) { + info.path = QString::fromWCharArray(path); + SHFILEINFOW sfi = {}; + if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), + SHGFI_ICON | SHGFI_SMALLICON) && sfi.hIcon) { +#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) + info.icon = QIcon(QPixmap::fromImage(QImage::fromHICON(sfi.hIcon))); +#else + info.icon = QIcon(QtWin::fromHICON(sfi.hIcon)); +#endif + DestroyIcon(sfi.hIcon); + } + } + CloseHandle(hProc); + } + procs.append(info); + } while (Process32NextW(snap, &entry)); + } + CloseHandle(snap); + +#else + QDir procDir(QStringLiteral("/proc")); + QIcon defIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon); + + for (const QString& entry : procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot)) { + bool ok; + uint32_t pid = entry.toUInt(&ok); + if (!ok || pid == 0) continue; + + QFile commFile(QStringLiteral("/proc/%1/comm").arg(pid)); + if (!commFile.open(QIODevice::ReadOnly)) continue; + QString procName = QString::fromUtf8(commFile.readAll()).trimmed(); + commFile.close(); + if (procName.isEmpty()) continue; + + QString memPath = QStringLiteral("/proc/%1/mem").arg(pid); + if (::access(memPath.toUtf8().constData(), R_OK) != 0) continue; + + QFileInfo exeInfo(QStringLiteral("/proc/%1/exe").arg(pid)); + PluginProcessInfo info; + info.pid = pid; + info.name = procName; + info.path = exeInfo.exists() ? exeInfo.symLinkTarget() : QString(); + info.icon = defIcon; + procs.append(info); + } +#endif + + return procs; +} + +std::shared_ptr +RemoteProcessMemoryPlugin::getOrCreateConnection( + uint32_t pid, const QString& nonce, QString* errorMsg) +{ + QMutexLocker lock(&m_connectionsMutex); + + auto it = m_connections.find(pid); + if (it != m_connections.end() && (*it)->connected) + return *it; + + auto ipc = std::make_shared(); + if (!ipc->connect(pid, nonce.toUtf8())) { + if (errorMsg) + *errorMsg = QStringLiteral("Failed to connect IPC to PID %1.\n" + "Is the payload running?").arg(pid); + return nullptr; + } + + m_connections[pid] = ipc; + return ipc; +} + +/* ── Plugin factory ───────────────────────────────────────────────── */ + +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin() +{ + return new RemoteProcessMemoryPlugin(); +} diff --git a/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h new file mode 100644 index 0000000..e487e5a --- /dev/null +++ b/plugins/RemoteProcessMemory/RemoteProcessMemoryPlugin.h @@ -0,0 +1,86 @@ +#pragma once +#include "../../src/iplugin.h" +#include "../../src/providers/provider.h" + +#include +#include +#include +#include +#include + +struct IpcClient; /* defined in .cpp */ + +/* ── Provider ─────────────────────────────────────────────────────── */ + +class RemoteProcessProvider : public rcx::Provider +{ +public: + struct ModuleInfo { QString name; uint64_t base; uint64_t size; }; + + RemoteProcessProvider(uint32_t pid, const QString& processName, + std::shared_ptr ipc); + ~RemoteProcessProvider() override; + + /* required */ + bool read(uint64_t addr, void* buf, int len) const override; + int size() const override; + + /* optional */ + bool write(uint64_t addr, const void* buf, int len) override; + bool isWritable() const override { return m_connected; } + QString name() const override { return m_processName; } + QString kind() const override { return QStringLiteral("RemoteProcess"); } + bool isLive() const override { return true; } + uint64_t base() const override { return m_base; } + bool isReadable(uint64_t, int len) const override { return m_connected && len >= 0; } + QString getSymbol(uint64_t addr) const override; + uint64_t symbolToAddress(const QString& n) const override; + + uint32_t pid() const { return m_pid; } + +private: + void cacheModules(); + + uint32_t m_pid; + QString m_processName; + bool m_connected; + uint64_t m_base; + mutable std::shared_ptr m_ipc; + QVector m_modules; +}; + +/* ── Plugin ───────────────────────────────────────────────────────── */ + +class RemoteProcessMemoryPlugin : public IProviderPlugin +{ +public: + RemoteProcessMemoryPlugin(); + ~RemoteProcessMemoryPlugin() override; + + std::string Name() const override { return "Remote Process Memory"; } + std::string Version() const override { return "1.0.0"; } + std::string Author() const override { return "Reclass"; } + std::string Description() const override { + return "Read/write memory via injected payload (shared-memory IPC)"; + } + k_ELoadType LoadType() const override { return k_ELoadTypeManual; } + QIcon Icon() const override; + + bool canHandle(const QString& target) const override; + std::unique_ptr createProvider(const QString& target, + QString* errorMsg) override; + uint64_t getInitialBaseAddress(const QString& target) const override; + bool selectTarget(QWidget* parent, QString* target) override; + + bool providesProcessList() const override { return true; } + QVector enumerateProcesses() override; + +private: + std::shared_ptr getOrCreateConnection( + uint32_t pid, const QString& nonce, QString* errorMsg); + + mutable QMutex m_connectionsMutex; + QHash> m_connections; +}; + +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin(); diff --git a/plugins/RemoteProcessMemory/payload/rcx_payload.cpp b/plugins/RemoteProcessMemory/payload/rcx_payload.cpp new file mode 100644 index 0000000..dad4ad2 --- /dev/null +++ b/plugins/RemoteProcessMemory/payload/rcx_payload.cpp @@ -0,0 +1,623 @@ +/* + * rcx_payload -- injected into target process. + * + * Pure Win32 / POSIX, NO Qt, minimal footprint. + * Reads a nonce from bootstrap shared memory, creates the main IPC + * channel (shared memory + events/semaphores), and runs a server + * thread that handles RPC commands from the editor plugin. + */ + +#include "../rcx_rpc_protocol.h" + +#ifdef _WIN32 +/* =================================================================== + * WINDOWS implementation + * =================================================================== */ +#define WIN32_LEAN_AND_MEAN +#include +#include + +/* ── globals ──────────────────────────────────────────────────────── */ +static HANDLE g_hShm = nullptr; +static void* g_mappedView = nullptr; +static HANDLE g_hReqEvent = nullptr; +static HANDLE g_hRspEvent = nullptr; +static HANDLE g_hThread = nullptr; +static volatile LONG g_shutdown = 0; + +/* ── memory safety via VirtualQuery ────────────────────────────────── */ + +inline bool IsReadableProtect(DWORD p) +{ + if (p & (PAGE_NOACCESS | PAGE_GUARD)) + return false; + + const DWORD readable = + PAGE_READONLY | PAGE_READWRITE | PAGE_WRITECOPY | + PAGE_EXECUTE_READ | PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; + + return (p & readable) != 0; +} + +inline bool IsWritableProtect(DWORD p) +{ + if (p & (PAGE_NOACCESS | PAGE_GUARD)) + return false; + + const DWORD writable = + PAGE_READWRITE | PAGE_WRITECOPY | + PAGE_EXECUTE_READWRITE | PAGE_EXECUTE_WRITECOPY; + + return (p & writable) != 0; +} + +/* Check that the full range [addr, addr+len) is covered by readable pages. */ +static bool IsRangeReadable(uintptr_t addr, uint32_t len) +{ + uintptr_t end = addr + len; + uintptr_t cur = addr; + while (cur < end) { + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(reinterpret_cast(cur), &mbi, sizeof(mbi)) == 0) + return false; + if (mbi.State != MEM_COMMIT || !IsReadableProtect(mbi.Protect)) + return false; + uintptr_t regionEnd = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; + cur = regionEnd; + } + return true; +} + +static bool IsRangeWritable(uintptr_t addr, uint32_t len) +{ + uintptr_t end = addr + len; + uintptr_t cur = addr; + while (cur < end) { + MEMORY_BASIC_INFORMATION mbi{}; + if (VirtualQuery(reinterpret_cast(cur), &mbi, sizeof(mbi)) == 0) + return false; + if (mbi.State != MEM_COMMIT || !IsWritableProtect(mbi.Protect)) + return false; + uintptr_t regionEnd = reinterpret_cast(mbi.BaseAddress) + mbi.RegionSize; + cur = regionEnd; + } + return true; +} + +/* ── command handlers ─────────────────────────────────────────────── */ + +static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data) +{ + auto* entries = reinterpret_cast(data); + for (uint32_t i = 0; i < hdr->requestCount; ++i) { + uint8_t* dest = data + entries[i].dataOffset; + uintptr_t src = static_cast(entries[i].address); + if (IsRangeReadable(src, entries[i].length)) { + memcpy(dest, reinterpret_cast(src), entries[i].length); + } else { + memset(dest, 0, entries[i].length); + hdr->status = RCX_RPC_STATUS_PARTIAL; + } + /* SEH fallback (commented out, kept for reference): + __try { + memcpy(dest, reinterpret_cast(src), entries[i].length); + } __except (EXCEPTION_EXECUTE_HANDLER) { + memset(dest, 0, entries[i].length); + hdr->status = RCX_RPC_STATUS_PARTIAL; + } + */ + } + hdr->responseCount = hdr->requestCount; +} + +static void handle_write(RcxRpcHeader* hdr, uint8_t* data) +{ + uintptr_t dst = static_cast(hdr->writeAddress); + if (IsRangeWritable(dst, hdr->writeLength)) { + memcpy(reinterpret_cast(dst), data, hdr->writeLength); + } else { + hdr->status = RCX_RPC_STATUS_ERROR; + } + /* SEH fallback (commented out, kept for reference): + __try { + memcpy(reinterpret_cast(dst), data, hdr->writeLength); + } __except (EXCEPTION_EXECUTE_HANDLER) { + hdr->status = RCX_RPC_STATUS_ERROR; + } + */ +} + +static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data) +{ + HANDLE hProc = GetCurrentProcess(); + HMODULE mods[1024]; + DWORD needed = 0; + if (!EnumProcessModules(hProc, mods, sizeof(mods), &needed)) { + hdr->status = RCX_RPC_STATUS_ERROR; + hdr->responseCount = 0; + return; + } + int count = (int)(needed / sizeof(HMODULE)); + if (count > 1024) count = 1024; + + uint32_t entryBytes = (uint32_t)(count * sizeof(RcxRpcModuleEntry)); + uint32_t nameDataOff = entryBytes; + + for (int i = 0; i < count; ++i) { + MODULEINFO mi{}; + WCHAR modName[MAX_PATH]; + GetModuleInformation(hProc, mods[i], &mi, sizeof(mi)); + int nameLen = (int)GetModuleBaseNameW(hProc, mods[i], modName, MAX_PATH); + uint32_t nameBytes = (uint32_t)(nameLen * sizeof(WCHAR)); + + auto* entry = reinterpret_cast(data + i * sizeof(RcxRpcModuleEntry)); + entry->base = reinterpret_cast(mi.lpBaseOfDll); + entry->size = static_cast(mi.SizeOfImage); + entry->nameOffset = nameDataOff; + entry->nameLength = nameBytes; + + if (nameDataOff + nameBytes <= RCX_RPC_DATA_SIZE) { + memcpy(data + nameDataOff, modName, nameBytes); + nameDataOff += nameBytes; + } + } + + hdr->responseCount = (uint32_t)count; + hdr->totalDataUsed = nameDataOff; + hdr->status = RCX_RPC_STATUS_OK; +} + +/* ── server thread ────────────────────────────────────────────────── */ + +static DWORD WINAPI ServerThread(LPVOID) +{ + auto* hdr = static_cast(g_mappedView); + auto* data = reinterpret_cast(g_mappedView) + RCX_RPC_DATA_OFFSET; + + /* signal readiness */ + InterlockedExchange(reinterpret_cast(&hdr->payloadReady), 1); + + while (!InterlockedCompareExchange(&g_shutdown, 0, 0)) { + DWORD rc = WaitForSingleObject(g_hReqEvent, 250); + if (rc == WAIT_TIMEOUT) + continue; + if (rc != WAIT_OBJECT_0) + break; + + hdr->status = RCX_RPC_STATUS_OK; + + switch (static_cast(hdr->command)) { + case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break; + case RPC_CMD_WRITE: handle_write(hdr, data); break; + case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break; + case RPC_CMD_PING: break; + case RPC_CMD_SHUTDOWN: + InterlockedExchange(&g_shutdown, 1); + break; + default: + hdr->status = RCX_RPC_STATUS_ERROR; + break; + } + + SetEvent(g_hRspEvent); + + if (static_cast(hdr->command) == RPC_CMD_SHUTDOWN) + break; + } + + /* mark not-ready so the host process can detect shutdown */ + InterlockedExchange(reinterpret_cast(&hdr->payloadReady), 0); + return 0; +} + +/* ── cleanup ──────────────────────────────────────────────────────── */ + +static void Cleanup(bool waitThread) +{ + InterlockedExchange(&g_shutdown, 1); + + /* wake the thread if it's blocked on REQ */ + if (g_hReqEvent) SetEvent(g_hReqEvent); + + if (waitThread && g_hThread) { + WaitForSingleObject(g_hThread, 2000); + } + if (g_hThread) { CloseHandle(g_hThread); g_hThread = nullptr; } + if (g_mappedView){ UnmapViewOfFile(g_mappedView); g_mappedView = nullptr; } + if (g_hShm) { CloseHandle(g_hShm); g_hShm = nullptr; } + if (g_hReqEvent) { CloseHandle(g_hReqEvent); g_hReqEvent = nullptr; } + if (g_hRspEvent) { CloseHandle(g_hRspEvent); g_hRspEvent = nullptr; } +} + +/* ── DllMain ──────────────────────────────────────────────────────── */ + +BOOL WINAPI DllMain(HINSTANCE, DWORD reason, LPVOID reserved) +{ + if (reason == DLL_PROCESS_ATTACH) { + uint32_t pid = GetCurrentProcessId(); + + /* ── read nonce from bootstrap shm ── */ + char bootName[128]; + rcx_rpc_boot_name(bootName, sizeof(bootName), pid); + + HANDLE hBoot = OpenFileMappingA(FILE_MAP_READ, FALSE, bootName); + if (!hBoot) return TRUE; /* no bootstrap = nothing to do */ + + auto* bootView = static_cast( + MapViewOfFile(hBoot, FILE_MAP_READ, 0, 0, RCX_RPC_BOOT_SIZE)); + if (!bootView) { CloseHandle(hBoot); return TRUE; } + + char nonce[64] = {}; + uint32_t nLen = bootView->nonceLength; + if (nLen > 59) nLen = 59; + memcpy(nonce, bootView->nonce, nLen); + nonce[nLen] = '\0'; + + UnmapViewOfFile(bootView); + CloseHandle(hBoot); + + /* ── create main shared memory ── */ + char shmName[128], reqName[128], rspName[128]; + rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce); + rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce); + rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce); + + g_hShm = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, 0, RCX_RPC_SHM_SIZE, shmName); + if (!g_hShm) return TRUE; + + g_mappedView = MapViewOfFile(g_hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE); + if (!g_mappedView) { CloseHandle(g_hShm); g_hShm = nullptr; return TRUE; } + + memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE); + auto* hdr = static_cast(g_mappedView); + hdr->version = RCX_RPC_VERSION; + + /* image base from PEB: gs:[0x60] → PEB, +0x18 → Ldr, Flink → first entry, +0x30 → DllBase */ + { + uint64_t peb; + asm volatile("mov %%gs:0x60, %0" : "=r"(peb)); + uint64_t ldr = *reinterpret_cast(peb + 0x18); + uint64_t firstLink = *reinterpret_cast(ldr + 0x10); /* InLoadOrderModuleList.Flink */ + hdr->imageBase = *reinterpret_cast(firstLink + 0x30); /* DllBase */ + } + + /* ── create events ── */ + g_hReqEvent = CreateEventA(nullptr, FALSE, FALSE, reqName); + g_hRspEvent = CreateEventA(nullptr, FALSE, FALSE, rspName); + if (!g_hReqEvent || !g_hRspEvent) { Cleanup(false); return TRUE; } + + /* ── start server thread (payloadReady set by the thread) ── */ + g_hThread = CreateThread(nullptr, 0, ServerThread, nullptr, 0, nullptr); + if (!g_hThread) { Cleanup(false); return TRUE; } + } + else if (reason == DLL_PROCESS_DETACH) { + /* reserved != NULL → process is terminating (threads already dead) */ + Cleanup(reserved == nullptr); + } + + return TRUE; +} + +#else +/* =================================================================== + * LINUX implementation + * =================================================================== */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* ── globals ──────────────────────────────────────────────────────── */ +static int g_shmFd = -1; +static void* g_mappedView = nullptr; +static sem_t* g_reqSem = SEM_FAILED; +static sem_t* g_rspSem = SEM_FAILED; +static pthread_t g_thread; +static volatile int g_shutdown = 0; +static volatile int g_threadRunning = 0; +static int g_memFd = -1; /* /proc/self/mem for safe access */ +static char g_shmName[128]; +static char g_reqName[128]; +static char g_rspName[128]; + +/* ── safe memory access via /proc/self/mem ────────────────────────── */ + +static void safe_read(uint64_t addr, void* dest, uint32_t len, uint32_t* status) +{ + ssize_t n = pread(g_memFd, dest, len, (off_t)addr); + if (n < (ssize_t)len) { + if (n > 0) + memset((uint8_t*)dest + n, 0, len - (uint32_t)n); + else + memset(dest, 0, len); + *status = RCX_RPC_STATUS_PARTIAL; + } +} + +static void safe_write(uint64_t addr, const void* src, uint32_t len, uint32_t* status) +{ + ssize_t n = pwrite(g_memFd, src, len, (off_t)addr); + if (n < (ssize_t)len) + *status = RCX_RPC_STATUS_ERROR; +} + +/* ── command handlers ─────────────────────────────────────────────── */ + +static void handle_read_batch(RcxRpcHeader* hdr, uint8_t* data) +{ + auto* entries = reinterpret_cast(data); + for (uint32_t i = 0; i < hdr->requestCount; ++i) { + uint8_t* dest = data + entries[i].dataOffset; + safe_read(entries[i].address, dest, entries[i].length, &hdr->status); + } + hdr->responseCount = hdr->requestCount; +} + +static void handle_write(RcxRpcHeader* hdr, uint8_t* data) +{ + safe_write(hdr->writeAddress, data, hdr->writeLength, &hdr->status); +} + +static void handle_enum_modules(RcxRpcHeader* hdr, uint8_t* data) +{ + FILE* f = fopen("/proc/self/maps", "r"); + if (!f) { + hdr->status = RCX_RPC_STATUS_ERROR; + hdr->responseCount = 0; + return; + } + + /* first pass: collect unique file-backed mappings */ + struct ModRange { uint64_t base; uint64_t end; char path[512]; }; + static ModRange modules[512]; /* static to avoid large stack alloc */ + int modCount = 0; + + char line[1024]; + while (fgets(line, sizeof(line), f) && modCount < 512) { + uint64_t start, end; + char perms[8] = {}, path[512] = {}; + if (sscanf(line, "%lx-%lx %7s %*x %*x:%*x %*u %511[^\n]", + &start, &end, perms, path) < 4) + continue; + + /* skip non-file / special mappings */ + /* trim leading whitespace from path */ + char* p = path; + while (*p == ' ' || *p == '\t') ++p; + if (*p != '/') continue; + if (strncmp(p, "/dev/", 5) == 0) continue; + if (strncmp(p, "/memfd:", 7) == 0) continue; + + bool found = false; + for (int i = 0; i < modCount; ++i) { + if (strcmp(modules[i].path, p) == 0) { + if (start < modules[i].base) modules[i].base = start; + if (end > modules[i].end) modules[i].end = end; + found = true; + break; + } + } + if (!found) { + modules[modCount].base = start; + modules[modCount].end = end; + strncpy(modules[modCount].path, p, 511); + modules[modCount].path[511] = '\0'; + ++modCount; + } + } + fclose(f); + + /* write entries + name strings into data region */ + uint32_t entryBytes = (uint32_t)(modCount * sizeof(RcxRpcModuleEntry)); + uint32_t nameDataOff = entryBytes; + + for (int i = 0; i < modCount; ++i) { + const char* basename = strrchr(modules[i].path, '/'); + basename = basename ? basename + 1 : modules[i].path; + uint32_t nameLen = (uint32_t)strlen(basename); + + auto* entry = reinterpret_cast( + data + (uint32_t)i * sizeof(RcxRpcModuleEntry)); + entry->base = modules[i].base; + entry->size = modules[i].end - modules[i].base; + entry->nameOffset = nameDataOff; + entry->nameLength = nameLen; + + if (nameDataOff + nameLen <= RCX_RPC_DATA_SIZE) { + memcpy(data + nameDataOff, basename, nameLen); + nameDataOff += nameLen; + } + } + + hdr->responseCount = (uint32_t)modCount; + hdr->totalDataUsed = nameDataOff; + hdr->status = RCX_RPC_STATUS_OK; +} + +/* ── server thread ────────────────────────────────────────────────── */ + +static void* server_thread_func(void*) +{ + auto* hdr = static_cast(g_mappedView); + auto* data = reinterpret_cast(g_mappedView) + RCX_RPC_DATA_OFFSET; + + __atomic_store_n(&hdr->payloadReady, 1, __ATOMIC_RELEASE); + + while (!__atomic_load_n(&g_shutdown, __ATOMIC_ACQUIRE)) { + /* timed wait: 250ms */ + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_nsec += 250000000; + if (ts.tv_nsec >= 1000000000) { + ts.tv_sec += 1; + ts.tv_nsec -= 1000000000; + } + + int rc = sem_timedwait(g_reqSem, &ts); + if (rc != 0) { + if (errno == ETIMEDOUT) continue; + break; + } + + hdr->status = RCX_RPC_STATUS_OK; + + switch (static_cast(hdr->command)) { + case RPC_CMD_READ_BATCH: handle_read_batch(hdr, data); break; + case RPC_CMD_WRITE: handle_write(hdr, data); break; + case RPC_CMD_ENUM_MODULES: handle_enum_modules(hdr, data); break; + case RPC_CMD_PING: break; + case RPC_CMD_SHUTDOWN: + __atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE); + break; + default: + hdr->status = RCX_RPC_STATUS_ERROR; + break; + } + + sem_post(g_rspSem); + + if (static_cast(hdr->command) == RPC_CMD_SHUTDOWN) + break; + } + + __atomic_store_n(&hdr->payloadReady, 0, __ATOMIC_RELEASE); + __atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE); + return nullptr; +} + +/* ── init / cleanup ───────────────────────────────────────────────── */ + +static void payload_cleanup() +{ + __atomic_store_n(&g_shutdown, 1, __ATOMIC_RELEASE); + + /* wake the thread if blocked */ + if (g_reqSem != SEM_FAILED) sem_post(g_reqSem); + + if (__atomic_load_n(&g_threadRunning, __ATOMIC_ACQUIRE)) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += 2; + pthread_timedjoin_np(g_thread, nullptr, &ts); + } + + if (g_mappedView && g_mappedView != MAP_FAILED) { + munmap(g_mappedView, RCX_RPC_SHM_SIZE); + g_mappedView = nullptr; + } + if (g_shmFd >= 0) { close(g_shmFd); g_shmFd = -1; } + if (g_reqSem != SEM_FAILED) { sem_close(g_reqSem); g_reqSem = SEM_FAILED; } + if (g_rspSem != SEM_FAILED) { sem_close(g_rspSem); g_rspSem = SEM_FAILED; } + + /* unlink named objects */ + if (g_shmName[0]) shm_unlink(g_shmName); + if (g_reqName[0]) sem_unlink(g_reqName); + if (g_rspName[0]) sem_unlink(g_rspName); + + if (g_memFd >= 0) { close(g_memFd); g_memFd = -1; } +} + +__attribute__((constructor)) +static void payload_init() +{ + uint32_t pid = (uint32_t)getpid(); + + /* ── read nonce from bootstrap shm ── */ + char bootName[128]; + rcx_rpc_boot_name(bootName, sizeof(bootName), pid); + + int bootFd = shm_open(bootName, O_RDONLY, 0); + if (bootFd < 0) return; + + void* bootView = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ, + MAP_SHARED, bootFd, 0); + close(bootFd); + if (bootView == MAP_FAILED) return; + + auto* boot = static_cast(bootView); + char nonce[64] = {}; + uint32_t nLen = boot->nonceLength; + if (nLen > 59) nLen = 59; + memcpy(nonce, boot->nonce, nLen); + nonce[nLen] = '\0'; + munmap(bootView, RCX_RPC_BOOT_SIZE); + + /* one-shot, unlink bootstrap */ + shm_unlink(bootName); + + /* ── open /proc/self/mem for safe access ── */ + g_memFd = open("/proc/self/mem", O_RDWR); + if (g_memFd < 0) return; + + /* ── create main shared memory ── */ + rcx_rpc_shm_name(g_shmName, sizeof(g_shmName), pid, nonce); + rcx_rpc_req_name(g_reqName, sizeof(g_reqName), pid, nonce); + rcx_rpc_rsp_name(g_rspName, sizeof(g_rspName), pid, nonce); + + g_shmFd = shm_open(g_shmName, O_CREAT | O_RDWR, 0600); + if (g_shmFd < 0) return; + if (ftruncate(g_shmFd, RCX_RPC_SHM_SIZE) != 0) { + close(g_shmFd); g_shmFd = -1; return; + } + + g_mappedView = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, g_shmFd, 0); + if (g_mappedView == MAP_FAILED) { + g_mappedView = nullptr; + close(g_shmFd); g_shmFd = -1; + return; + } + + memset(g_mappedView, 0, RCX_RPC_HEADER_SIZE); + auto* hdr = static_cast(g_mappedView); + hdr->version = RCX_RPC_VERSION; + + /* image base from /proc/self/maps: first executable mapping */ + { + FILE* f = fopen("/proc/self/maps", "r"); + if (f) { + char line[256]; + while (fgets(line, sizeof(line), f)) { + uint64_t start; + char perms[8] = {}; + if (sscanf(line, "%lx-%*x %7s", &start, perms) >= 2 && perms[2] == 'x') { + hdr->imageBase = start; + break; + } + } + fclose(f); + } + } + + /* ── create semaphores ── */ + g_reqSem = sem_open(g_reqName, O_CREAT, 0600, 0); + g_rspSem = sem_open(g_rspName, O_CREAT, 0600, 0); + if (g_reqSem == SEM_FAILED || g_rspSem == SEM_FAILED) { + payload_cleanup(); + return; + } + + /* ── start server thread (it will set payloadReady = 1) ── */ + __atomic_store_n(&g_threadRunning, 1, __ATOMIC_RELEASE); + if (pthread_create(&g_thread, nullptr, server_thread_func, nullptr) != 0) { + __atomic_store_n(&g_threadRunning, 0, __ATOMIC_RELEASE); + payload_cleanup(); + return; + } + pthread_detach(g_thread); +} + +__attribute__((destructor)) +static void payload_deinit() +{ + payload_cleanup(); +} + +#endif /* _WIN32 / linux */ diff --git a/plugins/RemoteProcessMemory/rcx_rpc_protocol.h b/plugins/RemoteProcessMemory/rcx_rpc_protocol.h new file mode 100644 index 0000000..a012f5a --- /dev/null +++ b/plugins/RemoteProcessMemory/rcx_rpc_protocol.h @@ -0,0 +1,129 @@ +/* + * RCX RPC Protocol -- shared between plugin DLL and payload DLL/SO. + * No dependencies beyond standard C headers. + */ +#pragma once + +#include +#include +#include + +/* ── constants ─────────────────────────────────────────────────────── */ +#define RCX_RPC_VERSION 1 +#define RCX_RPC_MAX_BATCH 256 +#define RCX_RPC_SHM_SIZE (1024 * 1024) /* 1 MB */ +#define RCX_RPC_HEADER_SIZE 4096 +#define RCX_RPC_DATA_OFFSET RCX_RPC_HEADER_SIZE +#define RCX_RPC_DATA_SIZE (RCX_RPC_SHM_SIZE - RCX_RPC_DATA_OFFSET) +#define RCX_RPC_BOOT_SIZE 64 + +/* status codes */ +#define RCX_RPC_STATUS_OK 0 +#define RCX_RPC_STATUS_ERROR 1 +#define RCX_RPC_STATUS_PARTIAL 2 + +/* ── commands ──────────────────────────────────────────────────────── */ +#ifdef __cplusplus +enum RcxRpcCommand : uint32_t { +#else +typedef uint32_t RcxRpcCommand; +enum { +#endif + RPC_CMD_NONE = 0, + RPC_CMD_READ_BATCH = 1, /* batch read: N {address, length} pairs */ + RPC_CMD_WRITE = 2, /* single write */ + RPC_CMD_ENUM_MODULES = 3, /* enumerate loaded modules */ + RPC_CMD_PING = 4, /* heartbeat */ + RPC_CMD_SHUTDOWN = 5, /* graceful teardown */ +}; + +/* ── wire structs (natural alignment, verified by static_assert) ─── */ + +struct RcxRpcReadEntry { + uint64_t address; + uint32_t length; + uint32_t dataOffset; /* offset into data region for response bytes */ +}; + +struct RcxRpcModuleEntry { + uint64_t base; + uint64_t size; + uint32_t nameOffset; /* offset into data region, UTF-16 on Win, UTF-8 on Linux */ + uint32_t nameLength; /* in bytes */ +}; + +/* + * Header -- lives at shared-memory offset 0, padded to 4096 bytes. + * + * offset field + * ------ ----- + * 0 version (4) + * 4 payloadReady (4) + * 8 command (4) + * 12 requestCount (4) + * 16 writeAddress (8) + * 24 writeLength (4) + * 28 status (4) + * 32 responseCount (4) + * 36 totalDataUsed (4) + * 40 imageBase (8) -- main module base from PEB / procfs + * 48 _pad[4048] + */ +struct RcxRpcHeader { + uint32_t version; + uint32_t payloadReady; /* payload sets to 1 after init */ + uint32_t command; /* RcxRpcCommand */ + uint32_t requestCount; + uint64_t writeAddress; + uint32_t writeLength; + uint32_t status; /* RCX_RPC_STATUS_* */ + uint32_t responseCount; + uint32_t totalDataUsed; + uint64_t imageBase; /* main module base (PEB on Win, /proc on Linux) */ + uint8_t _pad[RCX_RPC_HEADER_SIZE - 48]; +}; + +/* Bootstrap shm -- 64 bytes, carries the nonce from plugin to payload */ +struct RcxRpcBootHeader { + uint32_t nonceLength; + char nonce[60]; +}; + +/* ── name formatting helpers ───────────────────────────────────────── */ + +static inline void rcx_rpc_boot_name(char* buf, int n, uint32_t pid) { +#ifdef _WIN32 + snprintf(buf, n, "Local\\RCX_BOOT_%u", pid); +#else + snprintf(buf, n, "/rcx_boot_%u", pid); +#endif +} + +static inline void rcx_rpc_shm_name(char* buf, int n, uint32_t pid, const char* nonce) { +#ifdef _WIN32 + snprintf(buf, n, "Local\\RCX_SHM_%u_%s", pid, nonce); +#else + snprintf(buf, n, "/rcx_shm_%u_%s", pid, nonce); +#endif +} + +static inline void rcx_rpc_req_name(char* buf, int n, uint32_t pid, const char* nonce) { +#ifdef _WIN32 + snprintf(buf, n, "Local\\RCX_REQ_%u_%s", pid, nonce); +#else + snprintf(buf, n, "/rcx_req_%u_%s", pid, nonce); +#endif +} + +static inline void rcx_rpc_rsp_name(char* buf, int n, uint32_t pid, const char* nonce) { +#ifdef _WIN32 + snprintf(buf, n, "Local\\RCX_RSP_%u_%s", pid, nonce); +#else + snprintf(buf, n, "/rcx_rsp_%u_%s", pid, nonce); +#endif +} + +#ifdef __cplusplus +static_assert(sizeof(RcxRpcHeader) == RCX_RPC_HEADER_SIZE, "Header must be 4096 bytes"); +static_assert(sizeof(RcxRpcBootHeader) <= RCX_RPC_BOOT_SIZE, "Boot header must fit 64 bytes"); +#endif diff --git a/plugins/RemoteProcessMemory/tests/test_rpc_client.cpp b/plugins/RemoteProcessMemory/tests/test_rpc_client.cpp new file mode 100644 index 0000000..8d6b70d --- /dev/null +++ b/plugins/RemoteProcessMemory/tests/test_rpc_client.cpp @@ -0,0 +1,595 @@ +/* + * test_rpc_client -- connects to a running test_rpc_host (or spawns one), + * exercises every RPC command, and benchmarks throughput. + * + * Usage: + * test_rpc_client (auto-spawn host) + * test_rpc_client [testbuf_hex testlen] + */ + +#include "../rcx_rpc_protocol.h" + +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +#else +# include +# include +# include +# include +# include +# include +#endif + +/* ══════════════════════════════════════════════════════════════════════ + * Minimal standalone IPC client (no Qt, mirrors plugin's IpcClient) + * ══════════════════════════════════════════════════════════════════════ */ + +struct TestIpcClient { +#ifdef _WIN32 + HANDLE hShm = nullptr; + HANDLE hReqEvent = nullptr; + HANDLE hRspEvent = nullptr; +#else + int shmFd = -1; + sem_t* reqSem = SEM_FAILED; + sem_t* rspSem = SEM_FAILED; +#endif + void* view = nullptr; + bool ok = false; + + bool connect(uint32_t pid, const char* nonce, int timeoutMs = 5000) + { + char shmName[128], reqName[128], rspName[128]; + rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce); + rcx_rpc_req_name(reqName, sizeof(reqName), pid, nonce); + rcx_rpc_rsp_name(rspName, sizeof(rspName), pid, nonce); + +#ifdef _WIN32 + ULONGLONG deadline = GetTickCount64() + (ULONGLONG)timeoutMs; + while (!(hShm = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, shmName))) { + if (GetTickCount64() >= deadline) return false; + Sleep(10); + } + view = MapViewOfFile(hShm, FILE_MAP_ALL_ACCESS, 0, 0, RCX_RPC_SHM_SIZE); + if (!view) { CloseHandle(hShm); hShm = nullptr; return false; } + + hReqEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, reqName); + hRspEvent = OpenEventA(EVENT_ALL_ACCESS, FALSE, rspName); + if (!hReqEvent || !hRspEvent) return false; +#else + auto start = std::chrono::steady_clock::now(); + while (true) { + shmFd = shm_open(shmName, O_RDWR, 0); + if (shmFd >= 0) break; + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + if (elapsed >= timeoutMs) return false; + usleep(10000); + } + view = mmap(nullptr, RCX_RPC_SHM_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, shmFd, 0); + if (view == MAP_FAILED) { view = nullptr; close(shmFd); shmFd = -1; return false; } + + reqSem = sem_open(reqName, 0); + rspSem = sem_open(rspName, 0); + if (reqSem == SEM_FAILED || rspSem == SEM_FAILED) return false; +#endif + /* wait for payloadReady */ + auto* hdr = (RcxRpcHeader*)view; +#ifdef _WIN32 + while (!hdr->payloadReady) { + if (GetTickCount64() >= deadline) return false; + Sleep(5); + } +#else + while (!__atomic_load_n(&hdr->payloadReady, __ATOMIC_ACQUIRE)) { + auto elapsed = std::chrono::duration_cast( + std::chrono::steady_clock::now() - start).count(); + if (elapsed >= timeoutMs) return false; + usleep(5000); + } +#endif + ok = true; + return true; + } + + void disconnect() + { +#ifdef _WIN32 + if (view) { UnmapViewOfFile(view); view = nullptr; } + if (hShm) { CloseHandle(hShm); hShm = nullptr; } + if (hReqEvent) { CloseHandle(hReqEvent); hReqEvent = nullptr; } + if (hRspEvent) { CloseHandle(hRspEvent); hRspEvent = nullptr; } +#else + if (view) { munmap(view, RCX_RPC_SHM_SIZE); view = nullptr; } + if (shmFd >= 0) { close(shmFd); shmFd = -1; } + if (reqSem != SEM_FAILED) { sem_close(reqSem); reqSem = SEM_FAILED; } + if (rspSem != SEM_FAILED) { sem_close(rspSem); rspSem = SEM_FAILED; } +#endif + ok = false; + } + + bool signalAndWait(int timeoutMs = 2000) + { +#ifdef _WIN32 + SetEvent(hReqEvent); + return WaitForSingleObject(hRspEvent, (DWORD)timeoutMs) == WAIT_OBJECT_0; +#else + sem_post(reqSem); + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += timeoutMs / 1000; + ts.tv_nsec += (timeoutMs % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) { ts.tv_sec++; ts.tv_nsec -= 1000000000L; } + return sem_timedwait(rspSem, &ts) == 0; +#endif + } + + /* ── RPC helpers ──────────────────────────────────────────────── */ + + bool rpc_ping() + { + auto* hdr = (RcxRpcHeader*)view; + hdr->command = RPC_CMD_PING; + hdr->status = RCX_RPC_STATUS_OK; + return signalAndWait(); + } + + bool rpc_read(uint64_t addr, void* buf, uint32_t len) + { + auto* hdr = (RcxRpcHeader*)view; + auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET; + + hdr->command = RPC_CMD_READ_BATCH; + hdr->requestCount = 1; + hdr->status = RCX_RPC_STATUS_OK; + + auto* entry = (RcxRpcReadEntry*)data; + entry->address = addr; + entry->length = len; + entry->dataOffset = sizeof(RcxRpcReadEntry); + + if (!signalAndWait()) return false; + memcpy(buf, data + entry->dataOffset, len); + return true; + } + + bool rpc_read_batch(const uint64_t* addrs, const uint32_t* lens, + uint32_t count, uint8_t* outBuf) + { + auto* hdr = (RcxRpcHeader*)view; + auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET; + + hdr->command = RPC_CMD_READ_BATCH; + hdr->requestCount = count; + hdr->status = RCX_RPC_STATUS_OK; + + /* lay out entries, then data offsets after all entries */ + uint32_t entriesSize = count * (uint32_t)sizeof(RcxRpcReadEntry); + uint32_t dataOff = entriesSize; + + for (uint32_t i = 0; i < count; ++i) { + auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry)); + e->address = addrs[i]; + e->length = lens[i]; + e->dataOffset = dataOff; + dataOff += lens[i]; + } + + if (!signalAndWait()) return false; + + /* copy out response data */ + uint32_t off = 0; + for (uint32_t i = 0; i < count; ++i) { + auto* e = (RcxRpcReadEntry*)(data + i * sizeof(RcxRpcReadEntry)); + memcpy(outBuf + off, data + e->dataOffset, e->length); + off += e->length; + } + return true; + } + + bool rpc_write(uint64_t addr, const void* buf, uint32_t len) + { + auto* hdr = (RcxRpcHeader*)view; + auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET; + + hdr->command = RPC_CMD_WRITE; + hdr->writeAddress = addr; + hdr->writeLength = len; + hdr->status = RCX_RPC_STATUS_OK; + memcpy(data, buf, len); + + if (!signalAndWait()) return false; + return hdr->status == RCX_RPC_STATUS_OK; + } + + struct ModInfo { uint64_t base; uint64_t size; char name[256]; }; + + int rpc_enum_modules(ModInfo* out, int maxOut) + { + auto* hdr = (RcxRpcHeader*)view; + auto* data = (uint8_t*)view + RCX_RPC_DATA_OFFSET; + + hdr->command = RPC_CMD_ENUM_MODULES; + hdr->status = RCX_RPC_STATUS_OK; + + if (!signalAndWait()) return -1; + if (hdr->status != RCX_RPC_STATUS_OK) return -1; + + int count = (int)hdr->responseCount; + if (count > maxOut) count = maxOut; + + for (int i = 0; i < count; ++i) { + auto* entry = (RcxRpcModuleEntry*)(data + i * sizeof(RcxRpcModuleEntry)); + out[i].base = entry->base; + out[i].size = entry->size; +#ifdef _WIN32 + /* names are UTF-16 on Windows */ + int wchars = (int)(entry->nameLength / sizeof(wchar_t)); + WideCharToMultiByte(CP_UTF8, 0, + (const wchar_t*)(data + entry->nameOffset), wchars, + out[i].name, 255, nullptr, nullptr); + out[i].name[255] = '\0'; +#else + int nLen = (int)entry->nameLength; + if (nLen > 255) nLen = 255; + memcpy(out[i].name, data + entry->nameOffset, nLen); + out[i].name[nLen] = '\0'; +#endif + } + return count; + } + + void rpc_shutdown() + { + auto* hdr = (RcxRpcHeader*)view; + hdr->command = RPC_CMD_SHUTDOWN; + hdr->status = RCX_RPC_STATUS_OK; + signalAndWait(500); + } +}; + +/* ══════════════════════════════════════════════════════════════════════ + * Auto-spawn host + * ══════════════════════════════════════════════════════════════════════ */ + +#ifdef _WIN32 +static HANDLE g_hostProcess = nullptr; +#else +static pid_t g_hostPid = 0; +#endif +static FILE* g_hostPipe = nullptr; + +static bool spawn_host(uint32_t* outPid, char* outNonce, + uint64_t* outTestBuf, uint32_t* outTestLen) +{ + /* resolve path to test_rpc_host next to ourselves */ + char cmd[2048]; +#ifdef _WIN32 + char exePath[MAX_PATH]; + GetModuleFileNameA(nullptr, exePath, MAX_PATH); + char* slash = strrchr(exePath, '\\'); + if (!slash) slash = strrchr(exePath, '/'); + if (slash) *(slash + 1) = '\0'; + snprintf(cmd, sizeof(cmd), "\"%stest_rpc_host.exe\" autotest", exePath); + g_hostPipe = _popen(cmd, "r"); +#else + char exePath[PATH_MAX]; + ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (n <= 0) return false; + exePath[n] = '\0'; + char* dir = dirname(exePath); + snprintf(cmd, sizeof(cmd), "%s/test_rpc_host autotest", dir); + g_hostPipe = popen(cmd, "r"); +#endif + if (!g_hostPipe) { + fprintf(stderr, "ERROR: cannot spawn host: %s\n", cmd); + return false; + } + + /* read READY line */ + char line[512]; + if (!fgets(line, sizeof(line), g_hostPipe)) { + fprintf(stderr, "ERROR: no output from host\n"); + return false; + } + + /* parse: READY pid=X nonce=Y testbuf=0xZ testlen=N */ + unsigned long long tbuf = 0; + unsigned tlen = 0; + if (sscanf(line, "READY pid=%u nonce=%63s testbuf=0x%llx testlen=%u", + outPid, outNonce, &tbuf, &tlen) < 2) { + fprintf(stderr, "ERROR: cannot parse host output: %s\n", line); + return false; + } + *outTestBuf = (uint64_t)tbuf; + *outTestLen = (uint32_t)tlen; + return true; +} + +static void cleanup_host() +{ + if (g_hostPipe) { +#ifdef _WIN32 + _pclose(g_hostPipe); +#else + pclose(g_hostPipe); +#endif + g_hostPipe = nullptr; + } +} + +/* ══════════════════════════════════════════════════════════════════════ + * Printing helpers + * ══════════════════════════════════════════════════════════════════════ */ + +static void print_pass(const char* name) { printf(" [PASS] %s\n", name); } +static void print_fail(const char* name) { printf(" [FAIL] %s\n", name); exit(1); } + +/* ══════════════════════════════════════════════════════════════════════ + * main + * ══════════════════════════════════════════════════════════════════════ */ + +int main(int argc, char** argv) +{ + uint32_t pid = 0; + char nonce[64] = {}; + uint64_t testBuf = 0; + uint32_t testLen = 0; + bool autoMode = false; + + if (argc >= 3) { + pid = (uint32_t)atoi(argv[1]); + strncpy(nonce, argv[2], 63); + if (argc >= 5) { + testBuf = (uint64_t)strtoull(argv[3], nullptr, 0); + testLen = (uint32_t)atoi(argv[4]); + } + } else { + autoMode = true; + printf("Auto-spawning test_rpc_host...\n"); + if (!spawn_host(&pid, nonce, &testBuf, &testLen)) return 1; + } + + printf("Connecting to PID=%u nonce=%s testbuf=0x%llx testlen=%u\n\n", + pid, nonce, (unsigned long long)testBuf, testLen); + + /* ── connect ── */ + TestIpcClient ipc; + if (!ipc.connect(pid, nonce)) { + fprintf(stderr, "ERROR: IPC connect failed\n"); + if (autoMode) cleanup_host(); + return 1; + } + printf("=== Functional Tests ===\n"); + + /* ── test: ping ── */ + if (ipc.rpc_ping()) print_pass("Ping"); + else print_fail("Ping"); + + /* ── test: enumerate modules ── */ + TestIpcClient::ModInfo mods[512]; + int modCount = ipc.rpc_enum_modules(mods, 512); + if (modCount > 0) { + printf(" [PASS] EnumModules (%d modules)\n", modCount); + printf(" first: %s base=0x%llx size=0x%llx\n", + mods[0].name, + (unsigned long long)mods[0].base, + (unsigned long long)mods[0].size); + } else { + print_fail("EnumModules"); + } + + /* ── test: read module header (MZ / ELF magic) ── */ + if (modCount > 0) { + uint8_t header[4] = {}; + if (ipc.rpc_read(mods[0].base, header, 4)) { +#ifdef _WIN32 + if (header[0] == 'M' && header[1] == 'Z') + print_pass("ReadModuleHeader (MZ)"); + else + print_fail("ReadModuleHeader (expected MZ)"); +#else + if (header[0] == 0x7F && header[1] == 'E' && + header[2] == 'L' && header[3] == 'F') + print_pass("ReadModuleHeader (ELF)"); + else + print_fail("ReadModuleHeader (expected ELF)"); +#endif + } else { + print_fail("ReadModuleHeader (read failed)"); + } + } + + /* ── test: read test buffer (known pattern) ── */ + if (testBuf && testLen >= 4096) { + uint8_t buf[4096]; + if (ipc.rpc_read(testBuf, buf, 4096)) { + bool good = true; + for (int i = 0; i < 4096; ++i) { + if (buf[i] != (uint8_t)(i & 0xFF)) { good = false; break; } + } + if (good) print_pass("ReadTestBuffer (4096 bytes, pattern verified)"); + else print_fail("ReadTestBuffer (pattern mismatch)"); + } else { + print_fail("ReadTestBuffer (read failed)"); + } + } + + /* ── test: write ── */ + if (testBuf && testLen >= 16) { + uint8_t patch[4] = {0xDE, 0xAD, 0xBE, 0xEF}; + if (ipc.rpc_write(testBuf, patch, 4)) { + uint8_t verify[4] = {}; + ipc.rpc_read(testBuf, verify, 4); + if (memcmp(verify, patch, 4) == 0) + print_pass("Write + ReadBack (0xDEADBEEF)"); + else + print_fail("Write + ReadBack (readback mismatch)"); + } else { + print_fail("Write (write failed)"); + } + } + + /* ── test: batch read ── */ + if (testBuf && testLen >= 8192) { + const uint32_t N = 4; + uint64_t addrs[N]; + uint32_t lens[N]; + for (uint32_t i = 0; i < N; ++i) { + addrs[i] = testBuf + i * 1024; + lens[i] = 1024; + } + uint8_t out[4096]; + if (ipc.rpc_read_batch(addrs, lens, N, out)) { + print_pass("BatchRead (4 x 1024 bytes)"); + } else { + print_fail("BatchRead"); + } + } + + printf("\n=== Benchmarks ===\n"); + + /* choose a valid address for benchmarking */ + uint64_t benchAddr = testBuf ? testBuf : (modCount > 0 ? mods[0].base : 0); + if (!benchAddr) { + printf(" (no valid address for benchmarks, skipping)\n"); + } else { + + /* ── benchmark: single 4 KB reads ── */ + { + const int ITERS = 10000; + const int PAGE = 4096; + uint8_t tmp[4096]; + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < ITERS; ++i) + ipc.rpc_read(benchAddr, tmp, PAGE); + auto t1 = std::chrono::high_resolution_clock::now(); + + double us = (double)std::chrono::duration_cast(t1 - t0).count(); + double secs = us / 1e6; + double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0); + + printf(" Single 4 KB reads:\n"); + printf(" Iterations : %d\n", ITERS); + printf(" Total data : %.2f MB\n", totalMB); + printf(" Wall time : %.3f s\n", secs); + printf(" Throughput : %.2f MB/s\n", totalMB / secs); + printf(" Avg latency: %.2f us/read\n", us / ITERS); + } + + /* ── benchmark: single 64 B reads (pointer-chase-size) ── */ + { + const int ITERS = 50000; + const int SZ = 64; + uint8_t tmp[64]; + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < ITERS; ++i) + ipc.rpc_read(benchAddr, tmp, SZ); + auto t1 = std::chrono::high_resolution_clock::now(); + + double us = (double)std::chrono::duration_cast(t1 - t0).count(); + double secs = us / 1e6; + double totalKB = (double)ITERS * SZ / 1024.0; + + printf(" Single 64 B reads (pointer-chase):\n"); + printf(" Iterations : %d\n", ITERS); + printf(" Total data : %.2f KB\n", totalKB); + printf(" Wall time : %.3f s\n", secs); + printf(" Throughput : %.2f KB/s\n", totalKB / secs); + printf(" Avg latency: %.2f us/read\n", us / ITERS); + } + + /* ── benchmark: batch read (50 x 4 KB, simulating refresh) ── */ + { + const int ITERS = 2000; + const uint32_t BATCH = 50; + const uint32_t PAGE = 4096; + + uint64_t addrs[BATCH]; + uint32_t lens[BATCH]; + for (uint32_t i = 0; i < BATCH; ++i) { + /* wrap within test buffer or module */ + addrs[i] = benchAddr + (i * PAGE) % 65536; + lens[i] = PAGE; + } + + /* allocate response buffer */ + uint8_t* outBuf = (uint8_t*)malloc(BATCH * PAGE); + if (!outBuf) { + printf(" (batch malloc failed, skipping)\n"); + } else { + auto t0 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < ITERS; ++i) + ipc.rpc_read_batch(addrs, lens, BATCH, outBuf); + auto t1 = std::chrono::high_resolution_clock::now(); + + double us = (double)std::chrono::duration_cast(t1 - t0).count(); + double secs = us / 1e6; + double totalMB = (double)ITERS * BATCH * PAGE / (1024.0 * 1024.0); + + printf(" Batch read (%u x %u B, simulating refresh):\n", BATCH, PAGE); + printf(" Iterations : %d\n", ITERS); + printf(" Total data : %.2f MB\n", totalMB); + printf(" Wall time : %.3f s\n", secs); + printf(" Throughput : %.2f MB/s\n", totalMB / secs); + printf(" Avg latency: %.2f us/batch\n", us / ITERS); + printf(" Per-page : %.2f us/page\n", us / (ITERS * BATCH)); + + free(outBuf); + } + } + + /* ── benchmark: write 4 KB ── */ + if (testBuf && testLen >= 4096) { + const int ITERS = 10000; + const int PAGE = 4096; + uint8_t tmp[4096]; + memset(tmp, 0x42, sizeof(tmp)); + + auto t0 = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < ITERS; ++i) + ipc.rpc_write(testBuf, tmp, PAGE); + auto t1 = std::chrono::high_resolution_clock::now(); + + double us = (double)std::chrono::duration_cast(t1 - t0).count(); + double secs = us / 1e6; + double totalMB = (double)ITERS * PAGE / (1024.0 * 1024.0); + + printf(" Write 4 KB:\n"); + printf(" Iterations : %d\n", ITERS); + printf(" Total data : %.2f MB\n", totalMB); + printf(" Wall time : %.3f s\n", secs); + printf(" Throughput : %.2f MB/s\n", totalMB / secs); + printf(" Avg latency: %.2f us/write\n", us / ITERS); + } + } + + /* ── shutdown ── */ + printf("\nSending shutdown...\n"); + ipc.rpc_shutdown(); + ipc.disconnect(); + + if (autoMode) { + /* wait for host to exit */ +#ifdef _WIN32 + Sleep(500); +#else + usleep(500000); +#endif + cleanup_host(); + } + + printf("Done.\n"); + return 0; +} diff --git a/plugins/RemoteProcessMemory/tests/test_rpc_host.cpp b/plugins/RemoteProcessMemory/tests/test_rpc_host.cpp new file mode 100644 index 0000000..04e87d5 --- /dev/null +++ b/plugins/RemoteProcessMemory/tests/test_rpc_host.cpp @@ -0,0 +1,224 @@ +/* + * test_rpc_host -- loads rcx_payload in-process, acts as the "target". + * + * Usage: test_rpc_host [nonce] + * + * Prints a READY line (machine-parseable), then waits for the payload + * to shut down (RPC_CMD_SHUTDOWN from the client). + */ + +#include "../rcx_rpc_protocol.h" + +#include +#include +#include +#include + +#ifdef _WIN32 +# define WIN32_LEAN_AND_MEAN +# include +#else +# include +# include +# include +# include +# include +# include +# include +#endif + +/* ── Helpers ──────────────────────────────────────────────────────── */ + +static uint32_t current_pid() +{ +#ifdef _WIN32 + return (uint32_t)GetCurrentProcessId(); +#else + return (uint32_t)getpid(); +#endif +} + +static void sleep_ms(int ms) +{ +#ifdef _WIN32 + Sleep((DWORD)ms); +#else + usleep((useconds_t)ms * 1000); +#endif +} + +/* Resolve payload path relative to this executable */ +static int payload_path(char* out, int outLen) +{ +#ifdef _WIN32 + char exePath[MAX_PATH]; + GetModuleFileNameA(nullptr, exePath, MAX_PATH); + char* slash = strrchr(exePath, '\\'); + if (!slash) slash = strrchr(exePath, '/'); + if (slash) *(slash + 1) = '\0'; + snprintf(out, outLen, "%srcx_payload.dll", exePath); +#else + char exePath[PATH_MAX]; + ssize_t n = readlink("/proc/self/exe", exePath, sizeof(exePath) - 1); + if (n <= 0) return -1; + exePath[n] = '\0'; + char* dir = dirname(exePath); + snprintf(out, outLen, "%s/rcx_payload.so", dir); +#endif + return 0; +} + +/* Create bootstrap shared memory with the nonce */ +static int create_bootstrap(uint32_t pid, const char* nonce) +{ + char bootName[128]; + rcx_rpc_boot_name(bootName, sizeof(bootName), pid); + +#ifdef _WIN32 + HANDLE h = CreateFileMappingA(INVALID_HANDLE_VALUE, nullptr, + PAGE_READWRITE, 0, RCX_RPC_BOOT_SIZE, bootName); + if (!h) return -1; + void* v = MapViewOfFile(h, FILE_MAP_WRITE, 0, 0, RCX_RPC_BOOT_SIZE); + if (!v) { CloseHandle(h); return -1; } + + RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v; + memset(boot, 0, RCX_RPC_BOOT_SIZE); + boot->nonceLength = (uint32_t)strlen(nonce); + strncpy(boot->nonce, nonce, 59); + + UnmapViewOfFile(v); + /* keep h open for payload to read */ + return 0; +#else + int fd = shm_open(bootName, O_CREAT | O_RDWR, 0600); + if (fd < 0) return -1; + if (ftruncate(fd, RCX_RPC_BOOT_SIZE) != 0) { close(fd); return -1; } + void* v = mmap(nullptr, RCX_RPC_BOOT_SIZE, PROT_READ | PROT_WRITE, + MAP_SHARED, fd, 0); + close(fd); + if (v == MAP_FAILED) return -1; + + RcxRpcBootHeader* boot = (RcxRpcBootHeader*)v; + memset(boot, 0, RCX_RPC_BOOT_SIZE); + boot->nonceLength = (uint32_t)strlen(nonce); + strncpy(boot->nonce, nonce, 59); + munmap(v, RCX_RPC_BOOT_SIZE); + return 0; +#endif +} + +/* Open the main shared memory (read-only, just to monitor payloadReady) */ +static void* open_main_shm(uint32_t pid, const char* nonce) +{ + char shmName[128]; + rcx_rpc_shm_name(shmName, sizeof(shmName), pid, nonce); + +#ifdef _WIN32 + HANDLE h = nullptr; + for (int i = 0; i < 500; ++i) { + h = OpenFileMappingA(FILE_MAP_READ, FALSE, shmName); + if (h) break; + sleep_ms(10); + } + if (!h) return nullptr; + void* v = MapViewOfFile(h, FILE_MAP_READ, 0, 0, sizeof(RcxRpcHeader)); + return v; +#else + int fd = -1; + for (int i = 0; i < 500; ++i) { + fd = shm_open(shmName, O_RDONLY, 0); + if (fd >= 0) break; + sleep_ms(10); + } + if (fd < 0) return nullptr; + void* v = mmap(nullptr, sizeof(RcxRpcHeader), PROT_READ, MAP_SHARED, fd, 0); + close(fd); + return (v == MAP_FAILED) ? nullptr : v; +#endif +} + +/* ── Test buffer (known pattern for client to verify reads/writes) ── */ +static uint8_t g_testBuf[65536]; + +/* ── main ─────────────────────────────────────────────────────────── */ + +int main(int argc, char** argv) +{ + const char* nonce = (argc > 1) ? argv[1] : "test0001"; + uint32_t pid = current_pid(); + + /* fill test buffer with known pattern */ + for (int i = 0; i < (int)sizeof(g_testBuf); ++i) + g_testBuf[i] = (uint8_t)(i & 0xFF); + + /* create bootstrap shm */ + if (create_bootstrap(pid, nonce) != 0) { + fprintf(stderr, "ERROR: failed to create bootstrap shm\n"); + return 1; + } + + /* load payload */ + char plPath[1024]; + if (payload_path(plPath, sizeof(plPath)) != 0) { + fprintf(stderr, "ERROR: cannot determine payload path\n"); + return 1; + } + +#ifdef _WIN32 + HMODULE hPayload = LoadLibraryA(plPath); + if (!hPayload) { + fprintf(stderr, "ERROR: LoadLibrary(%s) failed (%lu)\n", + plPath, GetLastError()); + return 1; + } +#else + void* hPayload = dlopen(plPath, RTLD_NOW); + if (!hPayload) { + fprintf(stderr, "ERROR: dlopen(%s): %s\n", plPath, dlerror()); + return 1; + } +#endif + + /* open main shm and wait for payloadReady */ + void* shmView = open_main_shm(pid, nonce); + if (!shmView) { + fprintf(stderr, "ERROR: failed to open main shared memory\n"); + return 1; + } + + RcxRpcHeader* hdr = (RcxRpcHeader*)shmView; + for (int i = 0; i < 500; ++i) { + if (hdr->payloadReady) break; + sleep_ms(10); + } + if (!hdr->payloadReady) { + fprintf(stderr, "ERROR: payload did not become ready\n"); + return 1; + } + + /* print READY line for the client to parse */ + printf("READY pid=%u nonce=%s testbuf=0x%llx testlen=%u\n", + pid, nonce, + (unsigned long long)(uintptr_t)g_testBuf, + (unsigned)sizeof(g_testBuf)); + fflush(stdout); + + /* wait until payload shuts down */ + while (hdr->payloadReady) + sleep_ms(100); + + printf("Payload shut down, exiting.\n"); + +#ifdef _WIN32 + /* give the server thread a moment to exit */ + Sleep(200); + FreeLibrary(hPayload); + if (shmView) UnmapViewOfFile(shmView); +#else + usleep(200000); + dlclose(hPayload); + if (shmView) munmap(shmView, sizeof(RcxRpcHeader)); +#endif + + return 0; +} diff --git a/src/controller.cpp b/src/controller.cpp index 848dcb2..b2ec472 100644 --- a/src/controller.cpp +++ b/src/controller.cpp @@ -2302,8 +2302,7 @@ void RcxController::attachViaPlugin(const QString& providerIdentifier, const QSt m_doc->undoStack.clear(); m_doc->provider = std::move(provider); m_doc->dataPath.clear(); - if (m_doc->tree.baseAddress == 0) - m_doc->tree.baseAddress = newBase; + m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress; // Re-evaluate stored formula against the new provider if (!m_doc->tree.baseAddressFormula.isEmpty()) { @@ -2352,6 +2351,9 @@ void RcxController::switchToSavedSource(int idx) { // Restore formula before attach so it can be re-evaluated against the new provider m_doc->tree.baseAddressFormula = entry.baseAddressFormula; attachViaPlugin(entry.kind, entry.providerTarget); + // Restore saved base address (user may have navigated away from provider default) + if (entry.baseAddress != 0 && entry.baseAddressFormula.isEmpty()) + m_doc->tree.baseAddress = entry.baseAddress; } } @@ -2421,8 +2423,7 @@ void RcxController::selectSource(const QString& text) { m_doc->undoStack.clear(); m_doc->provider = std::move(provider); m_doc->dataPath.clear(); - if (m_doc->tree.baseAddress == 0) - m_doc->tree.baseAddress = newBase; + m_doc->tree.baseAddress = (newBase != 0) ? newBase : m_doc->tree.baseAddress; resetSnapshot(); emit m_doc->documentChanged(); diff --git a/src/editor.cpp b/src/editor.cpp index d720718..152f440 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -909,7 +909,7 @@ void RcxEditor::reformatMargins() { // Place offset in the parent's indent slot (one level above the field's own indent) // so the field's own 3-char indent acts as visual separator from the type column int col = kFoldCol + (lm.depth - 2) * 3; - int slotWidth = 3; + int slotWidth = 5; auto pos = [&](int c) -> long { return m_sci->SendScintilla(QsciScintillaBase::SCI_FINDCOLUMN, @@ -1756,8 +1756,8 @@ bool RcxEditor::eventFilter(QObject* obj, QEvent* event) { } commitInlineEdit(); - m_currentSelIds.clear(); // stale — normal handler will re-establish - // Fall through to normal click handler below + m_currentSelIds.clear(); + return true; // consume — metadata was recomposed; stale coords unsafe } // Single-click on fold column (" - " / " + ") toggles fold // Other left-clicks emit nodeClicked for selection diff --git a/src/main.cpp b/src/main.cpp index 5545b35..a40970e 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -97,6 +97,53 @@ static LONG WINAPI crashHandler(EXCEPTION_POINTERS* ep) { #endif fflush(stderr); + // Phase 1.5: write a full minidump next to the executable + { + // Build dump path: /reclass_crash_.dmp + wchar_t exePath[MAX_PATH] = {}; + GetModuleFileNameW(NULL, exePath, MAX_PATH); + // Strip exe filename to get directory + wchar_t* lastSlash = wcsrchr(exePath, L'\\'); + if (lastSlash) *(lastSlash + 1) = L'\0'; + + SYSTEMTIME st; + GetLocalTime(&st); + wchar_t dumpPath[MAX_PATH]; + _snwprintf(dumpPath, MAX_PATH, + L"%sreclass_crash_%04d%02d%02d_%02d%02d%02d.dmp", + exePath, st.wYear, st.wMonth, st.wDay, + st.wHour, st.wMinute, st.wSecond); + + HANDLE hFile = CreateFileW(dumpPath, GENERIC_WRITE, 0, NULL, + CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (hFile != INVALID_HANDLE_VALUE) { + MINIDUMP_EXCEPTION_INFORMATION mei; + mei.ThreadId = GetCurrentThreadId(); + mei.ExceptionPointers = ep; + mei.ClientPointers = FALSE; + + // MiniDumpWithFullMemory: captures entire process address space + // so we can inspect all heap objects, Qt state, node trees, etc. + BOOL ok = MiniDumpWriteDump( + GetCurrentProcess(), GetCurrentProcessId(), hFile, + static_cast(MiniDumpWithFullMemory + | MiniDumpWithHandleData + | MiniDumpWithThreadInfo + | MiniDumpWithUnloadedModules), + &mei, NULL, NULL); + CloseHandle(hFile); + + if (ok) { + fprintf(stderr, "Dump : %ls\n", dumpPath); + } else { + fprintf(stderr, "Dump : FAILED (error %lu)\n", GetLastError()); + } + } else { + fprintf(stderr, "Dump : could not create file (error %lu)\n", GetLastError()); + } + fflush(stderr); + } + // Phase 2: attempt symbol resolution + stack walk // Copy context so StackWalk64 can mutate it safely CONTEXT ctxCopy = *ep->ContextRecord; @@ -689,9 +736,11 @@ private: label->setGeometry(tw + 1 + gutter, 0, qMax(0, width() - (tw + 1 + gutter)), h); - // Shared baseline so tab text and status text align + // Shared baseline so tab text and status text align. + // Nudge up by half the accent-line height so text centres + // in the visible area below the accent bar, not in the full bar. QFontMetrics fm(font()); - int by = (h + fm.ascent()) / 2; + int by = (h + fm.ascent()) / 2 - (ViewTabButton::kAccentH + 1) / 2; // Push baseline to buttons auto* lay = tabRow->layout(); @@ -1136,6 +1185,7 @@ void MainWindow::selfTest() { // Attach process memory to self — provider base will be set to the editor address DWORD pid = GetCurrentProcessId(); QString target = QString("%1:Reclass.exe").arg(pid); + ctrl->attachViaPlugin(QStringLiteral("processmemory"), target); #else project_new(); @@ -2222,14 +2272,31 @@ void MainWindow::populateSourceMenu() { m_sourceMenu->clear(); auto* ctrl = activeController(); - m_sourceMenu->addAction("File", this, [this]() { + // Icon map for known provider identifiers + static const QHash 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")}, + }; + + m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/file-binary.svg")), + QStringLiteral("File"), this, [this]() { if (auto* c = activeController()) c->selectSource(QStringLiteral("File")); }); const auto& providers = ProviderRegistry::instance().providers(); for (const auto& prov : providers) { QString name = prov.name; - m_sourceMenu->addAction(name, this, [this, name]() { + auto it = s_providerIcons.constFind(prov.identifier); + QIcon icon(it != s_providerIcons.constEnd() ? *it + : QStringLiteral(":/vsicons/extensions.svg")); + + QString label = prov.dllFileName.isEmpty() + ? name + : QStringLiteral("%1 (%2)").arg(name, prov.dllFileName); + + m_sourceMenu->addAction(icon, label, this, [this, name]() { if (auto* c = activeController()) c->selectSource(name); }); } @@ -2247,7 +2314,8 @@ void MainWindow::populateSourceMenu() { act->setChecked(i == ctrl->activeSourceIndex()); } m_sourceMenu->addSeparator(); - m_sourceMenu->addAction("Clear All", this, [this]() { + m_sourceMenu->addAction(QIcon(QStringLiteral(":/vsicons/clear-all.svg")), + QStringLiteral("Clear All"), this, [this]() { if (auto* c = activeController()) c->clearSources(); }); } diff --git a/src/pluginmanager.cpp b/src/pluginmanager.cpp index 3c650be..8aa42e8 100644 --- a/src/pluginmanager.cpp +++ b/src/pluginmanager.cpp @@ -92,7 +92,8 @@ bool PluginManager::LoadPlugin(const QString& path) IProviderPlugin* provider = static_cast(plugin); QString name = QString::fromStdString(plugin->Name()); QString identifier = name.toLower().replace(" ", ""); - ProviderRegistry::instance().registerProvider(name, identifier, provider); + QString dllFileName = QFileInfo(path).fileName(); + ProviderRegistry::instance().registerProvider(name, identifier, provider, dllFileName); } return true; diff --git a/src/providerregistry.cpp b/src/providerregistry.cpp index 3f22af8..242cc83 100644 --- a/src/providerregistry.cpp +++ b/src/providerregistry.cpp @@ -6,7 +6,8 @@ ProviderRegistry& ProviderRegistry::instance() { return s_instance; } -void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin) { +void ProviderRegistry::registerProvider(const QString& name, const QString& identifier, + IProviderPlugin* plugin, const QString& dllFileName) { // Check if already registered for (const auto& info : m_providers) { if (info.identifier == identifier) { @@ -14,8 +15,8 @@ void ProviderRegistry::registerProvider(const QString& name, const QString& iden return; } } - - m_providers.append(ProviderInfo(name, identifier, plugin)); + + m_providers.append(ProviderInfo(name, identifier, plugin, dllFileName)); qDebug() << "ProviderRegistry: Registered plugin provider:" << name << "(" << identifier << ")"; } diff --git a/src/providerregistry.h b/src/providerregistry.h index 31401f6..9c13f4b 100644 --- a/src/providerregistry.h +++ b/src/providerregistry.h @@ -25,10 +25,13 @@ public: IProviderPlugin* plugin; // Plugin (if plugin-based) BuiltinFactory factory; // Factory (if built-in) bool isBuiltin; - - ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p) - : name(n), identifier(id), plugin(p), factory(nullptr), isBuiltin(false) {} - + QString dllFileName; // Original DLL/SO filename (plugin-based only) + + ProviderInfo(const QString& n, const QString& id, IProviderPlugin* p, + const QString& dll = {}) + : name(n), identifier(id), plugin(p), factory(nullptr), + isBuiltin(false), dllFileName(dll) {} + ProviderInfo(const QString& n, const QString& id, BuiltinFactory f) : name(n), identifier(id), plugin(nullptr), factory(f), isBuiltin(true) {} }; @@ -36,7 +39,8 @@ public: static ProviderRegistry& instance(); // Register a plugin-based provider - void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin); + void registerProvider(const QString& name, const QString& identifier, IProviderPlugin* plugin, + const QString& dllFileName = {}); // Register a built-in provider with a factory function void registerBuiltinProvider(const QString& name, const QString& identifier, BuiltinFactory factory); diff --git a/src/resources.qrc b/src/resources.qrc index 6db63dc..830ef7a 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -51,5 +51,9 @@ vsicons/chevron-down.svg vsicons/folder.svg vsicons/symbol-enum.svg + vsicons/server-process.svg + vsicons/remote.svg + vsicons/plug.svg + vsicons/clear-all.svg