mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
Add macOS support for ProcessMemory plugin
Implement macOS-specific support for the ProcessMemory plugin and update plugin discovery/build. - Add macOS build/install support: include plugins/ProcessMemory in top-level CMake and copy the built plugin into Reclass.app/Contents/PlugIns on macOS. - Implement Apple-specific ProcessMemoryProvider: task_for_pid usage, mach_vm_read_overwrite/mach_vm_write, proc_pidpath/proc_regionfilename based module caching, region enumeration, symbol formatting, module enumeration, and proper cleanup (mach_port_deallocate). - Extend plugin header to track m_task and adjust readability checks for macOS. - Add macOS handling in getInitialBaseAddress and process enumeration to find base addresses and processes using proc APIs. - Improve PluginManager to probe multiple plugin locations (including Contents/PlugIns inside macOS bundles), aggregate/log candidate counts, and continue scanning multiple dirs. - Add macOS screenshot to README (docs/README_PIC6.png) and reference it in README. These changes enable the ProcessMemory plugin to operate on macOS and make plugin discovery more robust on macOS app bundles.
This commit is contained in:
@@ -599,8 +599,8 @@ if(BUILD_TESTING)
|
||||
|
||||
endif() # BUILD_UI_TESTS
|
||||
endif()
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
if(NOT APPLE)
|
||||
add_subdirectory(plugins/ProcessMemory)
|
||||
add_subdirectory(plugins/RemoteProcessMemory)
|
||||
endif()
|
||||
if(WIN32)
|
||||
|
||||
@@ -32,6 +32,8 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Features
|
||||
|
||||
### Editor
|
||||
|
||||
BIN
docs/README_PIC6.png
Normal file
BIN
docs/README_PIC6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 312 KiB |
@@ -45,3 +45,12 @@ set_target_properties(ProcessMemoryPlugin PROPERTIES
|
||||
LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins"
|
||||
)
|
||||
|
||||
if(APPLE AND TARGET Reclass)
|
||||
add_custom_command(TARGET ProcessMemoryPlugin POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E make_directory "$<TARGET_FILE_DIR:Reclass>/../PlugIns"
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_if_different
|
||||
"$<TARGET_FILE:ProcessMemoryPlugin>"
|
||||
"$<TARGET_FILE_DIR:Reclass>/../PlugIns/"
|
||||
COMMENT "Copying ProcessMemoryPlugin into Reclass.app/Contents/PlugIns")
|
||||
endif()
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <QImage>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QMap>
|
||||
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32)
|
||||
#include <QtWin>
|
||||
#endif
|
||||
@@ -83,6 +84,13 @@ typedef struct alignas(8) _THREAD_BASIC_INFORMATION {
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cstring>
|
||||
#elif defined(__APPLE__)
|
||||
#include <mach/mach.h>
|
||||
#include <mach/mach_vm.h>
|
||||
#include <libproc.h>
|
||||
#include <sys/proc_info.h>
|
||||
#include <unistd.h>
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
// ──────────────────────────────────────────────────────────────────────────
|
||||
@@ -476,8 +484,239 @@ QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
return regions;
|
||||
}
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
|
||||
ProcessMemoryProvider::ProcessMemoryProvider(uint32_t pid, const QString& processName)
|
||||
: m_task(0)
|
||||
, m_pid(pid)
|
||||
, m_processName(processName)
|
||||
, m_writable(false)
|
||||
, m_base(0)
|
||||
{
|
||||
mach_port_t task = MACH_PORT_NULL;
|
||||
kern_return_t kr = task_for_pid(mach_task_self(), static_cast<int>(pid), &task);
|
||||
if (kr != KERN_SUCCESS || task == MACH_PORT_NULL)
|
||||
return;
|
||||
|
||||
m_task = static_cast<uint32_t>(task);
|
||||
m_writable = true;
|
||||
|
||||
proc_bsdinfo bsdInfo{};
|
||||
int infoLen = proc_pidinfo(static_cast<int>(pid), PROC_PIDTBSDINFO, 0, &bsdInfo, sizeof(bsdInfo));
|
||||
if (infoLen == (int)sizeof(bsdInfo)) {
|
||||
#ifdef PROC_FLAG_LP64
|
||||
m_pointerSize = (bsdInfo.pbi_flags & PROC_FLAG_LP64) ? 8 : 4;
|
||||
#else
|
||||
m_pointerSize = 8;
|
||||
#endif
|
||||
}
|
||||
|
||||
cacheModules();
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::read(uint64_t addr, void* buf, int len) const
|
||||
{
|
||||
if (m_task == 0 || len <= 0)
|
||||
return false;
|
||||
|
||||
mach_vm_size_t outSize = 0;
|
||||
kern_return_t kr = mach_vm_read_overwrite(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
static_cast<mach_vm_address_t>(addr),
|
||||
static_cast<mach_vm_size_t>(len),
|
||||
reinterpret_cast<mach_vm_address_t>(buf),
|
||||
&outSize);
|
||||
|
||||
if ((int)outSize < len)
|
||||
memset((char*)buf + outSize, 0, len - outSize);
|
||||
|
||||
return kr == KERN_SUCCESS && outSize > 0;
|
||||
}
|
||||
|
||||
bool ProcessMemoryProvider::write(uint64_t addr, const void* buf, int len)
|
||||
{
|
||||
if (m_task == 0 || !m_writable || len <= 0)
|
||||
return false;
|
||||
|
||||
kern_return_t kr = mach_vm_write(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
static_cast<mach_vm_address_t>(addr),
|
||||
reinterpret_cast<vm_offset_t>(const_cast<void*>(buf)),
|
||||
static_cast<mach_msg_type_number_t>(len));
|
||||
return kr == KERN_SUCCESS;
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
if (m_task == 0)
|
||||
return;
|
||||
|
||||
m_modules.clear();
|
||||
|
||||
char mainPathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
QString mainPath;
|
||||
if (proc_pidpath((int)m_pid, mainPathBuf, sizeof(mainPathBuf)) > 0)
|
||||
mainPath = QString::fromUtf8(mainPathBuf);
|
||||
|
||||
struct Range { uint64_t base; uint64_t end; };
|
||||
QMap<QString, Range> moduleRanges;
|
||||
|
||||
mach_vm_address_t addr = 0;
|
||||
uint32_t depth = 0;
|
||||
for (;;) {
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_submap_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
|
||||
kern_return_t kr = mach_vm_region_recurse(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
&addr,
|
||||
&size,
|
||||
&depth,
|
||||
reinterpret_cast<vm_region_recurse_info_t>(&info),
|
||||
&count);
|
||||
if (kr != KERN_SUCCESS)
|
||||
break;
|
||||
|
||||
if (info.is_submap) {
|
||||
++depth;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
++addr;
|
||||
continue;
|
||||
}
|
||||
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int pathLen = proc_regionfilename((int)m_pid, (uint64_t)addr, pathBuf, sizeof(pathBuf));
|
||||
if (pathLen > 0) {
|
||||
QString fullPath = QString::fromUtf8(pathBuf);
|
||||
|
||||
uint64_t regionBase = (uint64_t)addr;
|
||||
uint64_t regionEnd = regionBase + (uint64_t)size;
|
||||
auto it = moduleRanges.find(fullPath);
|
||||
if (it == moduleRanges.end()) {
|
||||
moduleRanges.insert(fullPath, {regionBase, regionEnd});
|
||||
} else {
|
||||
if (regionBase < it->base) it->base = regionBase;
|
||||
if (regionEnd > it->end) it->end = regionEnd;
|
||||
}
|
||||
|
||||
if (m_base == 0 && !mainPath.isEmpty() && fullPath == mainPath && (info.protection & VM_PROT_EXECUTE))
|
||||
m_base = regionBase;
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)addr + (uint64_t)size;
|
||||
if (next <= (uint64_t)addr)
|
||||
break;
|
||||
addr = (mach_vm_address_t)next;
|
||||
}
|
||||
|
||||
m_modules.reserve(moduleRanges.size());
|
||||
for (auto it = moduleRanges.begin(); it != moduleRanges.end(); ++it)
|
||||
{
|
||||
QFileInfo fi(it.key());
|
||||
m_modules.push_back(ModuleInfo{
|
||||
fi.fileName(),
|
||||
it.key(),
|
||||
it->base,
|
||||
it->end - it->base
|
||||
});
|
||||
}
|
||||
|
||||
if (m_base == 0 && !m_modules.isEmpty())
|
||||
m_base = m_modules.front().base;
|
||||
}
|
||||
|
||||
QVector<rcx::MemoryRegion> ProcessMemoryProvider::enumerateRegions() const
|
||||
{
|
||||
QVector<rcx::MemoryRegion> regions;
|
||||
if (m_task == 0)
|
||||
return regions;
|
||||
|
||||
mach_vm_address_t addr = 0;
|
||||
uint32_t depth = 0;
|
||||
for (;;) {
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_submap_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
|
||||
kern_return_t kr = mach_vm_region_recurse(
|
||||
static_cast<mach_port_name_t>(m_task),
|
||||
&addr,
|
||||
&size,
|
||||
&depth,
|
||||
reinterpret_cast<vm_region_recurse_info_t>(&info),
|
||||
&count);
|
||||
if (kr != KERN_SUCCESS)
|
||||
break;
|
||||
|
||||
if (info.is_submap) {
|
||||
++depth;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
++addr;
|
||||
continue;
|
||||
}
|
||||
|
||||
bool readable = (info.protection & VM_PROT_READ) != 0;
|
||||
if (readable)
|
||||
{
|
||||
rcx::MemoryRegion region;
|
||||
region.base = (uint64_t)addr;
|
||||
region.size = (uint64_t)size;
|
||||
region.readable = readable;
|
||||
region.writable = (info.protection & VM_PROT_WRITE) != 0;
|
||||
region.executable = (info.protection & VM_PROT_EXECUTE) != 0;
|
||||
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int pathLen = proc_regionfilename((int)m_pid, region.base, pathBuf, sizeof(pathBuf));
|
||||
if (pathLen > 0) {
|
||||
QFileInfo fi(QString::fromUtf8(pathBuf));
|
||||
region.moduleName = fi.fileName();
|
||||
}
|
||||
|
||||
regions.append(region);
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)addr + (uint64_t)size;
|
||||
if (next <= (uint64_t)addr)
|
||||
break;
|
||||
addr = (mach_vm_address_t)next;
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
#endif // platform
|
||||
|
||||
#ifndef _WIN32
|
||||
QVector<rcx::Provider::ModuleEntry> ProcessMemoryProvider::enumerateModules() const
|
||||
{
|
||||
QVector<ModuleEntry> result;
|
||||
result.reserve(m_modules.size());
|
||||
for (const auto& m : m_modules)
|
||||
result.push_back(ModuleEntry{m.name, m.fullPath, m.base, m.size});
|
||||
return result;
|
||||
}
|
||||
#endif
|
||||
|
||||
uint64_t ProcessMemoryProvider::symbolToAddress(const QString& name) const
|
||||
{
|
||||
for (const auto& mod : m_modules) {
|
||||
@@ -495,6 +734,9 @@ ProcessMemoryProvider::~ProcessMemoryProvider()
|
||||
#elif defined(__linux__)
|
||||
if (m_fd >= 0)
|
||||
::close(m_fd);
|
||||
#elif defined(__APPLE__)
|
||||
if (m_task != 0)
|
||||
mach_port_deallocate(mach_task_self(), static_cast<mach_port_name_t>(m_task));
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -504,6 +746,8 @@ int ProcessMemoryProvider::size() const
|
||||
return m_handle ? 0x10000 : 0;
|
||||
#elif defined(__linux__)
|
||||
return (m_fd >= 0) ? 0x10000 : 0;
|
||||
#elif defined(__APPLE__)
|
||||
return (m_task != 0) ? 0x10000 : 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -654,6 +898,68 @@ uint64_t ProcessMemoryPlugin::getInitialBaseAddress(const QString& target) const
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
#elif defined(__APPLE__)
|
||||
QStringList parts = target.split(':');
|
||||
bool ok = false;
|
||||
uint32_t pid = parts[0].toUInt(&ok);
|
||||
if (!ok || pid == 0)
|
||||
return 0;
|
||||
|
||||
mach_port_t task = MACH_PORT_NULL;
|
||||
kern_return_t tkr = task_for_pid(mach_task_self(), static_cast<int>(pid), &task);
|
||||
if (tkr != KERN_SUCCESS || task == MACH_PORT_NULL)
|
||||
return 0;
|
||||
|
||||
char mainPathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
QString mainPath;
|
||||
if (proc_pidpath((int)pid, mainPathBuf, sizeof(mainPathBuf)) > 0)
|
||||
mainPath = QString::fromUtf8(mainPathBuf);
|
||||
|
||||
uint64_t base = 0;
|
||||
mach_vm_address_t addr = 0;
|
||||
uint32_t depth = 0;
|
||||
for (;;) {
|
||||
mach_vm_size_t size = 0;
|
||||
vm_region_submap_info_data_64_t info{};
|
||||
mach_msg_type_number_t count = VM_REGION_SUBMAP_INFO_COUNT_64;
|
||||
kern_return_t kr = mach_vm_region_recurse(task, &addr, &size, &depth,
|
||||
reinterpret_cast<vm_region_recurse_info_t>(&info),
|
||||
&count);
|
||||
if (kr != KERN_SUCCESS)
|
||||
break;
|
||||
|
||||
if (info.is_submap) {
|
||||
++depth;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (size == 0) {
|
||||
++addr;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ((info.protection & VM_PROT_EXECUTE) != 0) {
|
||||
if (!mainPath.isEmpty()) {
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int pathLen = proc_regionfilename((int)pid, (uint64_t)addr, pathBuf, sizeof(pathBuf));
|
||||
if (pathLen > 0 && QString::fromUtf8(pathBuf) == mainPath) {
|
||||
base = (uint64_t)addr;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
base = (uint64_t)addr;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t next = (uint64_t)addr + (uint64_t)size;
|
||||
if (next <= (uint64_t)addr)
|
||||
break;
|
||||
addr = (mach_vm_address_t)next;
|
||||
}
|
||||
|
||||
mach_port_deallocate(mach_task_self(), task);
|
||||
return base;
|
||||
#else
|
||||
Q_UNUSED(target);
|
||||
return 0;
|
||||
@@ -797,6 +1103,61 @@ QVector<PluginProcessInfo> ProcessMemoryPlugin::enumerateProcesses()
|
||||
::close(exeFd);
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#elif defined(__APPLE__)
|
||||
QIcon defaultIcon = qApp->style()->standardIcon(QStyle::SP_ComputerIcon);
|
||||
|
||||
int bytes = proc_listpids(PROC_ALL_PIDS, 0, nullptr, 0);
|
||||
if (bytes <= 0)
|
||||
return processes;
|
||||
|
||||
int count = bytes / (int)sizeof(pid_t);
|
||||
QVector<pid_t> pids(count);
|
||||
bytes = proc_listpids(PROC_ALL_PIDS, 0, pids.data(), count * (int)sizeof(pid_t));
|
||||
if (bytes <= 0)
|
||||
return processes;
|
||||
|
||||
count = bytes / (int)sizeof(pid_t);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
pid_t pid = pids[i];
|
||||
if (pid <= 0)
|
||||
continue;
|
||||
|
||||
mach_port_t task = MACH_PORT_NULL;
|
||||
if (task_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS || task == MACH_PORT_NULL)
|
||||
continue;
|
||||
mach_port_deallocate(mach_task_self(), task);
|
||||
|
||||
char nameBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
int nameLen = proc_name(pid, nameBuf, sizeof(nameBuf));
|
||||
QString procName;
|
||||
if (nameLen > 0)
|
||||
procName = QString::fromUtf8(nameBuf);
|
||||
if (procName.isEmpty())
|
||||
continue;
|
||||
|
||||
char pathBuf[PROC_PIDPATHINFO_MAXSIZE] = {};
|
||||
QString procPath;
|
||||
if (proc_pidpath(pid, pathBuf, sizeof(pathBuf)) > 0)
|
||||
procPath = QString::fromUtf8(pathBuf);
|
||||
|
||||
PluginProcessInfo info;
|
||||
info.pid = static_cast<uint32_t>(pid);
|
||||
info.name = procName;
|
||||
info.path = procPath;
|
||||
info.icon = defaultIcon;
|
||||
|
||||
proc_bsdinfo bsdInfo{};
|
||||
int infoLen = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &bsdInfo, sizeof(bsdInfo));
|
||||
if (infoLen == (int)sizeof(bsdInfo)) {
|
||||
#ifdef PROC_FLAG_LP64
|
||||
info.is32Bit = (bsdInfo.pbi_flags & PROC_FLAG_LP64) == 0;
|
||||
#else
|
||||
info.is32Bit = false;
|
||||
#endif
|
||||
}
|
||||
|
||||
processes.append(info);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -35,6 +35,8 @@ public:
|
||||
return m_handle && len >= 0;
|
||||
#elif defined(__linux__)
|
||||
return m_fd >= 0 && len >= 0;
|
||||
#elif defined(__APPLE__)
|
||||
return m_task != 0 && len >= 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -53,6 +55,8 @@ private:
|
||||
void* m_handle;
|
||||
#elif defined(__linux__)
|
||||
int m_fd;
|
||||
#elif defined(__APPLE__)
|
||||
uint32_t m_task;
|
||||
#endif
|
||||
uint32_t m_pid;
|
||||
QString m_processName;
|
||||
|
||||
@@ -12,16 +12,15 @@ PluginManager::~PluginManager()
|
||||
|
||||
void PluginManager::LoadPlugins()
|
||||
{
|
||||
// Get the Plugins directory relative to the executable
|
||||
// Probe plugin locations relative to the executable.
|
||||
QString appDir = QCoreApplication::applicationDirPath();
|
||||
QString pluginsDir = appDir + "/Plugins";
|
||||
|
||||
QDir dir(pluginsDir);
|
||||
if (!dir.exists())
|
||||
{
|
||||
qWarning() << "PluginManager: Plugins directory not found:" << pluginsDir;
|
||||
return;
|
||||
}
|
||||
QStringList pluginDirs;
|
||||
pluginDirs << (appDir + "/Plugins");
|
||||
#ifdef __APPLE__
|
||||
// In macOS app bundles, plugins may live in Contents/PlugIns or in
|
||||
// the top-level build/Plugins directory during local development.
|
||||
pluginDirs << QDir::cleanPath(appDir + "/../PlugIns");
|
||||
#endif
|
||||
|
||||
// Find all DLL files
|
||||
QStringList filters;
|
||||
@@ -33,12 +32,20 @@ void PluginManager::LoadPlugins()
|
||||
filters << "*.so";
|
||||
#endif
|
||||
|
||||
int totalCandidates = 0;
|
||||
bool foundAnyDir = false;
|
||||
for (const QString& pluginsDir : pluginDirs)
|
||||
{
|
||||
QDir dir(pluginsDir);
|
||||
if (!dir.exists())
|
||||
continue;
|
||||
|
||||
foundAnyDir = true;
|
||||
dir.setNameFilters(filters);
|
||||
QFileInfoList files = dir.entryInfoList(QDir::Files);
|
||||
totalCandidates += files.count();
|
||||
|
||||
qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir;
|
||||
qDebug() << "PluginManager: Found" << files.count() << "potential plugin(s)";
|
||||
|
||||
for (const QFileInfo& fileInfo : files)
|
||||
{
|
||||
// Skip the remote-inject payload binary — it's not a plugin and
|
||||
@@ -48,6 +55,12 @@ void PluginManager::LoadPlugins()
|
||||
|
||||
LoadPlugin(fileInfo.absoluteFilePath());
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundAnyDir)
|
||||
qWarning() << "PluginManager: Plugins directory not found. Searched:" << pluginDirs;
|
||||
else
|
||||
qDebug() << "PluginManager: Found" << totalCandidates << "potential plugin(s)";
|
||||
|
||||
qDebug() << "PluginManager: Loaded" << m_plugins.count() << "plugin(s)";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user