diff --git a/expresso_recovered.h b/expresso_recovered.h index a1e4706..0b8415e 100644 --- a/expresso_recovered.h +++ b/expresso_recovered.h @@ -212,6 +212,151 @@ AutoBlockResult AutoBlockTick(IMemory& mem, uint64_t game_base, // pawn::kFloatF24 to 1.0f whenever the byte at pawn::kGateFA0 is set bool ForcePawnFloatField(IMemory& mem, uint64_t pawn); +// Reconstruction of the SteamID64 replacement engine and the ProcessEvent +// blocklist that hides it +namespace steam { + +// subtracted at RVA 0x166628 (`movabs rcx, 0x110000100000000`) +// its SteamID64 header for universe=Public, type=Individual, instance=1, +// so id - kIdBase is the 32-bit account ID. +constexpr uint64_t kIdBase = 0x0110000100000000ULL; + +// The upper bound the recovered code accepts +// id + 0xFEEFFFFF00000000 <= 0x7993DFFF (RVA 0x167BA1, 0x16733C, 0x166642) +// 0x7993DFFF is exactly the account ID of 76561199999999999 the fucktards own +// placeholder, which is also the value baked into .data 0x5DA8C0 +constexpr uint32_t kMaxAccountId = 0x7993DFFFu; + +// verbatim: a single unsigned compare after a subtract +constexpr bool IsPlausibleId64(uint64_t id) { + return static_cast(id - kIdBase) <= kMaxAccountId; +} + +} // namespace steam + +// the five APlayerState slots probed for a stored SteamID64, in the order the +// recovered code pushes them onto the stack (RVA 0x167B3C..0x167B5C) +extern const uint32_t kSteamIdCandidateSlots[5]; +constexpr size_t kSteamIdCandidateSlotCount = 5; + +// Bytes probed inside a slot's pointee before giving up: offsets 0, 8, 0x10, +// 0x18, 0x20 (RVA 0x167B90..0x167BB0, ebx += 8 while ebx < 0x28) +constexpr uint32_t kSteamIdProbeSpan = 0x28; + +// Reconstruction of RVA 0x167ABF..0x167BE9. Walks GWorld -> GameInstance -> +// LocalPlayers -> PlayerController -> PlayerState, then probes the five slots. +// Returns the player's real SteamID64, or 0 +uint64_t LocateOriginalSteamId(const IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms); + +// process-wide scan (RVA 0x1669F0, 0x1667A0, 0x166690) + +// One VirtualQuery result, the scan filters on exactly these fields +struct MemoryRegion { + uint64_t base = 0; + uint64_t size = 0; + uint32_t state = 0; // MEM_COMMIT = 0x1000 + uint32_t protect = 0; // PAGE_READWRITE 0x04 | PAGE_EXECUTE_READWRITE 0x40 + uint32_t type = 0; // MEM_IMAGE = 0x1000000 +}; + +// The region filter at RVA 0x166B21..0x166B48. Note what it excludes: mapped +// images. The scan only ever touches private/heap memory +bool IsScannableRegion(const MemoryRegion& region); + +// Hits collected per 256 KiB chunk before the matcher gives up (RVA 0x1666C4, +// cmp eax, 0x40), and the chunk size itself (RVA 0x1667EF, 0x40000) +constexpr size_t kMaxHitsPerChunk = 0x40; +constexpr uint64_t kScanChunkBytes = 0x40000; + +// Reconstruction of the matcher at RVA 0x166690: an 8-byte-aligned sweep +// comparing each qword against needle, capped at kMaxHitsPerChunk. Returns +// the number of addresses written to out +size_t ScanRegionForQword(const IMemory& mem, uint64_t base, uint64_t size, + uint64_t needle, uint64_t* out, size_t out_capacity); + +// RVA 0x1665E0. Re-validates that the slot still holds a +// plausible SteamID64 after making it writable, then overwrites it. The +// recovered code brackets this with VirtualProtect(.., PAGE_EXECUTE_READWRITE) +// and a restore; this reconstruction expresses the check and the store, and +// leaves page protection to the caller-supplied IMemory +bool ReplaceSteamIdAt(IMemory& mem, uint64_t addr, uint64_t new_id); + +// Reconstruction of RVA 0x167260. Writes new_id into every candidate slot +// that still holds a plausible SteamID64, and compacts the list down to the +// slots that took the write. Returns the number written. +size_t ReplaceSteamId(IMemory& mem, uint64_t* slots, size_t& slot_count, + uint64_t new_id); + +// ProcessEvent blocklist + +// eleven UFunction names the ProcessEvent detour refuses to dispatch, in +// the order the recovered initialiser inserts them (RVA 0x167F07..0x168097) +extern const char* const kBlockedIdentityFunctions[11]; +constexpr size_t kBlockedIdentityFunctionCount = 11; + +// FNV-1a 64, verbatim from RVA 0x168183..0x1681B1 (basis 0xCBF29CE484222325, +// prime 0x100000001B3). This is how the blocklist is keyed +uint64_t Fnv1a64(const char* s); + +// Reconstruction of RVA 0x167E70: hash-set membership over the table above. +bool IsBlockedIdentityFunction(const char* name); + +// The four engine events the ProcessEvent observer matches by substring, in +// the order it tests them (RVA 0x1676DB, 0x16772E, 0x1677C3, 0x167885) +enum class IdentityEvent { + kOther, + kReservationPending, // "ClientReservationPending" + kReservationAccepted, // "OnRep_ReservationAccepted" + kClientTravelInternal, // "ClientTravelInternal" + kClientAckTraveling, // "ClientAckTraveling" +}; +IdentityEvent ClassifyIdentityEvent(const char* function_name); + +// Delays, all from immediates in the recovered code +constexpr uint32_t kApplyDelayMs = 0x6A4; // 1700 ms, RVA 0x167787 +constexpr uint32_t kRestoreDelayMs = 0x320; // 800 ms, RVA 0x167831 / 0x1678ED +constexpr uint32_t kAutoRevertMs = 0x2710; // 10000 ms, RVA 0x167DD4 +constexpr int32_t kLocateRetryTicks = 0x77; // RVA 0x167CB1 -> 0x167CDE + +// The spoof state machine's variables, each mapped to its .data address. +// Flag *names* below are inferred from how each byte is used; the addresses +// and the transitions are confirmed +struct IdentitySpoofState { + bool enabled = false; // 0x5DE85B "Profile Spoof" + bool was_enabled = false; // 0x5DE858 edge detect for restore + bool reservation_pending = false; // 0x5DE881 + bool apply_armed = false; // 0x5DE882 + uint64_t apply_deadline_ms = 0; // 0x5DE860 + bool restore_armed = false; // 0x5DE85A + uint64_t restore_deadline_ms = 0; // 0x5DE868 + bool scan_in_progress = false; // 0x5DE870 + bool scan_complete = false; // 0x5DE871 + bool applied = false; // 0x5DE880 + uint64_t applied_at_ms = 0; // 0x5DE850 + uint64_t original_id = 0; // 0x5DE878 + int32_t retry_cooldown = 0; // 0x5DE874 + size_t candidate_count = 0; // (0x5DEA00 - 0x5DE9F8) / 8 +}; + +// Reconstruction of the observer at RVA 0x167610: called for every +// ProcessEvent in the process, and arms the apply/restore deadlines +void OnIdentityEvent(IdentitySpoofState& state, IdentityEvent event, + uint64_t now_ms); + +/// What the per-frame tick decided to do this call +enum class IdentityTickAction { + kNothing, + kLocateOriginal, // no ID yet, and the retry cooldown has expired + kApplyFake, // 0x1679CE + kRestoreOriginal, // 0x167A3A / 0x167D59 / 0x167DE8 +}; + +// Reconstruction of the decision logic in the tick at RVA 0x167950. The tick +// itself performs the writes; this returns what it would do, so the timing can +// be tested without a process +IdentityTickAction IdentitySpoofTick(IdentitySpoofState& state, uint64_t now_ms); + } // namespace expresso #endif // EXPRESSO_H \ No newline at end of file diff --git a/identity_spoof.cpp b/identity_spoof.cpp new file mode 100644 index 0000000..a08b90b --- /dev/null +++ b/identity_spoof.cpp @@ -0,0 +1,268 @@ +// lol id spoof +#include "expresso_recovered.h" + +#include + +namespace expresso { + +// The five APlayerState slots, in the order they are pushed at +// RVA 0x167B3C..0x167B5C +const uint32_t kSteamIdCandidateSlots[5] = {0x3C0, 0x3C8, 0x3D0, 0x3B8, 0x3B0}; + +// RVA 0x167ABF..0x167BE9 find the real SteamID64 +uint64_t LocateOriginalSteamId(const IMemory& mem, uint64_t game_base, + LocalPlayerCache& cache, uint64_t now_ms) { + const uint64_t pc = GetLocalPlayerController(mem, game_base, cache, now_ms); + if (pc == 0) return 0; // 0x167B10 + + const uint64_t player_state = SafeReadPtr(mem, pc + world::kPlayerState); + if (!IsCanonicalUserPointer(player_state)) return 0; // 0x167B2D + + for (size_t i = 0; i < kSteamIdCandidateSlotCount; ++i) { + const uint64_t slot = player_state + kSteamIdCandidateSlots[i]; + + // the slot is a pointer to an object holding the ID + const uint64_t pointee = SafeReadPtr(mem, slot); // 0x167B78 + if (pointee > 0x10000) { // 0x167B80 + for (uint32_t off = 0; off < kSteamIdProbeSpan; off += 8) { // 0x167BAD + const uint64_t v = SafeReadPtr(mem, pointee + off); // 0x167B95 + if (steam::IsPlausibleId64(v)) return v; // 0x167BA1 + } + } + + // the slot holds the ID inline + const uint64_t direct = SafeReadPtr(mem, slot); // 0x167BB6 + if (steam::IsPlausibleId64(direct)) return direct; // 0x167BC1 + } + return 0; // 0x167CB1 +} + +// RVA 0x166B21..0x166B48 which regions get scanned +namespace mem_flags { +constexpr uint32_t kMemCommit = 0x00001000; +constexpr uint32_t kMemImage = 0x01000000; +constexpr uint32_t kPageReadWrite = 0x00000004; +constexpr uint32_t kPageExecReadWrite= 0x00000040; +constexpr uint32_t kPageGuard = 0x00000100; +constexpr uint32_t kWritableMask = kPageReadWrite | kPageExecReadWrite; +} // namespace mem_flags + +bool IsScannableRegion(const MemoryRegion& region) { + if (region.state != mem_flags::kMemCommit) return false; // 0x166B21 + if (region.type == mem_flags::kMemImage) return false; // 0x166B2A + if ((region.protect & mem_flags::kWritableMask) == 0) + return false; // 0x166B36 + if (region.protect & mem_flags::kPageGuard) return false; // 0x166B3A + if (region.size < 8) return false; // 0x166B44 + return true; +} + +// RVA 0x166690 the matcher +size_t ScanRegionForQword(const IMemory& mem, uint64_t base, uint64_t size, + uint64_t needle, uint64_t* out, size_t out_capacity) { + if (out == nullptr || out_capacity == 0 || size < 8) return 0; + const size_t cap = (out_capacity < kMaxHitsPerChunk) ? out_capacity + : kMaxHitsPerChunk; + size_t hits = 0; + const uint64_t end = base + size - 7; // 0x1666A6 + for (uint64_t p = base; p < end && hits < cap; p += 8) { + uint64_t v = 0; + if (!mem.Read(p, &v, sizeof(v))) continue; + if (v == needle) out[hits++] = p; // 0x1666BF + } + return hits; +} + +// RVA 0x1665E0 one guarded write +bool ReplaceSteamIdAt(IMemory& mem, uint64_t addr, uint64_t new_id) { + if (!IsCanonicalUserPointer(addr)) return false; // 0x166573 + + uint64_t current = 0; + if (!mem.Read(addr, ¤t, sizeof(current))) return false; // 0x166625 + if (!steam::IsPlausibleId64(current)) return false; // 0x166642 + + return mem.Write(addr, &new_id, sizeof(new_id)); // 0x166659 +} + +// RVA 0x167260 apply across every located slot +size_t ReplaceSteamId(IMemory& mem, uint64_t* slots, size_t& slot_count, + uint64_t new_id) { + if (slots == nullptr) return 0; + + size_t written = 0; + for (size_t i = 0; i < slot_count; ++i) { + if (ReplaceSteamIdAt(mem, slots[i], new_id)) { // 0x16734E + slots[written++] = slots[i]; // 0x16736B compaction + } + } + slot_count = written; // 0x16745B + return written; +} + +// RVA 0x167E70 the ProcessEvent blocklist +const char* const kBlockedIdentityFunctions[11] = { + "CreateSteamIDFromString", // .rdata 0x59AA20, inserted at 0x167F07 + "GetUniqueNetIDFromPlayerState", // .rdata 0x59AA38 0x167F30 + "SteamID64", // .rdata 0x59AA58 0x167F58 + "GetLoginStatus", // 0x167F7F + "GetUserAccount", // 0x167FA5 + "GetUserPrivilege", // 0x167FCB + "bAntiCheatProtected", // 0x167FF1 + "GetUserID", // 0x168017 + "LoginUser", // 0x16803D + "OnPlayerLoginChanged", // 0x168063 + "HasMatchmakingBan", // 0x16808C +}; + +// RVA 0x168183: movabs r9, 0xCBF29CE484222325 +// RVA 0x168192: movabs r8, 0x100000001B3 +// RVA 0x1681A0: movzx eax,[rcx+rbx]; xor r9,rax; imul r9,r8 +uint64_t Fnv1a64(const char* s) { + uint64_t h = 0xCBF29CE484222325ULL; + if (s == nullptr) return h; + for (const unsigned char* p = reinterpret_cast(s); + *p != 0; ++p) { + h ^= static_cast(*p); + h *= 0x100000001B3ULL; + } + return h; +} + +bool IsBlockedIdentityFunction(const char* name) { + if (name == nullptr) return false; + const uint64_t h = Fnv1a64(name); + for (size_t i = 0; i < kBlockedIdentityFunctionCount; ++i) { + if (h == Fnv1a64(kBlockedIdentityFunctions[i]) && + std::strcmp(name, kBlockedIdentityFunctions[i]) == 0) { + return true; + } + } + return false; +} + +// RVA 0x167610 the event observer +IdentityEvent ClassifyIdentityEvent(const char* function_name) { + if (function_name == nullptr) return IdentityEvent::kOther; + if (std::strstr(function_name, "ClientReservationPending")) // 0x1676DB + return IdentityEvent::kReservationPending; + if (std::strstr(function_name, "OnRep_ReservationAccepted")) // 0x16772E + return IdentityEvent::kReservationAccepted; + if (std::strstr(function_name, "ClientTravelInternal")) // 0x1677C3 + return IdentityEvent::kClientTravelInternal; + if (std::strstr(function_name, "ClientAckTraveling")) // 0x167885 + return IdentityEvent::kClientAckTraveling; + return IdentityEvent::kOther; +} + +void OnIdentityEvent(IdentitySpoofState& state, IdentityEvent event, + uint64_t now_ms) { + if (!state.enabled) return; // 0x167633 + + switch (event) { + case IdentityEvent::kReservationPending: + // The client has asked the session for a slot. Mark it in flight and + // stop any scan, so nothing races the handshake (0x1676FB/0x167706) + state.reservation_pending = true; + state.scan_in_progress = false; + break; + + case IdentityEvent::kReservationAccepted: + // The session said yes. Arm the swap for 1700 ms from now, late + // enough that the reservation is settled, early enough to be in place + // before the server reads the identity again. (0x16774E..0x167797) + if (!state.reservation_pending && // 0x167755 + state.candidate_count > 0 && // 0x167759 + !state.applied && // 0x167762 + !state.scan_in_progress && // 0x16776B + !state.restore_armed) { // 0x167776 + state.apply_deadline_ms = now_ms + kApplyDelayMs; // 0x167787 + state.apply_armed = true; // 0x167797 + } + break; + + case IdentityEvent::kClientTravelInternal: // 0x1677E7 + case IdentityEvent::kClientAckTraveling: // 0x1678A5 + // Travel is under way: the identity has served its purpose. Disarm any + // pending apply and schedule the real ID to go back 800 ms from now + state.apply_armed = false; + if (!state.scan_in_progress && state.applied && !state.restore_armed) { + state.scan_in_progress = false; + state.restore_armed = true; // 0x167825 + state.restore_deadline_ms = now_ms + kRestoreDelayMs; // 0x167831 + } + state.reservation_pending = false; // 0x167859 + break; + + case IdentityEvent::kOther: + break; + } +} + +// RVA 0x167950 the per-frame tick, as a decision function +IdentityTickAction IdentitySpoofTick(IdentitySpoofState& state, + uint64_t now_ms) { + // deferred apply, armed by the observer (0x16798C) + if (state.apply_armed && now_ms >= state.apply_deadline_ms) { // 0x1679A4 + state.apply_armed = false; // 0x1679AB + if (!state.scan_in_progress && !state.restore_armed) { // 0x1679B1/BC + state.applied = true; // 0x1679D7 + state.applied_at_ms = now_ms; // 0x1679EB + return IdentityTickAction::kApplyFake; + } + } + + // the toggle went off: undo everything (0x167A13) + if (!state.enabled) { + if (!state.was_enabled) return IdentityTickAction::kNothing; // 0x167A18 + state.was_enabled = false; // 0x167A82 + if (state.applied && state.original_id != 0) { // 0x167A25/2E + state.applied = false; + state.applied_at_ms = 0; + state.reservation_pending = false; + state.scan_in_progress = false; + state.apply_armed = false; + return IdentityTickAction::kRestoreOriginal; + } + return IdentityTickAction::kNothing; + } + state.was_enabled = true; // 0x167A8E + + // no real ID yet: locate it, with a 119-tick retry back-off + if (state.original_id == 0) { // 0x167A95 + if (state.retry_cooldown > 0) { // 0x167AA2 + --state.retry_cooldown; // 0x167CDE + return IdentityTickAction::kNothing; + } + if (state.scan_complete) return IdentityTickAction::kNothing; // 0x167AB0 + return IdentityTickAction::kLocateOriginal; + } + + // deferred restore, armed on travel (0x167D1B) + if (state.restore_armed && now_ms >= state.restore_deadline_ms) { // 0x167D33 + state.restore_armed = false; // 0x167D3A + if (state.applied) { // 0x167D40 + state.applied = false; // 0x167D5E + state.applied_at_ms = 0; + state.scan_in_progress = false; + state.apply_armed = false; + state.enabled = false; // 0x167D81 + return IdentityTickAction::kRestoreOriginal; + } + } + + // --- hard auto-revert 10 s after the swap (0x167DA0) + if (state.applied && state.applied_at_ms != 0 && !state.restore_armed && + now_ms - state.applied_at_ms > kAutoRevertMs) { // 0x167DD4 + state.applied = false; // 0x167DED + state.applied_at_ms = 0; + state.reservation_pending = false; + state.scan_in_progress = false; + state.apply_armed = false; + state.enabled = false; // 0x167E16 + return IdentityTickAction::kRestoreOriginal; + } + + return IdentityTickAction::kNothing; +} + +} // namespace expresso \ No newline at end of file diff --git a/processevent_hook.cpp b/processevent_hook.cpp new file mode 100644 index 0000000..9e5c9bd --- /dev/null +++ b/processevent_hook.cpp @@ -0,0 +1,113 @@ +// 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 \ No newline at end of file