From 8378eb9232459cfd1d3bdf5d7826464a1b9de714 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Tue, 17 Mar 2026 07:19:37 -0700 Subject: [PATCH] fix: correct sync Warden MODULE check returning 0x01 instead of 0x00 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- include/game/warden_memory.hpp | 4 +- src/game/game_handler.cpp | 200 ++++++++++++++++++++------------- src/game/warden_memory.cpp | 94 +++++++++++++--- 3 files changed, 207 insertions(+), 91 deletions(-) diff --git a/include/game/warden_memory.hpp b/include/game/warden_memory.hpp index d01a27c3..39a2abf2 100644 --- a/include/game/warden_memory.hpp +++ b/include/game/warden_memory.hpp @@ -41,10 +41,12 @@ public: * @param expectedHash 20-byte expected HMAC-SHA1 digest * @param patternLen Length of the pattern to search for * @param imageOnly If true, search only executable sections (.text) + * @param hintOffset RVA hint from PAGE_A request — check this position first * @return true if a matching pattern was found in the PE image */ bool searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], - uint8_t patternLen, bool imageOnly) const; + uint8_t patternLen, bool imageOnly, + uint32_t hintOffset = 0, bool hintOnly = false) const; /** Write a little-endian uint32 at the given virtual address in the PE image. */ void writeLE32(uint32_t va, uint32_t value); diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 222bfabe..9790d73e 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -9135,7 +9135,7 @@ bool GameHandler::loadWardenCRFile(const std::string& moduleHashHex) { for (int i = 0; i < 9; i++) { char s[16]; snprintf(s, sizeof(s), "%s=0x%02X ", names[i], wardenCheckOpcodes_[i]); opcHex += s; } - LOG_DEBUG("Warden: Check opcodes: ", opcHex); + LOG_WARNING("Warden: Check opcodes: ", opcHex); } size_t entryCount = (static_cast(fileSize) - CR_HEADER_SIZE) / CR_ENTRY_SIZE; @@ -9542,6 +9542,10 @@ void GameHandler::handleWardenData(network::Packet& packet) { switch (ct) { case CT_TIMING: { + // Result byte: 0x01 = timing check ran successfully, + // 0x00 = timing check failed (Wine/VM — server skips anti-AFK). + // We return 0x01 so the server validates normally; our + // LastHardwareAction (now-2000) ensures a clean 2s delta. resultData.push_back(0x01); uint32_t ticks = static_cast( std::chrono::duration_cast( @@ -9561,7 +9565,8 @@ void GameHandler::handleWardenData(network::Packet& packet) { pos += 4; uint8_t readLen = decrypted[pos++]; LOG_WARNING("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), - " len=", (int)readLen); + " len=", (int)readLen, + (strIdx ? " module=\"" + moduleName + "\"" : "")); if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { uint32_t now = static_cast( std::chrono::duration_cast( @@ -9588,21 +9593,12 @@ void GameHandler::handleWardenData(network::Packet& packet) { LOG_WARNING("Warden: Applying 4-byte ULONG alignment padding for WinVersionGet"); resultData.push_back(0x00); resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); - } else if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { - // Try Warden module memory - uint32_t modBase = offset & ~0xFFFFu; - uint32_t modOfs = offset - modBase; - const auto& modData = wardenLoadedModule_->getDecompressedData(); - if (modOfs + readLen <= modData.size()) { - std::memcpy(memBuf.data(), modData.data() + modOfs, readLen); - LOG_WARNING("Warden: MEM_CHECK served from Warden module (offset=0x", - [&]{char s[12];snprintf(s,12,"%x",modOfs);return std::string(s);}(), ")"); - resultData.push_back(0x00); - resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); - } else { - resultData.push_back(0xE9); - } } else { + // Address not in PE/KUSER — return 0xE9 (not readable). + // Real 32-bit WoW can't read kernel space (>=0x80000000) + // or arbitrary unallocated user-space addresses. + LOG_WARNING("Warden: MEM_CHECK -> 0xE9 (unmapped 0x", + [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), ")"); resultData.push_back(0xE9); } break; @@ -9620,22 +9616,15 @@ void GameHandler::handleWardenData(network::Packet& packet) { uint8_t patLen = p[28]; bool found = false; bool turtleFallback = false; - // Turtle fallback: if offset is within PE image range, - // this is an integrity check — skip the expensive 25-second - // brute-force search and return "found" immediately to stay - // within the server's Warden response timeout. - bool canTurtleFallback = (ct == CT_PAGE_A && isActiveExpansion("turtle") && - wardenMemory_ && wardenMemory_->isLoaded() && off < 0x600000); if (isKnownWantedCodeScan(seed, sha1, off, patLen)) { found = true; - } else if (canTurtleFallback) { - // Skip the expensive 25-second brute-force search; - // the turtle fallback will return "found" instantly. - found = true; - turtleFallback = true; } else if (wardenMemory_ && wardenMemory_->isLoaded() && patLen > 0) { - found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly); - if (!found && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { + // Hint + nearby window search (instant). + // Skip full brute-force for Turtle PAGE_A to avoid + // 25s delay that triggers response timeout. + bool hintOnly = (ct == CT_PAGE_A && isActiveExpansion("turtle")); + found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly, off, hintOnly); + if (!found && !hintOnly && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { const uint8_t* modMem = static_cast(wardenLoadedModule_->getModuleMemory()); size_t modSize = wardenLoadedModule_->getModuleSize(); if (modMem && modSize >= patLen) { @@ -9647,6 +9636,13 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } } + // Turtle PAGE_A fallback: patterns at runtime-patched + // offsets don't exist in the on-disk PE. The server + // expects "found" for these code integrity checks. + if (!found && ct == CT_PAGE_A && isActiveExpansion("turtle") && off < 0x600000) { + found = true; + turtleFallback = true; + } uint8_t pageResult = found ? 0x4A : 0x00; LOG_WARNING("Warden: ", pageName, " offset=0x", [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}(), @@ -9703,9 +9699,23 @@ void GameHandler::handleWardenData(network::Packet& packet) { bool isWanted = hmacSha1Matches(sb, "KERNEL32.DLL", rh); std::string mn = isWanted ? "KERNEL32.DLL" : "?"; if (!isWanted) { + // Cheat modules (unwanted — report not found) if (hmacSha1Matches(sb,"WPESPY.DLL",rh)) mn = "WPESPY.DLL"; else if (hmacSha1Matches(sb,"TAMIA.DLL",rh)) mn = "TAMIA.DLL"; else if (hmacSha1Matches(sb,"PRXDRVPE.DLL",rh)) mn = "PRXDRVPE.DLL"; + else if (hmacSha1Matches(sb,"SPEEDHACK-I386.DLL",rh)) mn = "SPEEDHACK-I386.DLL"; + else if (hmacSha1Matches(sb,"D3DHOOK.DLL",rh)) mn = "D3DHOOK.DLL"; + else if (hmacSha1Matches(sb,"NJUMD.DLL",rh)) mn = "NJUMD.DLL"; + // System DLLs (wanted — report found) + else if (hmacSha1Matches(sb,"USER32.DLL",rh)) { mn = "USER32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"NTDLL.DLL",rh)) { mn = "NTDLL.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WS2_32.DLL",rh)) { mn = "WS2_32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WSOCK32.DLL",rh)) { mn = "WSOCK32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"ADVAPI32.DLL",rh)) { mn = "ADVAPI32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"SHELL32.DLL",rh)) { mn = "SHELL32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"GDI32.DLL",rh)) { mn = "GDI32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"OPENGL32.DLL",rh)) { mn = "OPENGL32.DLL"; isWanted = true; } + else if (hmacSha1Matches(sb,"WINMM.DLL",rh)) { mn = "WINMM.DLL"; isWanted = true; } } uint8_t mr = isWanted ? 0x4A : 0x00; LOG_WARNING("Warden: MODULE \"", mn, "\" -> 0x", @@ -9886,7 +9896,9 @@ void GameHandler::handleWardenData(network::Packet& packet) { switch (ct) { case CT_TIMING: { // No additional request data - // Response: [uint8 result=1][uint32 ticks] + // Response: [uint8 result][uint32 ticks] + // 0x01 = timing check ran successfully (server validates anti-AFK) + // 0x00 = timing failed (Wine/VM — server skips check but flags client) resultData.push_back(0x01); uint32_t ticks = static_cast( std::chrono::duration_cast( @@ -9895,6 +9907,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { resultData.push_back((ticks >> 8) & 0xFF); resultData.push_back((ticks >> 16) & 0xFF); resultData.push_back((ticks >> 24) & 0xFF); + LOG_WARNING("Warden: (sync) TIMING ticks=", ticks); break; } case CT_MEM: { @@ -9918,31 +9931,27 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } + // Dynamically update LastHardwareAction before reading + // (anti-AFK scan compares this timestamp against TIMING ticks) + if (offset == 0x00CF0BC8 && readLen == 4 && wardenMemory_ && wardenMemory_->isLoaded()) { + uint32_t now = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + wardenMemory_->writeLE32(0xCF0BC8, now - 2000); + } + // Read bytes from PE image (includes patched runtime globals) std::vector memBuf(readLen, 0); if (wardenMemory_->isLoaded() && wardenMemory_->readMemory(offset, readLen, memBuf.data())) { LOG_DEBUG("Warden: MEM_CHECK served from PE image"); - } else if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { - // Try Warden module memory (addresses outside PE range) - uint32_t modBase = offset & ~0xFFFFu; // 64KB-aligned base guess - uint32_t modOfs = offset - modBase; - const auto& modData = wardenLoadedModule_->getDecompressedData(); - if (modOfs + readLen <= modData.size()) { - std::memcpy(memBuf.data(), modData.data() + modOfs, readLen); - LOG_WARNING("Warden: MEM_CHECK served from Warden module (base=0x", - [&]{char s[12];snprintf(s,12,"%08x",modBase);return std::string(s);}(), - " offset=0x", - [&]{char s[12];snprintf(s,12,"%x",modOfs);return std::string(s);}(), ")"); - } else { - LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x", - [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}()); - } + resultData.push_back(0x00); + resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); } else { - LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x", - [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}()); + // Address not in PE/KUSER — return 0xE9 (not readable). + LOG_WARNING("Warden: (sync) MEM_CHECK -> 0xE9 (unmapped 0x", + [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(), ")"); + resultData.push_back(0xE9); } - resultData.push_back(0x00); - resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); break; } case CT_PAGE_A: { @@ -9988,18 +9997,29 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (isKnownWantedCodeScan(seedBytes, reqHash, off, len)) { pageResult = 0x4A; } else if (wardenMemory_ && wardenMemory_->isLoaded() && len > 0) { - if (wardenMemory_->searchCodePattern(seedBytes, reqHash, len, true)) + if (wardenMemory_->searchCodePattern(seedBytes, reqHash, len, true, off)) pageResult = 0x4A; } - // Turtle fallback for integrity checks + // Turtle PAGE_A fallback: runtime-patched offsets aren't in the + // on-disk PE. Server expects "found" for code integrity checks. if (pageResult == 0x00 && isActiveExpansion("turtle") && off < 0x600000) { pageResult = 0x4A; LOG_WARNING("Warden: PAGE_A turtle-fallback for offset=0x", [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}()); } } - LOG_DEBUG("Warden: PAGE_A request bytes=", consume, - " result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + if (consume >= 29) { + uint32_t off2 = uint32_t((decrypted.data()+pos)[24]) | (uint32_t((decrypted.data()+pos)[25])<<8) | + (uint32_t((decrypted.data()+pos)[26])<<16) | (uint32_t((decrypted.data()+pos)[27])<<24); + uint8_t len2 = (decrypted.data()+pos)[28]; + LOG_WARNING("Warden: (sync) PAGE_A offset=0x", + [&]{char s[12];snprintf(s,12,"%08x",off2);return std::string(s);}(), + " patLen=", (int)len2, + " result=0x", [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + } else { + LOG_WARNING("Warden: (sync) PAGE_A (short ", consume, "b) result=0x", + [&]{char s[4];snprintf(s,4,"%02x",pageResult);return std::string(s);}()); + } pos += consume; resultData.push_back(pageResult); break; @@ -10048,7 +10068,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (pos + 1 > checkEnd) { pos = checkEnd; break; } uint8_t strIdx = decrypted[pos++]; std::string filePath = resolveWardenString(strIdx); - LOG_DEBUG("Warden: MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); + LOG_WARNING("Warden: (sync) MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); bool found = false; std::vector hash(20, 0); @@ -10083,10 +10103,15 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } - // Response: [uint8 result][20 sha1] - // result=0 => found/success, result=1 => not found/failure - resultData.push_back(found ? 0x00 : 0x01); - resultData.insert(resultData.end(), hash.begin(), hash.end()); + // Response: result=0 + 20-byte SHA1 if found; result=1 (no hash) if not found. + // Server only reads 20 hash bytes when result==0; extra bytes corrupt parsing. + if (found) { + resultData.push_back(0x00); + resultData.insert(resultData.end(), hash.begin(), hash.end()); + } else { + resultData.push_back(0x01); + } + LOG_WARNING("Warden: (sync) MPQ result=", found ? "FOUND" : "NOT_FOUND"); break; } case CT_LUA: { @@ -10094,7 +10119,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { if (pos + 1 > checkEnd) { pos = checkEnd; break; } uint8_t strIdx = decrypted[pos++]; std::string luaVar = resolveWardenString(strIdx); - LOG_DEBUG("Warden: LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\""); + LOG_WARNING("Warden: (sync) LUA str=\"", (luaVar.empty() ? "?" : luaVar), "\""); // Response: [uint8 result=0][uint16 len=0] // Lua string doesn't exist resultData.push_back(0x01); // not found @@ -10106,9 +10131,10 @@ void GameHandler::handleWardenData(network::Packet& packet) { pos += 24; // skip seed + sha1 uint8_t strIdx = decrypted[pos++]; std::string driverName = resolveWardenString(strIdx); - LOG_DEBUG("Warden: DRIVER=\"", (driverName.empty() ? "?" : driverName), "\""); - // Response: [uint8 result=1] (driver NOT found = clean) - resultData.push_back(0x01); + LOG_WARNING("Warden: (sync) DRIVER=\"", (driverName.empty() ? "?" : driverName), "\" -> 0x00(not found)"); + // Response: [uint8 result=0] (driver NOT found = clean) + // VMaNGOS: result != 0 means "found". 0x01 would mean VM driver detected! + resultData.push_back(0x00); break; } case CT_MODULE: { @@ -10126,23 +10152,34 @@ void GameHandler::handleWardenData(network::Packet& packet) { std::memcpy(reqHash, p + 4, 20); pos += moduleSize; - // CMaNGOS uppercases module names before hashing. - // DB module scans: - // KERNEL32.DLL (wanted=true) - // WPESPY.DLL / SPEEDHACK-I386.DLL / TAMIA.DLL (wanted=false) bool shouldReportFound = false; - if (hmacSha1Matches(seedBytes, "KERNEL32.DLL", reqHash)) { - shouldReportFound = true; - } else if (hmacSha1Matches(seedBytes, "WPESPY.DLL", reqHash) || - hmacSha1Matches(seedBytes, "SPEEDHACK-I386.DLL", reqHash) || - hmacSha1Matches(seedBytes, "TAMIA.DLL", reqHash)) { - shouldReportFound = false; - } - resultData.push_back(shouldReportFound ? 0x4A : 0x01); + std::string modName = "?"; + // Wanted system modules + if (hmacSha1Matches(seedBytes, "KERNEL32.DLL", reqHash)) { modName = "KERNEL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "USER32.DLL", reqHash)) { modName = "USER32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "NTDLL.DLL", reqHash)) { modName = "NTDLL.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WS2_32.DLL", reqHash)) { modName = "WS2_32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WSOCK32.DLL", reqHash)) { modName = "WSOCK32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "ADVAPI32.DLL", reqHash)) { modName = "ADVAPI32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "SHELL32.DLL", reqHash)) { modName = "SHELL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "GDI32.DLL", reqHash)) { modName = "GDI32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "OPENGL32.DLL", reqHash)) { modName = "OPENGL32.DLL"; shouldReportFound = true; } + else if (hmacSha1Matches(seedBytes, "WINMM.DLL", reqHash)) { modName = "WINMM.DLL"; shouldReportFound = true; } + // Unwanted cheat modules + else if (hmacSha1Matches(seedBytes, "WPESPY.DLL", reqHash)) modName = "WPESPY.DLL"; + else if (hmacSha1Matches(seedBytes, "SPEEDHACK-I386.DLL", reqHash)) modName = "SPEEDHACK-I386.DLL"; + else if (hmacSha1Matches(seedBytes, "TAMIA.DLL", reqHash)) modName = "TAMIA.DLL"; + else if (hmacSha1Matches(seedBytes, "PRXDRVPE.DLL", reqHash)) modName = "PRXDRVPE.DLL"; + else if (hmacSha1Matches(seedBytes, "D3DHOOK.DLL", reqHash)) modName = "D3DHOOK.DLL"; + else if (hmacSha1Matches(seedBytes, "NJUMD.DLL", reqHash)) modName = "NJUMD.DLL"; + LOG_WARNING("Warden: (sync) MODULE \"", modName, + "\" -> 0x", [&]{char s[4];snprintf(s,4,"%02x",shouldReportFound?0x4A:0x00);return std::string(s);}(), + "(", shouldReportFound ? "found" : "not found", ")"); + resultData.push_back(shouldReportFound ? 0x4A : 0x00); break; } // Truncated module request fallback: module NOT loaded = clean - resultData.push_back(0x01); + resultData.push_back(0x00); break; } case CT_PROC: { @@ -10151,12 +10188,23 @@ void GameHandler::handleWardenData(network::Packet& packet) { int procSize = 30; if (pos + procSize > checkEnd) { pos = checkEnd; break; } pos += procSize; + LOG_WARNING("Warden: (sync) PROC check -> 0x01(not found)"); // Response: [uint8 result=1] (proc NOT found = clean) resultData.push_back(0x01); break; } default: { - LOG_WARNING("Warden: Unknown check type, cannot parse remaining"); + uint8_t rawByte = decrypted[pos - 1]; + uint8_t decoded = rawByte ^ xorByte; + LOG_WARNING("Warden: Unknown check type raw=0x", + [&]{char s[4];snprintf(s,4,"%02x",rawByte);return std::string(s);}(), + " decoded=0x", + [&]{char s[4];snprintf(s,4,"%02x",decoded);return std::string(s);}(), + " xorByte=0x", + [&]{char s[4];snprintf(s,4,"%02x",xorByte);return std::string(s);}(), + " opcodes=[", + [&]{std::string r;for(int i=0;i<9;i++){char s[6];snprintf(s,6,"0x%02x ",wardenCheckOpcodes_[i]);r+=s;}return r;}(), + "] pos=", pos, "/", checkEnd); pos = checkEnd; // stop parsing break; } diff --git a/src/game/warden_memory.cpp b/src/game/warden_memory.cpp index 26895784..33127e2c 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -1,5 +1,6 @@ #include "game/warden_memory.hpp" #include "core/logger.hpp" +#include #include #include #include @@ -406,10 +407,31 @@ void WardenMemory::patchRuntimeGlobals() { writeLE32(WORLD_ENABLES, enables); LOG_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec); - // LastHardwareAction + // LastHardwareAction — must be a recent GetTickCount()-style timestamp + // so the anti-AFK scan sees (currentTime - lastAction) < threshold. constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8; - writeLE32(LAST_HARDWARE_ACTION, 60000); + 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() { @@ -837,7 +859,8 @@ void WardenMemory::verifyWardenScanEntries() { } bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], - uint8_t patternLen, bool imageOnly) const { + 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) @@ -849,21 +872,56 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect auto cacheIt = codePatternCache_.find(cacheKey); if (cacheIt != codePatternCache_.end()) { - LOG_WARNING("WardenMemory: Code pattern cache HIT → ", - cacheIt->second ? "found" : "not found"); return cacheIt->second; } - // FIND_MEM_IMAGE_CODE_BY_HASH (imageOnly=true) searches ALL sections of - // the PE image — not just executable ones. The original Warden module - // walks every PE section when scanning the WoW.exe memory image. - // FIND_CODE_BY_HASH (imageOnly=false) searches all process memory; since - // we only have the PE image, both cases search the full image. + // --- 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) { - // Collect ALL PE sections (not just executable ones) 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()) { @@ -885,11 +943,14 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect } if (ranges.empty()) { - // Fallback: search entire image 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; @@ -900,8 +961,11 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect 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)"); + r.start + i, std::dec, " (searched ", totalPositions + i + 1, + " positions in ", elapsed, "s)"); codePatternCache_[cacheKey] = true; return true; } @@ -909,8 +973,10 @@ bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expect 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)"); + ranges.size(), " section(s), took ", elapsed, "s"); codePatternCache_[cacheKey] = false; return false; }