diff --git a/CMakeLists.txt b/CMakeLists.txt index befa380..d67cbbe 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -317,4 +317,5 @@ endif() add_subdirectory(plugins/ProcessMemory) if(WIN32) add_subdirectory(plugins/WinDbgMemory) + add_subdirectory(plugins/RcNetPluginCompatLayer) endif() diff --git a/plugins/RcNetPluginCompatLayer/CMakeLists.txt b/plugins/RcNetPluginCompatLayer/CMakeLists.txt new file mode 100644 index 0000000..f05edfe --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/CMakeLists.txt @@ -0,0 +1,93 @@ +cmake_minimum_required(VERSION 3.20) +project(RcNetCompatPlugin LANGUAGES CXX) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +# Qt is found by the parent project; QT variable (Qt5 or Qt6) is inherited + +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) +set(CMAKE_AUTOUIC ON) + +# Plugin sources +set(PLUGIN_SOURCES + RcNetCompatPlugin.h + RcNetCompatPlugin.cpp + RcNetCompatProvider.h + RcNetCompatProvider.cpp + ReClassNET_Plugin.hpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.h + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/../../src/processpicker.ui +) + +# -- Optional .NET bridge ------------------------------------------------- +# When the .NET SDK is available, build the C# bridge assembly and enable +# CLR hosting support in the C++ plugin. + +find_program(DOTNET_EXE dotnet) +if(DOTNET_EXE) + # Check that 'dotnet build' actually works for net472 + execute_process( + COMMAND ${DOTNET_EXE} --list-sdks + OUTPUT_VARIABLE _dotnet_sdks + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(_dotnet_sdks) + set(HAS_CLR_BRIDGE ON) + message(STATUS "RcNetCompat: .NET SDK found -- building managed bridge") + endif() +endif() + +if(HAS_CLR_BRIDGE) + list(APPEND PLUGIN_SOURCES + ClrHost.h + ClrHost.cpp + ) + + # Build the C# bridge assembly + set(_bridge_src "${CMAKE_CURRENT_SOURCE_DIR}/bridge") + set(_bridge_out "${CMAKE_BINARY_DIR}/Plugins/RcNetBridge.dll") + + add_custom_command( + OUTPUT "${_bridge_out}" + COMMAND ${DOTNET_EXE} build + "${_bridge_src}/RcNetBridge.csproj" + -c Release + -o "${CMAKE_BINARY_DIR}/Plugins" + --nologo -v quiet + DEPENDS + "${_bridge_src}/RcNetBridge.cs" + "${_bridge_src}/RcNetBridge.csproj" + COMMENT "Building RcNetBridge.dll (.NET bridge)..." + ) + add_custom_target(RcNetBridge ALL DEPENDS "${_bridge_out}") +else() + message(STATUS "RcNetCompat: .NET SDK not found -- managed plugin support disabled") +endif() + +# Create shared library (DLL) +add_library(RcNetCompatPlugin SHARED ${PLUGIN_SOURCES}) + +if(HAS_CLR_BRIDGE) + target_compile_definitions(RcNetCompatPlugin PRIVATE HAS_CLR_BRIDGE=1) + add_dependencies(RcNetCompatPlugin RcNetBridge) + # CLR hosting uses COM (ole32) + target_link_libraries(RcNetCompatPlugin PRIVATE ole32) +endif() + +# Link Qt +target_link_libraries(RcNetCompatPlugin PRIVATE ${QT}::Widgets ${_QT_WINEXTRAS}) + +# Include directories +target_include_directories(RcNetCompatPlugin PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../src +) + +# Output to Plugins folder +set_target_properties(RcNetCompatPlugin PROPERTIES + LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/Plugins" +) diff --git a/plugins/RcNetPluginCompatLayer/ClrHost.cpp b/plugins/RcNetPluginCompatLayer/ClrHost.cpp new file mode 100644 index 0000000..dd96c08 --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/ClrHost.cpp @@ -0,0 +1,162 @@ +#include "ClrHost.h" + +#include + +// -- GUIDs ---------------------------------------------------------------- + +using FnCLRCreateInstance = HRESULT(STDAPICALLTYPE*)(REFCLSID, REFIID, LPVOID*); + +// {9280188D-0E8E-4867-B30C-7FA83884E8DE} +static const GUID sCLSID_CLRMetaHost = + {0x9280188d, 0x0e8e, 0x4867, {0xb3, 0x0c, 0x7f, 0xa8, 0x38, 0x84, 0xe8, 0xde}}; + +// {D332DB9E-B9B3-4125-8207-A14884F53216} +static const GUID sIID_ICLRMetaHost = + {0xD332DB9E, 0xB9B3, 0x4125, {0x82, 0x07, 0xA1, 0x48, 0x84, 0xF5, 0x32, 0x16}}; + +// {BD39D1D2-BA2F-486A-89B0-B4B0CB466891} +static const GUID sIID_ICLRRuntimeInfo = + {0xBD39D1D2, 0xBA2F, 0x486a, {0x89, 0xB0, 0xB4, 0xB0, 0xCB, 0x46, 0x68, 0x91}}; + +// {90F1A06E-7712-4762-86B5-7A5EBA6BDB02} +static const GUID sCLSID_CLRRuntimeHost = + {0x90F1A06E, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}}; + +// {90F1A06C-7712-4762-86B5-7A5EBA6BDB02} +static const GUID sIID_ICLRRuntimeHost = + {0x90F1A06C, 0x7712, 0x4762, {0x86, 0xB5, 0x7A, 0x5E, 0xBA, 0x6B, 0xDB, 0x02}}; + +// -- ClrHost implementation ----------------------------------------------- + +ClrHost::ClrHost() +{ + startClr(); +} + +ClrHost::~ClrHost() +{ + if (m_runtimeHost) m_runtimeHost->Release(); + if (m_runtimeInfo) m_runtimeInfo->Release(); + if (m_metaHost) m_metaHost->Release(); + if (m_mscoree) FreeLibrary(m_mscoree); +} + +bool ClrHost::startClr() +{ + m_mscoree = LoadLibraryW(L"mscoree.dll"); + if (!m_mscoree) + return false; + + auto fnCreate = reinterpret_cast( + GetProcAddress(m_mscoree, "CLRCreateInstance")); + if (!fnCreate) + return false; + + HRESULT hr = fnCreate(sCLSID_CLRMetaHost, sIID_ICLRMetaHost, + reinterpret_cast(&m_metaHost)); + if (FAILED(hr) || !m_metaHost) + return false; + + hr = m_metaHost->GetRuntime(L"v4.0.30319", sIID_ICLRRuntimeInfo, + reinterpret_cast(&m_runtimeInfo)); + if (FAILED(hr) || !m_runtimeInfo) + return false; + + hr = m_runtimeInfo->GetInterface(sCLSID_CLRRuntimeHost, sIID_ICLRRuntimeHost, + (LPVOID*)&m_runtimeHost); + if (FAILED(hr) || !m_runtimeHost) + return false; + + hr = m_runtimeHost->Start(); + if (FAILED(hr)) + return false; + + m_clrStarted = true; + + return true; +} + +bool ClrHost::loadManagedPlugin(const QString& bridgeDllPath, + const QString& pluginPath, + RcNetFunctions* outFunctions, + QString* errorMsg) +{ + if (!m_runtimeHost || !m_clrStarted) { + if (errorMsg) + *errorMsg = QStringLiteral( + ".NET Framework 4.x is not available on this machine.\n" + "Install the .NET Framework 4.7.2+ runtime to load managed plugins."); + return false; + } + + + // Zero the function table -- the bridge will fill it + memset(outFunctions, 0, sizeof(RcNetFunctions)); + + // Build the argument string: "|" + // Use %ls (not %s) for wide strings -- MinGW follows POSIX conventions. + wchar_t arg[2048]; + swprintf(arg, sizeof(arg) / sizeof(wchar_t), + L"%llx|%ls", + reinterpret_cast(outFunctions), + reinterpret_cast(pluginPath.utf16())); + + DWORD retVal = 0; + HRESULT hr = m_runtimeHost->ExecuteInDefaultAppDomain( + reinterpret_cast(bridgeDllPath.utf16()), + L"RcNetBridge.Bridge", + L"Initialize", + arg, + &retVal + ); + + if (FAILED(hr)) { + if (errorMsg) + *errorMsg = QStringLiteral( + "Failed to execute .NET bridge (HRESULT 0x%1).\n" + "Bridge: %2\n" + "Plugin: %3") + .arg(static_cast(hr), 8, 16, QChar('0')) + .arg(bridgeDllPath) + .arg(pluginPath); + return false; + } + + if (retVal != 0) { + if (errorMsg) { + switch (retVal) { + case 1: + *errorMsg = QStringLiteral("Bridge: invalid argument format."); + break; + case 2: + *errorMsg = QStringLiteral( + "No ICoreProcessFunctions implementation found in the .NET plugin.\n" + "The DLL may not be a ReClass.NET plugin."); + break; + case 3: + *errorMsg = QStringLiteral( + "Failed to load the .NET plugin assembly.\n" + "Check that all its dependencies are available."); + break; + default: + *errorMsg = QStringLiteral("Bridge returned error code %1.").arg(retVal); + break; + } + } + return false; + } + + // Verify the bridge wrote at least the minimum required function pointers + if (!outFunctions->ReadRemoteMemory || + !outFunctions->OpenRemoteProcess || + !outFunctions->EnumerateProcesses || + !outFunctions->CloseRemoteProcess) { + if (errorMsg) + *errorMsg = QStringLiteral( + "The .NET bridge loaded but did not provide the required functions " + "(ReadRemoteMemory, OpenRemoteProcess, CloseRemoteProcess, EnumerateProcesses)."); + return false; + } + + return true; +} diff --git a/plugins/RcNetPluginCompatLayer/ClrHost.h b/plugins/RcNetPluginCompatLayer/ClrHost.h new file mode 100644 index 0000000..905e12b --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/ClrHost.h @@ -0,0 +1,99 @@ +#pragma once +// In-process CLR hosting for loading .NET ReClass.NET plugins. +// Dynamically loads mscoree.dll and uses ICLRMetaHost -> ICLRRuntimeInfo -> +// ICLRRuntimeHost::ExecuteInDefaultAppDomain to call into the C# bridge. + +#include "ReClassNET_Plugin.hpp" +#include +#include +#include + +// -- Minimal COM interface definitions for CLR hosting -------------------- +// Defined here to avoid depending on Windows SDK metahost.h / mscoree.h +// which may not be present in all MinGW distributions. +// Only methods we actually call have real signatures; the rest are stubs +// that preserve correct vtable offsets. + +#undef INTERFACE +#define INTERFACE ICLRMetaHost +DECLARE_INTERFACE_(ICLRMetaHost, IUnknown) +{ + // IUnknown + STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE; + STDMETHOD_(ULONG, AddRef)() PURE; + STDMETHOD_(ULONG, Release)() PURE; + // ICLRMetaHost + STDMETHOD(GetRuntime)(LPCWSTR pwzVersion, REFIID riid, LPVOID* ppRuntime) PURE; + STDMETHOD(GetVersionFromFile)(LPCWSTR, LPWSTR, DWORD*) PURE; + STDMETHOD(EnumerateInstalledRuntimes)(void**) PURE; + STDMETHOD(EnumerateLoadedRuntimes)(HANDLE, void**) PURE; + STDMETHOD(RequestRuntimeLoadedNotification)(void*) PURE; + STDMETHOD(QueryLegacyV2RuntimeBinding)(REFIID, LPVOID*) PURE; + STDMETHOD_(void, ExitProcess)(INT32) PURE; +}; +#undef INTERFACE + +#define INTERFACE ICLRRuntimeInfo +DECLARE_INTERFACE_(ICLRRuntimeInfo, IUnknown) +{ + // IUnknown + STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE; + STDMETHOD_(ULONG, AddRef)() PURE; + STDMETHOD_(ULONG, Release)() PURE; + // ICLRRuntimeInfo + STDMETHOD(GetVersionString)(LPWSTR, DWORD*) PURE; + STDMETHOD(GetRuntimeDirectory)(LPWSTR, DWORD*) PURE; + STDMETHOD(IsLoaded)(HANDLE, BOOL*) PURE; + STDMETHOD(LoadErrorString)(UINT, LPWSTR, DWORD*, LONG) PURE; + STDMETHOD(LoadLibrary)(LPCWSTR, HMODULE*) PURE; + STDMETHOD(GetProcAddress)(LPCSTR, LPVOID*) PURE; + STDMETHOD(GetInterface)(REFCLSID rclsid, REFIID riid, LPVOID* ppUnk) PURE; +}; +#undef INTERFACE + +#define INTERFACE ICLRRuntimeHost +DECLARE_INTERFACE_(ICLRRuntimeHost, IUnknown) +{ + // IUnknown + STDMETHOD(QueryInterface)(REFIID riid, void** ppv) PURE; + STDMETHOD_(ULONG, AddRef)() PURE; + STDMETHOD_(ULONG, Release)() PURE; + // ICLRRuntimeHost + STDMETHOD(Start)() PURE; + STDMETHOD(Stop)() PURE; + STDMETHOD(SetHostControl)(void*) PURE; + STDMETHOD(GetCLRControl)(void**) PURE; + STDMETHOD(UnloadAppDomain)(DWORD, BOOL) PURE; + STDMETHOD(ExecuteInAppDomain)(DWORD, void*, void*) PURE; + STDMETHOD(GetCurrentAppDomainId)(DWORD*) PURE; + STDMETHOD(ExecuteApplication)(LPCWSTR, DWORD, LPCWSTR*, DWORD, LPCWSTR*, int*) PURE; + STDMETHOD(ExecuteInDefaultAppDomain)(LPCWSTR, LPCWSTR, LPCWSTR, LPCWSTR, DWORD*) PURE; +}; +#undef INTERFACE + +// -- CLR Host wrapper ----------------------------------------------------- + +class ClrHost +{ +public: + ClrHost(); + ~ClrHost(); + + // True if the .NET Framework CLR (v4.0) is available on this machine. + bool isAvailable() const { return m_runtimeHost != nullptr && m_clrStarted; } + + // Load a managed ReClass.NET plugin via the C# bridge. + bool loadManagedPlugin(const QString& bridgeDllPath, + const QString& pluginPath, + RcNetFunctions* outFunctions, + QString* errorMsg = nullptr); + +private: + bool startClr(); + + HMODULE m_mscoree = nullptr; + ICLRMetaHost* m_metaHost = nullptr; + ICLRRuntimeInfo* m_runtimeInfo = nullptr; + ICLRRuntimeHost* m_runtimeHost = nullptr; + bool m_clrStarted = false; +}; diff --git a/plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp b/plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp new file mode 100644 index 0000000..e5f2167 --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.cpp @@ -0,0 +1,333 @@ +#include "RcNetCompatPlugin.h" +#include "RcNetCompatProvider.h" +#include "../../src/processpicker.h" + +#include +#include +#include +#include +#include +#include +#include + +#include + +// -- Helpers -------------------------------------------------------------- + +QIcon RcNetCompatPlugin::Icon() const +{ + return qApp->style()->standardIcon(QStyle::SP_TrashIcon); +} + +// --.NET assembly detection ---------------------------------------------- + +static bool isDotNetAssembly(const QString& path) +{ + // A .NET assembly has a non-zero CLR header directory entry in the PE + // optional header. We check this by loading the PE without running + // DllMain and inspecting the IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR. + HMODULE hMod = GetModuleHandleW(reinterpret_cast(path.utf16())); + if (!hMod) + hMod = LoadLibraryExW(reinterpret_cast(path.utf16()), + nullptr, DONT_RESOLVE_DLL_REFERENCES); + if (!hMod) return false; + + auto* dos = reinterpret_cast(hMod); + if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false; + + auto* nt = reinterpret_cast( + reinterpret_cast(hMod) + dos->e_lfanew); + if (nt->Signature != IMAGE_NT_SIGNATURE) return false; + + constexpr DWORD kClrIndex = IMAGE_DIRECTORY_ENTRY_COM_DESCRIPTOR; // 14 + DWORD rva = 0, dirSize = 0; + + if (nt->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { + auto* opt = reinterpret_cast(&nt->OptionalHeader); + if (opt->NumberOfRvaAndSizes > kClrIndex) { + rva = opt->DataDirectory[kClrIndex].VirtualAddress; + dirSize = opt->DataDirectory[kClrIndex].Size; + } + } else { + auto* opt = reinterpret_cast(&nt->OptionalHeader); + if (opt->NumberOfRvaAndSizes > kClrIndex) { + rva = opt->DataDirectory[kClrIndex].VirtualAddress; + dirSize = opt->DataDirectory[kClrIndex].Size; + } + } + + return rva != 0 && dirSize != 0; +} + +// --Unified loader (dispatches native vs managed) ------------------------ + +bool RcNetCompatPlugin::loadPlugin(const QString& path, QString* errorMsg) +{ + if (m_dllPath == path && (m_lib || m_isManaged)) + return true; // Already loaded + + if (isDotNetAssembly(path)) { +#ifdef HAS_CLR_BRIDGE + return loadManagedDll(path, errorMsg); +#else + if (errorMsg) + *errorMsg = QStringLiteral( + "This is a .NET assembly.\n\n" + "This build does not include .NET bridge support.\n" + "Rebuild with the .NET SDK installed to enable managed plugin loading."); + return false; +#endif + } + return loadNativeDll(path, errorMsg); +} + +// --Native DLL loading --------------------------------------------------- + +bool RcNetCompatPlugin::loadNativeDll(const QString& path, QString* errorMsg) +{ + unloadNativeDll(); + + m_lib = std::make_unique(path); + if (!m_lib->load()) { + if (errorMsg) + *errorMsg = QStringLiteral("Failed to load DLL: %1").arg(m_lib->errorString()); + m_lib.reset(); + return false; + } + + // Resolve all function pointers + m_fns.EnumerateProcesses = + reinterpret_cast(m_lib->resolve("EnumerateProcesses")); + m_fns.OpenRemoteProcess = + reinterpret_cast(m_lib->resolve("OpenRemoteProcess")); + m_fns.IsProcessValid = + reinterpret_cast(m_lib->resolve("IsProcessValid")); + m_fns.CloseRemoteProcess = + reinterpret_cast(m_lib->resolve("CloseRemoteProcess")); + m_fns.ReadRemoteMemory = + reinterpret_cast(m_lib->resolve("ReadRemoteMemory")); + m_fns.WriteRemoteMemory = + reinterpret_cast(m_lib->resolve("WriteRemoteMemory")); + m_fns.EnumerateRemoteSectionsAndModules = + reinterpret_cast( + m_lib->resolve("EnumerateRemoteSectionsAndModules")); + m_fns.ControlRemoteProcess = + reinterpret_cast(m_lib->resolve("ControlRemoteProcess")); + + // At minimum we need read + open + close + if (!m_fns.ReadRemoteMemory || !m_fns.OpenRemoteProcess || !m_fns.CloseRemoteProcess || !m_fns.EnumerateProcesses) { + if (errorMsg) + *errorMsg = QStringLiteral( + "DLL is missing required exports (ReadRemoteMemory, OpenRemoteProcess, " + "CloseRemoteProcess, EnumerateProcesses). Is this a ReClass.NET native plugin?"); + m_lib->unload(); + m_lib.reset(); + m_fns = {}; + return false; + } + + m_dllPath = path; + m_isManaged = false; + return true; +} + +void RcNetCompatPlugin::unloadNativeDll() +{ + if (m_lib) { + m_lib->unload(); + m_lib.reset(); + } + m_fns = {}; + m_dllPath.clear(); + m_isManaged = false; +} + +// --Managed (.NET) DLL loading via CLR bridge ---------------------------- + +#ifdef HAS_CLR_BRIDGE + +bool RcNetCompatPlugin::loadManagedDll(const QString& path, QString* errorMsg) +{ + unloadNativeDll(); + + // Lazily create the CLR host (one per plugin lifetime) + if (!m_clrHost) + m_clrHost = std::make_unique(); + + if (!m_clrHost->isAvailable()) { + if (errorMsg) + *errorMsg = QStringLiteral( + ".NET Framework 4.x is not available on this machine.\n" + "Install the .NET Framework 4.7.2+ runtime to load managed plugins."); + return false; + } + + // Locate RcNetBridge.dll next to our own plugin DLL + // Use native separators -- the CLR expects Windows-style backslash paths. + QString bridgePath = QDir::toNativeSeparators( + QCoreApplication::applicationDirPath() + + QStringLiteral("/Plugins/RcNetBridge.dll")); + + if (!QFileInfo::exists(bridgePath)) { + if (errorMsg) + *errorMsg = QStringLiteral( + "RcNetBridge.dll not found in the Plugins folder.\n" + "Expected at: %1").arg(bridgePath); + return false; + } + + m_fns = {}; + QString nativePath = QDir::toNativeSeparators(path); + if (!m_clrHost->loadManagedPlugin(bridgePath, nativePath, &m_fns, errorMsg)) + return false; + + m_dllPath = path; + m_isManaged = true; + return true; +} + +#endif // HAS_CLR_BRIDGE + +// --IProviderPlugin ------------------------------------------------------ + +bool RcNetCompatPlugin::canHandle(const QString& target) const +{ + // Target format: "dllpath|pid:name" + return target.contains('|'); +} + +std::unique_ptr RcNetCompatPlugin::createProvider( + const QString& target, QString* errorMsg) +{ + // Parse "dllpath|pid:name" + int sep = target.indexOf('|'); + if (sep < 0) { + if (errorMsg) *errorMsg = QStringLiteral("Invalid target format"); + return nullptr; + } + + QString dllPath = target.left(sep); + QString pidPart = target.mid(sep + 1); + + // Load (or reuse) the plugin DLL + if (!loadPlugin(dllPath, errorMsg)) + return nullptr; + + // Parse pid:name + QStringList parts = pidPart.split(':'); + bool ok = false; + uint32_t pid = parts[0].toUInt(&ok); + if (!ok || pid == 0) { + if (errorMsg) *errorMsg = QStringLiteral("Invalid PID: %1").arg(parts[0]); + return nullptr; + } + QString procName = parts.size() > 1 ? parts[1] : QStringLiteral("PID %1").arg(pid); + + auto provider = std::make_unique(m_fns, pid, procName); + if (!provider->isValid()) { + if (errorMsg) + *errorMsg = QStringLiteral( + "Failed to open process %1 (PID: %2) via ReClass.NET plugin.\n" + "Ensure the process is running and the plugin supports it.") + .arg(procName).arg(pid); + return nullptr; + } + + return provider; +} + +uint64_t RcNetCompatPlugin::getInitialBaseAddress(const QString& target) const +{ + Q_UNUSED(target); + // The provider sets its own base from module enumeration. + return 0; +} + +bool RcNetCompatPlugin::selectTarget(QWidget* parent, QString* target) +{ + // Step 1: Pick a ReClass.NET plugin DLL (native or .NET) + QString dllPath = QFileDialog::getOpenFileName( + parent, + QStringLiteral("Select ReClass.NET Plugin"), + QString(), + QStringLiteral("DLL Files (*.dll)")); + + if (dllPath.isEmpty()) + return false; + + // Step 2: Load and validate the DLL + QString loadErr; + if (!loadPlugin(dllPath, &loadErr)) { + QMessageBox::warning(parent, + QStringLiteral("ReClass.NET Compat Layer"), + loadErr); + return false; + } + + // Step 3: Enumerate processes and show picker + QVector pluginProcesses = enumerateProcesses(); + + QList processes; + for (const auto& p : pluginProcesses) { + ProcessInfo info; + info.pid = p.pid; + info.name = p.name; + info.path = p.path; + info.icon = p.icon; + processes.append(info); + } + + ProcessPicker picker(processes, parent); + if (picker.exec() != QDialog::Accepted) + return false; + + uint32_t pid = picker.selectedProcessId(); + QString name = picker.selectedProcessName(); + + // Step 4: Format target as "dllpath|pid:name" + *target = QStringLiteral("%1|%2:%3").arg(dllPath).arg(pid).arg(name); + return true; +} + +// --Process enumeration -------------------------------------------------- + +namespace { + +struct ProcessCollector { + QVector* dest = nullptr; +}; +thread_local ProcessCollector g_processCollector; + +void RC_CALLCONV processCallback(EnumerateProcessData* data) +{ + if (!data || !g_processCollector.dest) return; + + PluginProcessInfo info; + info.pid = static_cast(data->Id); + info.name = QString::fromUtf16(data->Name); + info.path = QString::fromUtf16(data->Path); + g_processCollector.dest->append(info); +} + +} // anonymous namespace + +QVector RcNetCompatPlugin::enumerateProcesses() +{ + QVector result; + + if (!m_fns.EnumerateProcesses) + return result; + + g_processCollector.dest = &result; + m_fns.EnumerateProcesses(processCallback); + g_processCollector.dest = nullptr; + + return result; +} + +// --Plugin factory ------------------------------------------------------- + +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin() +{ + return new RcNetCompatPlugin(); +} diff --git a/plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h b/plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h new file mode 100644 index 0000000..d0b3bfb --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/RcNetCompatPlugin.h @@ -0,0 +1,61 @@ +#pragma once +#include "../../src/iplugin.h" +#include "ReClassNET_Plugin.hpp" + +#include +#include + +#ifdef HAS_CLR_BRIDGE +#include "ClrHost.h" +#endif + +/** + * ReclassX plugin that loads ReClass.NET plugin DLLs + * and exposes them as ReclassX providers. + * + * Supports both native DLLs (C exports) and, when built with + * HAS_CLR_BRIDGE, managed .NET assemblies via in-process CLR hosting. + * + * Target string format: "dllpath|pid:processname" + */ +class RcNetCompatPlugin : public IProviderPlugin +{ +public: + // Plugin metadata + std::string Name() const override { return "ReClass.NET Compat Layer"; } + std::string Version() const override { return "1.0.0"; } + std::string Author() const override { return "Reclass"; } + std::string Description() const override { + return "Loads ReClass.NET native and .NET plugin DLLs as Reclass data sources"; + } + k_ELoadType LoadType() const override { return k_ELoadTypeAuto; } + QIcon Icon() const override; + + // IProviderPlugin interface + bool canHandle(const QString& target) const override; + std::unique_ptr createProvider(const QString& target, QString* errorMsg) override; + uint64_t getInitialBaseAddress(const QString& target) const override; + bool selectTarget(QWidget* parent, QString* target) override; + + // Override process enumeration -- we enumerate via the loaded DLL + bool providesProcessList() const override { return true; } + QVector enumerateProcesses() override; + +private: + bool loadPlugin(const QString& path, QString* errorMsg = nullptr); + bool loadNativeDll(const QString& path, QString* errorMsg = nullptr); + void unloadNativeDll(); + +#ifdef HAS_CLR_BRIDGE + bool loadManagedDll(const QString& path, QString* errorMsg = nullptr); + std::unique_ptr m_clrHost; +#endif + + std::unique_ptr m_lib; + RcNetFunctions m_fns; + QString m_dllPath; + bool m_isManaged = false; +}; + +// Plugin export +extern "C" RCX_PLUGIN_EXPORT IPlugin* CreatePlugin(); diff --git a/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp new file mode 100644 index 0000000..c26e009 --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.cpp @@ -0,0 +1,125 @@ +#include "RcNetCompatProvider.h" + +#include +#include + +// -- Construction / destruction ------------------------------------------- + +RcNetCompatProvider::RcNetCompatProvider(const RcNetFunctions& fns, + uint32_t pid, + const QString& processName) + : m_fns(fns) + , m_pid(pid) + , m_processName(processName) +{ + if (m_fns.OpenRemoteProcess) + m_handle = m_fns.OpenRemoteProcess(static_cast(pid), + ProcessAccess::Full); + + if (m_handle) + cacheModules(); +} + +RcNetCompatProvider::~RcNetCompatProvider() +{ + if (m_handle && m_fns.CloseRemoteProcess) + m_fns.CloseRemoteProcess(m_handle); +} + +// -- Required overrides --------------------------------------------------- + +bool RcNetCompatProvider::read(uint64_t addr, void* buf, int len) const +{ + if (!m_handle || !m_fns.ReadRemoteMemory || len <= 0) + return false; + + uint64_t absAddr = m_base + addr; + return m_fns.ReadRemoteMemory(m_handle, + reinterpret_cast(absAddr), + static_cast(buf), + 0, len); +} + +int RcNetCompatProvider::size() const +{ + if (!m_handle) return 0; + if (m_fns.IsProcessValid && !m_fns.IsProcessValid(m_handle)) return 0; + return 0x10000; +} + +// -- Optional overrides --------------------------------------------------- + +bool RcNetCompatProvider::write(uint64_t addr, const void* buf, int len) +{ + if (!m_handle || !m_fns.WriteRemoteMemory || len <= 0) + return false; + + uint64_t absAddr = m_base + addr; + return m_fns.WriteRemoteMemory(m_handle, + reinterpret_cast(absAddr), + const_cast(static_cast(buf)), + 0, len); +} + +QString RcNetCompatProvider::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 {}; +} + +// -- Module enumeration --------------------------------------------------- + +namespace { + +// Thread-local collector for the module enumeration callback. +// ReClass.NET callbacks are synchronous, so this is safe. +struct ModuleCollector { + QVector* dest = nullptr; +}; +thread_local ModuleCollector g_moduleCollector; + +void RC_CALLCONV moduleCallback(EnumerateRemoteModuleData* data) +{ + if (!data || !g_moduleCollector.dest) return; + + QString path = QString::fromUtf16(data->Path); + QFileInfo fi(path); + + RcNetCompatProvider::ModuleInfo info; + info.name = fi.fileName(); + info.base = reinterpret_cast(data->BaseAddress); + info.size = static_cast(data->Size); + g_moduleCollector.dest->append(info); +} + +// We still need a section callback even though we don't use it. +void RC_CALLCONV sectionCallback(EnumerateRemoteSectionData*) +{ + // Intentionally empty -- we only need module data. +} + +} // anonymous namespace + +void RcNetCompatProvider::cacheModules() +{ + if (!m_fns.EnumerateRemoteSectionsAndModules || !m_handle) + return; + + m_modules.clear(); + g_moduleCollector.dest = &m_modules; + m_fns.EnumerateRemoteSectionsAndModules(m_handle, sectionCallback, moduleCallback); + g_moduleCollector.dest = nullptr; + + // Set base to first module if we got any + if (!m_modules.isEmpty() && m_base == 0) + m_base = m_modules.first().base; +} diff --git a/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h new file mode 100644 index 0000000..ade4621 --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/RcNetCompatProvider.h @@ -0,0 +1,48 @@ +#pragma once +#include "../../src/providers/provider.h" +#include "ReClassNET_Plugin.hpp" + +#include +#include + +/** + * Provider that bridges ReClass.NET native plugin DLL calls + * to the ReclassX Provider interface. + */ +class RcNetCompatProvider : public rcx::Provider +{ +public: + RcNetCompatProvider(const RcNetFunctions& fns, uint32_t pid, + const QString& processName); + ~RcNetCompatProvider() override; + + // Required overrides + bool read(uint64_t addr, void* buf, int len) const override; + int size() const override; + + // Optional overrides + bool write(uint64_t addr, const void* buf, int len) override; + bool isWritable() const override { return m_fns.WriteRemoteMemory != nullptr; } + QString name() const override { return m_processName; } + QString kind() const override { return QStringLiteral("RcNet"); } + bool isLive() const override { return true; } + uint64_t base() const override { return m_base; } + void setBase(uint64_t b) override { m_base = b; } + QString getSymbol(uint64_t addr) const override; + + struct ModuleInfo { + QString name; + uint64_t base; + uint64_t size; + }; + +private: + void cacheModules(); + + RcNetFunctions m_fns; + RC_Pointer m_handle = nullptr; + uint32_t m_pid; + QString m_processName; + uint64_t m_base = 0; + QVector m_modules; +}; diff --git a/plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp b/plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp new file mode 100644 index 0000000..69d6c0d --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/ReClassNET_Plugin.hpp @@ -0,0 +1,140 @@ +#pragma once +// Subset of ReClass.NET native plugin types needed for the compatibility layer. +// Based on the ReClass.NET NativeCore plugin interface. +// Only types required by the 8 supported exports are included (no debug types). + +#include + +#ifdef _WIN32 +#define RC_CALLCONV __stdcall +#else +#define RC_CALLCONV +#endif + +// -- Basic types ---------------------------------------------------------- + +using RC_Pointer = void*; +using RC_Size = uint64_t; +using RC_UnicodeChar = char16_t; + +// -- Enums ---------------------------------------------------------------- + +enum class ProcessAccess +{ + Read = 0, + Write = 1, + Full = 2 +}; + +enum class SectionProtection +{ + NoAccess = 0, + Read = 1, + Write = 2, + Execute = 4, + Guard = 8 +}; + +enum class SectionType +{ + Unknown = 0, + Private = 1, + Mapped = 2, + Image = 3 +}; + +enum class SectionCategory +{ + Unknown = 0, + CODE = 1, + DATA = 2, + HEAP = 3 +}; + +enum class ControlRemoteProcessAction +{ + Suspend = 0, + Resume = 1, + Terminate = 2 +}; + +// -- Callback data structures --------------------------------------------- + +#pragma pack(push, 1) + +struct EnumerateProcessData +{ + RC_Size Id; + RC_UnicodeChar Name[260]; + RC_UnicodeChar Path[260]; +}; + +struct EnumerateRemoteSectionData +{ + RC_Pointer BaseAddress; + RC_Size Size; + SectionType Type; + SectionCategory Category; + SectionProtection Protection; + RC_UnicodeChar Name[16]; + RC_UnicodeChar ModulePath[260]; +}; + +struct EnumerateRemoteModuleData +{ + RC_Pointer BaseAddress; + RC_Size Size; + RC_UnicodeChar Path[260]; +}; + +#pragma pack(pop) + +// -- Callback typedefs ---------------------------------------------------- + +using EnumerateProcessCallback = void(RC_CALLCONV*)(EnumerateProcessData* data); +using EnumerateRemoteSectionsCallback = void(RC_CALLCONV*)(EnumerateRemoteSectionData* data); +using EnumerateRemoteModulesCallback = void(RC_CALLCONV*)(EnumerateRemoteModuleData* data); + +// -- Function pointer typedefs for resolved exports ----------------------- + +using FnEnumerateProcesses = void(RC_CALLCONV*)(EnumerateProcessCallback callback); + +using FnOpenRemoteProcess = RC_Pointer(RC_CALLCONV*)(RC_Size id, ProcessAccess desiredAccess); + +using FnIsProcessValid = bool(RC_CALLCONV*)(RC_Pointer handle); + +using FnCloseRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle); + +using FnReadRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle, + RC_Pointer address, + RC_Pointer buffer, + int offset, + int size); + +using FnWriteRemoteMemory = bool(RC_CALLCONV*)(RC_Pointer handle, + RC_Pointer address, + RC_Pointer buffer, + int offset, + int size); + +using FnEnumerateRemoteSectionsAndModules = + void(RC_CALLCONV*)(RC_Pointer handle, + EnumerateRemoteSectionsCallback sectionCallback, + EnumerateRemoteModulesCallback moduleCallback); + +using FnControlRemoteProcess = void(RC_CALLCONV*)(RC_Pointer handle, + ControlRemoteProcessAction action); + +// -- Resolved function table ---------------------------------------------- + +struct RcNetFunctions +{ + FnEnumerateProcesses EnumerateProcesses = nullptr; + FnOpenRemoteProcess OpenRemoteProcess = nullptr; + FnIsProcessValid IsProcessValid = nullptr; + FnCloseRemoteProcess CloseRemoteProcess = nullptr; + FnReadRemoteMemory ReadRemoteMemory = nullptr; + FnWriteRemoteMemory WriteRemoteMemory = nullptr; + FnEnumerateRemoteSectionsAndModules EnumerateRemoteSectionsAndModules = nullptr; + FnControlRemoteProcess ControlRemoteProcess = nullptr; +}; diff --git a/plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs b/plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs new file mode 100644 index 0000000..e059bd1 --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.cs @@ -0,0 +1,677 @@ +// RcNetBridge -- in-process C# bridge for loading .NET ReClass.NET plugins. +// +// Called from C++ via ICLRRuntimeHost::ExecuteInDefaultAppDomain(). +// The single entry point is Bridge.Initialize(string arg) where arg is: +// "|" +// +// The bridge: +// 1. Registers an AssemblyResolve handler that provides THIS assembly +// when a plugin asks for "ReClassNET", so the stub types below satisfy +// the plugin's type references. +// 2. Loads the plugin assembly and finds an ICoreProcessFunctions +// implementation. +// 3. Creates [UnmanagedFunctionPointer] delegates wrapping each method. +// 4. Writes the native-callable function pointers into the RcNetFunctions +// struct at the address provided by C++. + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; + +// =========================================================================== +// ReClass.NET stub types +// These mirror the subset of types from the ReClass.NET assembly that +// memory-reading plugins reference. When the CLR resolves "ReClassNET" +// via our AssemblyResolve handler, it gets THIS assembly, and these types +// satisfy the plugin's type references. +// +// Types are placed in the exact namespaces used by the real ReClass.NET +// assembly so that plugins compiled against it resolve correctly. +// =========================================================================== + +// -------------------------------------------------------------------------- +// ReClassNET.Memory -- section enums (referenced by EnumerateRemoteSectionData) +// -------------------------------------------------------------------------- +namespace ReClassNET.Memory +{ + public enum SectionProtection + { + NoAccess = 0, + Read = 1, + Write = 2, + Execute = 4, + Guard = 8 + } + + public enum SectionType + { + Unknown = 0, + Private = 1, + Mapped = 2, + Image = 3 + } + + public enum SectionCategory + { + Unknown = 0, + CODE = 1, + DATA = 2, + HEAP = 3 + } +} + +// -------------------------------------------------------------------------- +// ReClassNET.Debugger -- debugger types (used by ICoreProcessFunctions) +// -------------------------------------------------------------------------- +namespace ReClassNET.Debugger +{ + public enum DebugContinueStatus + { + Handled = 0, + NotHandled = 1 + } + + public enum HardwareBreakpointRegister + { + InvalidRegister = 0, + Dr0 = 1, + Dr1 = 2, + Dr2 = 3, + Dr3 = 4 + } + + public enum HardwareBreakpointTrigger + { + Execute = 0, + Access = 1, + Write = 2 + } + + public enum HardwareBreakpointSize + { + Size1 = 1, + Size2 = 2, + Size4 = 4, + Size8 = 8 + } + + public struct ExceptionDebugInfo + { + public IntPtr ExceptionCode; + public IntPtr ExceptionFlags; + public IntPtr ExceptionAddress; + public HardwareBreakpointRegister CausedBy; + public RegisterInfo Registers; + + public struct RegisterInfo + { + public IntPtr Rax, Rbx, Rcx, Rdx; + public IntPtr Rdi, Rsi, Rsp, Rbp, Rip; + public IntPtr R8, R9, R10, R11, R12, R13, R14, R15; + } + } + + public struct DebugEvent + { + public DebugContinueStatus ContinueStatus; + public IntPtr ProcessId; + public IntPtr ThreadId; + public ExceptionDebugInfo ExceptionInfo; + } +} + +// -------------------------------------------------------------------------- +// ReClassNET.Core -- interface, enums, delegates, and data structs +// -------------------------------------------------------------------------- +namespace ReClassNET.Core +{ + public enum ProcessAccess + { + Read = 0, + Write = 1, + Full = 2 + } + + public enum ControlRemoteProcessAction + { + Suspend = 0, + Resume = 1, + Terminate = 2 + } + + public struct EnumerateProcessData + { + public IntPtr Id; + public string Name; + public string Path; + } + + public struct EnumerateRemoteSectionData + { + public IntPtr BaseAddress; + public IntPtr Size; + public ReClassNET.Memory.SectionType Type; + public ReClassNET.Memory.SectionCategory Category; + public ReClassNET.Memory.SectionProtection Protection; + public string Name; + public string ModulePath; + } + + public struct EnumerateRemoteModuleData + { + public IntPtr BaseAddress; + public IntPtr Size; + public string Path; + } + + public delegate void EnumerateProcessCallback(ref EnumerateProcessData data); + public delegate void EnumerateRemoteSectionCallback(ref EnumerateRemoteSectionData data); + public delegate void EnumerateRemoteModuleCallback(ref EnumerateRemoteModuleData data); + + public interface ICoreProcessFunctions + { + void EnumerateProcesses(EnumerateProcessCallback callbackProcess); + IntPtr OpenRemoteProcess(IntPtr pid, ProcessAccess desiredAccess); + bool IsProcessValid(IntPtr process); + void CloseRemoteProcess(IntPtr process); + bool ReadRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size); + bool WriteRemoteMemory(IntPtr process, IntPtr address, ref byte[] buffer, int offset, int size); + void EnumerateRemoteSectionsAndModules( + IntPtr process, + EnumerateRemoteSectionCallback callbackSection, + EnumerateRemoteModuleCallback callbackModule); + void ControlRemoteProcess(IntPtr process, ControlRemoteProcessAction action); + + // Debugger methods -- stubs required for interface compatibility + bool AttachDebuggerToProcess(IntPtr id); + void DetachDebuggerFromProcess(IntPtr id); + bool AwaitDebugEvent(ref ReClassNET.Debugger.DebugEvent evt, int timeoutInMilliseconds); + void HandleDebugEvent(ref ReClassNET.Debugger.DebugEvent evt); + bool SetHardwareBreakpoint(IntPtr id, IntPtr address, + ReClassNET.Debugger.HardwareBreakpointRegister register, + ReClassNET.Debugger.HardwareBreakpointTrigger trigger, + ReClassNET.Debugger.HardwareBreakpointSize size, + bool set); + } +} + +// -------------------------------------------------------------------------- +// ReClassNET.Memory -- RemoteProcess stub +// -------------------------------------------------------------------------- +namespace ReClassNET.Memory +{ + public class RemoteProcess { } +} + +// -------------------------------------------------------------------------- +// ReClassNET.Logger -- ILogger stub +// -------------------------------------------------------------------------- +namespace ReClassNET.Logger +{ + public interface ILogger { } +} + +// -------------------------------------------------------------------------- +// Stub types for IPluginHost properties +// -------------------------------------------------------------------------- +namespace ReClassNET.Forms +{ + public class MainForm { } +} + +namespace ReClassNET +{ + public class Settings { } +} + +// -------------------------------------------------------------------------- +// ReClassNET.Plugins +// -------------------------------------------------------------------------- +namespace ReClassNET.Plugins +{ + public abstract class Plugin : IDisposable + { + public virtual bool Initialize(IPluginHost host) { return true; } + public virtual void Terminate() { } + public virtual void Dispose() { } + } + + public interface IPluginHost + { + ReClassNET.Forms.MainForm MainWindow { get; } + System.Resources.ResourceManager Resources { get; } + ReClassNET.Memory.RemoteProcess Process { get; } + ReClassNET.Logger.ILogger Logger { get; } + ReClassNET.Settings Settings { get; } + } +} + +// =========================================================================== +// Bridge +// =========================================================================== + +namespace RcNetBridge +{ + internal class StubPluginHost : ReClassNET.Plugins.IPluginHost + { + public ReClassNET.Forms.MainForm MainWindow => null; + public System.Resources.ResourceManager Resources => null; + public ReClassNET.Memory.RemoteProcess Process => null; + public ReClassNET.Logger.ILogger Logger => null; + public ReClassNET.Settings Settings => null; + } + + public class Bridge + { + // -- Persistent state (static so it survives after Initialize returns) -- + + private static ReClassNET.Core.ICoreProcessFunctions s_functions; + private static readonly List s_pinned = new List(); + + // -- Entry point called from C++ -------------------------------------- + + /// + /// Called by ICLRRuntimeHost::ExecuteInDefaultAppDomain. + /// arg = "<hex_address_of_RcNetFunctions>|<plugin_dll_path>" + /// Returns 0 on success, non-zero error code on failure. + /// + public static int Initialize(string arg) + { + try + { + int sep = arg.IndexOf('|'); + if (sep < 0) return 1; // bad arg + + long ptrValue = long.Parse(arg.Substring(0, sep), NumberStyles.HexNumber); + IntPtr funcTablePtr = new IntPtr(ptrValue); + string pluginPath = arg.Substring(sep + 1); + + // Set up assembly resolution + string pluginDir = Path.GetDirectoryName(pluginPath) ?? "."; + string parentDir = Path.GetDirectoryName(pluginDir); + + AppDomain.CurrentDomain.AssemblyResolve += (sender, resolveArgs) => + { + string asmName = new AssemblyName(resolveArgs.Name).Name; + + // Provide our own assembly as the "ReClass.NET" stub + if (string.Equals(asmName, "ReClass.NET", StringComparison.OrdinalIgnoreCase)) + return typeof(Bridge).Assembly; + + // Search plugin directory and parent for other dependencies + string dllName = asmName + ".dll"; + foreach (string dir in new[] { pluginDir, parentDir }) + { + if (dir == null) continue; + string path = Path.Combine(dir, dllName); + if (File.Exists(path)) + return Assembly.LoadFrom(path); + } + return null; + }; + + // Load plugin and find ICoreProcessFunctions + if (!LoadPlugin(pluginPath)) + return 2; // no implementation found + + // Write function pointers + WriteFunctionPointers(funcTablePtr); + return 0; + } + catch (Exception ex) when (ex is ReflectionTypeLoadException || ex is FileNotFoundException) + { + return 3; + } + catch + { + return 4; + } + } + + // -- Plugin loading --------------------------------------------------- + + private static bool LoadPlugin(string pluginPath) + { + Assembly asm = Assembly.LoadFrom(pluginPath); + + // Find a concrete type that implements ICoreProcessFunctions. + // ReClass.NET plugins typically extend Plugin and directly + // implement ICoreProcessFunctions on the same class. + foreach (Type type in asm.GetExportedTypes()) + { + if (type.IsAbstract || type.IsInterface) continue; + + Type iface = type.GetInterfaces().FirstOrDefault(i => + i.FullName == "ReClassNET.Core.ICoreProcessFunctions"); + if (iface == null) continue; + + object instance = Activator.CreateInstance(type); + + // Try calling Initialize() but don't fail if it throws -- + // plugins use it for UI integration with the host app, + // which we can't fully provide. The process functions + // (ReadRemoteMemory, etc.) work without it. + try + { + MethodInfo init = type.GetMethod("Initialize", + BindingFlags.Public | BindingFlags.Instance, + null, new[] { typeof(ReClassNET.Plugins.IPluginHost) }, null); + if (init != null) + init.Invoke(instance, new object[] { new StubPluginHost() }); + } + catch { } + + s_functions = (ReClassNET.Core.ICoreProcessFunctions)instance; + return true; + } + + return false; + } + + // -- Native-callable delegate types ----------------------------------- + // These match the C++ RcNetFunctions struct field order exactly. + // On x64 Windows all calling conventions collapse to the Microsoft + // x64 ABI, so StdCall is used for documentation / x86 correctness. + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void DelEnumProcesses(IntPtr callback); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate IntPtr DelOpenRemoteProcess(ulong id, int access); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.I1)] + delegate bool DelIsProcessValid(IntPtr handle); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void DelCloseRemoteProcess(IntPtr handle); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.I1)] + delegate bool DelReadRemoteMemory(IntPtr handle, IntPtr address, + IntPtr buffer, int offset, int size); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + [return: MarshalAs(UnmanagedType.I1)] + delegate bool DelWriteRemoteMemory(IntPtr handle, IntPtr address, + IntPtr buffer, int offset, int size); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void DelEnumSectionsAndModules(IntPtr handle, + IntPtr sectionCallback, IntPtr moduleCallback); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void DelControlRemoteProcess(IntPtr handle, int action); + + // Callback delegate types -- these point into C++ and are called by us. + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void NativeProcessCallback(IntPtr data); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void NativeSectionCallback(IntPtr data); + + [UnmanagedFunctionPointer(CallingConvention.StdCall)] + delegate void NativeModuleCallback(IntPtr data); + + // -- Write function pointers to the C++ struct ------------------------ + + private static void WriteFunctionPointers(IntPtr funcTable) + { + // RcNetFunctions layout: 8 consecutive function pointers. + int i = 0; + WriteSlot(funcTable, i++, Pin(EnumProcessesImpl)); + WriteSlot(funcTable, i++, Pin(OpenProcessImpl)); + WriteSlot(funcTable, i++, Pin(IsProcessValidImpl)); + WriteSlot(funcTable, i++, Pin(CloseProcessImpl)); + WriteSlot(funcTable, i++, Pin(ReadMemoryImpl)); + WriteSlot(funcTable, i++, Pin(WriteMemoryImpl)); + WriteSlot(funcTable, i++, Pin(EnumSectionsModulesImpl)); + WriteSlot(funcTable, i++, Pin(ControlProcessImpl)); + } + + private static IntPtr Pin(T del) where T : class + { + Delegate d = del as Delegate; + s_pinned.Add(d); // prevent GC + return Marshal.GetFunctionPointerForDelegate(d); + } + + private static void WriteSlot(IntPtr table, int index, IntPtr value) + { + Marshal.WriteIntPtr(table, index * IntPtr.Size, value); + } + + // -- Implementation methods ------------------------------------------- + + // -- EnumerateProcesses -- + // C++ passes a native callback; we call the plugin, convert each + // managed EnumerateProcessData to the packed native layout, and + // forward to the native callback. + + private static void EnumProcessesImpl(IntPtr nativeCallbackPtr) + { + try + { + if (s_functions == null || nativeCallbackPtr == IntPtr.Zero) return; + + NativeProcessCallback nativeCb = + Marshal.GetDelegateForFunctionPointer(nativeCallbackPtr); + + // Native layout (pack=1): uint64 Id + char16[260] Name + char16[260] Path + const int kStructSize = 8 + 520 + 520; // 1048 bytes + + s_functions.EnumerateProcesses( + (ref ReClassNET.Core.EnumerateProcessData data) => + { + IntPtr mem = Marshal.AllocHGlobal(kStructSize); + try + { + // Zero-fill + byte[] zeros = new byte[kStructSize]; + Marshal.Copy(zeros, 0, mem, kStructSize); + + // Id (8 bytes at offset 0) + Marshal.WriteInt64(mem, 0, data.Id.ToInt64()); + + // Name (char16[260] at offset 8) + if (data.Name != null) + { + char[] chars = data.Name.ToCharArray(); + int count = Math.Min(chars.Length, 259); + Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 8), count); + } + + // Path (char16[260] at offset 528) + if (data.Path != null) + { + char[] chars = data.Path.ToCharArray(); + int count = Math.Min(chars.Length, 259); + Marshal.Copy(chars, 0, new IntPtr(mem.ToInt64() + 528), count); + } + + nativeCb(mem); + } + finally + { + Marshal.FreeHGlobal(mem); + } + }); + } + catch { /* swallow -- don't crash the host process */ } + } + + // -- OpenRemoteProcess -- + private static IntPtr OpenProcessImpl(ulong id, int access) + { + try + { + if (s_functions == null) return IntPtr.Zero; + return s_functions.OpenRemoteProcess( + new IntPtr((long)id), + (ReClassNET.Core.ProcessAccess)access); + } + catch { return IntPtr.Zero; } + } + + // -- IsProcessValid -- + private static bool IsProcessValidImpl(IntPtr handle) + { + try + { + if (s_functions == null) return false; + return s_functions.IsProcessValid(handle); + } + catch { return false; } + } + + // -- CloseRemoteProcess -- + private static void CloseProcessImpl(IntPtr handle) + { + try { s_functions?.CloseRemoteProcess(handle); } + catch { } + } + + // -- ReadRemoteMemory -- + // C++ provides a native buffer pointer. We read into a managed array + // via the plugin's interface, then copy to the native buffer. + private static bool ReadMemoryImpl(IntPtr handle, IntPtr address, + IntPtr buffer, int offset, int size) + { + try + { + if (s_functions == null || size <= 0) return false; + + byte[] managed = new byte[size]; + bool ok = s_functions.ReadRemoteMemory( + handle, address, ref managed, 0, size); + + if (ok) + Marshal.Copy(managed, 0, new IntPtr(buffer.ToInt64() + offset), size); + + return ok; + } + catch { return false; } + } + + // -- WriteRemoteMemory -- + private static bool WriteMemoryImpl(IntPtr handle, IntPtr address, + IntPtr buffer, int offset, int size) + { + try + { + if (s_functions == null || size <= 0) return false; + + byte[] managed = new byte[size]; + Marshal.Copy(new IntPtr(buffer.ToInt64() + offset), managed, 0, size); + + return s_functions.WriteRemoteMemory( + handle, address, ref managed, 0, size); + } + catch { return false; } + } + + // -- EnumerateRemoteSectionsAndModules -- + private static void EnumSectionsModulesImpl(IntPtr handle, + IntPtr sectionCallbackPtr, IntPtr moduleCallbackPtr) + { + try + { + if (s_functions == null) return; + + // Section callback -- forward to native + // Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) + + // SectionType(4) + SectionCategory(4) + SectionProtection(4) + + // char16 Name[16](32) + char16 ModulePath[260](520) = 580 bytes + NativeSectionCallback nativeSectionCb = (sectionCallbackPtr != IntPtr.Zero) + ? Marshal.GetDelegateForFunctionPointer(sectionCallbackPtr) + : null; + + // Module callback -- forward to native + // Native layout (pack=1): RC_Pointer Base(8) + RC_Size Size(8) + + // char16 Path[260](520) = 536 bytes + NativeModuleCallback nativeModuleCb = (moduleCallbackPtr != IntPtr.Zero) + ? Marshal.GetDelegateForFunctionPointer(moduleCallbackPtr) + : null; + + s_functions.EnumerateRemoteSectionsAndModules(handle, + // Section callback + (ref ReClassNET.Core.EnumerateRemoteSectionData sdata) => + { + if (nativeSectionCb == null) return; + + const int kSize = 8 + 8 + 4 + 4 + 4 + 32 + 520; // 580 + IntPtr mem = Marshal.AllocHGlobal(kSize); + try + { + byte[] z = new byte[kSize]; + Marshal.Copy(z, 0, mem, kSize); + + Marshal.WriteInt64(mem, 0, sdata.BaseAddress.ToInt64()); + Marshal.WriteInt64(mem, 8, sdata.Size.ToInt64()); + Marshal.WriteInt32(mem, 16, (int)sdata.Type); + Marshal.WriteInt32(mem, 20, (int)sdata.Category); + Marshal.WriteInt32(mem, 24, (int)sdata.Protection); + + if (sdata.Name != null) + { + char[] c = sdata.Name.ToCharArray(); + Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 28), + Math.Min(c.Length, 15)); + } + if (sdata.ModulePath != null) + { + char[] c = sdata.ModulePath.ToCharArray(); + Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 60), + Math.Min(c.Length, 259)); + } + + nativeSectionCb(mem); + } + finally { Marshal.FreeHGlobal(mem); } + }, + // Module callback + (ref ReClassNET.Core.EnumerateRemoteModuleData mdata) => + { + if (nativeModuleCb == null) return; + + const int kSize = 8 + 8 + 520; // 536 + IntPtr mem = Marshal.AllocHGlobal(kSize); + try + { + byte[] z = new byte[kSize]; + Marshal.Copy(z, 0, mem, kSize); + + Marshal.WriteInt64(mem, 0, mdata.BaseAddress.ToInt64()); + Marshal.WriteInt64(mem, 8, mdata.Size.ToInt64()); + + if (mdata.Path != null) + { + char[] c = mdata.Path.ToCharArray(); + Marshal.Copy(c, 0, new IntPtr(mem.ToInt64() + 16), + Math.Min(c.Length, 259)); + } + + nativeModuleCb(mem); + } + finally { Marshal.FreeHGlobal(mem); } + }); + } + catch { } + } + + // -- ControlRemoteProcess -- + private static void ControlProcessImpl(IntPtr handle, int action) + { + try + { + s_functions?.ControlRemoteProcess(handle, + (ReClassNET.Core.ControlRemoteProcessAction)action); + } + catch { } + } + } +} diff --git a/plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj b/plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj new file mode 100644 index 0000000..0a4f561 --- /dev/null +++ b/plugins/RcNetPluginCompatLayer/bridge/RcNetBridge.csproj @@ -0,0 +1,12 @@ + + + netstandard2.0 + Library + RcNetBridge + RcNetBridge + false + 7.3 + false + false + +