// Standard PE hook :p #include "expresso_recovered.h" #include "overlay_state.h" #include "platform_shim.h" namespace expresso { namespace data_rva_pe { constexpr std::uint64_t kOrigProcessEvent = 0x5DE548; // MinHook trampoline constexpr std::uint64_t kObserver = 0x5DE550; // -> 0x167610 constexpr std::uint64_t kBlocklistEnabled = 0x5DA7F8; // read at 0x16828C } // namespace data_rva_pe using ProcessEventNative = void (*)(uint64_t object, uint64_t function, void* params); using ObserverFn = void (*)(uint64_t object, uint64_t function); extern ProcessEventNative g_origProcessEvent; extern ObserverFn g_processEventObserver; extern bool g_blocklistEnabled; ProcessEventNative g_origProcessEvent = nullptr; ObserverFn g_processEventObserver = nullptr; bool g_blocklistEnabled = true; // .data initial value IMemory& GameMemory(); // RVA 0x168210: a raw mov eax, [rcx+0x18]. The pointer was validated by the // caller, so this one load is deliberately not routed through SafeReadInt32 int32_t ReadFunctionNameIndex(uint64_t function); // RVA 0x167610, reconstructed as a pure state machine in identity_spoof.cpp // (ClassifyIdentityEvent + OnIdentityEvent) this is the thin shim that // resolves the UFunction name and feeds it in. void IdentityEventObserver(uint64_t object, uint64_t function); // RVA 0x168260 detour void ProcessEventDetour(uint64_t object, uint64_t function, void* params) { // passive feed (0x16827D) // The observer receives every event in the process. It is called before the // filter, so it sees blocked calls too if (g_processEventObserver != nullptr) g_processEventObserver(object, function); // 0x168287 // the censor (0x16828C..0x1682FE) if (g_blocklistEnabled) { // 0x16828C if (IsCanonicalUserPointer(function)) { // 0x1682AF // Read UFunction::NamePrivate.ComparisonIndex directly note this // is a *raw* load at +0x18 (0x168210), not one of the guarded // accessors, because the pointer was validated one line above. const int32_t name_index = ReadFunctionNameIndex(function); if (static_cast(name_index - 1) <= 0x1FFFFF) { // 0x1682CE char name[0xB0]; GetNameString(GameMemory(), g_gameBase, name_index, name, sizeof(name)); // 0x1682E1 if (IsBlockedIdentityFunction(name)) // 0x1682EA return; // <-- the engine's own function never runs } } } // forward (0x168300..0x168313) if (g_origProcessEvent != nullptr) g_origProcessEvent(object, function, params); // 0x168313 } // =========================================================================== // RVA 0x16AC72..0x16ACF3 -- the installer tail // // Runs inside InstallRenderHooks (01_dxgi_present_hook.cpp) once Present and // ResizeBuffers are live. // =========================================================================== bool InstallProcessEventHook() { if (g_gameBase == 0) return false; // 0x16AC79 const uint64_t target = g_gameBase + game_rva::kProcessEvent; // 0x16AC7E if (!IsCanonicalUserPointer(target)) return false; // 0x16AC96 void* const target_ptr = reinterpret_cast(target); const minhook::Status st = minhook::CreateHook( target_ptr, reinterpret_cast(&ProcessEventDetour), reinterpret_cast(&g_origProcessEvent)); // 0x16ACBB bool hooked = false; if (st == static_cast(3)) { // MH_ERROR_ALREADY_CREATED (0x16ACC0 `cmp eax, 3`). Someone else -- or a // previous injection of this same DLL -- already owns the hook, and the // cheat simply adopts the existing trampoline. hooked = true; } else if (st != minhook::kOk) { // 0x16ACC9: on failure it stores the *raw target address* into the // trampoline slot, so the cheat's own RPC path keeps working by calling // ProcessEvent directly. The filter is lost; the offence is not. g_origProcessEvent = reinterpret_cast(target); } else if (minhook::EnableHook(target_ptr) == minhook::kOk) { // 0x16ACD5 hooked = true; } if (hooked) { // 0x16ACE5: the cheat's outbound calls go through the trampoline, which // means they skip its own blocklist. g_processEvent = reinterpret_cast(g_origProcessEvent); } // 0x16ACEC: set unconditionally, outside every branch above -- the observer // is armed even when the hook did not install. g_processEventObserver = &IdentityEventObserver; return hooked; } } // namespace expresso