mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
- Add pointerSize() to Provider base; WoW64/ELF detection in ProcessMemory, WinDbg, and RemoteProcessMemory plugins - Wire pointer size through NodeTree, source/XML imports, C++ generator, controller, compose, address parser, and RPC protocol header - Add is32Bit to PluginProcessInfo and ProcessInfo; show (32-bit) in picker - Scanner rescan now filters results against the current input value - Go-to-address from scanner resets change tracking to prevent false flashing
940 lines
33 KiB
C++
940 lines
33 KiB
C++
#include "RemoteProcessMemoryPlugin.h"
|
|
#include "rcx_rpc_protocol.h"
|
|
#include "../../src/processpicker.h"
|
|
|
|
#include <QStyle>
|
|
#include <QApplication>
|
|
#include <QMessageBox>
|
|
#include <QPushButton>
|
|
#include <QDir>
|
|
#include <QFileInfo>
|
|
#include <QPixmap>
|
|
#include <QImage>
|
|
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
|
|
#include <QtWin>
|
|
#endif
|
|
|
|
#ifdef _WIN32
|
|
# define WIN32_LEAN_AND_MEAN
|
|
# include <windows.h>
|
|
# include <tlhelp32.h>
|
|
# include <psapi.h>
|
|
# include <shellapi.h>
|
|
#else
|
|
# include <unistd.h>
|
|
# include <fcntl.h>
|
|
# include <dlfcn.h>
|
|
# include <sys/mman.h>
|
|
# include <sys/wait.h>
|
|
# include <sys/ptrace.h>
|
|
# include <sys/user.h>
|
|
# include <semaphore.h>
|
|
# include <signal.h>
|
|
# include <link.h>
|
|
# include <climits>
|
|
# include <cstring>
|
|
# include <fstream>
|
|
# include <sstream>
|
|
#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;
|
|
|
|
RcxRpcHeader* header() const {
|
|
return mappedView ? reinterpret_cast<RcxRpcHeader*>(mappedView) : nullptr;
|
|
}
|
|
|
|
~IpcClient() { disconnect(); }
|
|
|
|
/* ── connect / disconnect ──────────────────────────────────────── */
|
|
|
|
bool connect(uint32_t pid, int timeoutMs = 5000)
|
|
{
|
|
char shmName[128], reqName[128], rspName[128];
|
|
rcx_rpc_shm_name(shmName, sizeof(shmName), pid);
|
|
rcx_rpc_req_name(reqName, sizeof(reqName), pid);
|
|
rcx_rpc_rsp_name(rspName, sizeof(rspName), pid);
|
|
|
|
#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::milliseconds>(
|
|
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<RcxRpcHeader*>(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::milliseconds>(
|
|
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<RcxRpcHeader*>(mappedView);
|
|
auto* data = static_cast<uint8_t*>(mappedView) + RCX_RPC_DATA_OFFSET;
|
|
|
|
hdr->command = RPC_CMD_READ_BATCH;
|
|
hdr->requestCount = 1;
|
|
hdr->status = RCX_RPC_STATUS_OK;
|
|
|
|
auto* entry = reinterpret_cast<RcxRpcReadEntry*>(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<RcxRpcHeader*>(mappedView);
|
|
auto* data = static_cast<uint8_t*>(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<RemoteProcessProvider::ModuleInfo> enumerateModules()
|
|
{
|
|
QVector<RemoteProcessProvider::ModuleInfo> result;
|
|
QMutexLocker lock(&mutex);
|
|
if (!connected) return result;
|
|
|
|
auto* hdr = static_cast<RcxRpcHeader*>(mappedView);
|
|
auto* data = static_cast<uint8_t*>(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<const RcxRpcModuleEntry*>(
|
|
data + i * sizeof(RcxRpcModuleEntry));
|
|
|
|
QString modName;
|
|
#ifdef _WIN32
|
|
modName = QString::fromWCharArray(
|
|
reinterpret_cast<const wchar_t*>(data + entry->nameOffset),
|
|
(int)(entry->nameLength / sizeof(wchar_t)));
|
|
#else
|
|
modName = QString::fromUtf8(
|
|
reinterpret_cast<const char*>(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<RcxRpcHeader*>(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<RcxRpcHeader*>(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<IpcClient> ipc)
|
|
: m_pid(pid)
|
|
, m_processName(processName)
|
|
, m_connected(ipc && ipc->connected)
|
|
, m_base(0)
|
|
, m_ipc(std::move(ipc))
|
|
{
|
|
if (m_connected) {
|
|
cacheModules();
|
|
// Read pointer size from payload's SHM header (0 means not set → default 8)
|
|
auto* hdr = m_ipc ? m_ipc->header() : nullptr;
|
|
if (hdr) {
|
|
uint32_t ps = hdr->pointerSize;
|
|
if (ps == 4 || ps == 8)
|
|
m_pointerSize = (int)ps;
|
|
}
|
|
}
|
|
}
|
|
|
|
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<RemoteProcessProvider*>(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<LPCWSTR>(&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<void*>(&payloadPath), &info);
|
|
QFileInfo fi(QString::fromUtf8(info.dli_fname));
|
|
return fi.absolutePath() + QStringLiteral("/rcx_payload.so");
|
|
#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);
|
|
|
|
/* Step 1: LoadLibraryA — loads the DLL (DllMain is minimal) */
|
|
HMODULE hK32 = GetModuleHandleA("kernel32.dll");
|
|
auto pLoadLib = reinterpret_cast<LPTHREAD_START_ROUTINE>(
|
|
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);
|
|
|
|
DWORD exitCode = 0;
|
|
GetExitCodeThread(hThread, &exitCode);
|
|
CloseHandle(hThread);
|
|
|
|
VirtualFreeEx(hProc, remotePath, 0, MEM_RELEASE);
|
|
|
|
if (exitCode == 0) {
|
|
CloseHandle(hProc);
|
|
if (errorMsg) *errorMsg = QStringLiteral("LoadLibrary returned NULL in target.\n"
|
|
"Ensure rcx_payload.dll is in: %1").arg(path);
|
|
return false;
|
|
}
|
|
|
|
/* Step 2: Call RcxPayloadInit() — safe to create timer queues now
|
|
(loader lock is no longer held after LoadLibrary returned) */
|
|
HMODULE hPayloadRemote = (HMODULE)(uintptr_t)exitCode;
|
|
auto pGetProcAddr = reinterpret_cast<FARPROC(WINAPI*)(HMODULE, LPCSTR)>(
|
|
GetProcAddress(hK32, "GetProcAddress"));
|
|
|
|
/* Write "RcxPayloadInit\0" into target, call GetProcAddress remotely */
|
|
const char initName[] = "RcxPayloadInit";
|
|
void* remoteInitName = VirtualAllocEx(hProc, nullptr, sizeof(initName),
|
|
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
|
if (remoteInitName) {
|
|
WriteProcessMemory(hProc, remoteInitName, initName, sizeof(initName), nullptr);
|
|
|
|
/* We need to call GetProcAddress(hPayload, "RcxPayloadInit") then call the result.
|
|
Simpler approach: write small shellcode that does both calls. */
|
|
uint8_t shellcode[128];
|
|
int off = 0;
|
|
|
|
/* sub rsp, 40 ; shadow space + alignment */
|
|
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xEC; shellcode[off++] = 0x28;
|
|
/* mov rcx, hPayloadRemote ; first arg = module handle */
|
|
shellcode[off++] = 0x48; shellcode[off++] = 0xB9;
|
|
uint64_t hMod = (uint64_t)(uintptr_t)hPayloadRemote;
|
|
memcpy(shellcode + off, &hMod, 8); off += 8;
|
|
/* mov rdx, remoteInitName ; second arg = "RcxPayloadInit" */
|
|
shellcode[off++] = 0x48; shellcode[off++] = 0xBA;
|
|
uint64_t pName = (uint64_t)(uintptr_t)remoteInitName;
|
|
memcpy(shellcode + off, &pName, 8); off += 8;
|
|
/* mov rax, GetProcAddress */
|
|
shellcode[off++] = 0x48; shellcode[off++] = 0xB8;
|
|
uint64_t pGPA = (uint64_t)(uintptr_t)pGetProcAddr;
|
|
memcpy(shellcode + off, &pGPA, 8); off += 8;
|
|
/* call rax ; rax = RcxPayloadInit */
|
|
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
|
/* test rax, rax */
|
|
shellcode[off++] = 0x48; shellcode[off++] = 0x85; shellcode[off++] = 0xC0;
|
|
/* jz skip (jump over the call if null) */
|
|
shellcode[off++] = 0x74; shellcode[off++] = 0x02;
|
|
/* call rax ; RcxPayloadInit() */
|
|
shellcode[off++] = 0xFF; shellcode[off++] = 0xD0;
|
|
/* skip: add rsp, 40 */
|
|
shellcode[off++] = 0x48; shellcode[off++] = 0x83; shellcode[off++] = 0xC4; shellcode[off++] = 0x28;
|
|
/* ret */
|
|
shellcode[off++] = 0xC3;
|
|
|
|
void* remoteCode = VirtualAllocEx(hProc, nullptr, (SIZE_T)off,
|
|
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
|
|
if (remoteCode) {
|
|
WriteProcessMemory(hProc, remoteCode, shellcode, (SIZE_T)off, nullptr);
|
|
|
|
HANDLE hThread2 = CreateRemoteThread(hProc, nullptr, 0,
|
|
(LPTHREAD_START_ROUTINE)remoteCode, nullptr, 0, nullptr);
|
|
if (hThread2) {
|
|
WaitForSingleObject(hThread2, 10000);
|
|
CloseHandle(hThread2);
|
|
}
|
|
VirtualFreeEx(hProc, remoteCode, 0, MEM_RELEASE);
|
|
}
|
|
VirtualFreeEx(hProc, remoteInitName, 0, MEM_RELEASE);
|
|
}
|
|
|
|
CloseHandle(hProc);
|
|
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<const uint8_t*>(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<rcx::Provider>
|
|
RemoteProcessMemoryPlugin::createProvider(const QString& target, QString* errorMsg)
|
|
{
|
|
/* target = "rpm:{pid}:{name}" */
|
|
QStringList parts = target.split(':');
|
|
if (parts.size() < 3 || parts[0] != QStringLiteral("rpm")) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid target: ") + target;
|
|
return nullptr;
|
|
}
|
|
|
|
bool ok;
|
|
uint32_t pid = parts[1].toUInt(&ok);
|
|
QString name = parts.mid(2).join(':'); /* name may contain colons */
|
|
|
|
if (!ok || pid == 0) {
|
|
if (errorMsg) *errorMsg = QStringLiteral("Invalid PID in target.");
|
|
return nullptr;
|
|
}
|
|
|
|
auto ipc = getOrCreateConnection(pid, errorMsg);
|
|
if (!ipc) return nullptr;
|
|
|
|
return std::make_unique<RemoteProcessProvider>(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() < 2 || 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<const RcxRpcHeader*>((*it)->mappedView);
|
|
return hdr->imageBase;
|
|
}
|
|
|
|
bool RemoteProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
|
|
{
|
|
/* ── 1. pick a process ── */
|
|
QVector<PluginProcessInfo> pluginProcs = enumerateProcesses();
|
|
QList<ProcessInfo> 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) {
|
|
QString injectErr;
|
|
if (!injectPayload(pid, &injectErr)) {
|
|
QMessageBox::critical(parent, QStringLiteral("Injection Failed"), injectErr);
|
|
return false;
|
|
}
|
|
|
|
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
|
return true;
|
|
}
|
|
else if (clicked == connectBtn) {
|
|
*target = QStringLiteral("rpm:%1:%2").arg(pid).arg(name);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
QVector<PluginProcessInfo> RemoteProcessMemoryPlugin::enumerateProcesses()
|
|
{
|
|
QVector<PluginProcessInfo> 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<IpcClient>
|
|
RemoteProcessMemoryPlugin::getOrCreateConnection(
|
|
uint32_t pid, 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<IpcClient>();
|
|
if (!ipc->connect(pid)) {
|
|
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();
|
|
}
|