id spoofing exposed

This commit is contained in:
ApfelTeeSaft
2026-08-11 10:19:16 +02:00
parent 6c4eb6fb20
commit c9788e5b2b
3 changed files with 526 additions and 0 deletions
+145
View File
@@ -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 // pawn::kFloatF24 to 1.0f whenever the byte at pawn::kGateFA0 is set
bool ForcePawnFloatField(IMemory& mem, uint64_t pawn); 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<uint64_t>(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 } // namespace expresso
#endif // EXPRESSO_H #endif // EXPRESSO_H
+268
View File
@@ -0,0 +1,268 @@
// lol id spoof
#include "expresso_recovered.h"
#include <cstring>
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, &current, 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<const unsigned char*>(s);
*p != 0; ++p) {
h ^= static_cast<uint64_t>(*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
+113
View File
@@ -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<uint32_t>(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<void*>(target);
const minhook::Status st = minhook::CreateHook(
target_ptr, reinterpret_cast<void*>(&ProcessEventDetour),
reinterpret_cast<void**>(&g_origProcessEvent)); // 0x16ACBB
bool hooked = false;
if (st == static_cast<minhook::Status>(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<ProcessEventNative>(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<uint64_t>(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