mirror of
https://github.com/Dingo-Shenanigans/ReSkate.git
synced 2026-10-17 22:07:05 +02:00
394 lines
18 KiB
C++
394 lines
18 KiB
C++
#include "offline_boot.h"
|
|
#include "Engine/Game/Build/supported_build.h"
|
|
#include "Engine/Game/Build/image_identity.h"
|
|
#include "Engine/Game/Build/addresses.h"
|
|
#include "Engine/Game/Build/20260929/engine.h"
|
|
#include "Engine/Game/Build/20260929/global_offline.h"
|
|
#include "Engine/Core/Platform/memory.h"
|
|
|
|
#include <Windows.h>
|
|
#include "Engine/Core/Hooks/hooks.h"
|
|
#include <array>
|
|
#include <atomic>
|
|
#include <cstdint>
|
|
#include <intrin.h>
|
|
#include <mutex>
|
|
#include <sstream>
|
|
|
|
namespace dingosdk {
|
|
namespace {
|
|
|
|
using OnlineEnabledFunction = std::uint8_t (*)();
|
|
using ImageVerifier = bool (*)(std::uintptr_t) noexcept;
|
|
using OptInReader = bool (*)() noexcept;
|
|
using TargetCallerClassifier = std::uintptr_t (*)(std::uintptr_t, std::uintptr_t) noexcept;
|
|
|
|
namespace offline = addr::global_offline;
|
|
constexpr std::uintptr_t function_rva = offline::online_enabled;
|
|
constexpr std::uintptr_t backend_singleton_rva = addr::engine::backend_services;
|
|
constexpr std::uintptr_t backend_predicate_return_rva = offline::backend_predicate_return;
|
|
constexpr std::uintptr_t async_prepare_character_return_rva =
|
|
offline::async_prepare_character_return;
|
|
constexpr std::uintptr_t character_readiness_return_rva = offline::character_readiness_return;
|
|
// The stable getIsOnlineEnabled callable GUID selects this invocation adapter.
|
|
// Its CALL enters a tail-JMP thunk, so the leaf sees the adapter return address.
|
|
constexpr std::uintptr_t flow_adapter_return_rva = offline::flow_adapter_return;
|
|
constexpr std::uintptr_t post_spawn_progression_return_rva =
|
|
offline::post_spawn_progression_return;
|
|
constexpr std::uintptr_t image_size = supported_build::game_image_size;
|
|
constexpr wchar_t opt_in_name[] = L"RESKATE_DEBUG_TEST_GLOBAL_OFFLINE";
|
|
|
|
static_assert(function_rva + 16 <= image_size,
|
|
"Shared OnlineEnabled fingerprint must remain inside the inspected image");
|
|
static_assert(backend_singleton_rva + sizeof(std::uintptr_t) <= image_size,
|
|
"Backend singleton must remain inside the inspected image");
|
|
|
|
// Detours hook service needs the INT3 padding in the fingerprint because the
|
|
// native function body itself is only three bytes.
|
|
constexpr auto function_fingerprint = offline::online_enabled_prefix;
|
|
|
|
enum class GateStatus : unsigned char {
|
|
not_attempted,
|
|
opted_out,
|
|
image_mismatch,
|
|
fingerprint_mismatch,
|
|
backend_probe_failed,
|
|
activation_too_late,
|
|
create_failed,
|
|
enable_failed,
|
|
enable_failed_disable_failed,
|
|
too_late_disable_failed,
|
|
activating,
|
|
active
|
|
};
|
|
|
|
bool environment_opted_in() noexcept;
|
|
std::uintptr_t exact_target_caller(
|
|
std::uintptr_t return_address, std::uintptr_t base) noexcept;
|
|
|
|
struct OfflineBootState {
|
|
std::atomic_bool local_profile_events{};
|
|
std::atomic<LocalObjectBrowserReady> local_object_browser{};
|
|
std::uintptr_t base{};
|
|
std::atomic<OnlineEnabledFunction> original{};
|
|
std::atomic<GateStatus> gate{GateStatus::not_attempted};
|
|
std::atomic<std::uint64_t> invocations{};
|
|
std::atomic<std::uint64_t> returned_false{};
|
|
std::atomic<std::uint64_t> native_true{};
|
|
std::atomic<std::uint64_t> native_false{};
|
|
std::atomic<std::uint64_t> native_exceptions{};
|
|
std::atomic<std::uint64_t> preactive_forwards{};
|
|
std::atomic<std::uint64_t> activating_invocations{};
|
|
std::atomic<std::uint64_t> backend_predicate_invocations{};
|
|
std::atomic<std::uint64_t> async_prepare_character_invocations{};
|
|
std::atomic<std::uint64_t> character_readiness_invocations{};
|
|
std::atomic<std::uint64_t> flow_adapter_invocations{};
|
|
std::atomic<std::uint64_t> post_spawn_progression_invocations{};
|
|
std::atomic<std::uint64_t> unmatched_caller_forwards{};
|
|
std::atomic<bool> backend_readable_before_enable{};
|
|
std::atomic<bool> backend_present_before_enable{};
|
|
std::atomic<bool> backend_readable_after_enable{};
|
|
std::atomic<bool> backend_present_after_enable{};
|
|
std::mutex initialization_mutex;
|
|
ImageVerifier verify_image{&supported_build::running_image_matches};
|
|
OptInReader read_opt_in{&environment_opted_in};
|
|
TargetCallerClassifier classify_target_caller{&exact_target_caller};
|
|
bool attempted{};
|
|
};
|
|
|
|
struct PreserveError {
|
|
DWORD value{GetLastError()};
|
|
~PreserveError() { SetLastError(value); }
|
|
};
|
|
|
|
OfflineBootState& offline_boot_state() {
|
|
static auto* value = new OfflineBootState;
|
|
return *value;
|
|
}
|
|
|
|
bool environment_opted_in() noexcept {
|
|
std::array<wchar_t, 2> value{};
|
|
const auto length = GetEnvironmentVariableW(opt_in_name, value.data(),
|
|
static_cast<DWORD>(value.size()));
|
|
return length == 1 && value[0] == L'1';
|
|
}
|
|
|
|
bool fingerprint_matches(std::uintptr_t base) noexcept {
|
|
std::array<unsigned char, function_fingerprint.size()> actual{};
|
|
return memory::read(base + function_rva, actual) && actual == function_fingerprint;
|
|
}
|
|
|
|
bool backend_singleton(std::uintptr_t base, bool& present) noexcept {
|
|
std::uintptr_t value{};
|
|
if (!memory::read(base + backend_singleton_rva, value)) return false;
|
|
present = value != 0;
|
|
return true;
|
|
}
|
|
|
|
std::uintptr_t exact_target_caller(
|
|
std::uintptr_t return_address, std::uintptr_t base) noexcept {
|
|
constexpr std::array target_return_rvas{
|
|
backend_predicate_return_rva,
|
|
async_prepare_character_return_rva,
|
|
character_readiness_return_rva,
|
|
flow_adapter_return_rva,
|
|
post_spawn_progression_return_rva};
|
|
for (const auto rva : target_return_rvas) {
|
|
if (return_address == base + rva) return rva;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
const char* gate_name(GateStatus gate) noexcept {
|
|
switch (gate) {
|
|
case GateStatus::not_attempted: return "not_attempted";
|
|
case GateStatus::opted_out: return "opted_out";
|
|
case GateStatus::image_mismatch: return "image_mismatch";
|
|
case GateStatus::fingerprint_mismatch: return "fingerprint_mismatch";
|
|
case GateStatus::backend_probe_failed: return "backend_probe_failed";
|
|
case GateStatus::activation_too_late: return "activation_too_late";
|
|
case GateStatus::create_failed: return "create_failed";
|
|
case GateStatus::enable_failed: return "enable_failed";
|
|
case GateStatus::enable_failed_disable_failed: return "enable_failed_disable_failed";
|
|
case GateStatus::too_late_disable_failed: return "too_late_disable_failed";
|
|
case GateStatus::activating: return "activating";
|
|
case GateStatus::active: return "active";
|
|
}
|
|
return "unknown";
|
|
}
|
|
|
|
std::uint8_t online_enabled_function_hook() {
|
|
const auto incoming_error = GetLastError();
|
|
auto& state = offline_boot_state();
|
|
const auto gate = state.gate.load(std::memory_order_acquire);
|
|
const bool activating = gate == GateStatus::activating;
|
|
const bool interception_enabled = activating || gate == GateStatus::active;
|
|
// Reading the release-published gate before the trampoline also publishes
|
|
// the preceding original-pointer and image-base stores.
|
|
const auto original = state.original.load(std::memory_order_acquire);
|
|
std::uintptr_t target_caller_rva{};
|
|
bool replace_result{};
|
|
if (interception_enabled) {
|
|
++state.invocations;
|
|
if (activating) ++state.activating_invocations;
|
|
target_caller_rva = state.classify_target_caller(
|
|
reinterpret_cast<std::uintptr_t>(_ReturnAddress()), state.base);
|
|
if (target_caller_rva == backend_predicate_return_rva) {
|
|
++state.backend_predicate_invocations;
|
|
replace_result = true;
|
|
}
|
|
else if (target_caller_rva == async_prepare_character_return_rva) {
|
|
++state.async_prepare_character_invocations;
|
|
replace_result = true;
|
|
}
|
|
else if (target_caller_rva == character_readiness_return_rva) {
|
|
++state.character_readiness_invocations;
|
|
replace_result = true;
|
|
}
|
|
else if (target_caller_rva == flow_adapter_return_rva) {
|
|
++state.flow_adapter_invocations;
|
|
const auto ready = state.local_object_browser.load(std::memory_order_acquire);
|
|
replace_result = !ready || !ready();
|
|
}
|
|
else if (target_caller_rva == post_spawn_progression_return_rva) {
|
|
++state.post_spawn_progression_invocations;
|
|
replace_result = !state.local_profile_events.load(std::memory_order_acquire);
|
|
}
|
|
else ++state.unmatched_caller_forwards;
|
|
}
|
|
else ++state.preactive_forwards;
|
|
|
|
SetLastError(incoming_error);
|
|
std::uint8_t native_result{};
|
|
try {
|
|
native_result = original();
|
|
} catch (...) {
|
|
const auto native_error = GetLastError();
|
|
if (interception_enabled) ++state.native_exceptions;
|
|
SetLastError(native_error);
|
|
throw;
|
|
}
|
|
const auto native_error = GetLastError();
|
|
if (interception_enabled) {
|
|
if (native_result) ++state.native_true;
|
|
else ++state.native_false;
|
|
}
|
|
if (replace_result) {
|
|
++state.returned_false;
|
|
native_result = 0;
|
|
}
|
|
SetLastError(native_error);
|
|
return native_result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void set_local_object_browser_provider(std::uintptr_t base, LocalObjectBrowserReady ready) noexcept {
|
|
PreserveError preserve;
|
|
auto& state = offline_boot_state();
|
|
if (state.base == base && state.gate.load(std::memory_order_acquire) == GateStatus::active)
|
|
state.local_object_browser.store(ready, std::memory_order_release);
|
|
}
|
|
|
|
bool start_offline_boot(std::uintptr_t base) noexcept {
|
|
PreserveError preserve;
|
|
auto& state = offline_boot_state();
|
|
try {
|
|
std::lock_guard lock(state.initialization_mutex);
|
|
if (state.attempted)
|
|
return state.base == base && state.gate.load(std::memory_order_acquire) == GateStatus::active;
|
|
state.attempted = true;
|
|
state.base = base;
|
|
if (!state.read_opt_in()) {
|
|
state.gate.store(GateStatus::opted_out, std::memory_order_release);
|
|
return false;
|
|
}
|
|
if (!state.verify_image(base)) {
|
|
state.gate.store(GateStatus::image_mismatch, std::memory_order_release);
|
|
return false;
|
|
}
|
|
if (!fingerprint_matches(base)) {
|
|
state.gate.store(GateStatus::fingerprint_mismatch, std::memory_order_release);
|
|
return false;
|
|
}
|
|
|
|
bool backend_present{};
|
|
const bool backend_readable = backend_singleton(base, backend_present);
|
|
state.backend_readable_before_enable.store(backend_readable, std::memory_order_relaxed);
|
|
state.backend_present_before_enable.store(backend_present, std::memory_order_relaxed);
|
|
if (!backend_readable) {
|
|
state.gate.store(GateStatus::backend_probe_failed, std::memory_order_release);
|
|
return false;
|
|
}
|
|
if (backend_present) {
|
|
state.gate.store(GateStatus::activation_too_late, std::memory_order_release);
|
|
return false;
|
|
}
|
|
|
|
auto* target = reinterpret_cast<void*>(base + function_rva);
|
|
void* trampoline{};
|
|
const auto created = hook_prepare(target,
|
|
reinterpret_cast<void*>(&online_enabled_function_hook), &trampoline);
|
|
if (created != HookOk || !trampoline) {
|
|
if (created == HookOk) hook_remove(target);
|
|
state.gate.store(GateStatus::create_failed, std::memory_order_release);
|
|
return false;
|
|
}
|
|
state.original.store(reinterpret_cast<OnlineEnabledFunction>(trampoline),
|
|
std::memory_order_release);
|
|
// Once executable redirection can become visible, every selected caller
|
|
// must already receive false. This closes the post-enable/pre-active window.
|
|
state.gate.store(GateStatus::activating, std::memory_order_release);
|
|
if (hook_enable(target) != HookOk) {
|
|
// Retain the trampoline allocation after an activation attempt so
|
|
// a thread already forwarding through it cannot use freed code.
|
|
state.gate.store(GateStatus::enable_failed, std::memory_order_release);
|
|
const auto disabled = hook_disable(target);
|
|
state.gate.store(disabled == HookOk || disabled == HookDisabled ?
|
|
GateStatus::enable_failed : GateStatus::enable_failed_disable_failed,
|
|
std::memory_order_release);
|
|
return false;
|
|
}
|
|
|
|
backend_present = false;
|
|
const bool backend_readable_after = backend_singleton(base, backend_present);
|
|
state.backend_readable_after_enable.store(backend_readable_after,
|
|
std::memory_order_relaxed);
|
|
state.backend_present_after_enable.store(backend_present, std::memory_order_relaxed);
|
|
const bool backend_call_intercepted =
|
|
state.backend_predicate_invocations.load(std::memory_order_acquire) != 0;
|
|
if (!backend_readable_after || (backend_present && !backend_call_intercepted) ||
|
|
state.preactive_forwards.load(std::memory_order_acquire) != 0) {
|
|
state.gate.store(backend_readable_after ? GateStatus::activation_too_late :
|
|
GateStatus::backend_probe_failed, std::memory_order_release);
|
|
const auto disabled = hook_disable(target);
|
|
state.gate.store(disabled == HookOk || disabled == HookDisabled ?
|
|
(backend_readable_after ? GateStatus::activation_too_late :
|
|
GateStatus::backend_probe_failed) : GateStatus::too_late_disable_failed,
|
|
std::memory_order_release);
|
|
return false;
|
|
}
|
|
state.gate.store(GateStatus::active, std::memory_order_release);
|
|
return true;
|
|
} catch (...) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
void set_local_profile_event_provider(std::uintptr_t base, bool active) noexcept {
|
|
auto& state = offline_boot_state();
|
|
if (state.base == base) state.local_profile_events.store(active, std::memory_order_release);
|
|
}
|
|
|
|
OfflineBootObservation offline_boot_observation() {
|
|
PreserveError preserve;
|
|
auto& state = offline_boot_state();
|
|
const auto gate = state.gate.load(std::memory_order_acquire);
|
|
const auto invocations = state.invocations.load(std::memory_order_relaxed);
|
|
const auto returned_false = state.returned_false.load(std::memory_order_relaxed);
|
|
const auto native_true = state.native_true.load(std::memory_order_relaxed);
|
|
const auto native_false = state.native_false.load(std::memory_order_relaxed);
|
|
const auto exceptions = state.native_exceptions.load(std::memory_order_relaxed);
|
|
const auto preactive = state.preactive_forwards.load(std::memory_order_relaxed);
|
|
const auto activating_invocations =
|
|
state.activating_invocations.load(std::memory_order_relaxed);
|
|
const auto backend_predicate_invocations =
|
|
state.backend_predicate_invocations.load(std::memory_order_relaxed);
|
|
const auto async_prepare_character_invocations =
|
|
state.async_prepare_character_invocations.load(std::memory_order_relaxed);
|
|
const auto character_readiness_invocations =
|
|
state.character_readiness_invocations.load(std::memory_order_relaxed);
|
|
const auto flow_adapter_invocations =
|
|
state.flow_adapter_invocations.load(std::memory_order_relaxed);
|
|
const auto post_spawn_progression_invocations =
|
|
state.post_spawn_progression_invocations.load(std::memory_order_relaxed);
|
|
const auto unmatched_caller_forwards =
|
|
state.unmatched_caller_forwards.load(std::memory_order_relaxed);
|
|
const auto backend_readable_before =
|
|
state.backend_readable_before_enable.load(std::memory_order_relaxed);
|
|
const auto backend_present_before =
|
|
state.backend_present_before_enable.load(std::memory_order_relaxed);
|
|
const auto backend_readable_after =
|
|
state.backend_readable_after_enable.load(std::memory_order_relaxed);
|
|
const auto backend_present_after =
|
|
state.backend_present_after_enable.load(std::memory_order_relaxed);
|
|
|
|
std::ostringstream json;
|
|
json << "{\"event\":\"global_offline_experiment_observation\",\"gate\":\""
|
|
<< gate_name(gate) << "\",\"opted_in\":"
|
|
<< (gate != GateStatus::not_attempted && gate != GateStatus::opted_out ? "true" : "false")
|
|
<< ",\"function_rva\":\"0x" << std::hex << function_rva << std::dec
|
|
<< "\",\"invocations\":" << invocations
|
|
<< ",\"returned_false\":" << returned_false
|
|
<< ",\"native_true\":" << native_true << ",\"native_false\":" << native_false
|
|
<< ",\"native_exceptions\":" << exceptions
|
|
<< ",\"preactive_forwards\":" << preactive
|
|
<< ",\"activating_invocations\":" << activating_invocations
|
|
<< ",\"backend_predicate_invocations\":" << backend_predicate_invocations
|
|
<< ",\"async_prepare_character_invocations\":"
|
|
<< async_prepare_character_invocations
|
|
<< ",\"character_readiness_invocations\":" << character_readiness_invocations
|
|
<< ",\"flow_adapter_invocations\":" << flow_adapter_invocations
|
|
<< ",\"post_spawn_progression_invocations\":"
|
|
<< post_spawn_progression_invocations
|
|
<< ",\"unmatched_caller_forwards\":" << unmatched_caller_forwards
|
|
<< std::hex
|
|
<< ",\"backend_predicate_return_rva\":\"0x" << backend_predicate_return_rva << '"'
|
|
<< ",\"async_prepare_character_return_rva\":\"0x" << async_prepare_character_return_rva << '"'
|
|
<< ",\"character_readiness_return_rva\":\"0x" << character_readiness_return_rva << '"'
|
|
<< ",\"flow_adapter_return_rva\":\"0x" << flow_adapter_return_rva << '"'
|
|
<< ",\"post_spawn_progression_return_rva\":\"0x" << post_spawn_progression_return_rva << '"'
|
|
<< ",\"backend_singleton_rva\":\"0x" << backend_singleton_rva << '"' << std::dec
|
|
<< ",\"backend_readable_before_enable\":"
|
|
<< (backend_readable_before ? "true" : "false")
|
|
<< ",\"backend_present_before_enable\":"
|
|
<< (backend_present_before ? "true" : "false")
|
|
<< ",\"backend_readable_after_enable\":"
|
|
<< (backend_readable_after ? "true" : "false")
|
|
<< ",\"backend_present_after_enable\":"
|
|
<< (backend_present_after ? "true" : "false") << '}';
|
|
return {json.str()};
|
|
}
|
|
|
|
} // namespace dingosdk
|