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:
@@ -29,6 +29,23 @@ if(MSVC)
|
|||||||
add_compile_definitions(UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
add_compile_definitions(UNICODE _UNICODE WIN32_LEAN_AND_MEAN NOMINMAX)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- x86 sub-build (internal, re-entrant) ----------------------------------
|
||||||
|
# When the ExternalProject below reconfigures this same tree with -A Win32 and
|
||||||
|
# -DCOOP_X86_HELPER_BUILD=ON, build ONLY the 32-bit hook DLL + injector helper
|
||||||
|
# (staged alongside the x64 binaries) and stop -- no host, tests, or imgui, and
|
||||||
|
# crucially no recursive ExternalProject.
|
||||||
|
option(COOP_X86_HELPER_BUILD "Internal: x86 sub-build (hook DLL + injector helper only)" OFF)
|
||||||
|
if(COOP_X86_HELPER_BUILD)
|
||||||
|
add_subdirectory(common)
|
||||||
|
set(SAFETYHOOK_BUILD_TEST OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SAFETYHOOK_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
|
||||||
|
set(SAFETYHOOK_BUILD_DOCS OFF CACHE BOOL "" FORCE)
|
||||||
|
add_subdirectory(third_party/safetyhook)
|
||||||
|
add_subdirectory(hook)
|
||||||
|
add_subdirectory(tools/inject_helper)
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
add_subdirectory(third_party)
|
add_subdirectory(third_party)
|
||||||
add_subdirectory(common)
|
add_subdirectory(common)
|
||||||
add_subdirectory(host)
|
add_subdirectory(host)
|
||||||
@@ -48,3 +65,25 @@ if(COOP_BUILD_HOOK)
|
|||||||
add_subdirectory(tools/audio_probe) # coop_audio_probe: inject + diagnose the render-hook
|
add_subdirectory(tools/audio_probe) # coop_audio_probe: inject + diagnose the render-hook
|
||||||
add_subdirectory(tests)
|
add_subdirectory(tests)
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
|
# --- x86 helper artifacts for 32-bit (WOW64) games -------------------------
|
||||||
|
# A 64-bit process can't cleanly inject a 32-bit one, so build the x86 hook DLL
|
||||||
|
# (coop_hook_x86.dll) + a 32-bit injector helper (coop_inject_x86.exe) via a
|
||||||
|
# nested Win32 configure of this same tree, staged next to the x64 binaries. Only
|
||||||
|
# the multi-config Visual Studio generator is supported here (the Ninja
|
||||||
|
# build-clangd/ tree is just for compile_commands and skips this).
|
||||||
|
option(COOP_BUILD_X86_HELPER "Build the x86 hook DLL + injector helper for 32-bit games" ON)
|
||||||
|
if(COOP_BUILD_HOOK AND COOP_BUILD_X86_HELPER AND CMAKE_GENERATOR MATCHES "Visual Studio")
|
||||||
|
include(ExternalProject)
|
||||||
|
ExternalProject_Add(coop_x86
|
||||||
|
SOURCE_DIR "${CMAKE_SOURCE_DIR}"
|
||||||
|
BINARY_DIR "${CMAKE_BINARY_DIR}/x86"
|
||||||
|
CMAKE_GENERATOR "${CMAKE_GENERATOR}"
|
||||||
|
CMAKE_GENERATOR_PLATFORM "Win32"
|
||||||
|
CMAKE_ARGS "-DCOOP_X86_HELPER_BUILD=ON"
|
||||||
|
BUILD_COMMAND "${CMAKE_COMMAND}" --build <BINARY_DIR> --config $<CONFIG>
|
||||||
|
INSTALL_COMMAND ""
|
||||||
|
BUILD_ALWAYS ON
|
||||||
|
USES_TERMINAL_CONFIGURE ON
|
||||||
|
USES_TERMINAL_BUILD ON)
|
||||||
|
endif()
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -37,8 +37,12 @@ XInput game becomes Remote-Play-Together-able.
|
|||||||
co-op/local-multiplayer titles without active anti-cheat.
|
co-op/local-multiplayer titles without active anti-cheat.
|
||||||
- **XInput only:** the game must read controllers via XInput (the common case).
|
- **XInput only:** the game must read controllers via XInput (the common case).
|
||||||
DirectInput-only / RawInput-only games are not handled.
|
DirectInput-only / RawInput-only games are not handled.
|
||||||
- **x64 only:** the host and hook DLL must match the game's bitness, and only x64
|
- **32-bit games supported via a helper:** the host is x64, but the build also
|
||||||
is built today. 32-bit games need the x86 hook + injector (see Roadmap).
|
produces an x86 hook DLL (`coop_hook_x86.dll`) and a 32-bit injector helper
|
||||||
|
(`coop_inject_x86.exe`). When the target is a 32-bit (WOW64) process the host
|
||||||
|
detects it (`IsWow64Process2`) and shells out to the helper to load the x86 DLL
|
||||||
|
(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.
|
||||||
- **Local audio echo (fixed via the hook; falls back otherwise):** when the
|
- **Local audio echo (fixed via the hook; falls back otherwise):** when the
|
||||||
render-hook is active it silences the game's local playback while mirroring it,
|
render-hook is active it silences the game's local playback while mirroring it,
|
||||||
so there is no echo. If the hook can't attach or the game uses an
|
so there is no echo. If the hook can't attach or the game uses an
|
||||||
@@ -143,14 +147,18 @@ Done:
|
|||||||
(drives a real D3D11 swapchain end-to-end and reads the pixels back through the
|
(drives a real D3D11 swapchain end-to-end and reads the pixels back through the
|
||||||
shared texture).
|
shared texture).
|
||||||
|
|
||||||
|
- **x86 (32-bit) game support. ✅** A nested Win32 sub-build (CMake
|
||||||
|
`ExternalProject`, driven from the normal x64 build) produces `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 inject
|
||||||
|
the x86 DLL. Validated end-to-end against Slaps and Beans (a 32-bit D3D11 game):
|
||||||
|
all subsystems hooked, and the IPC channels (status, audio ring, video share,
|
||||||
|
log) all flow across the x64↔x86 boundary.
|
||||||
|
|
||||||
Future work, roughly in priority order:
|
Future work, roughly in priority order:
|
||||||
|
|
||||||
- **Steam Input:** consume guest input through the Steam Input API directly
|
- **Steam Input:** consume guest input through the Steam Input API directly
|
||||||
rather than XInput.
|
rather than XInput.
|
||||||
- **x86 support:** add an x86 build of `coop_hook.dll` plus an x86 injector
|
|
||||||
helper the x64 host spawns, so 32-bit games work (a 64-bit process can't
|
|
||||||
cleanly inject a 32-bit one). Detect target bitness with `IsWow64Process2`.
|
|
||||||
The shared-memory IPC layout is already fixed-width / bitness-stable.
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -166,6 +174,11 @@ cmake --build build --config Debug
|
|||||||
# output: bin/Debug/coop_host.exe (+ coop_hook.dll, test exes)
|
# output: bin/Debug/coop_host.exe (+ coop_hook.dll, test exes)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The x64 build also drives a nested Win32 sub-build (CMake `ExternalProject`,
|
||||||
|
configured into `build/x86/`) that produces `coop_hook_x86.dll` and
|
||||||
|
`coop_inject_x86.exe` for 32-bit games, staged next to the x64 binaries. Disable
|
||||||
|
it with `-DCOOP_BUILD_X86_HELPER=OFF` if you don't need 32-bit support.
|
||||||
|
|
||||||
Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under
|
Third-party dependencies (Dear ImGui, SafetyHook) are git submodules under
|
||||||
`third_party/`; the Steamworks SDK is vendored manually there when wired. No
|
`third_party/`; the Steamworks SDK is vendored manually there when wired. No
|
||||||
vcpkg / package manager is used.
|
vcpkg / package manager is used.
|
||||||
@@ -259,8 +272,9 @@ Useful checks while developing without RPT: tick **Forward synthetic test input*
|
|||||||
in the Injection panel to make the game move on its own (proving forwarding is the
|
in the Injection panel to make the game move on its own (proving forwarding is the
|
||||||
source), and click away from the game to confirm focus spoofing keeps it running.
|
source), and click away from the game to confirm focus spoofing keeps it running.
|
||||||
|
|
||||||
> Injection access error → run the host as administrator. "Target is 32-bit" →
|
> Injection access error → run the host as administrator. A 32-bit (WOW64) target
|
||||||
> that game needs the x86 hook (see Roadmap).
|
> is injected automatically via `coop_inject_x86.exe` + `coop_hook_x86.dll`; if
|
||||||
|
> those aren't next to the host, rebuild (the x86 sub-build stages them there).
|
||||||
|
|
||||||
## Lessons learned
|
## Lessons learned
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,13 @@ target_link_libraries(coop_hook PRIVATE
|
|||||||
d3d11
|
d3d11
|
||||||
dxgi)
|
dxgi)
|
||||||
|
|
||||||
set_target_properties(coop_hook PROPERTIES OUTPUT_NAME "coop_hook")
|
# The x86 sub-build (COOP_X86_HELPER_BUILD) stages alongside the x64 DLL in the
|
||||||
|
# same bin/<config> dir, so give it a distinct name the host loads for 32-bit games.
|
||||||
|
if(COOP_X86_HELPER_BUILD)
|
||||||
|
set_target_properties(coop_hook PROPERTIES OUTPUT_NAME "coop_hook_x86")
|
||||||
|
else()
|
||||||
|
set_target_properties(coop_hook PROPERTIES OUTPUT_NAME "coop_hook")
|
||||||
|
endif()
|
||||||
|
|
||||||
# TODO(dist): statically link the VC runtime (/MT) so the DLL loads in games on
|
# TODO(dist): statically link the VC runtime (/MT) so the DLL loads in games on
|
||||||
# machines without the matching VC redist. Deferred until SafetyHook + Zydis are
|
# machines without the matching VC redist. Deferred until SafetyHook + Zydis are
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ const char* to_string(InjectStatus status)
|
|||||||
case InjectStatus::OpenProcessFailed:
|
case InjectStatus::OpenProcessFailed:
|
||||||
return "OpenProcess failed (try running as administrator)";
|
return "OpenProcess failed (try running as administrator)";
|
||||||
case InjectStatus::BitnessMismatch:
|
case InjectStatus::BitnessMismatch:
|
||||||
return "target is 32-bit; x86 hook not built yet";
|
return "target is 32-bit and the x86 hook/helper are unavailable";
|
||||||
|
case InjectStatus::HelperNotFound:
|
||||||
|
return "target is 32-bit; coop_inject_x86.exe / coop_hook_x86.dll not found next to the host";
|
||||||
|
case InjectStatus::HelperFailed:
|
||||||
|
return "the x86 injector helper failed (run the host as administrator?)";
|
||||||
case InjectStatus::DllNotFound:
|
case InjectStatus::DllNotFound:
|
||||||
return "coop_hook.dll not found next to the host";
|
return "coop_hook.dll not found next to the host";
|
||||||
case InjectStatus::AllocFailed:
|
case InjectStatus::AllocFailed:
|
||||||
@@ -49,6 +53,49 @@ bool is_wow64_process(HANDLE process)
|
|||||||
return false; // be permissive if the query is unavailable
|
return false; // be permissive if the query is unavailable
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Replace the trailing file name of `path` with `name` (same directory).
|
||||||
|
std::wstring sibling(const std::wstring& path, const wchar_t* name)
|
||||||
|
{
|
||||||
|
const std::size_t slash = path.find_last_of(L"\\/");
|
||||||
|
const std::wstring dir = (slash == std::wstring::npos) ? std::wstring() : path.substr(0, slash + 1);
|
||||||
|
return dir + name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject into a 32-bit (WOW64) target by spawning the x86 injector helper, which
|
||||||
|
// loads the x86 hook DLL from the same directory as the x64 dll_path.
|
||||||
|
InjectResult inject_via_helper(unsigned long pid, const std::wstring& dll_path)
|
||||||
|
{
|
||||||
|
const std::wstring helper = sibling(dll_path, L"coop_inject_x86.exe");
|
||||||
|
const std::wstring x86_dll = sibling(dll_path, L"coop_hook_x86.dll");
|
||||||
|
if (GetFileAttributesW(helper.c_str()) == INVALID_FILE_ATTRIBUTES ||
|
||||||
|
GetFileAttributesW(x86_dll.c_str()) == INVALID_FILE_ATTRIBUTES)
|
||||||
|
{
|
||||||
|
return InjectResult{InjectStatus::HelperNotFound, 0};
|
||||||
|
}
|
||||||
|
|
||||||
|
// coop_inject_x86 <pid> "<x86_dll>"
|
||||||
|
std::wstring cmd = L"\"" + helper + L"\" " + std::to_wstring(pid) + L" \"" + x86_dll + L"\"";
|
||||||
|
|
||||||
|
STARTUPINFOW si{};
|
||||||
|
si.cb = sizeof(si);
|
||||||
|
PROCESS_INFORMATION pi{};
|
||||||
|
if (!CreateProcessW(helper.c_str(), cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, nullptr, nullptr,
|
||||||
|
&si, &pi))
|
||||||
|
{
|
||||||
|
return fail(InjectStatus::HelperFailed);
|
||||||
|
}
|
||||||
|
WaitForSingleObject(pi.hProcess, INFINITE);
|
||||||
|
DWORD exit_code = 1;
|
||||||
|
GetExitCodeProcess(pi.hProcess, &exit_code);
|
||||||
|
CloseHandle(pi.hThread);
|
||||||
|
CloseHandle(pi.hProcess);
|
||||||
|
if (exit_code != 0)
|
||||||
|
{
|
||||||
|
return InjectResult{InjectStatus::HelperFailed, exit_code};
|
||||||
|
}
|
||||||
|
return InjectResult{InjectStatus::Ok, 0};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
|
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
|
||||||
@@ -80,7 +127,8 @@ InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path)
|
|||||||
|
|
||||||
if (is_wow64_process(process))
|
if (is_wow64_process(process))
|
||||||
{
|
{
|
||||||
return InjectResult{InjectStatus::BitnessMismatch, 0};
|
// The x64 host can't inject a 32-bit target directly; delegate to the helper.
|
||||||
|
return inject_via_helper(pid, dll_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
||||||
|
|||||||
@@ -11,12 +11,14 @@ enum class InjectStatus
|
|||||||
{
|
{
|
||||||
Ok,
|
Ok,
|
||||||
OpenProcessFailed, // insufficient rights (try running the host as admin)
|
OpenProcessFailed, // insufficient rights (try running the host as admin)
|
||||||
BitnessMismatch, // 32-bit target; needs the x86 hook (later phase)
|
BitnessMismatch, // 32-bit target; the x86 hook/helper aren't available
|
||||||
DllNotFound,
|
DllNotFound,
|
||||||
AllocFailed,
|
AllocFailed,
|
||||||
WriteFailed,
|
WriteFailed,
|
||||||
RemoteThreadFailed,
|
RemoteThreadFailed,
|
||||||
RemoteLoadFailed, // LoadLibraryW returned null inside the target
|
RemoteLoadFailed, // LoadLibraryW returned null inside the target
|
||||||
|
HelperNotFound, // 32-bit target but coop_inject_x86.exe / coop_hook_x86.dll missing
|
||||||
|
HelperFailed, // the x86 injector helper ran but reported failure
|
||||||
};
|
};
|
||||||
|
|
||||||
struct InjectResult
|
struct InjectResult
|
||||||
@@ -27,8 +29,9 @@ struct InjectResult
|
|||||||
|
|
||||||
const char* to_string(InjectStatus status);
|
const char* to_string(InjectStatus status);
|
||||||
|
|
||||||
// Injects `dll_path` (absolute) into the process with `pid`. The host and DLL
|
// Injects `dll_path` (absolute, the x64 coop_hook.dll) into `pid`. For a 32-bit
|
||||||
// must match the target's bitness; 32-bit targets are rejected up front.
|
// (WOW64) target the x64 host can't inject directly, so it spawns the sibling
|
||||||
|
// coop_inject_x86.exe to load coop_hook_x86.dll (both expected next to dll_path).
|
||||||
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path);
|
InjectResult inject_dll(unsigned long pid, const std::wstring& dll_path);
|
||||||
|
|
||||||
} // namespace coop
|
} // namespace coop
|
||||||
|
|||||||
@@ -40,6 +40,47 @@ std::wstring dll_path_next_to_self()
|
|||||||
return path + L"coop_hook.dll";
|
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)
|
bool inject(unsigned long pid, const std::wstring& dll_path)
|
||||||
{
|
{
|
||||||
if (GetFileAttributesW(dll_path.c_str()) == INVALID_FILE_ATTRIBUTES)
|
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());
|
std::printf("ERROR: OpenProcess(%lu) failed (%lu). Run as administrator?\n", pid, GetLastError());
|
||||||
return false;
|
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);
|
const SIZE_T bytes = (dll_path.size() + 1) * sizeof(wchar_t);
|
||||||
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
void* remote = VirtualAllocEx(process, nullptr, bytes, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
|
||||||
bool ok = false;
|
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)
|
for (std::uint32_t i = 0; i < status.hook_entry_count && i < coop::kMaxHookEntries; ++i)
|
||||||
{
|
{
|
||||||
const coop::HookEntry& e = status.hook_entries[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));
|
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