fix: async Warden PAGE_A/PAGE_B checks to prevent main-loop stalls

Move 5-second brute-force HMAC-SHA1 code pattern searches to a
background thread via std::async. The main loop now detects PAGE_A/B
checks, launches the response builder async, and drains the result
in update() — encrypting and sending on the main thread to keep
wardenCrypto_ RC4 state thread-safe.

Also adds Turtle WoW PE binary support (isTurtle flag, dedicated exe
search, runtime patches), searchCodePattern with result caching,
writeLE32 public API, and Warden scan entry verification.
This commit is contained in:
Kelsi 2026-03-16 16:46:29 -07:00
parent f0a515ff9c
commit a3279ea1ad
6 changed files with 890 additions and 116 deletions

View file

@ -7,6 +7,8 @@
#include <filesystem>
#include <sstream>
#include <iomanip>
#include <openssl/hmac.h>
#include <openssl/evp.h>
namespace wowee {
namespace game {
@ -106,19 +108,136 @@ bool WardenMemory::parsePE(const std::vector<uint8_t>& fileData) {
" size=0x", copySize, std::dec);
}
LOG_WARNING("WardenMemory: PE loaded — imageBase=0x", std::hex, imageBase_,
" imageSize=0x", imageSize_, std::dec,
" (", numSections, " sections, ", fileData.size(), " bytes on disk)");
return true;
}
void WardenMemory::initKuserSharedData() {
std::memset(kuserData_, 0, KUSER_SIZE);
// NtMajorVersion at offset 0x026C = 6 (Vista/7/8/10)
// Populate KUSER_SHARED_DATA with realistic Windows 7 SP1 values.
// Warden reads this in 238-byte chunks for OS fingerprinting.
// 0x0000: TickCountLowDeprecated (uint32) — non-zero uptime ticks
uint32_t tickCountLow = 0x003F4A00; // ~70 minutes uptime
std::memcpy(kuserData_ + 0x0000, &tickCountLow, 4);
// 0x0004: TickCountMultiplier (uint32) — standard value
uint32_t tickMult = 0x0FA00000;
std::memcpy(kuserData_ + 0x0004, &tickMult, 4);
// 0x0008: InterruptTime (KSYSTEM_TIME = LowPart(4) + High1Time(4) + High2Time(4))
uint32_t intTimeLow = 0x6B49D200; // plausible 100ns interrupt time
uint32_t intTimeHigh = 0x00000029;
std::memcpy(kuserData_ + 0x0008, &intTimeLow, 4);
std::memcpy(kuserData_ + 0x000C, &intTimeHigh, 4);
std::memcpy(kuserData_ + 0x0010, &intTimeHigh, 4);
// 0x0014: SystemTime (KSYSTEM_TIME) — ~2024 epoch in Windows FILETIME
uint32_t sysTimeLow = 0xA0B71B00;
uint32_t sysTimeHigh = 0x01DA5E80;
std::memcpy(kuserData_ + 0x0014, &sysTimeLow, 4);
std::memcpy(kuserData_ + 0x0018, &sysTimeHigh, 4);
std::memcpy(kuserData_ + 0x001C, &sysTimeHigh, 4);
// 0x0020: TimeZoneBias (KSYSTEM_TIME) — 0 for UTC
// Leave as zeros (UTC timezone)
// 0x002C: ImageNumberLow (uint16) = 0x014C (IMAGE_FILE_MACHINE_I386)
uint16_t imageNumLow = 0x014C;
std::memcpy(kuserData_ + 0x002C, &imageNumLow, 2);
// 0x002E: ImageNumberHigh (uint16) = 0x014C
uint16_t imageNumHigh = 0x014C;
std::memcpy(kuserData_ + 0x002E, &imageNumHigh, 2);
// 0x0030: NtSystemRoot (wchar_t[260]) = L"C:\\WINDOWS"
const wchar_t* sysRoot = L"C:\\WINDOWS";
size_t rootLen = 10; // chars including null
for (size_t i = 0; i < rootLen; i++) {
uint16_t wc = static_cast<uint16_t>(sysRoot[i]);
std::memcpy(kuserData_ + 0x0030 + i * 2, &wc, 2);
}
// 0x0238: MaxStackTraceDepth (uint32)
uint32_t maxStack = 0;
std::memcpy(kuserData_ + 0x0238, &maxStack, 4);
// 0x023C: CryptoExponent (uint32) — typical value
uint32_t cryptoExp = 0x00010001; // 65537
std::memcpy(kuserData_ + 0x023C, &cryptoExp, 4);
// 0x0240: TimeZoneId (uint32) = 0 (TIME_ZONE_ID_UNKNOWN)
uint32_t tzId = 0;
std::memcpy(kuserData_ + 0x0240, &tzId, 4);
// 0x0248: LargePageMinimum (uint32)
uint32_t largePage = 0x00200000; // 2MB
std::memcpy(kuserData_ + 0x0248, &largePage, 4);
// 0x0264: ActiveConsoleId (uint32) = 0
uint32_t consoleId = 0;
std::memcpy(kuserData_ + 0x0264, &consoleId, 4);
// 0x0268: DismountCount (uint32) = 0
// already zero
// 0x026C: NtMajorVersion (uint32) = 6 (Vista/7/8/10)
uint32_t ntMajor = 6;
std::memcpy(kuserData_ + 0x026C, &ntMajor, 4);
// NtMinorVersion at offset 0x0270 = 1 (Windows 7)
// 0x0270: NtMinorVersion (uint32) = 1 (Windows 7)
uint32_t ntMinor = 1;
std::memcpy(kuserData_ + 0x0270, &ntMinor, 4);
// 0x0274: NtProductType (uint32) = 1 (NtProductWinNt = workstation)
uint32_t productType = 1;
std::memcpy(kuserData_ + 0x0274, &productType, 4);
// 0x0278: ProductTypeIsValid (uint8) = 1
kuserData_[0x0278] = 1;
// 0x027C: NtMajorVersion (duplicate? actually NativeMajorVersion)
// 0x0280: NtMinorVersion (NativeMinorVersion)
// 0x0294: ProcessorFeatures (BOOLEAN[64]) — leave mostly zero
// PF_FLOATING_POINT_PRECISION_ERRATA = 0 at [0]
// PF_FLOATING_POINT_EMULATED = 0 at [1]
// PF_COMPARE_EXCHANGE_DOUBLE = 1 at [2]
kuserData_[0x0294 + 2] = 1;
// PF_MMX_INSTRUCTIONS_AVAILABLE = 1 at [3]
kuserData_[0x0294 + 3] = 1;
// PF_XMMI_INSTRUCTIONS_AVAILABLE (SSE) = 1 at [6]
kuserData_[0x0294 + 6] = 1;
// PF_XMMI64_INSTRUCTIONS_AVAILABLE (SSE2) = 1 at [10]
kuserData_[0x0294 + 10] = 1;
// PF_NX_ENABLED = 1 at [12]
kuserData_[0x0294 + 12] = 1;
// 0x02D4: Reserved1 (uint32)
// 0x02D8: ActiveProcessorCount (uint32) = 4
uint32_t procCount = 4;
std::memcpy(kuserData_ + 0x02D8, &procCount, 4);
// 0x0300: NumberOfPhysicalPages (uint32) — 4GB / 4KB = ~1M pages
uint32_t physPages = 0x000FF000;
std::memcpy(kuserData_ + 0x0300, &physPages, 4);
// 0x0304: SafeBootMode (uint8) = 0 (normal boot)
// already zero
// 0x0308: SharedDataFlags / TraceLogging (uint32) — leave zero
// 0x0320: TickCount (KSYSTEM_TIME) — same as TickCountLowDeprecated
std::memcpy(kuserData_ + 0x0320, &tickCountLow, 4);
// 0x0330: Cookie (uint32) — stack cookie, random value
uint32_t cookie = 0x4A2F8C15;
std::memcpy(kuserData_ + 0x0330, &cookie, 4);
}
void WardenMemory::writeLE32(uint32_t va, uint32_t value) {
@ -132,56 +251,52 @@ void WardenMemory::writeLE32(uint32_t va, uint32_t value) {
}
void WardenMemory::patchRuntimeGlobals() {
// Only patch Classic 1.12.1 (build 5875) WoW.exe
// Identified by: ImageBase=0x400000, ImageSize=0x906000 (unique to 1.12.1)
// Other expansions have different image sizes and different global addresses.
if (imageBase_ != 0x00400000 || imageSize_ != 0x00906000) {
LOG_INFO("WardenMemory: Not Classic 1.12.1 WoW.exe (imageSize=0x",
std::hex, imageSize_, std::dec, "), skipping runtime global patches");
if (imageBase_ != 0x00400000) {
LOG_WARNING("WardenMemory: unexpected imageBase=0x", std::hex, imageBase_, std::dec,
" — skipping runtime global patches");
return;
}
// Classic 1.12.1 (build 5875) runtime globals
// These are in the .data BSS region - zero on disk, populated at runtime.
// We patch them with fake but valid values so Warden checks pass.
// VMaNGOS has TWO types of Warden scans that read these addresses:
//
// Offsets from CMaNGOS anticheat module (wardenwin.cpp):
// WardenModule = 0xCE897C
// OfsWardenSysInfo = 0x228
// OfsWardenWinSysInfo = 0x08
// g_theGxDevicePtr = 0xC0ED38
// OfsDevice2 = 0x38A8
// OfsDevice3 = 0x0
// OfsDevice4 = 0xA8
// WorldEnables = 0xC7B2A4
// LastHardwareAction= 0xCF0BC8
// 1. DB-driven scans (warden_scans table): memcmp against expected bytes.
// These check CODE sections for integrity — never check runtime data addresses.
//
// 2. Scripted scans (WardenWin::LoadScriptedScans): READ and INTERPRET values.
// - "Warden locate" reads 0xCE897C as a pointer, follows chain to SYSTEM_INFO
// - "Anti-AFK hack" reads 0xCF0BC8 as a timestamp, compares vs TIMING ticks
// - "CWorld::enables" reads 0xC7B2A4, checks flag bits
// - "EndScene" reads 0xC0ED38, follows pointer chain to find EndScene address
//
// We MUST patch these for ALL clients (including Turtle WoW) because the scripted
// scans interpret the values as runtime state, not static code bytes. Returning
// raw PE data causes the Anti-AFK scan to see lastHardwareAction > currentTime
// (PE bytes happen to be a large value), triggering a kick after ~3.5 minutes.
// === Warden SYSTEM_INFO chain (3-level pointer chain) ===
// Stage 0: [0xCE897C] → fake warden struct base
// === Runtime global patches (applied unconditionally for all image variants) ===
// Warden SYSTEM_INFO chain
constexpr uint32_t WARDEN_MODULE_PTR = 0xCE897C;
constexpr uint32_t FAKE_WARDEN_BASE = 0xCE8000;
writeLE32(WARDEN_MODULE_PTR, FAKE_WARDEN_BASE);
// Stage 1: [FAKE_WARDEN_BASE + 0x228] → pointer to sysinfo container
constexpr uint32_t OFS_WARDEN_SYSINFO = 0x228;
constexpr uint32_t FAKE_SYSINFO_CONTAINER = 0xCE8300;
writeLE32(FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, FAKE_SYSINFO_CONTAINER);
writeLE32(FAKE_WARDEN_BASE + 0x228, FAKE_SYSINFO_CONTAINER);
// Stage 2: [FAKE_SYSINFO_CONTAINER + 0x08] → 36-byte SYSTEM_INFO struct
constexpr uint32_t OFS_WARDEN_WIN_SYSINFO = 0x08;
uint32_t sysInfoAddr = FAKE_SYSINFO_CONTAINER + OFS_WARDEN_WIN_SYSINFO; // 0xCE8308
// WIN_SYSTEM_INFO is 36 bytes (0x24):
// uint16 wProcessorArchitecture (must be 0 = x86)
// uint16 wReserved
// uint32 dwPageSize
// uint32 lpMinimumApplicationAddress
// uint32 lpMaximumApplicationAddress (MUST be non-zero!)
// uint32 dwActiveProcessorMask
// uint32 dwNumberOfProcessors
// uint32 dwProcessorType (must be 386, 486, or 586)
// uint32 dwAllocationGranularity
// uint16 wProcessorLevel
// uint16 wProcessorRevision
// Write SYSINFO pointer at many offsets from FAKE_WARDEN_BASE so the
// chain works regardless of which module-specific offset the server uses.
// MUST be done BEFORE writing the actual SYSTEM_INFO struct, because this
// loop's range (0xCE8200-0xCE8400) overlaps with the struct at 0xCE8308.
for (uint32_t off = 0x200; off <= 0x400; off += 4) {
uint32_t addr = FAKE_WARDEN_BASE + off;
if (addr >= imageBase_ && (addr - imageBase_) + 4 <= imageSize_) {
writeLE32(addr, FAKE_SYSINFO_CONTAINER);
}
}
// Now write the actual WIN_SYSTEM_INFO struct AFTER the pointer fill loop,
// so it overwrites any values the loop placed in the 0xCE8308+ range.
uint32_t sysInfoAddr = FAKE_SYSINFO_CONTAINER + 0x08;
#pragma pack(push, 1)
struct {
uint16_t wProcessorArchitecture;
@ -195,19 +310,7 @@ void WardenMemory::patchRuntimeGlobals() {
uint32_t dwAllocationGranularity;
uint16_t wProcessorLevel;
uint16_t wProcessorRevision;
} sysInfo = {
0, // x86
0,
4096, // 4K page size
0x00010000, // min app address
0x7FFEFFFF, // max app address (CRITICAL: must be non-zero)
0x0F, // 4 processors
4, // 4 CPUs
586, // Pentium
65536, // 64K granularity
6, // P6 family
0x3A09 // revision
};
} sysInfo = {0, 0, 4096, 0x00010000, 0x7FFEFFFF, 0x0F, 4, 586, 65536, 6, 0x3A09};
#pragma pack(pop)
static_assert(sizeof(sysInfo) == 36, "SYSTEM_INFO must be 36 bytes");
uint32_t rva = sysInfoAddr - imageBase_;
@ -215,52 +318,182 @@ void WardenMemory::patchRuntimeGlobals() {
std::memcpy(image_.data() + rva, &sysInfo, 36);
}
LOG_INFO("WardenMemory: Patched SYSTEM_INFO chain: [0x", std::hex,
WARDEN_MODULE_PTR, "]→0x", FAKE_WARDEN_BASE,
" [0x", FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, "]→0x", FAKE_SYSINFO_CONTAINER,
" SYSTEM_INFO@0x", sysInfoAddr, std::dec);
// Fallback: if the pointer chain breaks and stage 3 reads from address
// 0x00000000 + 0x08 = 8, write valid SYSINFO at RVA 8 (PE DOS header area).
if (8 + 36 <= imageSize_) {
std::memcpy(image_.data() + 8, &sysInfo, 36);
}
// === EndScene chain (4-level pointer chain) ===
// Stage 1: [0xC0ED38] → fake D3D device
LOG_WARNING("WardenMemory: Patched SYSINFO chain @0x", std::hex, WARDEN_MODULE_PTR, std::dec);
// EndScene chain
// VMaNGOS reads g_theGxDevicePtr → device, then device+0x1FC for API kind
// (0=OpenGL, 1=Direct3D). If Direct3D, follows device+0x38A8 → ptr → ptr+0xA8 → EndScene.
// We set API=1 (Direct3D) and provide the full pointer chain.
constexpr uint32_t GX_DEVICE_PTR = 0xC0ED38;
constexpr uint32_t FAKE_DEVICE = 0xCE8400;
writeLE32(GX_DEVICE_PTR, FAKE_DEVICE);
writeLE32(FAKE_DEVICE + 0x1FC, 1); // API kind = Direct3D
// Set up the full EndScene pointer chain at the canonical offsets.
constexpr uint32_t FAKE_VTABLE1 = 0xCE8500;
constexpr uint32_t FAKE_VTABLE2 = 0xCE8600;
constexpr uint32_t FAKE_ENDSCENE = 0x00401000; // start of .text
writeLE32(FAKE_DEVICE + 0x38A8, FAKE_VTABLE1);
writeLE32(FAKE_VTABLE1, FAKE_VTABLE2);
writeLE32(FAKE_VTABLE2 + 0xA8, FAKE_ENDSCENE);
// Stage 2: [FAKE_DEVICE + 0x38A8] → fake intermediate
constexpr uint32_t OFS_DEVICE2 = 0x38A8;
constexpr uint32_t FAKE_INTERMEDIATE = 0xCE8500;
writeLE32(FAKE_DEVICE + OFS_DEVICE2, FAKE_INTERMEDIATE);
// The EndScene device+sOfsDevice2 offset may differ from 0x38A8 in Turtle WoW.
// Also set API=1 (Direct3D) at multiple offsets so the API kind check passes.
// Fill the entire fake device area with the vtable pointer for robustness.
for (uint32_t off = 0x3800; off <= 0x3A00; off += 4) {
uint32_t addr = FAKE_DEVICE + off;
if (addr >= imageBase_ && (addr - imageBase_) + 4 <= imageSize_) {
writeLE32(addr, FAKE_VTABLE1);
}
}
LOG_WARNING("WardenMemory: Patched EndScene chain @0x", std::hex, GX_DEVICE_PTR, std::dec);
// Stage 3: [FAKE_INTERMEDIATE + 0x0] → fake vtable
constexpr uint32_t OFS_DEVICE3 = 0x0;
constexpr uint32_t FAKE_VTABLE = 0xCE8600;
writeLE32(FAKE_INTERMEDIATE + OFS_DEVICE3, FAKE_VTABLE);
// Stage 4: [FAKE_VTABLE + 0xA8] → address of "EndScene" function
// Point to a real .text address with normal code (not 0xE9/0xCC = not hooked)
constexpr uint32_t OFS_DEVICE4 = 0xA8;
constexpr uint32_t FAKE_ENDSCENE = 0x00401000; // Start of .text section
writeLE32(FAKE_VTABLE + OFS_DEVICE4, FAKE_ENDSCENE);
LOG_INFO("WardenMemory: Patched EndScene chain: [0x", std::hex,
GX_DEVICE_PTR, "]→0x", FAKE_DEVICE,
" ... →EndScene@0x", FAKE_ENDSCENE, std::dec);
// === WorldEnables (single value) ===
// Required flags: TerrainDoodads|Terrain|MapObjects|MapObjectLighting|MapObjectTextures|Water
// Plus typical defaults (no Prohibited bits set)
// WorldEnables
constexpr uint32_t WORLD_ENABLES = 0xC7B2A4;
uint32_t enables = 0x1 | 0x2 | 0x10 | 0x20 | 0x40 | 0x100 | 0x200 | 0x400 | 0x800
| 0x8000 | 0x10000 | 0x100000 | 0x1000000 | 0x2000000
| 0x4000000 | 0x8000000 | 0x10000000;
writeLE32(WORLD_ENABLES, enables);
LOG_INFO("WardenMemory: Patched WorldEnables=0x", std::hex, enables, std::dec);
LOG_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec);
// === LastHardwareAction (tick count) ===
// Must be <= currentTime from timing check. Set to a plausible value.
// LastHardwareAction
constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8;
writeLE32(LAST_HARDWARE_ACTION, 60000); // 1 minute
LOG_INFO("WardenMemory: Patched LastHardwareAction=60000ms");
writeLE32(LAST_HARDWARE_ACTION, 60000);
LOG_WARNING("WardenMemory: Patched LastHardwareAction @0x", std::hex, LAST_HARDWARE_ACTION, std::dec);
}
void WardenMemory::patchTurtleWowBinary() {
// Apply TurtlePatcher byte patches to make our PE image match a real Turtle WoW client.
// These patches are applied at file offsets which equal RVAs for this PE.
// Source: TurtlePatcher/Main.cpp PatchBinary() + PatchVersion()
auto patchBytes = [&](uint32_t fileOffset, const std::vector<uint8_t>& bytes) {
if (fileOffset + bytes.size() > imageSize_) {
LOG_WARNING("WardenMemory: Turtle patch at 0x", std::hex, fileOffset,
" exceeds image size, skipping");
return;
}
std::memcpy(image_.data() + fileOffset, bytes.data(), bytes.size());
};
auto patchString = [&](uint32_t fileOffset, const char* str) {
size_t len = std::strlen(str) + 1; // include null terminator
if (fileOffset + len > imageSize_) return;
std::memcpy(image_.data() + fileOffset, str, len);
};
// --- PatchBinary() patches ---
// Patches 1-4: Unknown purpose code patches in .text
patchBytes(0x2F113A, {0xEB, 0x19});
patchBytes(0x2F1158, {0x03});
patchBytes(0x2F11A7, {0x03});
patchBytes(0x2F11F0, {0xEB, 0xB2});
// PvP rank check removal (6x NOP)
patchBytes(0x2093B0, {0x90, 0x90, 0x90, 0x90, 0x90, 0x90});
// Dwarf mage hackfix removal
patchBytes(0x0706E5, {0xFE});
patchBytes(0x0706EB, {0xFE});
patchBytes(0x07075D, {0xFE});
patchBytes(0x070763, {0xFE});
// Emote sound race ID checks (High Elf support)
patchBytes(0x059289, {0x40});
patchBytes(0x057C81, {0x40});
// Nameplate distance (41 yards)
patchBytes(0x40C448, {0x00, 0x00, 0x24, 0x42});
// Large address aware flag in PE header
patchBytes(0x000126, {0x2F, 0x01});
// Sound channel patches
patchBytes(0x05728C, {0x38, 0x5D, 0x83, 0x00}); // software channels
patchBytes(0x057250, {0x38, 0x5D, 0x83, 0x00}); // hardware channels
patchBytes(0x0572C8, {0x6C, 0x5C, 0x83, 0x00}); // memory cache
// Sound in background (non-FoV build)
patchBytes(0x3A4869, {0x14});
// Hardcore chat patches
patchBytes(0x09B0B8, {0x5F});
patchBytes(0x09B193, {0xE9, 0xA8, 0xAE, 0x86});
patchBytes(0x09F7A5, {0x70, 0x53, 0x56, 0x33, 0xF6, 0xE9, 0x71, 0x68, 0x86, 0x00});
patchBytes(0x09F864, {0x94});
patchBytes(0x09F878, {0x0E});
patchBytes(0x09F887, {0x90});
patchBytes(0x11BAE1, {0x0C, 0x60, 0xD0});
// Hardcore chat code cave at 0x48E000 (85 bytes)
patchBytes(0x48E000, {
0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00, 0x00, 0x43, 0x48, 0x41, 0x54,
0x5F, 0x4D, 0x53, 0x47, 0x5F, 0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00,
0x57, 0x8B, 0xDA, 0x8B, 0xF9, 0xC7, 0x45, 0x94, 0x00, 0x60, 0xD0, 0x00, 0xC7, 0x45, 0x90, 0x5E,
0x00, 0x00, 0x00, 0xE9, 0x77, 0x97, 0x79, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x68, 0x08, 0x46, 0x84, 0x00, 0x83, 0x7D, 0xF0, 0x5E, 0x75, 0x05, 0xB9, 0x1F, 0x02, 0x00, 0x00,
0xE9, 0x43, 0x51, 0x79, 0xFF
});
// Blue child moon patch
patchBytes(0x3E5B83, {
0xC7, 0x05, 0xA4, 0x98, 0xCE, 0x00, 0xD4, 0xE2, 0xE7, 0xFF, 0xC2, 0x04, 0x00
});
// Blue child moon timer
patchBytes(0x2D2095, {0x00, 0x00, 0x80, 0x3F});
// SetUnit codecave jump
patchBytes(0x105E19, {0xE9, 0x02, 0x03, 0x80, 0x00});
// SetUnit main code cave at 0x48E060 (291 bytes)
patchBytes(0x48E060, {
0x55, 0x89, 0xE5, 0x83, 0xEC, 0x10, 0x85, 0xD2, 0x53, 0x56, 0x57, 0x89, 0xCF, 0x0F, 0x84, 0xA2,
0x00, 0x00, 0x00, 0x89, 0xD0, 0x85, 0xC0, 0x0F, 0x8C, 0x98, 0x00, 0x00, 0x00, 0x3B, 0x05, 0x94,
0xDE, 0xC0, 0x00, 0x0F, 0x8F, 0x8C, 0x00, 0x00, 0x00, 0x8B, 0x0D, 0x90, 0xDE, 0xC0, 0x00, 0x8B,
0x04, 0x81, 0x85, 0xC0, 0x89, 0x45, 0xF0, 0x74, 0x7C, 0x8B, 0x40, 0x04, 0x85, 0xC0, 0x7C, 0x75,
0x3B, 0x05, 0x6C, 0xDE, 0xC0, 0x00, 0x7F, 0x6D, 0x8B, 0x15, 0x68, 0xDE, 0xC0, 0x00, 0x8B, 0x1C,
0x82, 0x85, 0xDB, 0x74, 0x60, 0x8B, 0x43, 0x08, 0x6A, 0x00, 0x50, 0x89, 0xF9, 0xE8, 0xFE, 0x6E,
0xA6, 0xFF, 0x89, 0xC1, 0xE8, 0x87, 0x12, 0xA0, 0xFF, 0x89, 0xC6, 0x85, 0xF6, 0x74, 0x46, 0x8B,
0x55, 0xF0, 0x53, 0x89, 0xF1, 0xE8, 0xD6, 0x36, 0x77, 0xFF, 0x8B, 0x17, 0x56, 0x89, 0xF9, 0xFF,
0x92, 0x90, 0x00, 0x00, 0x00, 0x89, 0xF8, 0x99, 0x52, 0x50, 0x68, 0xA0, 0x62, 0x50, 0x00, 0x89,
0xF1, 0xE8, 0xBA, 0xBA, 0xA0, 0xFF, 0x6A, 0x01, 0x6A, 0x01, 0x68, 0x00, 0x00, 0x80, 0x3F, 0x6A,
0x00, 0x6A, 0xFF, 0x6A, 0x00, 0x6A, 0xFF, 0x89, 0xF1, 0xE8, 0x92, 0xC0, 0xA0, 0xFF, 0x89, 0xF1,
0xE8, 0x8B, 0xA2, 0xA0, 0xFF, 0x5F, 0x5E, 0x5B, 0x89, 0xEC, 0x5D, 0xC3, 0x90, 0x90, 0x90, 0x90,
0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xD4, 0xD2, 0x9E, 0xFF, 0x83, 0xF8, 0x03, 0x75,
0x43, 0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xE3, 0xD4, 0x9E, 0xFF, 0xE8, 0x6E, 0x41,
0x70, 0xFF, 0x56, 0x8B, 0xB7, 0xD4, 0x00, 0x00, 0x00, 0x31, 0xD2, 0x39, 0xD6, 0x89, 0x97, 0xE0,
0x03, 0x00, 0x00, 0x89, 0x97, 0xE4, 0x03, 0x00, 0x00, 0x89, 0x97, 0xF0, 0x03, 0x00, 0x00, 0x5E,
0x0F, 0x84, 0xD3, 0xFC, 0x7F, 0xFF, 0x89, 0xC2, 0x89, 0xF9, 0xE8, 0xF1, 0xFE, 0xFF, 0xFF, 0xE9,
0xC5, 0xFC, 0x7F, 0xFF, 0xBA, 0x02, 0x00, 0x00, 0x00, 0xE9, 0xA0, 0xFC, 0x7F, 0xFF, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90
});
// --- PatchVersion() patches ---
// Net version: build 7199 (0x1C1F LE)
patchBytes(0x1B2122, {0x1F, 0x1C});
// Visual version string
patchString(0x437C04, "1.17.2");
// Visual build string
patchString(0x437BFC, "7199");
// Build date string
patchString(0x434798, "May 20 2024");
// Website filters
patchString(0x45CCD8, "*.turtle-wow.org");
patchString(0x45CC9C, "*.discord.gg");
LOG_WARNING("WardenMemory: Applied TurtlePatcher binary patches (build 7199)");
}
bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) const {
@ -300,9 +533,12 @@ bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) cons
return true;
}
uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build) {
uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build, bool isTurtle) {
switch (build) {
case 5875: return 0x00906000; // Classic 1.12.1
case 5875:
// Turtle WoW uses a custom WoW.exe with different code bytes.
// Their warden_scans DB expects bytes from this custom exe.
return isTurtle ? 0x00906000 : 0x009FD000;
default: return 0; // Unknown — accept any
}
}
@ -320,6 +556,7 @@ std::string WardenMemory::findWowExe(uint16_t build) const {
}
}
candidateDirs.push_back("Data/misc");
candidateDirs.push_back("Data/expansions/turtle/overlay/misc");
const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" };
@ -337,7 +574,7 @@ std::string WardenMemory::findWowExe(uint16_t build) const {
}
// If we know the expected imageSize for this build, try to find a matching PE
uint32_t expectedSize = expectedImageSizeForBuild(build);
uint32_t expectedSize = expectedImageSizeForBuild(build, isTurtle_);
if (expectedSize != 0 && allPaths.size() > 1) {
for (const auto& path : allPaths) {
std::ifstream f(path, std::ios::binary);
@ -361,17 +598,29 @@ std::string WardenMemory::findWowExe(uint16_t build) const {
}
}
// Fallback: return first available
return allPaths.empty() ? "" : allPaths[0];
// Fallback: prefer the largest PE file (modified clients like Turtle WoW are
// larger than vanilla, and Warden checks target the actual running client).
std::string bestPath;
uintmax_t bestSize = 0;
for (const auto& path : allPaths) {
std::error_code ec;
auto sz = std::filesystem::file_size(path, ec);
if (!ec && sz > bestSize) {
bestSize = sz;
bestPath = path;
}
}
return bestPath.empty() && !allPaths.empty() ? allPaths[0] : bestPath;
}
bool WardenMemory::load(uint16_t build) {
bool WardenMemory::load(uint16_t build, bool isTurtle) {
isTurtle_ = isTurtle;
std::string path = findWowExe(build);
if (path.empty()) {
LOG_WARNING("WardenMemory: WoW.exe not found in any candidate directory");
return false;
}
LOG_INFO("WardenMemory: Found ", path);
LOG_WARNING("WardenMemory: Loading PE image: ", path, " (build=", build, ")");
return loadFromFile(path);
}
@ -396,11 +645,225 @@ bool WardenMemory::loadFromFile(const std::string& exePath) {
initKuserSharedData();
patchRuntimeGlobals();
if (isTurtle_) {
patchTurtleWowBinary();
}
loaded_ = true;
LOG_INFO("WardenMemory: Loaded PE image (", fileData.size(), " bytes on disk, ",
imageSize_, " bytes virtual)");
// Verify all known warden_scans MEM_CHECK entries against our PE image.
// This checks the exact bytes the server will memcmp against.
verifyWardenScanEntries();
return true;
}
void WardenMemory::verifyWardenScanEntries() {
struct ScanEntry { int id; uint32_t address; uint8_t length; const char* expectedHex; const char* comment; };
static const ScanEntry entries[] = {
{ 1, 8679268, 6, "686561646572", "Packet internal sign - header"},
{ 3, 8530960, 6, "53595354454D", "Packet internal sign - SYSTEM"},
{ 8, 8151666, 4, "D893FEC0", "Jump gravity"},
{ 9, 8151646, 2, "3075", "Jump gravity water"},
{10, 6382555, 2, "8A47", "Anti root"},
{11, 6380789, 1, "F8", "Anti move"},
{12, 8151647, 1, "75", "Anti jump"},
{13, 8152026, 4, "8B4F7889", "No fall damage"},
{14, 6504892, 2, "7425", "Super fly"},
{15, 6383433, 2, "780F", "Heartbeat interval speedhack"},
{16, 6284623, 1, "F4", "Anti slow hack"},
{17, 6504931, 2, "85D2", "No fall damage"},
{18, 8151565, 2, "2000", "Fly hack"},
{19, 7153475, 6, "890D509CCE00", "General hacks"},
{20, 7138894, 6, "A3D89BCE00EB", "Wall climb"},
{21, 7138907, 6, "890DD89BCE00", "Wall climb"},
{22, 6993044, 1, "74", "Zero gravity"},
{23, 6502300, 1, "FC", "Air walk"},
{24, 6340512, 2, "7F7D", "Wall climb"},
{25, 6380455, 4, "F4010000", "Wall climb"},
{26, 8151657, 4, "488C11C1", "Wall climb"},
{27, 6992319, 3, "894704", "Wall climb"},
{28, 6340529, 2, "746C", "No water hack"},
{29, 6356016, 10, "C70588D8C4000C000000", "No water hack"},
{30, 4730584, 6, "0F8CE1000000", "WMO collision"},
{31, 4803152, 7, "A1C0EACE0085C0", "noclip hack"},
{32, 5946704, 6, "8BD18B0D80E0", "M2 collision"},
{33, 6340543, 2, "7546", "M2 collision"},
{34, 5341282, 1, "7F", "Warden disable"},
{35, 4989376, 1, "72", "No fog hack"},
{36, 8145237, 1, "8B", "No fog hack"},
{37, 6392083, 8, "8B450850E824DA1A", "No fog hack"},
{38, 8146241, 10, "D9818C0000008BE55DC2", "tp2plane hack"},
{39, 6995731, 1, "74", "Air swim hack"},
{40, 6964859, 1, "75", "Infinite jump hack"},
{41, 6382558, 10, "84C074178B86A4000000", "Gravity water hack"},
{42, 8151997, 3, "895108", "Gravity hack"},
{43, 8152025, 1, "34", "Plane teleport"},
{44, 6516436, 1, "FC", "Zero fall time"},
{45, 6501616, 1, "FC", "No fall damage"},
{46, 6511674, 1, "FC", "Fall time hack"},
{47, 6513048, 1, "FC", "Death bug hack"},
{48, 6514072, 1, "FC", "Anti slow hack"},
{49, 8152029, 3, "894E38", "Anti slow hack"},
{50, 4847346, 3, "8B45D4", "Max camera distance hack"},
{51, 4847069, 1, "74", "Wall climb"},
{52, 8155231, 3, "000000", "Signature check"},
{53, 6356849, 1, "74", "Signature check"},
{54, 6354889, 6, "0F8A71FFFFFF", "Signature check"},
{55, 4657642, 1, "74", "Max interact distance hack"},
{56, 6211360, 8, "558BEC83EC0C8B45", "Hover speed hack"},
{57, 8153504, 3, "558BEC", "Flight speed hack"},
{58, 6214285, 6, "8B82500E0000", "Track all units hack"},
{59, 8151558, 11, "25FFFFDFFB0D0020000089", "No fall damage"},
{60, 8155228, 6, "89868C000000", "Run speed hack"},
{61, 6356837, 2, "7474", "Follow anything hack"},
{62, 6751806, 1, "74", "No water hack"},
{63, 4657632, 2, "740A", "Any name hack"},
{64, 8151976, 4, "84E5FFFF", "Plane teleport"},
{65, 6214371, 6, "8BB1540E0000", "Object tracking hack"},
{66, 6818689, 5, "A388F2C700", "No water hack"},
{67, 6186028, 5, "C705ACD2C4", "No fog hack"},
{68, 5473808, 4, "30855300", "Warden disable hack"},
{69, 4208171, 3, "6B2C00", "Warden disable hack"},
{70, 7119285, 1, "74", "Warden disable hack"},
{71, 4729827, 1, "5E", "Daylight hack"},
{72, 6354512, 6, "0F84EA000000", "Ranged attack stop hack"},
{73, 5053463, 2, "7415", "Officer note hack"},
{79, 8139737, 5, "D84E14DEC1", "UNKNOWN movement hack"},
{80, 8902804, 4, "8E977042", "Wall climb hack"},
{81, 8902808, 4, "0000E040", "Run speed hack"},
{82, 8154755, 7, "8166403FFFDFFF", "Moveflag hack"},
{83, 8445948, 4, "BB8D243F", "Wall climb hack"},
{84, 6493717, 2, "741D", "Speed hack"},
};
auto hexToByte = [](char hi, char lo) -> uint8_t {
auto nibble = [](char c) -> uint8_t {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
return 0;
};
return (nibble(hi) << 4) | nibble(lo);
};
int mismatches = 0;
for (const auto& e : entries) {
std::string hexStr(e.expectedHex);
std::vector<uint8_t> expected;
for (size_t i = 0; i + 1 < hexStr.size(); i += 2)
expected.push_back(hexToByte(hexStr[i], hexStr[i+1]));
std::vector<uint8_t> actual(e.length, 0);
bool ok = readMemory(e.address, e.length, actual.data());
if (!ok || actual != expected) {
mismatches++;
std::string expHex, actHex;
for (auto b : expected) { char s[4]; snprintf(s, 4, "%02X", b); expHex += s; }
for (auto b : actual) { char s[4]; snprintf(s, 4, "%02X", b); actHex += s; }
LOG_WARNING("WardenScan MISMATCH id=", e.id,
" addr=0x", [&]{char s[12];snprintf(s,12,"%08X",e.address);return std::string(s);}(),
" (", e.comment, ")",
" expected=[", expHex, "] actual=[", actHex, "]",
ok ? "" : " (readMemory FAILED)");
}
}
if (mismatches == 0) {
LOG_WARNING("WardenScan: All ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MATCH PE image ✓");
} else {
LOG_WARNING("WardenScan: ", mismatches, " / ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MISMATCH! These will trigger cheat flags.");
}
}
bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20],
uint8_t patternLen, bool imageOnly) const {
if (!loaded_ || patternLen == 0 || patternLen > 255) return false;
// Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1)
std::string cacheKey(26, '\0');
std::memcpy(&cacheKey[0], seed, 4);
std::memcpy(&cacheKey[4], expectedHash, 20);
cacheKey[24] = patternLen;
cacheKey[25] = imageOnly ? 1 : 0;
auto cacheIt = codePatternCache_.find(cacheKey);
if (cacheIt != codePatternCache_.end()) {
LOG_WARNING("WardenMemory: Code pattern cache HIT → ",
cacheIt->second ? "found" : "not found");
return cacheIt->second;
}
// Determine search range. For imageOnly (FIND_MEM_IMAGE_CODE_BY_HASH),
// search only the .text section (RVA 0x1000, typically the first code section).
// For FIND_CODE_BY_HASH, search the entire PE image.
size_t searchStart = 0;
size_t searchEnd = imageSize_;
// Collect search ranges: for imageOnly, all executable sections; otherwise entire image
struct Range { size_t start; size_t end; };
std::vector<Range> ranges;
if (imageOnly && image_.size() >= 64) {
uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8)
| (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24);
if (peOffset + 4 + 20 <= image_.size()) {
uint16_t numSections = image_[peOffset+4+2] | (uint16_t(image_[peOffset+4+3]) << 8);
uint16_t optHeaderSize = image_[peOffset+4+16] | (uint16_t(image_[peOffset+4+17]) << 8);
size_t secTable = peOffset + 4 + 20 + optHeaderSize;
for (uint16_t i = 0; i < numSections; i++) {
size_t secOfs = secTable + i * 40;
if (secOfs + 40 > image_.size()) break;
uint32_t characteristics = image_[secOfs+36] | (uint32_t(image_[secOfs+37]) << 8)
| (uint32_t(image_[secOfs+38]) << 16) | (uint32_t(image_[secOfs+39]) << 24);
// Include sections with MEM_EXECUTE or CNT_CODE
if (characteristics & (0x20000000 | 0x20)) {
uint32_t va = image_[secOfs+12] | (uint32_t(image_[secOfs+13]) << 8)
| (uint32_t(image_[secOfs+14]) << 16) | (uint32_t(image_[secOfs+15]) << 24);
uint32_t vsize = image_[secOfs+8] | (uint32_t(image_[secOfs+9]) << 8)
| (uint32_t(image_[secOfs+10]) << 16) | (uint32_t(image_[secOfs+11]) << 24);
size_t rEnd = std::min(static_cast<size_t>(va + vsize), static_cast<size_t>(imageSize_));
if (va + patternLen <= rEnd)
ranges.push_back({va, rEnd});
}
}
}
}
if (ranges.empty()) {
// Search entire image
if (searchStart + patternLen <= searchEnd)
ranges.push_back({searchStart, searchEnd});
}
size_t totalPositions = 0;
for (const auto& r : ranges) {
size_t positions = r.end - r.start - patternLen + 1;
for (size_t i = 0; i < positions; i++) {
uint8_t hmacOut[20];
unsigned int hmacLen = 0;
HMAC(EVP_sha1(), seed, 4,
image_.data() + r.start + i, patternLen,
hmacOut, &hmacLen);
if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) {
LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex,
r.start + i, std::dec, " (searched ", totalPositions + i + 1, " positions)");
codePatternCache_[cacheKey] = true;
return true;
}
}
totalPositions += positions;
}
LOG_WARNING("WardenMemory: Code pattern NOT found after ", totalPositions, " positions in ",
ranges.size(), " section(s)");
codePatternCache_[cacheKey] = false;
return false;
}
} // namespace game
} // namespace wowee