mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Added linux support (tested on Ubuntu)
CMakeList: fixed for building on linux processpicker: linux process enumeration main.cpp: "_Exit()" works on linux & windows "ProcessMemory" plugin: added linux support
This commit is contained in:
@@ -43,9 +43,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
|
||||||
@@ -140,7 +141,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
|
||||||
@@ -149,7 +153,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
|
||||||
@@ -164,7 +171,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_new_features tests/test_new_features.cpp
|
add_executable(test_new_features tests/test_new_features.cpp
|
||||||
@@ -173,7 +183,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)
|
||||||
endif()
|
endif()
|
||||||
add_subdirectory(plugins/ProcessMemory)
|
add_subdirectory(plugins/ProcessMemory)
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -1188,7 +1188,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
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
Reference in New Issue
Block a user