| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252 |
- /**
- * Inactivity timeout override.
- *
- * The Client keeps one timeout per activity lane and ends a session whose controller has been
- * idle for longer. The lanes are not image data: they sit at a fixed offset inside a live object,
- * and the pointer to that object is stored obfuscated, so the Client reaches it through a getter
- * that decodes the pointer on each call. This module resolves that getter by signature and calls
- * it the same way, which is the same shape the camera pose block is reached by.
- *
- * No lane the Client authors is written down anywhere. A block that is not the one this module
- * last wrote is the Client's own, so reading before each hold both takes the value a lane is put
- * back to and follows an activity change, which re-authors the whole block.
- */
- #include "inactivity_override.h"
- #include <Windows.h>
- #include <algorithm>
- #include <array>
- #include <cstddef>
- #include <cstdint>
- #include <string_view>
- #include "../../../core/logging/log.h"
- #include "../../inactivity/inactivity_settings_store.h"
- #include "../../patterns/image_scan.h"
- namespace sunrise::client::hooks::inactivity {
- namespace {
- namespace settings = client::inactivity;
- using patterns::scan_main_image_unique;
- using patterns::signature;
- using patterns::signature_length;
- /**
- * The activity config getter. Its body is the shared shape every obfuscated pointer getter has,
- * so the load of its own global is what tells it apart: a RIP-relative displacement encodes a
- * distance rather than an address, carries no position-dependent bytes, and is the only part of
- * this prologue unique to this getter. Call and branch displacements stay wildcarded.
- */
- constexpr std::string_view kConfigGetterText =
- "40 53 48 83 EC 20 48 8B 1D 2B 10 1A 02 48 85 DB 0F 84 ? ? ? ? 48 89 5C 24 30 "
- "E8 ? ? ? ? 33 C3";
- /** Compiled pattern bytes of the config getter signature. */
- constexpr auto kConfigGetter = signature<signature_length(kConfigGetterText)>(kConfigGetterText);
- /** Where the lanes start in the object the getter returns. */
- constexpr std::size_t kTimeoutBlockOffset = 0xAC;
- /** Milliseconds between re-applications, so an activity change cannot outlast the hold. */
- constexpr std::uint64_t kHoldIntervalMs = 2000;
- /** Fourteen consecutive milliseconds, in block order. */
- using Lanes = std::array<std::uint32_t, settings::kActivityCount>;
- /** Bytes of the block. */
- constexpr std::size_t kBlockBytes = sizeof(Lanes);
- /** Returns the activity config object. The pointer in its global is obfuscated, so we call it. */
- using ConfigGetter = std::byte*(__fastcall*)();
- SRWLOCK g_lock{SRWLOCK_INIT};
- ConfigGetter g_getter{};
- /** The object the last call returned, kept only so the interface can show it. */
- std::uintptr_t g_object{};
- std::uint64_t g_nextHoldTick{};
- /** The block this module last wrote. Anything else in the object is the Client's own. */
- Lanes g_applied{};
- bool g_appliedValid{};
- /** The Client's own lanes for the activity in play. */
- Lanes g_captured{};
- bool g_capturedValid{};
- /** Set while a hold is in place, so releasing it writes the captured lanes exactly once. */
- bool g_holding{};
- /**
- * Calls the getter without faulting. The body is obfuscated game code, and it runs before the
- * Client has published its global on an early frame.
- * @return The activity config object, or null.
- */
- [[nodiscard]] std::byte* config_object() noexcept {
- if (g_getter == nullptr) {
- return nullptr;
- }
- __try {
- return g_getter();
- } __except (EXCEPTION_EXECUTE_HANDLER) {
- return nullptr;
- }
- }
- /**
- * Reads the block out of the object.
- * @param object Config object.
- * @param values Receives the lanes.
- * @return True when Windows copied all of them.
- */
- [[nodiscard]] bool read_block(const std::byte* object, Lanes& values) noexcept {
- SIZE_T read = 0;
- return ReadProcessMemory(GetCurrentProcess(),
- object + kTimeoutBlockOffset,
- values.data(),
- kBlockBytes,
- &read)
- != FALSE
- && read == kBlockBytes;
- }
- /**
- * Writes one run of milliseconds into the object.
- * @param object Config object.
- * @param values Lanes in block order.
- * @return True when Windows copied all of them.
- */
- [[nodiscard]] bool write_block(std::byte* object, const Lanes& values) noexcept {
- SIZE_T written = 0;
- return WriteProcessMemory(GetCurrentProcess(),
- object + kTimeoutBlockOffset,
- values.data(),
- kBlockBytes,
- &written)
- != FALSE
- && written == kBlockBytes;
- }
- /**
- * Takes the Client's own lanes, which are any lanes this module did not write.
- * @param current Block just read out of the object.
- */
- void capture_locked(const Lanes& current) noexcept {
- // A zero lane is an object the Client has published but not authored yet.
- const bool authored =
- std::none_of(current.begin(), current.end(), [](std::uint32_t value) noexcept {
- return value == 0;
- });
- if (!authored || (g_appliedValid && current == g_applied)) {
- return;
- }
- g_captured = current;
- g_capturedValid = true;
- }
- /**
- * @param configured Current configuration.
- * @return The lanes a hold puts in place.
- */
- [[nodiscard]] Lanes held_lanes(const settings::Settings& configured) noexcept {
- Lanes values = configured.custom ? configured.timeouts : settings::kDefaultTimeouts;
- // Orbit is held at its longest whatever the grid or the file carries, because a timeout that
- // fires there ends a session this Client cannot re-establish.
- values[settings::kOrbitLane] = settings::kMaximumTimeoutMs;
- return values;
- }
- /** Writes the captured lanes back and ends the hold. */
- void release_locked(std::byte* object) noexcept {
- if (!g_holding || !g_capturedValid || !write_block(object, g_captured)) {
- return;
- }
- g_holding = false;
- g_appliedValid = false;
- }
- } // namespace
- /** Resolves the activity config getter. */
- bool install() noexcept {
- AcquireSRWLockExclusive(&g_lock);
- if (g_getter != nullptr) {
- ReleaseSRWLockExclusive(&g_lock);
- return true;
- }
- std::byte* const match = scan_main_image_unique(kConfigGetter, "inactivity_config_getter");
- if (match == nullptr) {
- ReleaseSRWLockExclusive(&g_lock);
- core::log::write(core::log::Channel::client,
- core::log::Level::warn,
- "ev=inactivity stage=install result=fail reason=target");
- return false;
- }
- g_getter = reinterpret_cast<ConfigGetter>(match);
- ReleaseSRWLockExclusive(&g_lock);
- core::log::write(core::log::Channel::client,
- core::log::Level::info,
- "ev=inactivity stage=install result=ok");
- return true;
- }
- /** Puts the Client's own lanes back and drops the resolved getter. */
- void uninstall() noexcept {
- AcquireSRWLockExclusive(&g_lock);
- if (std::byte* const object = config_object(); object != nullptr) {
- release_locked(object);
- }
- g_getter = nullptr;
- g_object = 0;
- g_nextHoldTick = 0;
- g_applied = Lanes{};
- g_appliedValid = false;
- g_captured = Lanes{};
- g_capturedValid = false;
- g_holding = false;
- ReleaseSRWLockExclusive(&g_lock);
- }
- /** Holds the configured milliseconds, or puts back the ones the Client authored. */
- void poll() noexcept {
- const settings::Settings configured = settings::get();
- AcquireSRWLockExclusive(&g_lock);
- const std::uint64_t now = GetTickCount64();
- if (g_getter == nullptr || now < g_nextHoldTick) {
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- g_nextHoldTick = now + kHoldIntervalMs;
- std::byte* const object = config_object();
- g_object = reinterpret_cast<std::uintptr_t>(object);
- if (object == nullptr) {
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- if (Lanes current{}; read_block(object, current)) {
- capture_locked(current);
- }
- if (!configured.enabled) {
- release_locked(object);
- ReleaseSRWLockExclusive(&g_lock);
- return;
- }
- // Held rather than written once, because an activity change re-authors these lanes.
- const Lanes desired = held_lanes(configured);
- if (write_block(object, desired)) {
- g_applied = desired;
- g_appliedValid = true;
- g_holding = true;
- }
- ReleaseSRWLockExclusive(&g_lock);
- }
- /** Reports what the override reached. */
- Status status() noexcept {
- Status output{};
- AcquireSRWLockShared(&g_lock);
- output.resolved = g_getter != nullptr;
- output.address = g_object;
- output.captured = g_capturedValid;
- ReleaseSRWLockShared(&g_lock);
- return output;
- }
- } // namespace sunrise::client::hooks::inactivity
|