mirror of
https://github.com/NohamR/Reclass.git
synced 2026-05-10 19:59:21 +00:00
334 lines
10 KiB
C++
334 lines
10 KiB
C++
#include "RcNetCompatPlugin.h"
|
|
#include "RcNetCompatProvider.h"
|
|
#include "../../src/processpicker.h"
|
|
|
|
#include <QApplication>
|
|
#include <QCoreApplication>
|
|
#include <QDir>
|
|
#include <QFileDialog>
|
|
#include <QFileInfo>
|
|
#include <QMessageBox>
|
|
#include <QStyle>
|
|
|
|
#include <windows.h>
|
|
|
|
// -- 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<LPCWSTR>(path.utf16()));
|
|
if (!hMod)
|
|
hMod = LoadLibraryExW(reinterpret_cast<LPCWSTR>(path.utf16()),
|
|
nullptr, DONT_RESOLVE_DLL_REFERENCES);
|
|
if (!hMod) return false;
|
|
|
|
auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(hMod);
|
|
if (dos->e_magic != IMAGE_DOS_SIGNATURE) return false;
|
|
|
|
auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(
|
|
reinterpret_cast<const char*>(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<const IMAGE_OPTIONAL_HEADER64*>(&nt->OptionalHeader);
|
|
if (opt->NumberOfRvaAndSizes > kClrIndex) {
|
|
rva = opt->DataDirectory[kClrIndex].VirtualAddress;
|
|
dirSize = opt->DataDirectory[kClrIndex].Size;
|
|
}
|
|
} else {
|
|
auto* opt = reinterpret_cast<const IMAGE_OPTIONAL_HEADER32*>(&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<QLibrary>(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<FnEnumerateProcesses>(m_lib->resolve("EnumerateProcesses"));
|
|
m_fns.OpenRemoteProcess =
|
|
reinterpret_cast<FnOpenRemoteProcess>(m_lib->resolve("OpenRemoteProcess"));
|
|
m_fns.IsProcessValid =
|
|
reinterpret_cast<FnIsProcessValid>(m_lib->resolve("IsProcessValid"));
|
|
m_fns.CloseRemoteProcess =
|
|
reinterpret_cast<FnCloseRemoteProcess>(m_lib->resolve("CloseRemoteProcess"));
|
|
m_fns.ReadRemoteMemory =
|
|
reinterpret_cast<FnReadRemoteMemory>(m_lib->resolve("ReadRemoteMemory"));
|
|
m_fns.WriteRemoteMemory =
|
|
reinterpret_cast<FnWriteRemoteMemory>(m_lib->resolve("WriteRemoteMemory"));
|
|
m_fns.EnumerateRemoteSectionsAndModules =
|
|
reinterpret_cast<FnEnumerateRemoteSectionsAndModules>(
|
|
m_lib->resolve("EnumerateRemoteSectionsAndModules"));
|
|
m_fns.ControlRemoteProcess =
|
|
reinterpret_cast<FnControlRemoteProcess>(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<ClrHost>();
|
|
|
|
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<rcx::Provider> 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<RcNetCompatProvider>(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<PluginProcessInfo> pluginProcesses = enumerateProcesses();
|
|
|
|
QList<ProcessInfo> 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<PluginProcessInfo>* 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<uint32_t>(data->Id);
|
|
info.name = QString::fromUtf16(data->Name);
|
|
info.path = QString::fromUtf16(data->Path);
|
|
g_processCollector.dest->append(info);
|
|
}
|
|
|
|
} // anonymous namespace
|
|
|
|
QVector<PluginProcessInfo> RcNetCompatPlugin::enumerateProcesses()
|
|
{
|
|
QVector<PluginProcessInfo> 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();
|
|
}
|