Kelsidavis-WoWee/src/game/warden_memory.cpp
Kelsi 8378eb9232 fix: correct sync Warden MODULE check returning 0x01 instead of 0x00
The sync path's MODULE handler was returning 0x01 (module found) for
unwanted cheat DLLs (WPESPY, TAMIA, PRXDRVPE, etc.) instead of 0x00
(not found). Since VMaNGOS compares the result as a boolean, returning
any non-zero value for a cheat module tells the server "this cheat DLL
is loaded," triggering Warden penalties that accumulate into a kick
after ~3-5 minutes.

Also adds ±4KB hint window search to searchCodePattern for faster
PAGE_A resolution without full brute-force, and restores the turtle
PAGE_A fallback (confirmed patterns are runtime-patched offsets not
present in the on-disk PE).
2026-03-17 07:19:37 -07:00

985 lines
40 KiB
C++

#include "game/warden_memory.hpp"
#include "core/logger.hpp"
#include <chrono>
#include <fstream>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#include <filesystem>
#include <sstream>
#include <iomanip>
#include <openssl/hmac.h>
#include <openssl/evp.h>
namespace wowee {
namespace game {
static inline uint32_t readLE32(const std::vector<uint8_t>& data, size_t offset) {
return data[offset] | (uint32_t(data[offset+1]) << 8)
| (uint32_t(data[offset+2]) << 16) | (uint32_t(data[offset+3]) << 24);
}
static inline uint16_t readLE16(const std::vector<uint8_t>& data, size_t offset) {
return data[offset] | (uint16_t(data[offset+1]) << 8);
}
WardenMemory::WardenMemory() = default;
WardenMemory::~WardenMemory() = default;
bool WardenMemory::parsePE(const std::vector<uint8_t>& fileData) {
// DOS header: MZ magic
if (fileData.size() < 64) return false;
if (fileData[0] != 'M' || fileData[1] != 'Z') {
LOG_ERROR("WardenMemory: Not a valid PE file (no MZ header)");
return false;
}
// e_lfanew at offset 0x3C -> PE signature offset
uint32_t peOffset = readLE32(fileData, 0x3C);
if (peOffset + 4 > fileData.size()) return false;
// PE signature "PE\0\0"
if (fileData[peOffset] != 'P' || fileData[peOffset+1] != 'E'
|| fileData[peOffset+2] != 0 || fileData[peOffset+3] != 0) {
LOG_ERROR("WardenMemory: Invalid PE signature");
return false;
}
// COFF header at peOffset + 4
size_t coffOfs = peOffset + 4;
if (coffOfs + 20 > fileData.size()) return false;
uint16_t numSections = readLE16(fileData, coffOfs + 2);
uint16_t optHeaderSize = readLE16(fileData, coffOfs + 16);
// Optional header
size_t optOfs = coffOfs + 20;
if (optOfs + optHeaderSize > fileData.size()) return false;
uint16_t magic = readLE16(fileData, optOfs);
if (magic != 0x10B) {
LOG_ERROR("WardenMemory: Not PE32 (magic=0x", std::hex, magic, std::dec, ")");
return false;
}
// PE32 fields
imageBase_ = readLE32(fileData, optOfs + 28);
imageSize_ = readLE32(fileData, optOfs + 56);
uint32_t sizeOfHeaders = readLE32(fileData, optOfs + 60);
LOG_INFO("WardenMemory: PE ImageBase=0x", std::hex, imageBase_,
" ImageSize=0x", imageSize_,
" Sections=", std::dec, numSections);
// Allocate flat image (zero-filled)
image_.resize(imageSize_, 0);
// Copy headers
uint32_t headerCopy = std::min({sizeOfHeaders, imageSize_, static_cast<uint32_t>(fileData.size())});
std::memcpy(image_.data(), fileData.data(), headerCopy);
// Section table follows optional header
size_t secTableOfs = optOfs + optHeaderSize;
for (uint16_t i = 0; i < numSections; i++) {
size_t secOfs = secTableOfs + i * 40;
if (secOfs + 40 > fileData.size()) break;
char secName[9] = {};
std::memcpy(secName, fileData.data() + secOfs, 8);
uint32_t virtualSize = readLE32(fileData, secOfs + 8);
uint32_t virtualAddr = readLE32(fileData, secOfs + 12);
uint32_t rawDataSize = readLE32(fileData, secOfs + 16);
uint32_t rawDataOffset = readLE32(fileData, secOfs + 20);
if (rawDataSize == 0 || rawDataOffset == 0) continue;
// Clamp copy size to file and image bounds
uint32_t copySize = std::min(rawDataSize, virtualSize);
if (rawDataOffset + copySize > fileData.size())
copySize = static_cast<uint32_t>(fileData.size()) - rawDataOffset;
if (virtualAddr + copySize > imageSize_)
copySize = imageSize_ - virtualAddr;
std::memcpy(image_.data() + virtualAddr, fileData.data() + rawDataOffset, copySize);
LOG_INFO("WardenMemory: Section '", secName,
"' VA=0x", std::hex, imageBase_ + virtualAddr,
" 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);
// -------------------------------------------------------------------
// KUSER_SHARED_DATA layout — Windows 7 SP1 x86 (from ntddk.h PDB)
// Warden reads this in 238-byte chunks for OS fingerprinting.
// All offsets verified against the canonical _KUSER_SHARED_DATA struct.
// -------------------------------------------------------------------
auto w32 = [&](uint32_t off, uint32_t v) { std::memcpy(kuserData_ + off, &v, 4); };
auto w16 = [&](uint32_t off, uint16_t v) { std::memcpy(kuserData_ + off, &v, 2); };
auto w8 = [&](uint32_t off, uint8_t v) { kuserData_[off] = v; };
// +0x000 TickCountLowDeprecated (ULONG)
w32(0x0000, 0x003F4A00); // ~70 min uptime
// +0x004 TickCountMultiplier (ULONG)
w32(0x0004, 0x0FA00000);
// +0x008 InterruptTime (KSYSTEM_TIME: Low4 + High1_4 + High2_4)
w32(0x0008, 0x6B49D200);
w32(0x000C, 0x00000029);
w32(0x0010, 0x00000029);
// +0x014 SystemTime (KSYSTEM_TIME) — ~2024 epoch FILETIME
w32(0x0014, 0xA0B71B00);
w32(0x0018, 0x01DA5E80);
w32(0x001C, 0x01DA5E80);
// +0x020 TimeZoneBias (KSYSTEM_TIME) — 0 = UTC
// (leave zeros)
// +0x02C ImageNumberLow / ImageNumberHigh (USHORT each)
w16(0x002C, 0x014C); // IMAGE_FILE_MACHINE_I386
w16(0x002E, 0x014C);
// +0x030 NtSystemRoot (WCHAR[260] = 520 bytes, ends at +0x238)
const wchar_t* sysRoot = L"C:\\WINDOWS";
for (size_t i = 0; i < 10; i++) {
w16(0x0030 + static_cast<uint32_t>(i) * 2, static_cast<uint16_t>(sysRoot[i]));
}
// +0x238 MaxStackTraceDepth (ULONG)
w32(0x0238, 0);
// +0x23C CryptoExponent (ULONG) — 65537
w32(0x023C, 0x00010001);
// +0x240 TimeZoneId (ULONG) — TIME_ZONE_ID_UNKNOWN
w32(0x0240, 0);
// +0x244 LargePageMinimum (ULONG) — 2 MB
w32(0x0244, 0x00200000);
// +0x248 Reserved2[7] (28 bytes) — zeros
// (leave zeros)
// +0x264 NtProductType (NT_PRODUCT_TYPE = ULONG) — VER_NT_WORKSTATION
w32(0x0264, 1);
// +0x268 ProductTypeIsValid (BOOLEAN = UCHAR)
w8(0x0268, 1);
// +0x269 Reserved9[3] — padding
// (leave zeros)
// +0x26C NtMajorVersion (ULONG) — 6 (Windows Vista/7/8/10)
w32(0x026C, 6);
// +0x270 NtMinorVersion (ULONG) — 1 (Windows 7)
w32(0x0270, 1);
// +0x274 ProcessorFeatures (BOOLEAN[64] = 64 bytes, ends at +0x2B4)
// Each entry is a single UCHAR (0 or 1).
// Index Name Value
// [0] PF_FLOATING_POINT_PRECISION_ERRATA 0
// [1] PF_FLOATING_POINT_EMULATED 0
// [2] PF_COMPARE_EXCHANGE_DOUBLE 1
// [3] PF_MMX_INSTRUCTIONS_AVAILABLE 1
// [4] PF_PPC_MOVEMEM_64BIT_OK 0
// [5] PF_ALPHA_BYTE_INSTRUCTIONS 0
// [6] PF_XMMI_INSTRUCTIONS_AVAILABLE (SSE) 1
// [7] PF_3DNOW_INSTRUCTIONS_AVAILABLE 0
// [8] PF_RDTSC_INSTRUCTION_AVAILABLE 1
// [9] PF_PAE_ENABLED 1
// [10] PF_XMMI64_INSTRUCTIONS_AVAILABLE(SSE2)1
// [11] PF_SSE_DAZ_MODE_AVAILABLE 0
// [12] PF_NX_ENABLED 1
// [13] PF_SSE3_INSTRUCTIONS_AVAILABLE 1
// [14] PF_COMPARE_EXCHANGE128 0 (x86 typically 0)
// [15] PF_COMPARE64_EXCHANGE128 0
// [16] PF_CHANNELS_ENABLED 0
// [17] PF_XSAVE_ENABLED 0
w8(0x0274 + 2, 1); // PF_COMPARE_EXCHANGE_DOUBLE
w8(0x0274 + 3, 1); // PF_MMX
w8(0x0274 + 6, 1); // PF_SSE
w8(0x0274 + 8, 1); // PF_RDTSC
w8(0x0274 + 9, 1); // PF_PAE_ENABLED
w8(0x0274 + 10, 1); // PF_SSE2
w8(0x0274 + 12, 1); // PF_NX_ENABLED
w8(0x0274 + 13, 1); // PF_SSE3
// +0x2B4 Reserved1 (ULONG)
// +0x2B8 Reserved3 (ULONG)
// +0x2BC TimeSlip (ULONG)
// +0x2C0 AlternativeArchitecture (ULONG) = 0 (StandardDesign)
// +0x2C4 AltArchitecturePad[1] (ULONG)
// +0x2C8 SystemExpirationDate (LARGE_INTEGER = 8 bytes)
// (leave zeros)
// +0x2D0 SuiteMask (ULONG) — VER_SUITE_SINGLEUSERTS | VER_SUITE_TERMINAL
w32(0x02D0, 0x0110); // 0x0100=SINGLEUSERTS, 0x0010=TERMINAL
// +0x2D4 KdDebuggerEnabled (BOOLEAN = UCHAR)
w8(0x02D4, 0);
// +0x2D5 NXSupportPolicy (UCHAR) — 2 = OptIn
w8(0x02D5, 2);
// +0x2D6 Reserved6[2]
// (leave zeros)
// +0x2D8 ActiveConsoleId (ULONG) — session 0 or 1
w32(0x02D8, 1);
// +0x2DC DismountCount (ULONG)
w32(0x02DC, 0);
// +0x2E0 ComPlusPackage (ULONG)
w32(0x02E0, 0);
// +0x2E4 LastSystemRITEventTickCount (ULONG) — recent input tick
w32(0x02E4, 0x003F4900);
// +0x2E8 NumberOfPhysicalPages (ULONG) — 4GB / 4KB ≈ 1M pages
w32(0x02E8, 0x000FF000);
// +0x2EC SafeBootMode (BOOLEAN) — 0 = normal boot
w8(0x02EC, 0);
// +0x2F0 SharedDataFlags / TraceLogging (ULONG)
w32(0x02F0, 0);
// +0x2F8 TestRetInstruction (ULONGLONG = 8 bytes) — RET opcode
w8(0x02F8, 0xC3); // x86 RET instruction
// +0x300 SystemCall (ULONG)
w32(0x0300, 0);
// +0x304 SystemCallReturn (ULONG)
w32(0x0304, 0);
// +0x308 SystemCallPad[3] (24 bytes)
// (leave zeros)
// +0x320 TickCount (KSYSTEM_TIME) — matches TickCountLowDeprecated
w32(0x0320, 0x003F4A00);
// +0x32C TickCountPad[1]
// (leave zeros)
// +0x330 Cookie (ULONG) — stack cookie, random-looking value
w32(0x0330, 0x4A2F8C15);
// +0x334 ConsoleSessionForegroundProcessId (ULONG) — some PID
w32(0x0334, 0x00001234);
// Everything after +0x338 is typically zero on Win7 x86
}
void WardenMemory::writeLE32(uint32_t va, uint32_t value) {
if (va < imageBase_) return;
uint32_t rva = va - imageBase_;
if (rva + 4 > imageSize_) return;
image_[rva] = value & 0xFF;
image_[rva+1] = (value >> 8) & 0xFF;
image_[rva+2] = (value >> 16) & 0xFF;
image_[rva+3] = (value >> 24) & 0xFF;
}
void WardenMemory::patchRuntimeGlobals() {
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
// VMaNGOS has TWO types of Warden scans that read these addresses:
//
// 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.
// === 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);
constexpr uint32_t FAKE_SYSINFO_CONTAINER = 0xCE8300;
writeLE32(FAKE_WARDEN_BASE + 0x228, FAKE_SYSINFO_CONTAINER);
// 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;
uint16_t wReserved;
uint32_t dwPageSize;
uint32_t lpMinimumApplicationAddress;
uint32_t lpMaximumApplicationAddress;
uint32_t dwActiveProcessorMask;
uint32_t dwNumberOfProcessors;
uint32_t dwProcessorType;
uint32_t dwAllocationGranularity;
uint16_t wProcessorLevel;
uint16_t wProcessorRevision;
} 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_;
if (rva + 36 <= imageSize_) {
std::memcpy(image_.data() + rva, &sysInfo, 36);
}
// 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);
}
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);
// 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);
// 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_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec);
// LastHardwareAction — must be a recent GetTickCount()-style timestamp
// so the anti-AFK scan sees (currentTime - lastAction) < threshold.
constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8;
uint32_t nowMs = static_cast<uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
writeLE32(LAST_HARDWARE_ACTION, nowMs - 2000);
LOG_WARNING("WardenMemory: Patched LastHardwareAction @0x", std::hex, LAST_HARDWARE_ACTION, std::dec);
// Embed the 37-byte Warden module memcpy pattern in BSS so that
// FIND_CODE_BY_HASH (PAGE_B) brute-force search can find it.
// This is the pattern VMaNGOS's "Warden Memory Read check" looks for.
constexpr uint32_t MEMCPY_PATTERN_VA = 0xCE8700;
static const uint8_t kWardenMemcpyPattern[37] = {
0x56, 0x57, 0xFC, 0x8B, 0x54, 0x24, 0x14, 0x8B,
0x74, 0x24, 0x10, 0x8B, 0x44, 0x24, 0x0C, 0x8B,
0xCA, 0x8B, 0xF8, 0xC1, 0xE9, 0x02, 0x74, 0x02,
0xF3, 0xA5, 0xB1, 0x03, 0x23, 0xCA, 0x74, 0x02,
0xF3, 0xA4, 0x5F, 0x5E, 0xC3
};
uint32_t patRva = MEMCPY_PATTERN_VA - imageBase_;
if (patRva + sizeof(kWardenMemcpyPattern) <= imageSize_) {
std::memcpy(image_.data() + patRva, kWardenMemcpyPattern, sizeof(kWardenMemcpyPattern));
LOG_WARNING("WardenMemory: Embedded Warden memcpy pattern at 0x", std::hex, MEMCPY_PATTERN_VA, 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 {
if (length == 0) return true;
// KUSER_SHARED_DATA range
if (va >= KUSER_BASE && static_cast<uint64_t>(va) + length <= KUSER_BASE + KUSER_SIZE) {
std::memcpy(outBuf, kuserData_ + (va - KUSER_BASE), length);
return true;
}
if (!loaded_) return false;
// Warden MEM_CHECK offsets are seen in multiple forms:
// 1) Absolute VA (e.g. 0x00401337)
// 2) RVA (e.g. 0x000139A9)
// 3) Tiny module-relative offsets (e.g. 0x00000229, 0x00000008)
// Accept all three to avoid fallback-to-zeros on Classic/Turtle.
uint32_t offset = 0;
if (va >= imageBase_) {
// Absolute VA.
offset = va - imageBase_;
} else if (va < imageSize_) {
// RVA into WoW.exe image.
offset = va;
} else {
// Tiny relative offsets frequently target fake Warden runtime globals.
constexpr uint32_t kFakeWardenBase = 0xCE8000;
const uint32_t remappedVa = kFakeWardenBase + va;
if (remappedVa < imageBase_) return false;
offset = remappedVa - imageBase_;
}
if (static_cast<uint64_t>(offset) + length > imageSize_) return false;
std::memcpy(outBuf, image_.data() + offset, length);
return true;
}
uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build, bool isTurtle) {
switch (build) {
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
}
}
std::string WardenMemory::findWowExe(uint16_t build) const {
std::vector<std::string> candidateDirs;
if (const char* env = std::getenv("WOWEE_INTEGRITY_DIR")) {
if (env && *env) candidateDirs.push_back(env);
}
if (const char* home = std::getenv("HOME")) {
if (home && *home) {
candidateDirs.push_back(std::string(home) + "/Downloads");
candidateDirs.push_back(std::string(home) + "/Downloads/twmoa_1180");
candidateDirs.push_back(std::string(home) + "/twmoa_1180");
}
}
candidateDirs.push_back("Data/misc");
candidateDirs.push_back("Data/expansions/turtle/overlay/misc");
const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" };
// Collect all candidate paths
std::vector<std::string> allPaths;
for (const auto& dir : candidateDirs) {
for (const char* exe : candidateExes) {
std::string path = dir;
if (!path.empty() && path.back() != '/') path += '/';
path += exe;
if (std::filesystem::exists(path)) {
allPaths.push_back(path);
}
}
}
// If we know the expected imageSize for this build, try to find a matching PE
uint32_t expectedSize = expectedImageSizeForBuild(build, isTurtle_);
if (expectedSize != 0 && allPaths.size() > 1) {
for (const auto& path : allPaths) {
std::ifstream f(path, std::ios::binary);
if (!f.is_open()) continue;
// Read PE headers to get imageSize
f.seekg(0, std::ios::end);
auto fileSize = f.tellg();
if (fileSize < 256) continue;
f.seekg(0x3C);
uint32_t peOfs = 0;
f.read(reinterpret_cast<char*>(&peOfs), 4);
if (peOfs + 4 + 20 + 60 > static_cast<uint32_t>(fileSize)) continue;
f.seekg(peOfs + 4 + 20 + 56); // OptionalHeader + 56 = SizeOfImage
uint32_t imgSize = 0;
f.read(reinterpret_cast<char*>(&imgSize), 4);
if (imgSize == expectedSize) {
LOG_INFO("WardenMemory: Matched build ", build, " to ", path,
" (imageSize=0x", std::hex, imgSize, std::dec, ")");
return path;
}
}
}
// 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 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_WARNING("WardenMemory: Loading PE image: ", path, " (build=", build, ")");
return loadFromFile(path);
}
bool WardenMemory::loadFromFile(const std::string& exePath) {
std::ifstream f(exePath, std::ios::binary);
if (!f.is_open()) {
LOG_ERROR("WardenMemory: Cannot open ", exePath);
return false;
}
f.seekg(0, std::ios::end);
auto fileSize = f.tellg();
f.seekg(0, std::ios::beg);
std::vector<uint8_t> fileData(static_cast<size_t>(fileSize));
f.read(reinterpret_cast<char*>(fileData.data()), fileSize);
if (!parsePE(fileData)) {
LOG_ERROR("WardenMemory: Failed to parse PE from ", exePath);
return false;
}
initKuserSharedData();
patchRuntimeGlobals();
if (isTurtle_ && imageSize_ != 0x00906000) {
// Only apply TurtlePatcher patches if we loaded the vanilla exe.
// The real Turtle WoW.exe (imageSize=0x906000) already has these bytes.
patchTurtleWowBinary();
LOG_WARNING("WardenMemory: Applied Turtle patches to vanilla PE (imageSize=0x", std::hex, imageSize_, std::dec, ")");
} else if (isTurtle_) {
LOG_WARNING("WardenMemory: Loaded native Turtle PE — skipping patches");
}
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;
int patched = 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++;
// In Turtle mode, write the expected bytes into the PE image so
// MEM_CHECK responses return what the server expects.
if (isTurtle_ && e.address >= imageBase_) {
uint32_t offset = e.address - imageBase_;
if (offset + expected.size() <= imageSize_) {
std::memcpy(image_.data() + offset, expected.data(), expected.size());
patched++;
}
}
}
}
if (mismatches == 0) {
LOG_WARNING("WardenScan: All ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MATCH PE image");
} else if (patched > 0) {
LOG_WARNING("WardenScan: Patched ", patched, "/", mismatches,
" mismatched scan entries into PE image");
} else {
LOG_WARNING("WardenScan: ", mismatches, " / ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MISMATCH");
}
}
bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20],
uint8_t patternLen, bool imageOnly,
uint32_t hintOffset, bool hintOnly) 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()) {
return cacheIt->second;
}
// --- Fast path: check the hint offset directly (single HMAC) ---
// The PAGE_A offset field is the RVA where the server expects the pattern.
if (hintOffset > 0 && hintOffset + patternLen <= imageSize_) {
uint8_t hmacOut[20];
unsigned int hmacLen = 0;
HMAC(EVP_sha1(), seed, 4,
image_.data() + hintOffset, patternLen,
hmacOut, &hmacLen);
if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) {
LOG_WARNING("WardenMemory: Code pattern found at hint RVA 0x", std::hex,
hintOffset, std::dec, " (direct hit)");
codePatternCache_[cacheKey] = true;
return true;
}
}
// --- Wider hint window: search ±4096 bytes around hint offset ---
if (hintOffset > 0) {
size_t winStart = (hintOffset > 4096) ? hintOffset - 4096 : 0;
size_t winEnd = std::min(static_cast<size_t>(hintOffset) + 4096 + patternLen,
static_cast<size_t>(imageSize_));
if (winEnd > winStart + patternLen) {
for (size_t i = winStart; i + patternLen <= winEnd; i++) {
if (i == hintOffset) continue; // already checked
uint8_t hmacOut[20];
unsigned int hmacLen = 0;
HMAC(EVP_sha1(), seed, 4,
image_.data() + i, patternLen,
hmacOut, &hmacLen);
if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) {
LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, i,
std::dec, " (hint window, delta=", static_cast<int>(i) - static_cast<int>(hintOffset), ")");
codePatternCache_[cacheKey] = true;
return true;
}
}
}
}
// If hint-only mode, skip the expensive brute-force search.
if (hintOnly) return false;
// --- Brute-force fallback: search all PE sections ---
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 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()) {
if (patternLen <= imageSize_)
ranges.push_back({0, imageSize_});
}
auto bruteStart = std::chrono::steady_clock::now();
LOG_WARNING("WardenMemory: Brute-force searching ", ranges.size(), " section(s), hint=0x",
std::hex, hintOffset, std::dec, " patLen=", (int)patternLen);
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) {
auto elapsed = std::chrono::duration<float>(
std::chrono::steady_clock::now() - bruteStart).count();
LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex,
r.start + i, std::dec, " (searched ", totalPositions + i + 1,
" positions in ", elapsed, "s)");
codePatternCache_[cacheKey] = true;
return true;
}
}
totalPositions += positions;
}
auto elapsed = std::chrono::duration<float>(
std::chrono::steady_clock::now() - bruteStart).count();
LOG_WARNING("WardenMemory: Code pattern NOT found after ", totalPositions, " positions in ",
ranges.size(), " section(s), took ", elapsed, "s");
codePatternCache_[cacheKey] = false;
return false;
}
} // namespace game
} // namespace wowee