mirror of
https://github.com/ApfelTeeSaft/expresso.git
synced 2026-08-26 19:23:27 +00:00
362 lines
15 KiB
C++
362 lines
15 KiB
C++
// offsets, constants and other shit recovered from dll
|
|
#ifndef EXPRESSO_H
|
|
#define EXPRESSO_H
|
|
|
|
#include <cstdint>
|
|
#include <cstddef>
|
|
|
|
namespace expresso {
|
|
|
|
// Engine globals
|
|
namespace game_rva {
|
|
constexpr uint64_t kGWorld = 0x2DC7380; // RVA 0x15AE96, 0x15A592
|
|
constexpr uint64_t kGNames = 0x314A7D0; // RVA 0x15AB18
|
|
constexpr uint64_t kProcessEvent = 0x7A8010; // RVA 0x165EBF, 0x160BBF
|
|
} // namespace game_rva
|
|
|
|
// UObject model offsets
|
|
namespace uobject {
|
|
constexpr uint32_t kClassPrivate = 0x10; // RVA 0x15C265
|
|
constexpr uint32_t kNameIndex = 0x18; // RVA 0x15C2D6 FName::ComparisonIndex
|
|
constexpr uint32_t kFieldNext = 0x28; // RVA 0x15C351 UField::Next
|
|
constexpr uint32_t kSuperStruct = 0x30; // RVA 0x15C36C UStruct::SuperStruct
|
|
constexpr uint32_t kChildren = 0x38; // RVA 0x15C297 UStruct::Children
|
|
constexpr uint32_t kFunctionFlags = 0x88; // RVA 0x15C19E UFunction::FunctionFlags
|
|
} // namespace uobject
|
|
|
|
// World related stuff
|
|
namespace world {
|
|
constexpr uint32_t kOwningGameInstance = 0x140; // RVA 0x15AF35 UWorld
|
|
constexpr uint32_t kLocalPlayers = 0x38; // RVA 0x15AF5A UGameInstance
|
|
constexpr uint32_t kPlayerController = 0x30; // RVA 0x15AF98 ULocalPlayer
|
|
constexpr uint32_t kPlayerState = 0x388; // RVA 0x166036 APlayerController
|
|
constexpr uint32_t kPlayerNameFString = 0x370; // RVA 0x166065 APlayerState
|
|
} // namespace world
|
|
|
|
// the three pc slots the dll tries in order
|
|
namespace pc_slot {
|
|
constexpr uint32_t kA = 0x3E0; // RVA 0x15B785
|
|
constexpr uint32_t kB = 0x6A0; // RVA 0x15B7E7
|
|
constexpr uint32_t kC = 0x3D0; // RVA 0x15B835
|
|
constexpr uint32_t kCInner = 0x20; // RVA 0x15B85A, dereferenced from kC
|
|
} // namespace pc_slot
|
|
|
|
// pawn field stuff, symbols i could not name are named after the offset
|
|
namespace pawn {
|
|
// "Mask HP: %.2f / %.2f" (.rdata 0x59A3B0) at the call site that consumes both reads
|
|
constexpr uint32_t kMaskHpCurrent = 0xE58; // RVA 0x160E0A
|
|
constexpr uint32_t kMaskHpMax = 0xE64; // RVA 0x160E38
|
|
// "Stamina: %.0f / %.0f" (.rdata 0x59A5D8) the same way
|
|
constexpr uint32_t kStaminaCurrent = 0x1300; // RVA 0x1637D7
|
|
constexpr uint32_t kStaminaMax = 0x130C; // RVA 0x1637E3
|
|
|
|
// Auto-block preconditions, three bytes that must all read false
|
|
constexpr uint32_t kGateE54 = 0xE54; // RVA 0x16015E
|
|
constexpr uint32_t kGate1060 = 0x1060; // RVA 0x160186
|
|
constexpr uint32_t kGate1604 = 0x1604; // RVA 0x160172
|
|
// Set to 1 immediately before the SERVER_OnBlock RPC
|
|
constexpr uint32_t kFlag1010 = 0x1010; // RVA 0x160206
|
|
|
|
// bool gate and fload thats being forced to 1, no clue what it is
|
|
// not the stamina, thats defined above
|
|
constexpr uint32_t kGateFA0 = 0xFA0; // RVA 0x1600EC
|
|
constexpr uint32_t kFloatF24 = 0xF24; // RVA 0x160104
|
|
} // namespace pawn
|
|
|
|
// FName pool RVA 0x15AB63/0x15AB66
|
|
namespace fname {
|
|
constexpr uint32_t kChunkShift = 14;
|
|
constexpr uint32_t kChunkMask = 0x3FFF;
|
|
} // namespace fname
|
|
|
|
// EFunctionFlags values expresso ORs into UFunction::FunctionFlags before
|
|
// calling ProcessEvent, then restores. RVA 0x165BCF and 0x160BF7
|
|
namespace func_flags {
|
|
constexpr uint32_t kNative = 0x00000400;
|
|
constexpr uint32_t kPrivate = 0x00040000;
|
|
} // namespace func_flags
|
|
|
|
// corrupt pointer cannot hang the render thread (RVA 0x15C28D, 0x15C2C9, 0x15C2E2).
|
|
// bounds are:
|
|
namespace limits {
|
|
constexpr uint32_t kMaxClassDepth = 0x40;
|
|
constexpr uint32_t kMaxFieldsPerCls = 0x7D0;
|
|
constexpr uint32_t kMaxNameIndex = 0x1FFFFE;
|
|
constexpr uint32_t kMaxLocalPlayers = 7; // RVA 0x15B042: (count-1) <= 6
|
|
constexpr uint32_t kPcCacheMs = 2000; // RVA 0x15AF10: cmp rax, 0x7D0
|
|
constexpr uint32_t kRenameCooldown = 3000; // RVA 0x165E5A: cmp rcx, 0xBB8
|
|
constexpr uint32_t kMaxNameChars = 0x78; // RVA 0x165E2A
|
|
constexpr uint32_t kMinClassNameIdx = 0x10000; // RVA 0x15B7DC / 0x15B82A
|
|
constexpr uint32_t kMaxClassNameIdx = 0x200000; // RVA 0x15B8B2 / 0x15BFFF
|
|
constexpr int32_t kAutoBlockCooldownFrames = 30; // RVA 0x160280: 0x1E
|
|
} // namespace limits
|
|
|
|
class IMemory {
|
|
public:
|
|
virtual ~IMemory() = default;
|
|
virtual bool Read(uint64_t addr, void* out, size_t size) const = 0;
|
|
virtual bool Write(uint64_t addr, const void* src, size_t size) = 0;
|
|
};
|
|
|
|
// Guarded accessors, self explainatory
|
|
bool IsCanonicalUserPointer(uint64_t addr); // RVA 0x15A6A0 prologue
|
|
|
|
uint64_t SafeReadPtr(const IMemory& mem, uint64_t addr); // 0x15A6A0
|
|
bool SafeReadBool(const IMemory& mem, uint64_t addr); // 0x16B030
|
|
int32_t SafeReadInt32(const IMemory& mem, uint64_t addr); // 0x16B0F0
|
|
float SafeReadFloat(const IMemory& mem, uint64_t addr); // 0x16B120
|
|
bool SafeWriteInt32(IMemory& mem, uint64_t addr, int32_t v); // 0x16B160
|
|
bool SafeWriteFloat(IMemory& mem, uint64_t addr, float v); // 0x16B560
|
|
bool SafeSetTrue(IMemory& mem, uint64_t addr); // 0x16B5A0
|
|
bool SafeWritePtr(IMemory& mem, uint64_t addr, uint64_t v); // 0x16B5E0
|
|
|
|
// resolve fname
|
|
bool GetNameString(const IMemory& mem, uint64_t game_base,
|
|
int32_t name_index, char* out, size_t out_size);
|
|
|
|
// ClassPrivate -> Children -> Next -> SuperStruct comparing FNames
|
|
uint64_t FindFunctionByName(const IMemory& mem, uint64_t game_base,
|
|
uint64_t object, const char* name);
|
|
|
|
using ProcessEventFn = void (*)(uint64_t object, uint64_t function,
|
|
void* params, void* user);
|
|
bool CallUFunction(IMemory& mem, uint64_t object, uint64_t function,
|
|
void* params, uint32_t or_flags,
|
|
ProcessEventFn invoke, void* user);
|
|
|
|
// Cached local-player resolution, RVA 0x15AE60
|
|
struct LocalPlayerCache {
|
|
uint64_t controller = 0; // .data 0x5DE790
|
|
uint64_t world = 0; // .data 0x5DE530
|
|
uint64_t timestamp = 0; // .data 0x5DE788
|
|
};
|
|
uint64_t GetLocalPlayerController(const IMemory& mem, uint64_t game_base,
|
|
LocalPlayerCache& cache, uint64_t now_ms);
|
|
|
|
// Resolves an object's ClassPrivate->NamePrivate
|
|
bool GetObjectClassName(const IMemory& mem, uint64_t game_base, uint64_t object,
|
|
char* out, size_t out_size);
|
|
|
|
extern const char* const kPawnRoleNames[5];
|
|
constexpr size_t kPawnRoleCount = 5;
|
|
|
|
extern const char* const kKillerRoleNames[5];
|
|
constexpr size_t kKillerRoleCount = 5;
|
|
|
|
bool NameContains(const char* haystack, const char* needle);
|
|
|
|
uint64_t GetLocalPawn(const IMemory& mem, uint64_t game_base,
|
|
LocalPlayerCache& cache, uint64_t now_ms);
|
|
|
|
// True when the local pawn's class name contains one of kKillerRoleNames.
|
|
bool IsLocalPawnKiller(const IMemory& mem, uint64_t game_base,
|
|
LocalPlayerCache& cache, uint64_t now_ms);
|
|
|
|
/// One entry of the recovered SERVER_* table
|
|
struct ServerRpcCall {
|
|
const char* function_name;
|
|
uint32_t or_flags; // 0x400 or 0x40400
|
|
const void* params;
|
|
size_t params_size;
|
|
};
|
|
|
|
bool InvokeServerRpc(IMemory& mem, uint64_t game_base, uint64_t target_object,
|
|
const ServerRpcCall& call,
|
|
ProcessEventFn invoke, void* user);
|
|
|
|
/// The `[DW:*]` direct-write name spoof, RVA 0x16606C..0x16609D. Overwrites the
|
|
/// FString at PlayerState+0x370 in place, bypassing every RPC
|
|
bool DirectWritePlayerName(IMemory& mem, uint64_t player_state,
|
|
uint64_t wide_buffer, int32_t length);
|
|
|
|
/// The full three-fallback rename, RVA 0x165D90
|
|
enum class RenameResult { kOk, kEmpty, kTooLong, kCooldown, kNoProcessEvent,
|
|
kNoPlayerController, kFailed };
|
|
RenameResult SpoofPlayerName(IMemory& mem, uint64_t game_base,
|
|
const char* utf8_name, uint64_t now_ms,
|
|
uint64_t& last_rename_ms,
|
|
uint64_t wide_buffer, int32_t& wide_length,
|
|
ProcessEventFn invoke, void* user);
|
|
|
|
// some input stuff E F
|
|
struct AutoBlockInput {
|
|
bool interact_down = false;
|
|
bool attack_down = false;
|
|
};
|
|
|
|
// Frame counter at .data 0x5DE6D4, decremented once per tick and reloaded
|
|
struct AutoBlockState {
|
|
int32_t cooldown_frames = 0;
|
|
};
|
|
|
|
enum class AutoBlockResult {
|
|
kDisabled, // feature toggle off (0x160122)
|
|
kNoPawn, // pawn did not validate (0x160135)
|
|
kWrongRole, // local pawn is the antagonist (0x160151)
|
|
kPawnBusy, // one of the three gate bytes set (0x16015E/172/186)
|
|
kMenuOpen, // g_menuOpenMirror set (0x16019A)
|
|
kInputHeld, // interact or attack held (0x1601F5)
|
|
kCoolingDown, // cooldown_frames > 0 (0x160218)
|
|
kNoFunction, // SERVER_OnBlock not found (0x160260)
|
|
kFired, // RPC dispatched (0x16027B)
|
|
};
|
|
|
|
AutoBlockResult AutoBlockTick(IMemory& mem, uint64_t game_base,
|
|
LocalPlayerCache& cache, uint64_t now_ms,
|
|
bool feature_enabled, bool menu_open_mirror,
|
|
const AutoBlockInput& input,
|
|
AutoBlockState& state,
|
|
ProcessEventFn invoke, void* user);
|
|
|
|
// Reconstruction of RVA 0x1600CF..0x160110: pins the float at
|
|
// 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
|