Merge remote-tracking branch 'origin/linux'

This commit is contained in:
sysadmin
2026-02-10 04:25:39 -07:00
8 changed files with 425 additions and 81 deletions

View File

@@ -45,9 +45,10 @@ target_link_libraries(ReclassX PRIVATE
Qt6::Svg Qt6::Svg
Qt6::Concurrent Qt6::Concurrent
QScintilla::QScintilla QScintilla::QScintilla
dbghelp
psapi
) )
if(WIN32)
target_link_libraries(ReclassX PRIVATE dbghelp psapi)
endif()
add_custom_target(screenshot ALL add_custom_target(screenshot ALL
COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png COMMAND ReclassX --screenshot ${CMAKE_BINARY_DIR}/screenshot.png
@@ -143,7 +144,10 @@ if(BUILD_TESTING)
target_include_directories(test_controller PRIVATE src) target_include_directories(test_controller PRIVATE src)
target_link_libraries(test_controller PRIVATE target_link_libraries(test_controller PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi) QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_controller PRIVATE dbghelp psapi)
endif()
add_test(NAME test_controller COMMAND test_controller) add_test(NAME test_controller COMMAND test_controller)
add_executable(test_validation tests/test_validation.cpp add_executable(test_validation tests/test_validation.cpp
@@ -153,7 +157,10 @@ if(BUILD_TESTING)
target_include_directories(test_validation PRIVATE src) target_include_directories(test_validation PRIVATE src)
target_link_libraries(test_validation PRIVATE target_link_libraries(test_validation PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi) QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_validation PRIVATE dbghelp psapi)
endif()
add_test(NAME test_validation COMMAND test_validation) add_test(NAME test_validation COMMAND test_validation)
add_executable(test_generator tests/test_generator.cpp add_executable(test_generator tests/test_generator.cpp
@@ -169,7 +176,10 @@ if(BUILD_TESTING)
target_include_directories(test_context_menu PRIVATE src) target_include_directories(test_context_menu PRIVATE src)
target_link_libraries(test_context_menu PRIVATE target_link_libraries(test_context_menu PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi) QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_context_menu PRIVATE dbghelp psapi)
endif()
add_test(NAME test_context_menu COMMAND test_context_menu) add_test(NAME test_context_menu COMMAND test_context_menu)
add_executable(test_rendered_view tests/test_rendered_view.cpp add_executable(test_rendered_view tests/test_rendered_view.cpp
@@ -187,7 +197,10 @@ if(BUILD_TESTING)
target_include_directories(test_new_features PRIVATE src) target_include_directories(test_new_features PRIVATE src)
target_link_libraries(test_new_features PRIVATE target_link_libraries(test_new_features PRIVATE
Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test Qt6::Widgets Qt6::PrintSupport Qt6::Concurrent Qt6::Test
QScintilla::QScintilla dbghelp psapi) QScintilla::QScintilla)
if(WIN32)
target_link_libraries(test_new_features PRIVATE dbghelp psapi)
endif()
add_test(NAME test_new_features COMMAND test_new_features) add_test(NAME test_new_features COMMAND test_new_features)
add_executable(test_type_selector tests/test_type_selector.cpp add_executable(test_type_selector tests/test_type_selector.cpp

View File

@@ -1,12 +1,12 @@
This tool helps you inspect raw bytes,and interpret those bytes as (int,float,struct, array, etc) instead of just hex. it supports ptrs/arrays so you can see the relationship between data. And help deduce paths to these classes from long pointer chains. Either modify the values inside the editor or export as .h to reuse these structures in your code. This tool helps you inspect raw bytes and interpret those as (int, float, struct, array, etc.) instead of just hex. It supports ptrs/arrays so you can see the relationship between data. And help deduce paths to these classes from long pointer chains. Either modify the values inside the editor or export as .h to reuse these structures in your code.
Video: https://github.com/IChooseYou/ReclassX/raw/main/video.mp4 Video: https://github.com/IChooseYou/ReclassX/raw/main/video.mp4
![screenshot](screenshot.png) ![screenshot](screenshot.png)
QScintilla (github: https://github.com/brCreate/QScintilla) happens to be a really nice fit for our project. QScintilla (github: https://github.com/brCreate/QScintilla) happens to be a really nice fit for our project.
We can treat parts of a line as a spans. It's very easy to implement inline editing. Additionally the syntax coloring and keyboard navigation were very useful also! We can treat parts of a line as a spans. It is very easy to implement inline editing. Additionally the syntax coloring and keyboard navigation were very useful also!
We hit some big milestones (first plugin+first linux build) however project is still currently a work in progress. Mainly developed/tested on windows (for now). We hit some big milestones (plugin support with a first POC plugihn, first working linux build) however project is still currently a work in progress. Mainly developed/tested on windows (for now).
## Build ## Build
@@ -22,7 +22,7 @@ We hit some big milestones (first plugin+first linux build) however project is s
cd ReclassX cd ReclassX
.\scripts\build_qscintilla.ps1 .\scripts\build_qscintilla.ps1
.\scripts\build.ps1 .\scripts\build.ps1
^ script above tries to detect autodetect Qt install (as we learned not every installs to C) ^ script above tries to autodetect Qt install (as we learned not everyone installs to C:/Qt/)
3. Manual Build 3. Manual Build

View File

@@ -26,8 +26,18 @@ add_library(ProcessMemoryPlugin SHARED ${PLUGIN_SOURCES})
# Link Qt # Link Qt
target_link_libraries(ProcessMemoryPlugin PRIVATE Qt6::Widgets) target_link_libraries(ProcessMemoryPlugin PRIVATE Qt6::Widgets)
# Platform-specific linking
if(WIN32)
target_link_libraries(ProcessMemoryPlugin PRIVATE psapi shell32)
endif()
# On Linux, hide all symbols by default so only RCX_PLUGIN_EXPORT-marked ones are exported
if(UNIX AND NOT APPLE)
target_compile_options(ProcessMemoryPlugin PRIVATE -fvisibility=hidden)
endif()
# Include directories # Include directories
target_include_directories(ProcessMemoryPlugin PRIVATE target_include_directories(ProcessMemoryPlugin PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/../../src ${CMAKE_CURRENT_SOURCE_DIR}/../../src
) )

View File

@@ -1,17 +1,40 @@
#include "ProcessMemoryPlugin.h" #include "ProcessMemoryPlugin.h"
#include "../../src/processpicker.h" #include "../../src/processpicker.h"
#include <QStyle> #include <QStyle>
#include <QApplication> #include <QApplication>
#include <QRegularExpression> #include <QRegularExpression>
#include <QMessageBox> #include <QMessageBox>
#include <QPixmap> #include <QPixmap>
#include <QImage> #include <QImage>
#include <QDir>
#include <QFileInfo>
#ifdef _WIN32
#include <windows.h>
#include <tlhelp32.h>
#include <psapi.h>
#include <shellapi.h>
#elif defined(__linux__)
#include <climits>
#include <sys/types.h>
#include <dirent.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/uio.h>
#include <fstream>
#include <sstream>
#include <cstring>
#endif
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryProvider implementation // ProcessMemoryProvider implementation
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
ProcessMemoryProvider::ProcessMemoryProvider(DWORD pid, const QString& processName) #ifdef _WIN32
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_handle(nullptr) : m_handle(nullptr)
, m_pid(pid) , m_pid(pid)
, m_processName(processName) , m_processName(processName)
@@ -19,7 +42,7 @@ ProcessMemoryProvider::ProcessMemoryProvider(DWORD pid, const QString& processNa
, m_base(0) , m_base(0)
{ {
// Try to open with write access first // Try to open with write access first
m_handle = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION, m_handle = OpenProcess(PROCESS_VM_READ | PROCESS_VM_WRITE | PROCESS_VM_OPERATION | PROCESS_QUERY_INFORMATION,
FALSE, pid); FALSE, pid);
if (m_handle) if (m_handle)
m_writable = true; m_writable = true;
@@ -31,21 +54,13 @@ ProcessMemoryProvider::ProcessMemoryProvider(DWORD pid, const QString& processNa
} }
if (m_handle) if (m_handle)
{
cacheModules(); cacheModules();
}
}
ProcessMemoryProvider::~ProcessMemoryProvider()
{
if (m_handle)
CloseHandle(m_handle);
} }
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{ {
if (!m_handle || len <= 0) return false; if (!m_handle || len <= 0) return false;
SIZE_T bytesRead = 0; SIZE_T bytesRead = 0;
if (ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead)) if (ReadProcessMemory(m_handle, (LPCVOID)(m_base + addr), buf, (SIZE_T)len, &bytesRead))
return bytesRead == (SIZE_T)len; return bytesRead == (SIZE_T)len;
@@ -55,7 +70,7 @@ bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len) bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{ {
if (!m_handle || !m_writable || len <= 0) return false; if (!m_handle || !m_writable || len <= 0) return false;
SIZE_T bytesWritten = 0; SIZE_T bytesWritten = 0;
if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten)) if (WriteProcessMemory(m_handle, (LPVOID)(m_base + addr), buf, (SIZE_T)len, &bytesWritten))
return bytesWritten == (SIZE_T)len; return bytesWritten == (SIZE_T)len;
@@ -64,9 +79,16 @@ bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{ {
// TODO: Implement module enumeration with EnumProcessModules for (const auto& mod : m_modules)
// For now, just return empty (no symbol resolution) {
Q_UNUSED(addr); if (addr >= mod.base && addr < mod.base + mod.size)
{
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {}; return {};
} }
@@ -98,6 +120,190 @@ void ProcessMemoryProvider::cacheModules()
} }
} }
#elif defined(__linux__)
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
: m_fd(-1)
, m_pid(pid)
, m_processName(processName)
, m_writable(false)
, m_base(0)
{
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
QByteArray pathUtf8 = memPath.toUtf8();
// Try read-write first
m_fd = ::open(pathUtf8.constData(), O_RDWR);
if (m_fd >= 0)
m_writable = true;
else
{
// Fall back to read-only
m_fd = ::open(pathUtf8.constData(), O_RDONLY);
m_writable = false;
}
if (m_fd >= 0)
cacheModules();
}
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
{
if (m_fd < 0 || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_readv first (faster, no fd seek contention)
struct iovec local;
local.iov_base = buf;
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nread = process_vm_readv(m_pid, &local, 1, &remote, 1, 0);
if (nread == static_cast<ssize_t>(len))
return true;
// Fallback: pread on /proc/<pid>/mem
nread = ::pread(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
return nread == static_cast<ssize_t>(len);
}
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
{
if (m_fd < 0 || !m_writable || len <= 0) return false;
uint64_t absAddr = m_base + addr;
// Try process_vm_writev first
struct iovec local;
local.iov_base = const_cast<void*>(buf);
local.iov_len = static_cast<size_t>(len);
struct iovec remote;
remote.iov_base = reinterpret_cast<void*>(absAddr);
remote.iov_len = static_cast<size_t>(len);
ssize_t nwritten = process_vm_writev(m_pid, &local, 1, &remote, 1, 0);
if (nwritten == static_cast<ssize_t>(len))
return true;
// Fallback: pwrite on /proc/<pid>/mem
nwritten = ::pwrite(m_fd, buf, static_cast<size_t>(len), static_cast<off_t>(absAddr));
return nwritten == static_cast<ssize_t>(len);
}
QString ProcessMemoryProvider::getSymbol(uint64_t addr) const
{
for (const auto& mod : m_modules)
{
if (addr >= mod.base && addr < mod.base + mod.size)
{
uint64_t offset = addr - mod.base;
return QStringLiteral("%1+0x%2")
.arg(mod.name)
.arg(offset, 0, 16, QChar('0'));
}
}
return {};
}
void ProcessMemoryProvider::cacheModules()
{
// Parse /proc/<pid>/maps to discover loaded modules
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(m_pid);
std::ifstream mapsFile(mapsPath.toStdString());
if (!mapsFile.is_open()) return;
// Accumulate base/end per path, then convert to ModuleInfo
struct Range { uint64_t base; uint64_t end; };
QMap<QString, Range> moduleRanges;
std::string line;
bool firstExec = true;
while (std::getline(mapsFile, line))
{
// Format: addr_start-addr_end perms offset dev inode pathname
// Example: 00400000-00452000 r-xp 00000000 08:02 173521 /usr/bin/foo
std::istringstream iss(line);
std::string addrRange, perms, offset, dev, inode, pathname;
iss >> addrRange >> perms >> offset >> dev >> inode;
std::getline(iss, pathname);
// Trim leading whitespace from pathname
size_t start = pathname.find_first_not_of(" \t");
if (start == std::string::npos) continue;
pathname = pathname.substr(start);
// Skip non-file mappings
if (pathname.empty() || pathname[0] != '/') continue;
// Skip special mappings
if (pathname.find("/dev/") == 0 || pathname.find("/memfd:") == 0) continue;
// Parse address range
auto dash = addrRange.find('-');
if (dash == std::string::npos) continue;
uint64_t addrStart = std::stoull(addrRange.substr(0, dash), nullptr, 16);
uint64_t addrEnd = std::stoull(addrRange.substr(dash + 1), nullptr, 16);
QString qpath = QString::fromStdString(pathname);
// Track first executable mapping as the base address
if (firstExec && perms.size() >= 3 && perms[2] == 'x')
{
m_base = addrStart;
firstExec = false;
}
auto it = moduleRanges.find(qpath);
if (it != moduleRanges.end())
{
if (addrStart < it->base) it->base = addrStart;
if (addrEnd > it->end) it->end = addrEnd;
}
else
{
moduleRanges.insert(qpath, {addrStart, addrEnd});
}
}
m_modules.reserve(moduleRanges.size());
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
{
QFileInfo fi(it.key());
m_modules.append({
fi.fileName(),
it->base,
it->end - it->base
});
}
}
#endif // platform
ProcessMemoryProvider::~ProcessMemoryProvider()
{
#ifdef _WIN32
if (m_handle)
CloseHandle(m_handle);
#elif defined(__linux__)
if (m_fd >= 0)
::close(m_fd);
#endif
}
int ProcessMemoryProvider::size() const
{
#ifdef _WIN32
return m_handle ? INT_MAX : 0;
#elif defined(__linux__)
return m_fd ? INT_MAX : 0;
#endif
}
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
// ProcessMemoryPlugin implementation // ProcessMemoryPlugin implementation
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
@@ -119,27 +325,26 @@ std::unique_ptr<rcx::Provider> ProcessMemoryPlugin::createProvider(const QString
// Parse target: "pid:name" or just "pid" // Parse target: "pid:name" or just "pid"
QStringList parts = target.split(':'); QStringList parts = target.split(':');
bool ok = false; bool ok = false;
DWORD pid = parts[0].toUInt(&ok); uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) { if (!ok || pid == 0)
{
if (errorMsg) *errorMsg = "Invalid PID: " + target; if (errorMsg) *errorMsg = "Invalid PID: " + target;
return nullptr; return nullptr;
} }
QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid); QString name = parts.size() > 1 ? parts[1] : QString("PID %1").arg(pid);
auto provider = std::make_unique<ProcessMemoryProvider>(pid, name); auto provider = std::make_unique<ProcessMemoryProvider>(pid, name);
if (!provider->isValid()) if (!provider->isValid())
{ {
if (errorMsg) if (errorMsg)
{
*errorMsg = QString("Failed to open process %1 (PID: %2)\n" *errorMsg = QString("Failed to open process %1 (PID: %2)\n"
"Ensure the process is running and you have sufficient permissions.") "Ensure the process is running and you have sufficient permissions.")
.arg(name).arg(pid); .arg(name).arg(pid);
}
return nullptr; return nullptr;
} }
return provider; return provider;
} }
@@ -151,26 +356,49 @@ uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
bool ok = false; bool ok = false;
DWORD pid = parts[0].toUInt(&ok); DWORD pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) return 0; if (!ok || pid == 0) return 0;
// Open process to get main module base // Open process to get main module base
HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid); HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, pid);
if (!hProc) return 0; if (!hProc) return 0;
uint64_t base = 0; uint64_t base = 0;
HMODULE hMod = nullptr; HMODULE hMod = nullptr;
DWORD needed = 0; DWORD needed = 0;
if (EnumProcessModulesEx(hProc, &hMod, sizeof(hMod), &needed, LIST_MODULES_ALL) && hMod) if (EnumProcessModulesEx(hProc, &hMod, sizeof(hMod), &needed, LIST_MODULES_ALL) && hMod)
{ {
MODULEINFO mi{}; MODULEINFO mi{};
if (GetModuleInformation(hProc, hMod, &mi, sizeof(mi))) if (GetModuleInformation(hProc, hMod, &mi, sizeof(mi)))
{
base = (uint64_t)mi.lpBaseOfDll; base = (uint64_t)mi.lpBaseOfDll;
}
} }
CloseHandle(hProc); CloseHandle(hProc);
return base; return base;
#elif defined(__linux__)
// Parse PID from target
QStringList parts = target.split(':');
bool ok = false;
uint32_t pid = parts[0].toUInt(&ok);
if (!ok || pid == 0) return 0;
// Find first executable mapping from /proc/<pid>/maps
QString mapsPath = QStringLiteral("/proc/%1/maps").arg(pid);
std::ifstream mapsFile(mapsPath.toStdString());
if (!mapsFile.is_open()) return 0;
std::string line;
while (std::getline(mapsFile, line)) {
std::istringstream iss(line);
std::string addrRange, perms;
iss >> addrRange >> perms;
if (perms.size() >= 3 && perms[2] == 'x') {
auto dash = addrRange.find('-');
if (dash != std::string::npos) {
return std::stoull(addrRange.substr(0, dash), nullptr, 16);
}
}
}
return 0;
#else #else
Q_UNUSED(target); Q_UNUSED(target);
return 0; return 0;
@@ -181,7 +409,7 @@ bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
{ {
// Use custom process enumeration from plugin // Use custom process enumeration from plugin
QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses(); QVector<PluginProcessInfo> pluginProcesses = enumerateProcesses();
// Convert to ProcessInfo for ProcessPicker // Convert to ProcessInfo for ProcessPicker
QList<ProcessInfo> processes; QList<ProcessInfo> processes;
for (const auto& pinfo : pluginProcesses) for (const auto& pinfo : pluginProcesses)
@@ -193,50 +421,50 @@ bool ProcessMemoryPlugin::selectTarget(QWidget* parent, QString* target)
info.icon = pinfo.icon; info.icon = pinfo.icon;
processes.append(info); processes.append(info);
} }
// Show ProcessPicker with custom process list // Show ProcessPicker with custom process list
ProcessPicker picker(processes, parent); ProcessPicker picker(processes, parent);
if (picker.exec() == QDialog::Accepted) { if (picker.exec() == QDialog::Accepted) {
uint32_t pid = picker.selectedProcessId(); uint32_t pid = picker.selectedProcessId();
QString name = picker.selectedProcessName(); QString name = picker.selectedProcessName();
// Format target as "pid:name" // Format target as "pid:name"
*target = QString("%1:%2").arg(pid).arg(name); *target = QString("%1:%2").arg(pid).arg(name);
return true; return true;
} }
return false; return false;
} }
QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses() QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
{ {
QVector<PluginProcessInfo> processes; QVector<PluginProcessInfo> processes;
#ifdef _WIN32 #ifdef _WIN32
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0); HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (snapshot == INVALID_HANDLE_VALUE) { if (snapshot == INVALID_HANDLE_VALUE) {
return processes; return processes;
} }
PROCESSENTRY32W entry; PROCESSENTRY32W entry;
entry.dwSize = sizeof(entry); entry.dwSize = sizeof(entry);
if (Process32FirstW(snapshot, &entry)) { if (Process32FirstW(snapshot, &entry)) {
do { do {
PluginProcessInfo info; PluginProcessInfo info;
info.pid = entry.th32ProcessID; info.pid = entry.th32ProcessID;
info.name = QString::fromWCharArray(entry.szExeFile); info.name = QString::fromWCharArray(entry.szExeFile);
// Try to get full path and icon // Try to get full path and icon
HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID); HANDLE hProcess = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, entry.th32ProcessID);
if (hProcess) { if (hProcess) {
wchar_t path[MAX_PATH * 2]; wchar_t path[MAX_PATH * 2];
DWORD pathLen = sizeof(path) / sizeof(wchar_t); DWORD pathLen = sizeof(path) / sizeof(wchar_t);
// Try QueryFullProcessImageNameW first // Try QueryFullProcessImageNameW first
if (QueryFullProcessImageNameW(hProcess, 0, path, &pathLen)) { if (QueryFullProcessImageNameW(hProcess, 0, path, &pathLen)) {
info.path = QString::fromWCharArray(path); info.path = QString::fromWCharArray(path);
// Extract icon // Extract icon
SHFILEINFOW sfi = {}; SHFILEINFOW sfi = {};
if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) { if (SHGetFileInfoW(path, 0, &sfi, sizeof(sfi), SHGFI_ICON | SHGFI_SMALLICON)) {
@@ -247,18 +475,57 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
} }
} }
} }
CloseHandle(hProcess); CloseHandle(hProcess);
} }
processes.append(info); processes.append(info);
} while (Process32NextW(snapshot, &entry)); } while (Process32NextW(snapshot, &entry));
} }
CloseHandle(snapshot); CloseHandle(snapshot);
#elif defined(__linux__)
QDir procDir("/proc");
QStringList entries = procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QIcon defaultIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
for (const QString& entry : entries) {
bool ok = false;
uint32_t pid = entry.toUInt(&ok);
if (!ok || pid == 0) continue;
// Read process name from /proc/<pid>/comm
QString commPath = QStringLiteral("/proc/%1/comm").arg(pid);
QFile commFile(commPath);
QString procName;
if (commFile.open(QIODevice::ReadOnly)) {
procName = QString::fromUtf8(commFile.readAll()).trimmed();
commFile.close();
}
if (procName.isEmpty()) continue; // Skip kernel threads with no name
// Read exe path from /proc/<pid>/exe symlink
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
QFileInfo exeInfo(exePath);
QString resolvedPath;
if (exeInfo.exists())
resolvedPath = exeInfo.symLinkTarget();
// Skip if we can't read the process memory (no access)
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
if (::access(memPath.toUtf8().constData(), R_OK) != 0)
continue;
PluginProcessInfo info;
info.pid = pid;
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
processes.append(info);
}
#endif #endif
return processes; return processes;
} }
@@ -266,7 +533,7 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
// Plugin factory // Plugin factory
// ────────────────────────────────────────────────────────────────────────── // ──────────────────────────────────────────────────────────────────────────
extern "C" __declspec(dllexport) IPlugin* CreatePlugin() extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin()
{ {
return new ProcessMemoryPlugin(); return new ProcessMemoryPlugin();
} }

View File

@@ -1,42 +1,45 @@
#pragma once #pragma once
#include "../../src/iplugin.h" #include "../../src/iplugin.h"
#include "../../src/core.h" #include "../../src/core.h"
#include <windows.h>
#include <tlhelp32.h> #include <cstdint>
#include <psapi.h>
#include <shellapi.h>
/** /**
* Windows process memory provider * Process memory provider
* Reads/writes memory from a live process using Win32 API * Reads/writes memory from a live process using platform APIs
*/ */
class ProcessMemoryProvider : public rcx::Provider { class ProcessMemoryProvider : public rcx::Provider
{
public: public:
ProcessMemoryProvider(DWORD pid, const QString& processName); ProcessMemoryProvider(uint32_t pid, const QString& processName);
~ProcessMemoryProvider() override; ~ProcessMemoryProvider() override;
// Required overrides // Required overrides
bool read(uint64_t addr, void* buf, int len) const override; bool read(uint64_t addr, void* buf, int len) const override;
int size() const override { return m_handle ? INT_MAX : NULL; } // Process memory has no fixed size int size() const override;
// Optional overrides // Optional overrides
bool write(uint64_t addr, const void* buf, int len) override; bool write(uint64_t addr, const void* buf, int len) override;
bool isWritable() const override { return m_writable; } bool isWritable() const override { return m_writable; }
QString name() const override { return m_processName; } QString name() const override { return m_processName; }
QString kind() const override { return QStringLiteral("LocalProcess"); } QString kind() const override { return QStringLiteral("LocalProcess"); }
QString getSymbol(uint64_t addr) const override; QString getSymbol(uint64_t addr) const override;
// Process-specific helpers // Process-specific helpers
DWORD pid() const { return m_pid; } uint32_t pid() const { return m_pid; }
uint64_t baseAddress() const { return m_base; } uint64_t baseAddress() const { return m_base; }
void refreshModules() { m_modules.clear(); cacheModules(); } void refreshModules() { m_modules.clear(); cacheModules(); }
private: private:
void cacheModules(); void cacheModules();
private: private:
HANDLE m_handle; #ifdef _WIN32
DWORD m_pid; void* m_handle;
#elif defined(__linux__)
int m_fd;
#endif
uint32_t m_pid;
QString m_processName; QString m_processName;
bool m_writable; bool m_writable;
uint64_t m_base; uint64_t m_base;
@@ -52,24 +55,25 @@ private:
/** /**
* Plugin that provides ProcessMemoryProvider * Plugin that provides ProcessMemoryProvider
*/ */
class ProcessMemoryPlugin : public IProviderPlugin { class ProcessMemoryPlugin : public IProviderPlugin
{
public: public:
std::string Name() const override { return "Process Memory"; } std::string Name() const override { return "Process Memory"; }
std::string Version() const override { return "1.0.0"; } std::string Version() const override { return "1.0.0"; }
std::string Author() const override { return "ReclassX"; } std::string Author() const override { return "ReclassX"; }
std::string Description() const override { return "Read and write memory from local running Windows processes"; } std::string Description() const override { return "Read and write memory from local running processes"; }
k_ELoadType LoadType() const override { return k_ELoadTypeAuto; } k_ELoadType LoadType() const override { return k_ELoadTypeAuto; }
QIcon Icon() const override; QIcon Icon() const override;
bool canHandle(const QString& target) const override; bool canHandle(const QString& target) const override;
std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override; std::unique_ptr<rcx::Provider> createProvider(const QString& target, QString* errorMsg) override;
uint64_t getInitialBaseAddress(const QString& target) const override; uint64_t getInitialBaseAddress(const QString& target) const override;
bool selectTarget(QWidget* parent, QString* target) override; bool selectTarget(QWidget* parent, QString* target) override;
// Optional: provide custom process list // Optional: provide custom process list
bool providesProcessList() const override { return true; } bool providesProcessList() const override { return true; }
QVector<PluginProcessInfo> enumerateProcesses() override; QVector<PluginProcessInfo> enumerateProcesses() override;
}; };
// Plugin export // Plugin export
extern "C" __declspec(dllexport) IPlugin* CreatePlugin(); extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();

View File

@@ -4,14 +4,20 @@
#include <memory> #include <memory>
#include <string> #include <string>
#ifdef _WIN32
#define RCX_PLUGIN_EXPORT __declspec(dllexport)
#else
#define RCX_PLUGIN_EXPORT __attribute__((visibility("default")))
#endif
// Forward declaration // Forward declaration
namespace rcx { class Provider; } namespace rcx { class Provider; }
/** /**
* Plugin interface for ReclassX * Plugin interface for ReclassX
* *
* Plugins are loaded from the "Plugins" folder as DLLs. * Plugins are loaded from the "Plugins" folder as shared libraries.
* Each plugin must export a C function: extern "C" __declspec(dllexport) IPlugin* CreatePlugin(); * Each plugin must export a C function: extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin();
*/ */
class IPlugin { class IPlugin {
public: public:

View File

@@ -1376,7 +1376,7 @@ int main(int argc, char* argv[]) {
QTimer::singleShot(1000, [&window, out]() { QTimer::singleShot(1000, [&window, out]() {
QDir().mkpath(QFileInfo(out).absolutePath()); QDir().mkpath(QFileInfo(out).absolutePath());
window.grab().save(out); window.grab().save(out);
::_exit(0); // immediate exit — no need for clean shutdown in screenshot mode ::_Exit(0); // immediate exit — no need for clean shutdown in screenshot mode
}); });
} }

View File

@@ -11,6 +11,11 @@
#include <tlhelp32.h> #include <tlhelp32.h>
#include <psapi.h> #include <psapi.h>
#include <shellapi.h> #include <shellapi.h>
#elif defined(__linux__)
#include <QDir>
#include <QStyle>
#include <QApplication>
#include <unistd.h>
#endif #endif
ProcessPicker::ProcessPicker(QWidget *parent) ProcessPicker::ProcessPicker(QWidget *parent)
@@ -155,6 +160,45 @@ void ProcessPicker::enumerateProcesses()
} }
CloseHandle(snapshot); CloseHandle(snapshot);
#elif defined(__linux__)
QDir procDir("/proc");
QStringList entries = procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
QIcon defaultIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
for (const QString& entry : entries) {
bool ok = false;
uint32_t pid = entry.toUInt(&ok);
if (!ok || pid == 0) continue;
// Read process name from /proc/<pid>/comm
QString commPath = QStringLiteral("/proc/%1/comm").arg(pid);
QFile commFile(commPath);
QString procName;
if (commFile.open(QIODevice::ReadOnly)) {
procName = QString::fromUtf8(commFile.readAll()).trimmed();
commFile.close();
}
if (procName.isEmpty()) continue;
// Read exe path from /proc/<pid>/exe symlink
QString exePath = QStringLiteral("/proc/%1/exe").arg(pid);
QFileInfo exeInfo(exePath);
QString resolvedPath;
if (exeInfo.exists())
resolvedPath = exeInfo.symLinkTarget();
// Skip if we can't read the process memory
QString memPath = QStringLiteral("/proc/%1/mem").arg(pid);
if (::access(memPath.toUtf8().constData(), R_OK) != 0)
continue;
ProcessInfo info;
info.pid = pid;
info.name = procName;
info.path = resolvedPath;
info.icon = defaultIcon;
processes.append(info);
}
#else #else
// Platform not supported // Platform not supported
QMessageBox::warning(this, "Error", "Process enumeration not supported on this platform."); QMessageBox::warning(this, "Error", "Process enumeration not supported on this platform.");