Phase 3: x86 (32-bit) game support via injector helper
Drive a nested Win32 sub-build (CMake ExternalProject, re-entrant via COOP_X86_HELPER_BUILD) from the normal x64 build to produce coop_hook_x86.dll and a 32-bit coop_inject_x86.exe, staged next to the x64 binaries. The host detects a WOW64 target with IsWow64Process2 and spawns the helper to load the x86 DLL, since a 64-bit process can't cleanly inject a 32-bit one. The shared-memory IPC is fixed-width / bitness-stable, so the x64 host and x86 hook interoperate. Validated end-to-end against Slaps and Beans (32-bit D3D11): all 15 hooks installed, heartbeat advancing, the Present hook engaged (shared a 1920x1080 backbuffer -- the real-game video-hook proof Phantom Brave's D3D9 couldn't give), and status/audio/video/log IPC all crossed the x64<->x86 boundary. coop_audio_probe now also delegates to the helper for WOW64 targets. All 5 tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -40,6 +40,47 @@ std::wstring dll_path_next_to_self()
|
||||
return path + L"coop_hook.dll";
|
||||
}
|
||||
|
||||
std::wstring sibling_of(const std::wstring& path, const wchar_t* name)
|
||||
{
|
||||
const size_t slash = path.find_last_of(L"\\/");
|
||||
return (slash == std::wstring::npos ? std::wstring() : path.substr(0, slash + 1)) + name;
|
||||
}
|
||||
|
||||
// Inject a 32-bit (WOW64) target via the x86 helper, mirroring the host. Lets the
|
||||
// probe exercise the x86 hook end-to-end (the IPC channels are bitness-agnostic).
|
||||
bool inject_via_helper(unsigned long pid, const std::wstring& dll_path)
|
||||
{
|
||||
const std::wstring helper = sibling_of(dll_path, L"coop_inject_x86.exe");
|
||||
const std::wstring x86_dll = sibling_of(dll_path, L"coop_hook_x86.dll");
|
||||
if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES ||
|
||||
GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
std::printf("ERROR: x86 helper/dll missing next to the probe.\n");
|
||||
return false;
|
||||
}
|
||||
std::wstring cmd = L"\"" + helper + L"\" " + std::to_wstring(pid) + L" \"" + x86_dll + L"\"";
|
||||
std::printf("32-bit target: injecting coop_hook_x86.dll via coop_inject_x86.exe ...\n");
|
||||
STARTUPINFOW si{};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi{};
|
||||
if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi))
|
||||
{
|
||||
std::printf("ERROR: CreateProcess(coop_inject_x86) failed (%lu).\n", GetLastError());
|
||||
return false;
|
||||
}
|
||||
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||
DWORD code = 1;
|
||||
GetExitCodeProcess(pi.hProcess, &code);
|
||||
CloseHandle(pi.hThread);
|
||||
CloseHandle(pi.hProcess);
|
||||
if (code != 0)
|
||||
{
|
||||
std::printf("ERROR: coop_inject_x86 reported failure (exit %lu).\n", code);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool inject(unsigned long pid, const std::wstring& dll_path)
|
||||
{
|
||||
if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES)
|
||||
@@ -55,6 +96,14 @@ bool inject(unsigned long pid, const std::wstring& dll_path)
|
||||
std::printf("ERROR: OpenProcess(%lu) failed (%lu). Run as administrator?\n", pid, GetLastError());
|
||||
return false;
|
||||
}
|
||||
|
||||
// 32-bit target -> delegate to the x86 helper (a 64-bit process can't inject it).
|
||||
USHORT proc_machine = IMAGE_FILE_MACHINE_UNKNOWN, native_machine = IMAGE_FILE_MACHINE_UNKNOWN;
|
||||
if (IsWow64Process2(process, &proc_machine, &native_machine) && proc_machine != IMAGE_FILE_MACHINE_UNKNOWN)
|
||||
{
|
||||
CloseHandle(process);
|
||||
return inject_via_helper(pid, dll_path);
|
||||
}
|
||||
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
||||
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||
bool ok = false;
|
||||
@@ -269,7 +318,7 @@ int wmain(int argc, wchar_t** argv)
|
||||
for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i)
|
||||
{
|
||||
const coop::HookEntry& e = status.hook_entries[i];
|
||||
std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 3 ? kSubsys[e.subsystem] : "?", e.name,
|
||||
std::printf(" [%-5s] %-34s %s calls=%llu\n", e.subsystem < 4 ? kSubsys[e.subsystem] : "?", e.name,
|
||||
e.installed ? "ON " : "off", static_cast<unsigned long long>(e.calls));
|
||||
}
|
||||
|
||||
|
||||
5
tools/inject_helper/CMakeLists.txt
Normal file
5
tools/inject_helper/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
# 32-bit injector helper. Built only in the x86 sub-build (COOP_X86_HELPER_BUILD);
|
||||
# the x64 host spawns it to inject the x86 hook DLL into 32-bit (WOW64) games.
|
||||
add_executable(coop_inject_x86 main.cpp)
|
||||
|
||||
set_target_properties(coop_inject_x86 PROPERTIES OUTPUT_NAME "coop_inject_x86")
|
||||
82
tools/inject_helper/main.cpp
Normal file
82
tools/inject_helper/main.cpp
Normal file
@@ -0,0 +1,82 @@
|
||||
// coop_inject_x86 -- a 32-bit injector helper the (x64) host spawns to load the
|
||||
// x86 hook DLL into a 32-bit (WOW64) game. A 64-bit process can't cleanly
|
||||
// CreateRemoteThread(LoadLibraryW) into a 32-bit target (its LoadLibraryW lives
|
||||
// in the 32-bit kernel32 at an address the 64-bit host doesn't have), so the
|
||||
// host shells out to this same-bitness helper instead.
|
||||
//
|
||||
// coop_inject_x86 <pid> <dll_path>
|
||||
//
|
||||
// Exit code 0 = injected, 1 = failure, 2 = bad arguments.
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace
|
||||
{
|
||||
|
||||
int inject(unsigned long pid, const std::wstring& dll_path)
|
||||
{
|
||||
if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
std::fprintf(stderr, "coop_inject_x86: dll not found: %ls\n", dll_path.c_str());
|
||||
return 1;
|
||||
}
|
||||
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)
|
||||
{
|
||||
std::fprintf(stderr, "coop_inject_x86: OpenProcess(%lu) failed (%lu)\n", pid, GetLastError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int result = 1;
|
||||
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 && WriteProcessMemory(process, remote, dll_path.c_str(), bytes, nullptr))
|
||||
{
|
||||
// In a 32-bit process kernel32 is mapped at the same base as in this 32-bit
|
||||
// helper, so LoadLibraryW's address here is valid as the remote 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)
|
||||
{
|
||||
WaitForSingleObject(thread, INFINITE);
|
||||
DWORD exit_code = 0;
|
||||
GetExitCodeThread(thread, &exit_code);
|
||||
CloseHandle(thread);
|
||||
result = (exit_code != 0) ? 0 : 1; // LoadLibraryW returns the module handle
|
||||
}
|
||||
else
|
||||
{
|
||||
std::fprintf(stderr, "coop_inject_x86: CreateRemoteThread failed (%lu)\n", GetLastError());
|
||||
}
|
||||
}
|
||||
if (remote != nullptr)
|
||||
{
|
||||
VirtualFreeEx(process, remote, 0, MEM_RELEASE);
|
||||
}
|
||||
CloseHandle(process);
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int wmain(int argc, wchar_t** argv)
|
||||
{
|
||||
if (argc < 3)
|
||||
{
|
||||
std::printf("usage: coop_inject_x86 <pid> <dll_path>\n");
|
||||
return 2;
|
||||
}
|
||||
const unsigned long pid = std::wcstoul(argv[1], nullptr, 10);
|
||||
if (pid == 0)
|
||||
{
|
||||
std::fprintf(stderr, "coop_inject_x86: invalid pid\n");
|
||||
return 2;
|
||||
}
|
||||
return inject(pid, argv[2]);
|
||||
}
|
||||
Reference in New Issue
Block a user