diff --git a/CMakeLists.txt b/CMakeLists.txt index 510329f..b963c2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/README.md b/README.md index 693cac0..a0a40cf 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ Built with C++17, Qt 6 (Qt 5 also supported), and QScintilla. The entire editor ![Memory scanner](docs/README_PIC3.png) +![MacOS - Process Memory scanner](docs/README_PIC6.png) + ## Features ### Editor diff --git a/docs/README_PIC6.png b/docs/README_PIC6.png new file mode 100644 index 0000000..9c27f58 Binary files /dev/null and b/docs/README_PIC6.png differ diff --git a/plugins/ProcessMemory/CMakeLists.txt b/plugins/ProcessMemory/CMakeLists.txt index 4bd2e9b..6356edd 100644 --- a/plugins/ProcessMemory/CMakeLists.txt +++ b/plugins/ProcessMemory/CMakeLists.txt @@ -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 "$/../PlugIns" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "$" + "$/../PlugIns/" + COMMENT "Copying ProcessMemoryPlugin into Reclass.app/Contents/PlugIns") +endif() diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp index 722168a..022318a 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.cpp +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) && defined(_WIN32) #include #endif @@ -83,6 +84,13 @@ typedef struct alignas(8) _THREAD_BASIC_INFORMATION { #include #include #include +#elif defined(__APPLE__) +#include +#include +#include +#include +#include +#include #endif // ────────────────────────────────────────────────────────────────────────── @@ -476,8 +484,239 @@ QVector 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(pid), &task); + if (kr != KERN_SUCCESS || task == MACH_PORT_NULL) + return; + + m_task = static_cast(task); + m_writable = true; + + proc_bsdinfo bsdInfo{}; + int infoLen = proc_pidinfo(static_cast(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(m_task), + static_cast(addr), + static_cast(len), + reinterpret_cast(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(m_task), + static_cast(addr), + reinterpret_cast(const_cast(buf)), + static_cast(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 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(m_task), + &addr, + &size, + &depth, + reinterpret_cast(&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 ProcessMemoryProvider::enumerateRegions() const +{ + QVector 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(m_task), + &addr, + &size, + &depth, + reinterpret_cast(&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 ProcessMemoryProvider::enumerateModules() const +{ + QVector 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(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(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(&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 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 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(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 diff --git a/plugins/ProcessMemory/ProcessMemoryPlugin.h b/plugins/ProcessMemory/ProcessMemoryPlugin.h index 77592be..5551a69 100644 --- a/plugins/ProcessMemory/ProcessMemoryPlugin.h +++ b/plugins/ProcessMemory/ProcessMemoryPlugin.h @@ -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; diff --git a/src/pluginmanager.cpp b/src/pluginmanager.cpp index 400942e..db5b558 100644 --- a/src/pluginmanager.cpp +++ b/src/pluginmanager.cpp @@ -12,17 +12,16 @@ 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; #ifdef _WIN32 @@ -32,22 +31,36 @@ void PluginManager::LoadPlugins() #else filters << "*.so"; #endif - - dir.setNameFilters(filters); - QFileInfoList files = dir.entryInfoList(QDir::Files); - - qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir; - qDebug() << "PluginManager: Found" << files.count() << "potential plugin(s)"; - - for (const QFileInfo& fileInfo : files) + + int totalCandidates = 0; + bool foundAnyDir = false; + for (const QString& pluginsDir : pluginDirs) { - // Skip the remote-inject payload binary — it's not a plugin and - // loading it (especially on Linux) spawns a rogue thread. - if (fileInfo.baseName().startsWith("rcx_payload")) + QDir dir(pluginsDir); + if (!dir.exists()) continue; - LoadPlugin(fileInfo.absoluteFilePath()); + foundAnyDir = true; + dir.setNameFilters(filters); + QFileInfoList files = dir.entryInfoList(QDir::Files); + totalCandidates += files.count(); + + qDebug() << "PluginManager: Scanning for plugins in:" << pluginsDir; + for (const QFileInfo& fileInfo : files) + { + // Skip the remote-inject payload binary — it's not a plugin and + // loading it (especially on Linux) spawns a rogue thread. + if (fileInfo.baseName().startsWith("rcx_payload")) + continue; + + 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)"; }