Kelsidavis-WoWee/src/game/warden_memory.cpp
2026-03-22 21:39:40 +03: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) 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