mirror of
https://github.com/ApfelTeeSaft/expresso.git
synced 2026-08-26 19:23:27 +00:00
id spoofing exposed
This commit is contained in:
@@ -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
|
||||||
@@ -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, ¤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<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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user