Phase 1a: input forwarding via DLL injection + XInput hook
The host can now inject coop_hook.dll into a running game and forward controller state to it over shared memory, so the game reads the host's (eventually the guest's) input and nothing else. - hook/: coop_hook.dll. DllMain spawns a worker that opens the shared-memory channel (named by the game's pid) and installs SafetyHook inline hooks on XInputGetState/GetStateEx/GetCapabilities/SetState. Detours synthesize state from shared memory; unmanaged slots report disconnected, hiding physical pads. - host/: process picker (Toolhelp32), CreateRemoteThread(LoadLibraryW) injector with an IsWow64Process2 bitness guard, IPC server publishing pads each frame, and an ImGui Injection panel wiring it together. - tests/: hook_selftest exercises the IPC seqlock + hook detours in-process (no game/controller needed); passes. Build: SafetyHook wired in (COOP_BUILD_HOOK=ON), Zydis via FetchContent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
130
host/src/inject/injector.cpp
Normal file
130
host/src/inject/injector.cpp
Normal file
@@ -0,0 +1,130 @@
|
||||
#include "inject/injector.hpp"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
const char* to_string(InjectStatus status)
|
||||
{
|
||||
switch (status)
|
||||
{
|
||||
case InjectStatus::Ok:
|
||||
return "OK";
|
||||
case InjectStatus::OpenProcessFailed:
|
||||
return "OpenProcess failed (try running as administrator)";
|
||||
case InjectStatus::BitnessMismatch:
|
||||
return "target is 32-bit; x86 hook not built yet";
|
||||
case InjectStatus::DllNotFound:
|
||||
return "coop_hook.dll not found next to the host";
|
||||
case InjectStatus::AllocFailed:
|
||||
return "VirtualAllocEx failed";
|
||||
case InjectStatus::WriteFailed:
|
||||
return "WriteProcessMemory failed";
|
||||
case InjectStatus::RemoteThreadFailed:
|
||||
return "CreateRemoteThread failed";
|
||||
case InjectStatus::RemoteLoadFailed:
|
||||
return "LoadLibraryW returned null in the target";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
InjectResult fail(InjectStatus status)
|
||||
{
|
||||
return InjectResult{status, GetLastError()};
|
||||
}
|
||||
|
||||
// Returns true if `process` runs as a 32-bit (WOW64) process on this 64-bit host.
|
||||
bool is_wow64_process(HANDLE process)
|
||||
{
|
||||
USHORT process_machine = IMAGE_FILE_MACHINE_UNKNOWN;
|
||||
USHORT native_machine = IMAGE_FILE_MACHINE_UNKNOWN;
|
||||
if (IsWow64Process2(process, &process_machine, &native_machine))
|
||||
{
|
||||
return process_machine != IMAGE_FILE_MACHINE_UNKNOWN;
|
||||
}
|
||||
return false; // be permissive if the query is unavailable
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
|
||||
{
|
||||
if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
return fail(InjectStatus::DllNotFound);
|
||||
}
|
||||
|
||||
const DWORD access = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION |
|
||||
PROCESS_VM_WRITE | PROCESS_VM_READ;
|
||||
HANDLE process = OpenProcess(access, FALSE, pid);
|
||||
if (process == nullptr)
|
||||
{
|
||||
return fail(InjectStatus::OpenProcessFailed);
|
||||
}
|
||||
|
||||
struct HandleGuard
|
||||
{
|
||||
HANDLE h;
|
||||
~HandleGuard()
|
||||
{
|
||||
if (h != nullptr)
|
||||
{
|
||||
CloseHandle(h);
|
||||
}
|
||||
}
|
||||
} process_guard{process};
|
||||
|
||||
if (is_wow64_process(process))
|
||||
{
|
||||
return InjectResult{InjectStatus::BitnessMismatch, 0};
|
||||
}
|
||||
|
||||
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
||||
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
if (remote == nullptr)
|
||||
{
|
||||
return fail(InjectStatus::AllocFailed);
|
||||
}
|
||||
|
||||
InjectResult result{InjectStatus::Ok, 0};
|
||||
|
||||
if (!WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr))
|
||||
{
|
||||
result = fail(InjectStatus::WriteFailed);
|
||||
}
|
||||
else
|
||||
{
|
||||
// kernel32 is mapped at the same address in every process, so LoadLibraryW's
|
||||
// address in this process is valid as the remote thread's start routine.
|
||||
auto load_library =
|
||||
reinterpret_cast<LPTHREAD_START_ROUTINE>(GetProcAddress(GetModuleHandleW(L"kernel32.dll"), "LoadLibraryW"));
|
||||
HANDLE thread = CreateRemoteThread(process, nullptr, 0, load_library, remote, 0, nullptr);
|
||||
if (thread == nullptr)
|
||||
{
|
||||
result = fail(InjectStatus::RemoteThreadFailed);
|
||||
}
|
||||
else
|
||||
{
|
||||
WaitForSingleObject(thread, INFINITE);
|
||||
DWORD exit_code = 0;
|
||||
GetExitCodeThread(thread, &exit_code);
|
||||
CloseHandle(thread);
|
||||
// LoadLibraryW returns the module handle; 0 means it failed to load.
|
||||
// (The handle is truncated to 32 bits here, but zero vs non-zero is
|
||||
// all we need to distinguish success from failure.)
|
||||
if (exit_code == 0)
|
||||
{
|
||||
result = InjectResult{InjectStatus::RemoteLoadFailed, 0};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
34
host/src/inject/injector.hpp
Normal file
34
host/src/inject/injector.hpp
Normal file
@@ -0,0 +1,34 @@
|
||||
// Loads coop_hook.dll into a target process via the classic
|
||||
// CreateRemoteThread(LoadLibraryW) technique.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
enum class InjectStatus
|
||||
{
|
||||
Ok,
|
||||
OpenProcessFailed, // insufficient rights (try running the host as admin)
|
||||
BitnessMismatch, // 32-bit target; needs the x86 hook (later phase)
|
||||
DllNotFound,
|
||||
AllocFailed,
|
||||
WriteFailed,
|
||||
RemoteThreadFailed,
|
||||
RemoteLoadFailed, // LoadLibraryW returned null inside the target
|
||||
};
|
||||
|
||||
struct InjectResult
|
||||
{
|
||||
InjectStatus status = InjectStatus::OpenProcessFailed;
|
||||
unsigned long os_error = 0; // GetLastError at the point of failure, if any
|
||||
};
|
||||
|
||||
const char* to_string(InjectStatus status);
|
||||
|
||||
// Injects `dll_path` (absolute) into the process with `pid`. The host and DLL
|
||||
// must match the target's bitness; 32-bit targets are rejected up front.
|
||||
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path);
|
||||
|
||||
} // namespace coop
|
||||
43
host/src/inject/process_list.cpp
Normal file
43
host/src/inject/process_list.cpp
Normal file
@@ -0,0 +1,43 @@
|
||||
#include "inject/process_list.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include <windows.h>
|
||||
#include <tlhelp32.h>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
std::vector<ProcessEntry> list_processes()
|
||||
{
|
||||
std::vector<ProcessEntry> result;
|
||||
|
||||
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (snapshot == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
PROCESSENTRY32W entry = {};
|
||||
entry.dwSize = sizeof(entry);
|
||||
if (Process32FirstW(snapshot, &entry))
|
||||
{
|
||||
do
|
||||
{
|
||||
if (entry.th32ProcessID == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
result.push_back(ProcessEntry{entry.th32ProcessID, entry.szExeFile});
|
||||
} while (Process32NextW(snapshot, &entry));
|
||||
}
|
||||
CloseHandle(snapshot);
|
||||
|
||||
std::sort(result.begin(), result.end(), [](const ProcessEntry& a, const ProcessEntry& b) {
|
||||
const int cmp = _wcsicmp(a.exe_name.c_str(), b.exe_name.c_str());
|
||||
return cmp != 0 ? cmp < 0 : a.pid < b.pid;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace coop
|
||||
20
host/src/inject/process_list.hpp
Normal file
20
host/src/inject/process_list.hpp
Normal file
@@ -0,0 +1,20 @@
|
||||
// Enumerates running processes for the injection target picker.
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace coop
|
||||
{
|
||||
|
||||
struct ProcessEntry
|
||||
{
|
||||
unsigned long pid = 0;
|
||||
std::wstring exe_name; // image base name, e.g. "game.exe"
|
||||
};
|
||||
|
||||
// Snapshot of current processes, sorted by exe name (case-insensitive). System
|
||||
// idle/0 pids are skipped.
|
||||
std::vector<ProcessEntry> list_processes();
|
||||
|
||||
} // namespace coop
|
||||
Reference in New Issue
Block a user