#include "game/warden_memory.hpp" #include "core/logger.hpp" #include #include #include #include #include #include #include #include #include #include namespace wowee { namespace game { static inline uint32_t readLE32(const std::vector& 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& 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& 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(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(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(i) * 2, static_cast(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( std::chrono::duration_cast( 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& 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(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(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 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 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(&peOfs), 4); if (peOfs + 4 + 20 + 60 > static_cast(fileSize)) continue; f.seekg(peOfs + 4 + 20 + 56); // OptionalHeader + 56 = SizeOfImage uint32_t imgSize = 0; f.read(reinterpret_cast(&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 fileData(static_cast(fileSize)); f.read(reinterpret_cast(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 expected; for (size_t i = 0; i + 1 < hexStr.size(); i += 2) expected.push_back(hexToByte(hexStr[i], hexStr[i+1])); std::vector 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(hintOffset) + 4096 + patternLen, static_cast(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(i) - static_cast(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 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(va + vsize), static_cast(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( 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( 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