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
|
||||
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
|
||||
|
||||
#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