diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 72b25a59..4d13171c 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace wowee::game { class TransportManager; @@ -3144,6 +3145,10 @@ private: uint8_t wardenCheckOpcodes_[9] = {}; bool loadWardenCRFile(const std::string& moduleHashHex); + // Async Warden response: avoids 5-second main-loop stalls from PAGE_A/PAGE_B code pattern searches + std::future> wardenPendingEncrypted_; // encrypted response bytes + bool wardenResponsePending_ = false; + // ---- XP tracking ---- uint32_t playerXp_ = 0; uint32_t playerNextLevelXp_ = 0; diff --git a/include/game/warden_memory.hpp b/include/game/warden_memory.hpp index 335ad7a9..d01a27c3 100644 --- a/include/game/warden_memory.hpp +++ b/include/game/warden_memory.hpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace wowee { namespace game { @@ -18,8 +19,9 @@ public: ~WardenMemory(); /** Search standard candidate dirs for WoW.exe and load it. - * @param build Client build number (e.g. 5875 for Classic 1.12.1) to select the right exe. */ - bool load(uint16_t build = 0); + * @param build Client build number (e.g. 5875 for Classic 1.12.1) to select the right exe. + * @param isTurtle If true, prefer the Turtle WoW custom exe (different code bytes). */ + bool load(uint16_t build = 0, bool isTurtle = false); /** Load PE image from a specific file path. */ bool loadFromFile(const std::string& exePath); @@ -32,6 +34,21 @@ public: bool isLoaded() const { return loaded_; } + /** + * Search PE image for a byte pattern matching HMAC-SHA1(seed, pattern). + * Used for FIND_MEM_IMAGE_CODE_BY_HASH and FIND_CODE_BY_HASH scans. + * @param seed 4-byte HMAC key + * @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) + * @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; + + /** Write a little-endian uint32 at the given virtual address in the PE image. */ + void writeLE32(uint32_t va, uint32_t value); + private: bool loaded_ = false; uint32_t imageBase_ = 0; @@ -46,9 +63,15 @@ private: bool parsePE(const std::vector& fileData); void initKuserSharedData(); void patchRuntimeGlobals(); - void writeLE32(uint32_t va, uint32_t value); + void patchTurtleWowBinary(); + void verifyWardenScanEntries(); + bool isTurtle_ = false; std::string findWowExe(uint16_t build) const; - static uint32_t expectedImageSizeForBuild(uint16_t build); + static uint32_t expectedImageSizeForBuild(uint16_t build, bool isTurtle); + + // Cache for searchCodePattern results to avoid repeated 5-second brute-force searches. + // Key: hex string of seed(4)+hash(20)+patLen(1)+imageOnly(1) = 26 bytes. + mutable std::unordered_map codePatternCache_; }; } // namespace game diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index a7eb5cb7..e667c549 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -836,6 +836,26 @@ void GameHandler::update(float deltaTime) { } } + // Drain pending async Warden response (built on background thread to avoid 5s stalls) + if (wardenResponsePending_) { + auto status = wardenPendingEncrypted_.wait_for(std::chrono::milliseconds(0)); + if (status == std::future_status::ready) { + auto plaintext = wardenPendingEncrypted_.get(); + wardenResponsePending_ = false; + if (!plaintext.empty() && wardenCrypto_) { + std::vector encrypted = wardenCrypto_->encrypt(plaintext); + network::Packet response(wireOpcode(Opcode::CMSG_WARDEN_DATA)); + for (uint8_t byte : encrypted) { + response.writeUInt8(byte); + } + if (socket && socket->isConnected()) { + socket->send(response); + LOG_WARNING("Warden: Sent async CHEAT_CHECKS_RESULT (", plaintext.size(), " bytes plaintext)"); + } + } + } + } + // Detect server-side disconnect (socket closed during update) if (socket && !socket->isConnected() && state != WorldState::DISCONNECTED) { if (pendingIncomingPackets_.empty() && pendingUpdateObjectWork_.empty()) { @@ -9512,6 +9532,281 @@ void GameHandler::handleWardenData(network::Packet& packet) { uint8_t xorByte = decrypted.back(); LOG_DEBUG("Warden: XOR byte = 0x", [&]{ char s[4]; snprintf(s,4,"%02x",xorByte); return std::string(s); }()); + // Quick-scan for PAGE_A/PAGE_B checks (these trigger 5-second brute-force searches) + { + bool hasSlowChecks = false; + for (size_t i = pos; i < decrypted.size() - 1; i++) { + uint8_t d = decrypted[i] ^ xorByte; + if (d == wardenCheckOpcodes_[2] || d == wardenCheckOpcodes_[3]) { + hasSlowChecks = true; + break; + } + } + if (hasSlowChecks && !wardenResponsePending_) { + LOG_WARNING("Warden: PAGE_A/PAGE_B detected — building response async to avoid main-loop stall"); + // Ensure wardenMemory_ is loaded on main thread before launching async task + if (!wardenMemory_) { + wardenMemory_ = std::make_unique(); + if (!wardenMemory_->load(static_cast(build), isActiveExpansion("turtle"))) { + LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK"); + } + } + // Capture state by value (decrypted, strings) and launch async. + // The async task returns plaintext response bytes; main thread encrypts+sends in update(). + size_t capturedPos = pos; + wardenPendingEncrypted_ = std::async(std::launch::async, + [this, decrypted, strings, xorByte, capturedPos]() -> std::vector { + // This runs on a background thread — same logic as the synchronous path below. + // BEGIN: duplicated check processing (kept in sync with synchronous path) + enum CheckType { CT_MEM=0, CT_PAGE_A=1, CT_PAGE_B=2, CT_MPQ=3, CT_LUA=4, + CT_DRIVER=5, CT_TIMING=6, CT_PROC=7, CT_MODULE=8, CT_UNKNOWN=9 }; + size_t checkEnd = decrypted.size() - 1; + size_t pos = capturedPos; + + auto decodeCheckType = [&](uint8_t raw) -> CheckType { + uint8_t decoded = raw ^ xorByte; + if (decoded == wardenCheckOpcodes_[0]) return CT_MEM; + if (decoded == wardenCheckOpcodes_[1]) return CT_MODULE; + if (decoded == wardenCheckOpcodes_[2]) return CT_PAGE_A; + if (decoded == wardenCheckOpcodes_[3]) return CT_PAGE_B; + if (decoded == wardenCheckOpcodes_[4]) return CT_MPQ; + if (decoded == wardenCheckOpcodes_[5]) return CT_LUA; + if (decoded == wardenCheckOpcodes_[6]) return CT_PROC; + if (decoded == wardenCheckOpcodes_[7]) return CT_DRIVER; + if (decoded == wardenCheckOpcodes_[8]) return CT_TIMING; + return CT_UNKNOWN; + }; + auto resolveString = [&](uint8_t idx) -> std::string { + if (idx == 0) return {}; + size_t i = idx - 1; + return i < strings.size() ? strings[i] : std::string(); + }; + auto isKnownWantedCodeScan = [&](const uint8_t seed[4], const uint8_t hash[20], + uint32_t off, uint8_t len) -> bool { + auto tryMatch = [&](const uint8_t* pat, size_t patLen) { + uint8_t out[SHA_DIGEST_LENGTH]; unsigned int outLen = 0; + HMAC(EVP_sha1(), seed, 4, pat, patLen, out, &outLen); + return outLen == SHA_DIGEST_LENGTH && !std::memcmp(out, hash, SHA_DIGEST_LENGTH); + }; + static const uint8_t p1[] = {0x33,0xD2,0x33,0xC9,0xE8,0x87,0x07,0x1B,0x00,0xE8}; + if (off == 13856 && len == sizeof(p1) && tryMatch(p1, sizeof(p1))) return true; + static const uint8_t p2[] = {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}; + if (len == sizeof(p2) && tryMatch(p2, sizeof(p2))) return true; + return false; + }; + + std::vector resultData; + int checkCount = 0; + int checkTypeCounts[10] = {}; + + #define WARDEN_ASYNC_HANDLER 1 + // The check processing loop is identical to the synchronous path. + // See the synchronous case 0x02 below for the canonical version. + while (pos < checkEnd) { + CheckType ct = decodeCheckType(decrypted[pos]); + pos++; + checkCount++; + if (ct <= CT_UNKNOWN) checkTypeCounts[ct]++; + + switch (ct) { + case CT_TIMING: { + resultData.push_back(0x01); + uint32_t ticks = static_cast( + std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count()); + resultData.push_back(ticks & 0xFF); + resultData.push_back((ticks >> 8) & 0xFF); + resultData.push_back((ticks >> 16) & 0xFF); + resultData.push_back((ticks >> 24) & 0xFF); + break; + } + case CT_MEM: { + if (pos + 6 > checkEnd) { pos = checkEnd; break; } + uint8_t strIdx = decrypted[pos++]; + std::string moduleName = resolveString(strIdx); + uint32_t offset = decrypted[pos] | (uint32_t(decrypted[pos+1])<<8) + | (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24); + 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); + 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); + } + std::vector memBuf(readLen, 0); + bool memOk = wardenMemory_ && wardenMemory_->isLoaded() && + wardenMemory_->readMemory(offset, readLen, memBuf.data()); + if (memOk) { + const char* region = "?"; + if (offset >= 0x7FFE0000 && offset < 0x7FFF0000) region = "KUSER"; + else if (offset >= 0x400000 && offset < 0x800000) region = ".text/.code"; + else if (offset >= 0x7FF000 && offset < 0x827000) region = ".rdata"; + else if (offset >= 0x827000 && offset < 0x883000) region = ".data(raw)"; + else if (offset >= 0x883000 && offset < 0xD06000) region = ".data(BSS)"; + bool allZero = true; + for (int i = 0; i < (int)readLen; i++) { if (memBuf[i] != 0) { allZero = false; break; } } + std::string hexDump; + for (int i = 0; i < (int)readLen; i++) { char hx[4]; snprintf(hx,4,"%02x ",memBuf[i]); hexDump += hx; } + LOG_WARNING("Warden: MEM_CHECK served: [", hexDump, "] region=", region, + (allZero && offset >= 0x883000 ? " \xe2\x98\x85""BSS_ZERO\xe2\x98\x85" : "")); + if (offset == 0x7FFE026C && readLen == 12) + LOG_WARNING("Warden: Applying 4-byte ULONG alignment padding for WinVersionGet"); + resultData.push_back(0x00); + resultData.insert(resultData.end(), memBuf.begin(), memBuf.end()); + } else { + resultData.push_back(0xE9); + } + break; + } + case CT_PAGE_A: + case CT_PAGE_B: { + constexpr size_t kPageSize = 29; + const char* pageName = (ct == CT_PAGE_A) ? "PAGE_A" : "PAGE_B"; + bool isImageOnly = (ct == CT_PAGE_A); + if (pos + kPageSize > checkEnd) { pos = checkEnd; resultData.push_back(0x00); break; } + const uint8_t* p = decrypted.data() + pos; + const uint8_t* seed = p; + const uint8_t* sha1 = p + 4; + uint32_t off = uint32_t(p[24])|(uint32_t(p[25])<<8)|(uint32_t(p[26])<<16)|(uint32_t(p[27])<<24); + uint8_t patLen = p[28]; + bool found = false; + if (isKnownWantedCodeScan(seed, sha1, off, patLen)) { + found = true; + } else if (wardenMemory_ && wardenMemory_->isLoaded() && patLen > 0) { + found = wardenMemory_->searchCodePattern(seed, sha1, patLen, isImageOnly); + if (!found && wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) { + const auto& md = wardenLoadedModule_->getDecompressedData(); + if (md.size() >= patLen) { + for (size_t i = 0; i < md.size() - patLen + 1; i++) { + uint8_t h[20]; unsigned int hl = 0; + HMAC(EVP_sha1(), seed, 4, md.data()+i, patLen, h, &hl); + if (hl == 20 && !std::memcmp(h, sha1, 20)) { found = true; break; } + } + } + } + } + uint8_t pageResult = found ? 0x4A : 0x00; + LOG_WARNING("Warden: ", pageName, " offset=0x", + [&]{char s[12];snprintf(s,12,"%08x",off);return std::string(s);}(), + " patLen=", (int)patLen, " found=", found ? "yes" : "no"); + pos += kPageSize; + resultData.push_back(pageResult); + break; + } + case CT_MPQ: { + if (pos + 1 > checkEnd) { pos = checkEnd; break; } + uint8_t strIdx = decrypted[pos++]; + std::string filePath = resolveString(strIdx); + LOG_WARNING("Warden: MPQ file=\"", (filePath.empty() ? "?" : filePath), "\""); + bool found = false; + std::vector hash(20, 0); + if (!filePath.empty()) { + std::string np = asciiLower(filePath); + std::replace(np.begin(), np.end(), '/', '\\'); + auto knownIt = knownDoorHashes().find(np); + if (knownIt != knownDoorHashes().end()) { found = true; hash.assign(knownIt->second.begin(), knownIt->second.end()); } + auto* am = core::Application::getInstance().getAssetManager(); + if (am && am->isInitialized() && !found) { + std::vector fd; + std::string rp = resolveCaseInsensitiveDataPath(am->getDataPath(), filePath); + if (!rp.empty()) fd = readFileBinary(rp); + if (fd.empty()) fd = am->readFile(filePath); + if (!fd.empty()) { found = true; hash = auth::Crypto::sha1(fd); } + } + } + LOG_WARNING("Warden: MPQ result=", (found ? "FOUND" : "NOT_FOUND")); + if (found) { resultData.push_back(0x00); resultData.insert(resultData.end(), hash.begin(), hash.end()); } + else { resultData.push_back(0x01); } + break; + } + case CT_LUA: { + if (pos + 1 > checkEnd) { pos = checkEnd; break; } + pos++; resultData.push_back(0x01); break; + } + case CT_DRIVER: { + if (pos + 25 > checkEnd) { pos = checkEnd; break; } + pos += 24; + uint8_t strIdx = decrypted[pos++]; + std::string dn = resolveString(strIdx); + LOG_WARNING("Warden: DRIVER=\"", (dn.empty() ? "?" : dn), "\" -> 0x00(not found)"); + resultData.push_back(0x00); break; + } + case CT_MODULE: { + if (pos + 24 > checkEnd) { pos = checkEnd; resultData.push_back(0x00); break; } + const uint8_t* p = decrypted.data() + pos; + uint8_t sb[4] = {p[0],p[1],p[2],p[3]}; + uint8_t rh[20]; std::memcpy(rh, p+4, 20); + pos += 24; + bool isWanted = hmacSha1Matches(sb, "KERNEL32.DLL", rh); + std::string mn = isWanted ? "KERNEL32.DLL" : "?"; + if (!isWanted) { + 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"; + } + uint8_t mr = isWanted ? 0x4A : 0x00; + LOG_WARNING("Warden: MODULE \"", mn, "\" -> 0x", + [&]{char s[4];snprintf(s,4,"%02x",mr);return std::string(s);}(), + isWanted ? "(found)" : "(not found)"); + resultData.push_back(mr); break; + } + case CT_PROC: { + if (pos + 30 > checkEnd) { pos = checkEnd; break; } + pos += 30; resultData.push_back(0x01); break; + } + default: pos = checkEnd; break; + } + } + #undef WARDEN_ASYNC_HANDLER + + // Log summary + { + std::string summary; + const char* ctNames[] = {"MEM","PAGE_A","PAGE_B","MPQ","LUA","DRIVER","TIMING","PROC","MODULE","UNK"}; + for (int i = 0; i < 10; i++) { + if (checkTypeCounts[i] > 0) { + if (!summary.empty()) summary += " "; + summary += ctNames[i]; summary += "="; summary += std::to_string(checkTypeCounts[i]); + } + } + LOG_WARNING("Warden: (async) Parsed ", checkCount, " checks [", summary, + "] resultSize=", resultData.size()); + std::string fullHex; + for (size_t bi = 0; bi < resultData.size(); bi++) { + char hx[4]; snprintf(hx, 4, "%02x ", resultData[bi]); fullHex += hx; + if ((bi + 1) % 32 == 0 && bi + 1 < resultData.size()) fullHex += "\n "; + } + LOG_WARNING("Warden: RESPONSE_HEX [", fullHex, "]"); + } + + // Build plaintext response: [0x02][uint16 len][uint32 checksum][resultData] + auto resultHash = auth::Crypto::sha1(resultData); + uint32_t checksum = 0; + for (int i = 0; i < 5; i++) { + uint32_t word = resultHash[i*4] | (uint32_t(resultHash[i*4+1])<<8) + | (uint32_t(resultHash[i*4+2])<<16) | (uint32_t(resultHash[i*4+3])<<24); + checksum ^= word; + } + uint16_t rl = static_cast(resultData.size()); + std::vector resp; + resp.push_back(0x02); + resp.push_back(rl & 0xFF); resp.push_back((rl >> 8) & 0xFF); + resp.push_back(checksum & 0xFF); resp.push_back((checksum >> 8) & 0xFF); + resp.push_back((checksum >> 16) & 0xFF); resp.push_back((checksum >> 24) & 0xFF); + resp.insert(resp.end(), resultData.begin(), resultData.end()); + return resp; // plaintext; main thread will encrypt + send + }); + wardenResponsePending_ = true; + break; // exit case 0x02 — response will be sent from update() + } + } + // Check type enum indices enum CheckType { CT_MEM=0, CT_PAGE_A=1, CT_PAGE_B=2, CT_MPQ=3, CT_LUA=4, CT_DRIVER=5, CT_TIMING=6, CT_PROC=7, CT_MODULE=8, CT_UNKNOWN=9 }; diff --git a/src/game/warden_memory.cpp b/src/game/warden_memory.cpp index aa921155..9793951e 100644 --- a/src/game/warden_memory.cpp +++ b/src/game/warden_memory.cpp @@ -7,6 +7,8 @@ #include #include #include +#include +#include namespace wowee { namespace game { @@ -106,19 +108,136 @@ bool WardenMemory::parsePE(const std::vector& fileData) { " size=0x", copySize, std::dec); } + LOG_WARNING("WardenMemory: PE loaded — imageBase=0x", std::hex, imageBase_, + " imageSize=0x", imageSize_, std::dec, + " (", numSections, " sections, ", fileData.size(), " bytes on disk)"); + return true; } void WardenMemory::initKuserSharedData() { std::memset(kuserData_, 0, KUSER_SIZE); - // NtMajorVersion at offset 0x026C = 6 (Vista/7/8/10) + // Populate KUSER_SHARED_DATA with realistic Windows 7 SP1 values. + // Warden reads this in 238-byte chunks for OS fingerprinting. + + // 0x0000: TickCountLowDeprecated (uint32) — non-zero uptime ticks + uint32_t tickCountLow = 0x003F4A00; // ~70 minutes uptime + std::memcpy(kuserData_ + 0x0000, &tickCountLow, 4); + + // 0x0004: TickCountMultiplier (uint32) — standard value + uint32_t tickMult = 0x0FA00000; + std::memcpy(kuserData_ + 0x0004, &tickMult, 4); + + // 0x0008: InterruptTime (KSYSTEM_TIME = LowPart(4) + High1Time(4) + High2Time(4)) + uint32_t intTimeLow = 0x6B49D200; // plausible 100ns interrupt time + uint32_t intTimeHigh = 0x00000029; + std::memcpy(kuserData_ + 0x0008, &intTimeLow, 4); + std::memcpy(kuserData_ + 0x000C, &intTimeHigh, 4); + std::memcpy(kuserData_ + 0x0010, &intTimeHigh, 4); + + // 0x0014: SystemTime (KSYSTEM_TIME) — ~2024 epoch in Windows FILETIME + uint32_t sysTimeLow = 0xA0B71B00; + uint32_t sysTimeHigh = 0x01DA5E80; + std::memcpy(kuserData_ + 0x0014, &sysTimeLow, 4); + std::memcpy(kuserData_ + 0x0018, &sysTimeHigh, 4); + std::memcpy(kuserData_ + 0x001C, &sysTimeHigh, 4); + + // 0x0020: TimeZoneBias (KSYSTEM_TIME) — 0 for UTC + // Leave as zeros (UTC timezone) + + // 0x002C: ImageNumberLow (uint16) = 0x014C (IMAGE_FILE_MACHINE_I386) + uint16_t imageNumLow = 0x014C; + std::memcpy(kuserData_ + 0x002C, &imageNumLow, 2); + + // 0x002E: ImageNumberHigh (uint16) = 0x014C + uint16_t imageNumHigh = 0x014C; + std::memcpy(kuserData_ + 0x002E, &imageNumHigh, 2); + + // 0x0030: NtSystemRoot (wchar_t[260]) = L"C:\\WINDOWS" + const wchar_t* sysRoot = L"C:\\WINDOWS"; + size_t rootLen = 10; // chars including null + for (size_t i = 0; i < rootLen; i++) { + uint16_t wc = static_cast(sysRoot[i]); + std::memcpy(kuserData_ + 0x0030 + i * 2, &wc, 2); + } + + // 0x0238: MaxStackTraceDepth (uint32) + uint32_t maxStack = 0; + std::memcpy(kuserData_ + 0x0238, &maxStack, 4); + + // 0x023C: CryptoExponent (uint32) — typical value + uint32_t cryptoExp = 0x00010001; // 65537 + std::memcpy(kuserData_ + 0x023C, &cryptoExp, 4); + + // 0x0240: TimeZoneId (uint32) = 0 (TIME_ZONE_ID_UNKNOWN) + uint32_t tzId = 0; + std::memcpy(kuserData_ + 0x0240, &tzId, 4); + + // 0x0248: LargePageMinimum (uint32) + uint32_t largePage = 0x00200000; // 2MB + std::memcpy(kuserData_ + 0x0248, &largePage, 4); + + // 0x0264: ActiveConsoleId (uint32) = 0 + uint32_t consoleId = 0; + std::memcpy(kuserData_ + 0x0264, &consoleId, 4); + + // 0x0268: DismountCount (uint32) = 0 + // already zero + + // 0x026C: NtMajorVersion (uint32) = 6 (Vista/7/8/10) uint32_t ntMajor = 6; std::memcpy(kuserData_ + 0x026C, &ntMajor, 4); - // NtMinorVersion at offset 0x0270 = 1 (Windows 7) + // 0x0270: NtMinorVersion (uint32) = 1 (Windows 7) uint32_t ntMinor = 1; std::memcpy(kuserData_ + 0x0270, &ntMinor, 4); + + // 0x0274: NtProductType (uint32) = 1 (NtProductWinNt = workstation) + uint32_t productType = 1; + std::memcpy(kuserData_ + 0x0274, &productType, 4); + + // 0x0278: ProductTypeIsValid (uint8) = 1 + kuserData_[0x0278] = 1; + + // 0x027C: NtMajorVersion (duplicate? actually NativeMajorVersion) + // 0x0280: NtMinorVersion (NativeMinorVersion) + + // 0x0294: ProcessorFeatures (BOOLEAN[64]) — leave mostly zero + // PF_FLOATING_POINT_PRECISION_ERRATA = 0 at [0] + // PF_FLOATING_POINT_EMULATED = 0 at [1] + // PF_COMPARE_EXCHANGE_DOUBLE = 1 at [2] + kuserData_[0x0294 + 2] = 1; + // PF_MMX_INSTRUCTIONS_AVAILABLE = 1 at [3] + kuserData_[0x0294 + 3] = 1; + // PF_XMMI_INSTRUCTIONS_AVAILABLE (SSE) = 1 at [6] + kuserData_[0x0294 + 6] = 1; + // PF_XMMI64_INSTRUCTIONS_AVAILABLE (SSE2) = 1 at [10] + kuserData_[0x0294 + 10] = 1; + // PF_NX_ENABLED = 1 at [12] + kuserData_[0x0294 + 12] = 1; + + // 0x02D4: Reserved1 (uint32) + + // 0x02D8: ActiveProcessorCount (uint32) = 4 + uint32_t procCount = 4; + std::memcpy(kuserData_ + 0x02D8, &procCount, 4); + + // 0x0300: NumberOfPhysicalPages (uint32) — 4GB / 4KB = ~1M pages + uint32_t physPages = 0x000FF000; + std::memcpy(kuserData_ + 0x0300, &physPages, 4); + + // 0x0304: SafeBootMode (uint8) = 0 (normal boot) + // already zero + + // 0x0308: SharedDataFlags / TraceLogging (uint32) — leave zero + + // 0x0320: TickCount (KSYSTEM_TIME) — same as TickCountLowDeprecated + std::memcpy(kuserData_ + 0x0320, &tickCountLow, 4); + + // 0x0330: Cookie (uint32) — stack cookie, random value + uint32_t cookie = 0x4A2F8C15; + std::memcpy(kuserData_ + 0x0330, &cookie, 4); } void WardenMemory::writeLE32(uint32_t va, uint32_t value) { @@ -132,56 +251,52 @@ void WardenMemory::writeLE32(uint32_t va, uint32_t value) { } void WardenMemory::patchRuntimeGlobals() { - // Only patch Classic 1.12.1 (build 5875) WoW.exe - // Identified by: ImageBase=0x400000, ImageSize=0x906000 (unique to 1.12.1) - // Other expansions have different image sizes and different global addresses. - if (imageBase_ != 0x00400000 || imageSize_ != 0x00906000) { - LOG_INFO("WardenMemory: Not Classic 1.12.1 WoW.exe (imageSize=0x", - std::hex, imageSize_, std::dec, "), skipping runtime global patches"); + if (imageBase_ != 0x00400000) { + LOG_WARNING("WardenMemory: unexpected imageBase=0x", std::hex, imageBase_, std::dec, + " — skipping runtime global patches"); return; } // Classic 1.12.1 (build 5875) runtime globals - // These are in the .data BSS region - zero on disk, populated at runtime. - // We patch them with fake but valid values so Warden checks pass. + // VMaNGOS has TWO types of Warden scans that read these addresses: // - // Offsets from CMaNGOS anticheat module (wardenwin.cpp): - // WardenModule = 0xCE897C - // OfsWardenSysInfo = 0x228 - // OfsWardenWinSysInfo = 0x08 - // g_theGxDevicePtr = 0xC0ED38 - // OfsDevice2 = 0x38A8 - // OfsDevice3 = 0x0 - // OfsDevice4 = 0xA8 - // WorldEnables = 0xC7B2A4 - // LastHardwareAction= 0xCF0BC8 + // 1. DB-driven scans (warden_scans table): memcmp against expected bytes. + // These check CODE sections for integrity — never check runtime data addresses. + // + // 2. Scripted scans (WardenWin::LoadScriptedScans): READ and INTERPRET values. + // - "Warden locate" reads 0xCE897C as a pointer, follows chain to SYSTEM_INFO + // - "Anti-AFK hack" reads 0xCF0BC8 as a timestamp, compares vs TIMING ticks + // - "CWorld::enables" reads 0xC7B2A4, checks flag bits + // - "EndScene" reads 0xC0ED38, follows pointer chain to find EndScene address + // + // We MUST patch these for ALL clients (including Turtle WoW) because the scripted + // scans interpret the values as runtime state, not static code bytes. Returning + // raw PE data causes the Anti-AFK scan to see lastHardwareAction > currentTime + // (PE bytes happen to be a large value), triggering a kick after ~3.5 minutes. - // === Warden SYSTEM_INFO chain (3-level pointer chain) === - // Stage 0: [0xCE897C] → fake warden struct base + // === Runtime global patches (applied unconditionally for all image variants) === + + // Warden SYSTEM_INFO chain constexpr uint32_t WARDEN_MODULE_PTR = 0xCE897C; constexpr uint32_t FAKE_WARDEN_BASE = 0xCE8000; writeLE32(WARDEN_MODULE_PTR, FAKE_WARDEN_BASE); - - // Stage 1: [FAKE_WARDEN_BASE + 0x228] → pointer to sysinfo container - constexpr uint32_t OFS_WARDEN_SYSINFO = 0x228; constexpr uint32_t FAKE_SYSINFO_CONTAINER = 0xCE8300; - writeLE32(FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, FAKE_SYSINFO_CONTAINER); + writeLE32(FAKE_WARDEN_BASE + 0x228, FAKE_SYSINFO_CONTAINER); - // Stage 2: [FAKE_SYSINFO_CONTAINER + 0x08] → 36-byte SYSTEM_INFO struct - constexpr uint32_t OFS_WARDEN_WIN_SYSINFO = 0x08; - uint32_t sysInfoAddr = FAKE_SYSINFO_CONTAINER + OFS_WARDEN_WIN_SYSINFO; // 0xCE8308 - // WIN_SYSTEM_INFO is 36 bytes (0x24): - // uint16 wProcessorArchitecture (must be 0 = x86) - // uint16 wReserved - // uint32 dwPageSize - // uint32 lpMinimumApplicationAddress - // uint32 lpMaximumApplicationAddress (MUST be non-zero!) - // uint32 dwActiveProcessorMask - // uint32 dwNumberOfProcessors - // uint32 dwProcessorType (must be 386, 486, or 586) - // uint32 dwAllocationGranularity - // uint16 wProcessorLevel - // uint16 wProcessorRevision + // Write SYSINFO pointer at many offsets from FAKE_WARDEN_BASE so the + // chain works regardless of which module-specific offset the server uses. + // MUST be done BEFORE writing the actual SYSTEM_INFO struct, because this + // loop's range (0xCE8200-0xCE8400) overlaps with the struct at 0xCE8308. + for (uint32_t off = 0x200; off <= 0x400; off += 4) { + uint32_t addr = FAKE_WARDEN_BASE + off; + if (addr >= imageBase_ && (addr - imageBase_) + 4 <= imageSize_) { + writeLE32(addr, FAKE_SYSINFO_CONTAINER); + } + } + + // Now write the actual WIN_SYSTEM_INFO struct AFTER the pointer fill loop, + // so it overwrites any values the loop placed in the 0xCE8308+ range. + uint32_t sysInfoAddr = FAKE_SYSINFO_CONTAINER + 0x08; #pragma pack(push, 1) struct { uint16_t wProcessorArchitecture; @@ -195,19 +310,7 @@ void WardenMemory::patchRuntimeGlobals() { uint32_t dwAllocationGranularity; uint16_t wProcessorLevel; uint16_t wProcessorRevision; - } sysInfo = { - 0, // x86 - 0, - 4096, // 4K page size - 0x00010000, // min app address - 0x7FFEFFFF, // max app address (CRITICAL: must be non-zero) - 0x0F, // 4 processors - 4, // 4 CPUs - 586, // Pentium - 65536, // 64K granularity - 6, // P6 family - 0x3A09 // revision - }; + } sysInfo = {0, 0, 4096, 0x00010000, 0x7FFEFFFF, 0x0F, 4, 586, 65536, 6, 0x3A09}; #pragma pack(pop) static_assert(sizeof(sysInfo) == 36, "SYSTEM_INFO must be 36 bytes"); uint32_t rva = sysInfoAddr - imageBase_; @@ -215,52 +318,182 @@ void WardenMemory::patchRuntimeGlobals() { std::memcpy(image_.data() + rva, &sysInfo, 36); } - LOG_INFO("WardenMemory: Patched SYSTEM_INFO chain: [0x", std::hex, - WARDEN_MODULE_PTR, "]→0x", FAKE_WARDEN_BASE, - " [0x", FAKE_WARDEN_BASE + OFS_WARDEN_SYSINFO, "]→0x", FAKE_SYSINFO_CONTAINER, - " SYSTEM_INFO@0x", sysInfoAddr, std::dec); + // Fallback: if the pointer chain breaks and stage 3 reads from address + // 0x00000000 + 0x08 = 8, write valid SYSINFO at RVA 8 (PE DOS header area). + if (8 + 36 <= imageSize_) { + std::memcpy(image_.data() + 8, &sysInfo, 36); + } - // === EndScene chain (4-level pointer chain) === - // Stage 1: [0xC0ED38] → fake D3D device + LOG_WARNING("WardenMemory: Patched SYSINFO chain @0x", std::hex, WARDEN_MODULE_PTR, std::dec); + + // EndScene chain + // VMaNGOS reads g_theGxDevicePtr → device, then device+0x1FC for API kind + // (0=OpenGL, 1=Direct3D). If Direct3D, follows device+0x38A8 → ptr → ptr+0xA8 → EndScene. + // We set API=1 (Direct3D) and provide the full pointer chain. constexpr uint32_t GX_DEVICE_PTR = 0xC0ED38; constexpr uint32_t FAKE_DEVICE = 0xCE8400; writeLE32(GX_DEVICE_PTR, FAKE_DEVICE); + writeLE32(FAKE_DEVICE + 0x1FC, 1); // API kind = Direct3D + // Set up the full EndScene pointer chain at the canonical offsets. + constexpr uint32_t FAKE_VTABLE1 = 0xCE8500; + constexpr uint32_t FAKE_VTABLE2 = 0xCE8600; + constexpr uint32_t FAKE_ENDSCENE = 0x00401000; // start of .text + writeLE32(FAKE_DEVICE + 0x38A8, FAKE_VTABLE1); + writeLE32(FAKE_VTABLE1, FAKE_VTABLE2); + writeLE32(FAKE_VTABLE2 + 0xA8, FAKE_ENDSCENE); - // Stage 2: [FAKE_DEVICE + 0x38A8] → fake intermediate - constexpr uint32_t OFS_DEVICE2 = 0x38A8; - constexpr uint32_t FAKE_INTERMEDIATE = 0xCE8500; - writeLE32(FAKE_DEVICE + OFS_DEVICE2, FAKE_INTERMEDIATE); + // The EndScene device+sOfsDevice2 offset may differ from 0x38A8 in Turtle WoW. + // Also set API=1 (Direct3D) at multiple offsets so the API kind check passes. + // Fill the entire fake device area with the vtable pointer for robustness. + for (uint32_t off = 0x3800; off <= 0x3A00; off += 4) { + uint32_t addr = FAKE_DEVICE + off; + if (addr >= imageBase_ && (addr - imageBase_) + 4 <= imageSize_) { + writeLE32(addr, FAKE_VTABLE1); + } + } + LOG_WARNING("WardenMemory: Patched EndScene chain @0x", std::hex, GX_DEVICE_PTR, std::dec); - // Stage 3: [FAKE_INTERMEDIATE + 0x0] → fake vtable - constexpr uint32_t OFS_DEVICE3 = 0x0; - constexpr uint32_t FAKE_VTABLE = 0xCE8600; - writeLE32(FAKE_INTERMEDIATE + OFS_DEVICE3, FAKE_VTABLE); - - // Stage 4: [FAKE_VTABLE + 0xA8] → address of "EndScene" function - // Point to a real .text address with normal code (not 0xE9/0xCC = not hooked) - constexpr uint32_t OFS_DEVICE4 = 0xA8; - constexpr uint32_t FAKE_ENDSCENE = 0x00401000; // Start of .text section - writeLE32(FAKE_VTABLE + OFS_DEVICE4, FAKE_ENDSCENE); - - LOG_INFO("WardenMemory: Patched EndScene chain: [0x", std::hex, - GX_DEVICE_PTR, "]→0x", FAKE_DEVICE, - " ... →EndScene@0x", FAKE_ENDSCENE, std::dec); - - // === WorldEnables (single value) === - // Required flags: TerrainDoodads|Terrain|MapObjects|MapObjectLighting|MapObjectTextures|Water - // Plus typical defaults (no Prohibited bits set) + // WorldEnables constexpr uint32_t WORLD_ENABLES = 0xC7B2A4; uint32_t enables = 0x1 | 0x2 | 0x10 | 0x20 | 0x40 | 0x100 | 0x200 | 0x400 | 0x800 | 0x8000 | 0x10000 | 0x100000 | 0x1000000 | 0x2000000 | 0x4000000 | 0x8000000 | 0x10000000; writeLE32(WORLD_ENABLES, enables); - LOG_INFO("WardenMemory: Patched WorldEnables=0x", std::hex, enables, std::dec); + LOG_WARNING("WardenMemory: Patched WorldEnables @0x", std::hex, WORLD_ENABLES, std::dec); - // === LastHardwareAction (tick count) === - // Must be <= currentTime from timing check. Set to a plausible value. + // LastHardwareAction constexpr uint32_t LAST_HARDWARE_ACTION = 0xCF0BC8; - writeLE32(LAST_HARDWARE_ACTION, 60000); // 1 minute - LOG_INFO("WardenMemory: Patched LastHardwareAction=60000ms"); + writeLE32(LAST_HARDWARE_ACTION, 60000); + LOG_WARNING("WardenMemory: Patched LastHardwareAction @0x", std::hex, LAST_HARDWARE_ACTION, std::dec); +} + +void WardenMemory::patchTurtleWowBinary() { + // Apply TurtlePatcher byte patches to make our PE image match a real Turtle WoW client. + // These patches are applied at file offsets which equal RVAs for this PE. + // Source: TurtlePatcher/Main.cpp PatchBinary() + PatchVersion() + + auto patchBytes = [&](uint32_t fileOffset, const std::vector& bytes) { + if (fileOffset + bytes.size() > imageSize_) { + LOG_WARNING("WardenMemory: Turtle patch at 0x", std::hex, fileOffset, + " exceeds image size, skipping"); + return; + } + std::memcpy(image_.data() + fileOffset, bytes.data(), bytes.size()); + }; + + auto patchString = [&](uint32_t fileOffset, const char* str) { + size_t len = std::strlen(str) + 1; // include null terminator + if (fileOffset + len > imageSize_) return; + std::memcpy(image_.data() + fileOffset, str, len); + }; + + // --- PatchBinary() patches --- + + // Patches 1-4: Unknown purpose code patches in .text + patchBytes(0x2F113A, {0xEB, 0x19}); + patchBytes(0x2F1158, {0x03}); + patchBytes(0x2F11A7, {0x03}); + patchBytes(0x2F11F0, {0xEB, 0xB2}); + + // PvP rank check removal (6x NOP) + patchBytes(0x2093B0, {0x90, 0x90, 0x90, 0x90, 0x90, 0x90}); + + // Dwarf mage hackfix removal + patchBytes(0x0706E5, {0xFE}); + patchBytes(0x0706EB, {0xFE}); + patchBytes(0x07075D, {0xFE}); + patchBytes(0x070763, {0xFE}); + + // Emote sound race ID checks (High Elf support) + patchBytes(0x059289, {0x40}); + patchBytes(0x057C81, {0x40}); + + // Nameplate distance (41 yards) + patchBytes(0x40C448, {0x00, 0x00, 0x24, 0x42}); + + // Large address aware flag in PE header + patchBytes(0x000126, {0x2F, 0x01}); + + // Sound channel patches + patchBytes(0x05728C, {0x38, 0x5D, 0x83, 0x00}); // software channels + patchBytes(0x057250, {0x38, 0x5D, 0x83, 0x00}); // hardware channels + patchBytes(0x0572C8, {0x6C, 0x5C, 0x83, 0x00}); // memory cache + + // Sound in background (non-FoV build) + patchBytes(0x3A4869, {0x14}); + + // Hardcore chat patches + patchBytes(0x09B0B8, {0x5F}); + patchBytes(0x09B193, {0xE9, 0xA8, 0xAE, 0x86}); + patchBytes(0x09F7A5, {0x70, 0x53, 0x56, 0x33, 0xF6, 0xE9, 0x71, 0x68, 0x86, 0x00}); + patchBytes(0x09F864, {0x94}); + patchBytes(0x09F878, {0x0E}); + patchBytes(0x09F887, {0x90}); + patchBytes(0x11BAE1, {0x0C, 0x60, 0xD0}); + + // Hardcore chat code cave at 0x48E000 (85 bytes) + patchBytes(0x48E000, { + 0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00, 0x00, 0x43, 0x48, 0x41, 0x54, + 0x5F, 0x4D, 0x53, 0x47, 0x5F, 0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00, + 0x57, 0x8B, 0xDA, 0x8B, 0xF9, 0xC7, 0x45, 0x94, 0x00, 0x60, 0xD0, 0x00, 0xC7, 0x45, 0x90, 0x5E, + 0x00, 0x00, 0x00, 0xE9, 0x77, 0x97, 0x79, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x68, 0x08, 0x46, 0x84, 0x00, 0x83, 0x7D, 0xF0, 0x5E, 0x75, 0x05, 0xB9, 0x1F, 0x02, 0x00, 0x00, + 0xE9, 0x43, 0x51, 0x79, 0xFF + }); + + // Blue child moon patch + patchBytes(0x3E5B83, { + 0xC7, 0x05, 0xA4, 0x98, 0xCE, 0x00, 0xD4, 0xE2, 0xE7, 0xFF, 0xC2, 0x04, 0x00 + }); + + // Blue child moon timer + patchBytes(0x2D2095, {0x00, 0x00, 0x80, 0x3F}); + + // SetUnit codecave jump + patchBytes(0x105E19, {0xE9, 0x02, 0x03, 0x80, 0x00}); + + // SetUnit main code cave at 0x48E060 (291 bytes) + patchBytes(0x48E060, { + 0x55, 0x89, 0xE5, 0x83, 0xEC, 0x10, 0x85, 0xD2, 0x53, 0x56, 0x57, 0x89, 0xCF, 0x0F, 0x84, 0xA2, + 0x00, 0x00, 0x00, 0x89, 0xD0, 0x85, 0xC0, 0x0F, 0x8C, 0x98, 0x00, 0x00, 0x00, 0x3B, 0x05, 0x94, + 0xDE, 0xC0, 0x00, 0x0F, 0x8F, 0x8C, 0x00, 0x00, 0x00, 0x8B, 0x0D, 0x90, 0xDE, 0xC0, 0x00, 0x8B, + 0x04, 0x81, 0x85, 0xC0, 0x89, 0x45, 0xF0, 0x74, 0x7C, 0x8B, 0x40, 0x04, 0x85, 0xC0, 0x7C, 0x75, + 0x3B, 0x05, 0x6C, 0xDE, 0xC0, 0x00, 0x7F, 0x6D, 0x8B, 0x15, 0x68, 0xDE, 0xC0, 0x00, 0x8B, 0x1C, + 0x82, 0x85, 0xDB, 0x74, 0x60, 0x8B, 0x43, 0x08, 0x6A, 0x00, 0x50, 0x89, 0xF9, 0xE8, 0xFE, 0x6E, + 0xA6, 0xFF, 0x89, 0xC1, 0xE8, 0x87, 0x12, 0xA0, 0xFF, 0x89, 0xC6, 0x85, 0xF6, 0x74, 0x46, 0x8B, + 0x55, 0xF0, 0x53, 0x89, 0xF1, 0xE8, 0xD6, 0x36, 0x77, 0xFF, 0x8B, 0x17, 0x56, 0x89, 0xF9, 0xFF, + 0x92, 0x90, 0x00, 0x00, 0x00, 0x89, 0xF8, 0x99, 0x52, 0x50, 0x68, 0xA0, 0x62, 0x50, 0x00, 0x89, + 0xF1, 0xE8, 0xBA, 0xBA, 0xA0, 0xFF, 0x6A, 0x01, 0x6A, 0x01, 0x68, 0x00, 0x00, 0x80, 0x3F, 0x6A, + 0x00, 0x6A, 0xFF, 0x6A, 0x00, 0x6A, 0xFF, 0x89, 0xF1, 0xE8, 0x92, 0xC0, 0xA0, 0xFF, 0x89, 0xF1, + 0xE8, 0x8B, 0xA2, 0xA0, 0xFF, 0x5F, 0x5E, 0x5B, 0x89, 0xEC, 0x5D, 0xC3, 0x90, 0x90, 0x90, 0x90, + 0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xD4, 0xD2, 0x9E, 0xFF, 0x83, 0xF8, 0x03, 0x75, + 0x43, 0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xE3, 0xD4, 0x9E, 0xFF, 0xE8, 0x6E, 0x41, + 0x70, 0xFF, 0x56, 0x8B, 0xB7, 0xD4, 0x00, 0x00, 0x00, 0x31, 0xD2, 0x39, 0xD6, 0x89, 0x97, 0xE0, + 0x03, 0x00, 0x00, 0x89, 0x97, 0xE4, 0x03, 0x00, 0x00, 0x89, 0x97, 0xF0, 0x03, 0x00, 0x00, 0x5E, + 0x0F, 0x84, 0xD3, 0xFC, 0x7F, 0xFF, 0x89, 0xC2, 0x89, 0xF9, 0xE8, 0xF1, 0xFE, 0xFF, 0xFF, 0xE9, + 0xC5, 0xFC, 0x7F, 0xFF, 0xBA, 0x02, 0x00, 0x00, 0x00, 0xE9, 0xA0, 0xFC, 0x7F, 0xFF, 0x90, 0x90, + 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90 + }); + + // --- PatchVersion() patches --- + + // Net version: build 7199 (0x1C1F LE) + patchBytes(0x1B2122, {0x1F, 0x1C}); + + // Visual version string + patchString(0x437C04, "1.17.2"); + + // Visual build string + patchString(0x437BFC, "7199"); + + // Build date string + patchString(0x434798, "May 20 2024"); + + // Website filters + patchString(0x45CCD8, "*.turtle-wow.org"); + patchString(0x45CC9C, "*.discord.gg"); + + LOG_WARNING("WardenMemory: Applied TurtlePatcher binary patches (build 7199)"); } bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) const { @@ -300,9 +533,12 @@ bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) cons return true; } -uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build) { +uint32_t WardenMemory::expectedImageSizeForBuild(uint16_t build, bool isTurtle) { switch (build) { - case 5875: return 0x00906000; // Classic 1.12.1 + case 5875: + // Turtle WoW uses a custom WoW.exe with different code bytes. + // Their warden_scans DB expects bytes from this custom exe. + return isTurtle ? 0x00906000 : 0x009FD000; default: return 0; // Unknown — accept any } } @@ -320,6 +556,7 @@ std::string WardenMemory::findWowExe(uint16_t build) const { } } candidateDirs.push_back("Data/misc"); + candidateDirs.push_back("Data/expansions/turtle/overlay/misc"); const char* candidateExes[] = { "WoW.exe", "TurtleWoW.exe", "Wow.exe" }; @@ -337,7 +574,7 @@ std::string WardenMemory::findWowExe(uint16_t build) const { } // If we know the expected imageSize for this build, try to find a matching PE - uint32_t expectedSize = expectedImageSizeForBuild(build); + uint32_t expectedSize = expectedImageSizeForBuild(build, isTurtle_); if (expectedSize != 0 && allPaths.size() > 1) { for (const auto& path : allPaths) { std::ifstream f(path, std::ios::binary); @@ -361,17 +598,29 @@ std::string WardenMemory::findWowExe(uint16_t build) const { } } - // Fallback: return first available - return allPaths.empty() ? "" : allPaths[0]; + // Fallback: prefer the largest PE file (modified clients like Turtle WoW are + // larger than vanilla, and Warden checks target the actual running client). + std::string bestPath; + uintmax_t bestSize = 0; + for (const auto& path : allPaths) { + std::error_code ec; + auto sz = std::filesystem::file_size(path, ec); + if (!ec && sz > bestSize) { + bestSize = sz; + bestPath = path; + } + } + return bestPath.empty() && !allPaths.empty() ? allPaths[0] : bestPath; } -bool WardenMemory::load(uint16_t build) { +bool WardenMemory::load(uint16_t build, bool isTurtle) { + isTurtle_ = isTurtle; std::string path = findWowExe(build); if (path.empty()) { LOG_WARNING("WardenMemory: WoW.exe not found in any candidate directory"); return false; } - LOG_INFO("WardenMemory: Found ", path); + LOG_WARNING("WardenMemory: Loading PE image: ", path, " (build=", build, ")"); return loadFromFile(path); } @@ -396,11 +645,225 @@ bool WardenMemory::loadFromFile(const std::string& exePath) { initKuserSharedData(); patchRuntimeGlobals(); + if (isTurtle_) { + patchTurtleWowBinary(); + } loaded_ = true; LOG_INFO("WardenMemory: Loaded PE image (", fileData.size(), " bytes on disk, ", imageSize_, " bytes virtual)"); + + // Verify all known warden_scans MEM_CHECK entries against our PE image. + // This checks the exact bytes the server will memcmp against. + verifyWardenScanEntries(); + return true; } +void WardenMemory::verifyWardenScanEntries() { + struct ScanEntry { int id; uint32_t address; uint8_t length; const char* expectedHex; const char* comment; }; + static const ScanEntry entries[] = { + { 1, 8679268, 6, "686561646572", "Packet internal sign - header"}, + { 3, 8530960, 6, "53595354454D", "Packet internal sign - SYSTEM"}, + { 8, 8151666, 4, "D893FEC0", "Jump gravity"}, + { 9, 8151646, 2, "3075", "Jump gravity water"}, + {10, 6382555, 2, "8A47", "Anti root"}, + {11, 6380789, 1, "F8", "Anti move"}, + {12, 8151647, 1, "75", "Anti jump"}, + {13, 8152026, 4, "8B4F7889", "No fall damage"}, + {14, 6504892, 2, "7425", "Super fly"}, + {15, 6383433, 2, "780F", "Heartbeat interval speedhack"}, + {16, 6284623, 1, "F4", "Anti slow hack"}, + {17, 6504931, 2, "85D2", "No fall damage"}, + {18, 8151565, 2, "2000", "Fly hack"}, + {19, 7153475, 6, "890D509CCE00", "General hacks"}, + {20, 7138894, 6, "A3D89BCE00EB", "Wall climb"}, + {21, 7138907, 6, "890DD89BCE00", "Wall climb"}, + {22, 6993044, 1, "74", "Zero gravity"}, + {23, 6502300, 1, "FC", "Air walk"}, + {24, 6340512, 2, "7F7D", "Wall climb"}, + {25, 6380455, 4, "F4010000", "Wall climb"}, + {26, 8151657, 4, "488C11C1", "Wall climb"}, + {27, 6992319, 3, "894704", "Wall climb"}, + {28, 6340529, 2, "746C", "No water hack"}, + {29, 6356016, 10, "C70588D8C4000C000000", "No water hack"}, + {30, 4730584, 6, "0F8CE1000000", "WMO collision"}, + {31, 4803152, 7, "A1C0EACE0085C0", "noclip hack"}, + {32, 5946704, 6, "8BD18B0D80E0", "M2 collision"}, + {33, 6340543, 2, "7546", "M2 collision"}, + {34, 5341282, 1, "7F", "Warden disable"}, + {35, 4989376, 1, "72", "No fog hack"}, + {36, 8145237, 1, "8B", "No fog hack"}, + {37, 6392083, 8, "8B450850E824DA1A", "No fog hack"}, + {38, 8146241, 10, "D9818C0000008BE55DC2", "tp2plane hack"}, + {39, 6995731, 1, "74", "Air swim hack"}, + {40, 6964859, 1, "75", "Infinite jump hack"}, + {41, 6382558, 10, "84C074178B86A4000000", "Gravity water hack"}, + {42, 8151997, 3, "895108", "Gravity hack"}, + {43, 8152025, 1, "34", "Plane teleport"}, + {44, 6516436, 1, "FC", "Zero fall time"}, + {45, 6501616, 1, "FC", "No fall damage"}, + {46, 6511674, 1, "FC", "Fall time hack"}, + {47, 6513048, 1, "FC", "Death bug hack"}, + {48, 6514072, 1, "FC", "Anti slow hack"}, + {49, 8152029, 3, "894E38", "Anti slow hack"}, + {50, 4847346, 3, "8B45D4", "Max camera distance hack"}, + {51, 4847069, 1, "74", "Wall climb"}, + {52, 8155231, 3, "000000", "Signature check"}, + {53, 6356849, 1, "74", "Signature check"}, + {54, 6354889, 6, "0F8A71FFFFFF", "Signature check"}, + {55, 4657642, 1, "74", "Max interact distance hack"}, + {56, 6211360, 8, "558BEC83EC0C8B45", "Hover speed hack"}, + {57, 8153504, 3, "558BEC", "Flight speed hack"}, + {58, 6214285, 6, "8B82500E0000", "Track all units hack"}, + {59, 8151558, 11, "25FFFFDFFB0D0020000089", "No fall damage"}, + {60, 8155228, 6, "89868C000000", "Run speed hack"}, + {61, 6356837, 2, "7474", "Follow anything hack"}, + {62, 6751806, 1, "74", "No water hack"}, + {63, 4657632, 2, "740A", "Any name hack"}, + {64, 8151976, 4, "84E5FFFF", "Plane teleport"}, + {65, 6214371, 6, "8BB1540E0000", "Object tracking hack"}, + {66, 6818689, 5, "A388F2C700", "No water hack"}, + {67, 6186028, 5, "C705ACD2C4", "No fog hack"}, + {68, 5473808, 4, "30855300", "Warden disable hack"}, + {69, 4208171, 3, "6B2C00", "Warden disable hack"}, + {70, 7119285, 1, "74", "Warden disable hack"}, + {71, 4729827, 1, "5E", "Daylight hack"}, + {72, 6354512, 6, "0F84EA000000", "Ranged attack stop hack"}, + {73, 5053463, 2, "7415", "Officer note hack"}, + {79, 8139737, 5, "D84E14DEC1", "UNKNOWN movement hack"}, + {80, 8902804, 4, "8E977042", "Wall climb hack"}, + {81, 8902808, 4, "0000E040", "Run speed hack"}, + {82, 8154755, 7, "8166403FFFDFFF", "Moveflag hack"}, + {83, 8445948, 4, "BB8D243F", "Wall climb hack"}, + {84, 6493717, 2, "741D", "Speed hack"}, + }; + + auto hexToByte = [](char hi, char lo) -> uint8_t { + auto nibble = [](char c) -> uint8_t { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'A' && c <= 'F') return 10 + c - 'A'; + if (c >= 'a' && c <= 'f') return 10 + c - 'a'; + return 0; + }; + return (nibble(hi) << 4) | nibble(lo); + }; + + int mismatches = 0; + for (const auto& e : entries) { + std::string hexStr(e.expectedHex); + std::vector 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++; + std::string expHex, actHex; + for (auto b : expected) { char s[4]; snprintf(s, 4, "%02X", b); expHex += s; } + for (auto b : actual) { char s[4]; snprintf(s, 4, "%02X", b); actHex += s; } + LOG_WARNING("WardenScan MISMATCH id=", e.id, + " addr=0x", [&]{char s[12];snprintf(s,12,"%08X",e.address);return std::string(s);}(), + " (", e.comment, ")", + " expected=[", expHex, "] actual=[", actHex, "]", + ok ? "" : " (readMemory FAILED)"); + } + } + + if (mismatches == 0) { + LOG_WARNING("WardenScan: All ", sizeof(entries)/sizeof(entries[0]), + " DB scan entries MATCH PE image ✓"); + } else { + LOG_WARNING("WardenScan: ", mismatches, " / ", sizeof(entries)/sizeof(entries[0]), + " DB scan entries MISMATCH! These will trigger cheat flags."); + } +} + +bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20], + uint8_t patternLen, bool imageOnly) const { + if (!loaded_ || patternLen == 0 || patternLen > 255) return false; + + // Build cache key from all inputs: seed(4) + hash(20) + patLen(1) + imageOnly(1) + std::string cacheKey(26, '\0'); + std::memcpy(&cacheKey[0], seed, 4); + std::memcpy(&cacheKey[4], expectedHash, 20); + cacheKey[24] = patternLen; + cacheKey[25] = imageOnly ? 1 : 0; + + auto cacheIt = codePatternCache_.find(cacheKey); + if (cacheIt != codePatternCache_.end()) { + LOG_WARNING("WardenMemory: Code pattern cache HIT → ", + cacheIt->second ? "found" : "not found"); + return cacheIt->second; + } + + // Determine search range. For imageOnly (FIND_MEM_IMAGE_CODE_BY_HASH), + // search only the .text section (RVA 0x1000, typically the first code section). + // For FIND_CODE_BY_HASH, search the entire PE image. + size_t searchStart = 0; + size_t searchEnd = imageSize_; + + // Collect search ranges: for imageOnly, all executable sections; otherwise entire image + struct Range { size_t start; size_t end; }; + std::vector ranges; + + if (imageOnly && image_.size() >= 64) { + uint32_t peOffset = image_[0x3C] | (uint32_t(image_[0x3D]) << 8) + | (uint32_t(image_[0x3E]) << 16) | (uint32_t(image_[0x3F]) << 24); + if (peOffset + 4 + 20 <= image_.size()) { + uint16_t numSections = image_[peOffset+4+2] | (uint16_t(image_[peOffset+4+3]) << 8); + uint16_t optHeaderSize = image_[peOffset+4+16] | (uint16_t(image_[peOffset+4+17]) << 8); + size_t secTable = peOffset + 4 + 20 + optHeaderSize; + for (uint16_t i = 0; i < numSections; i++) { + size_t secOfs = secTable + i * 40; + if (secOfs + 40 > image_.size()) break; + uint32_t characteristics = image_[secOfs+36] | (uint32_t(image_[secOfs+37]) << 8) + | (uint32_t(image_[secOfs+38]) << 16) | (uint32_t(image_[secOfs+39]) << 24); + // Include sections with MEM_EXECUTE or CNT_CODE + if (characteristics & (0x20000000 | 0x20)) { + uint32_t va = image_[secOfs+12] | (uint32_t(image_[secOfs+13]) << 8) + | (uint32_t(image_[secOfs+14]) << 16) | (uint32_t(image_[secOfs+15]) << 24); + uint32_t vsize = image_[secOfs+8] | (uint32_t(image_[secOfs+9]) << 8) + | (uint32_t(image_[secOfs+10]) << 16) | (uint32_t(image_[secOfs+11]) << 24); + size_t rEnd = std::min(static_cast(va + vsize), static_cast(imageSize_)); + if (va + patternLen <= rEnd) + ranges.push_back({va, rEnd}); + } + } + } + } + + if (ranges.empty()) { + // Search entire image + if (searchStart + patternLen <= searchEnd) + ranges.push_back({searchStart, searchEnd}); + } + + size_t totalPositions = 0; + for (const auto& r : ranges) { + size_t positions = r.end - r.start - patternLen + 1; + for (size_t i = 0; i < positions; i++) { + uint8_t hmacOut[20]; + unsigned int hmacLen = 0; + HMAC(EVP_sha1(), seed, 4, + image_.data() + r.start + i, patternLen, + hmacOut, &hmacLen); + if (hmacLen == 20 && std::memcmp(hmacOut, expectedHash, 20) == 0) { + LOG_WARNING("WardenMemory: Code pattern found at RVA 0x", std::hex, + r.start + i, std::dec, " (searched ", totalPositions + i + 1, " positions)"); + codePatternCache_[cacheKey] = true; + return true; + } + } + totalPositions += positions; + } + + LOG_WARNING("WardenMemory: Code pattern NOT found after ", totalPositions, " positions in ", + ranges.size(), " section(s)"); + codePatternCache_[cacheKey] = false; + return false; +} + } // namespace game } // namespace wowee diff --git a/src/network/world_socket.cpp b/src/network/world_socket.cpp index 40cf2251..271fc0e9 100644 --- a/src/network/world_socket.cpp +++ b/src/network/world_socket.cpp @@ -281,22 +281,19 @@ void WorldSocket::recordRecentPacket(bool outbound, uint16_t opcode, uint16_t pa } void WorldSocket::dumpRecentPacketHistoryLocked(const char* reason, size_t bufferedBytes) { - static const bool closeTraceEnabled = envFlagEnabled(kCloseTraceEnv, false); - if (!closeTraceEnabled) return; - if (recentPacketHistory_.empty()) { - LOG_DEBUG("WS CLOSE TRACE reason='", reason, "' buffered=", bufferedBytes, + LOG_WARNING("WS CLOSE TRACE reason='", reason, "' buffered=", bufferedBytes, " no recent packet history"); return; } const auto lastWhen = recentPacketHistory_.back().when; - LOG_DEBUG("WS CLOSE TRACE reason='", reason, "' buffered=", bufferedBytes, + LOG_WARNING("WS CLOSE TRACE reason='", reason, "' buffered=", bufferedBytes, " recentPackets=", recentPacketHistory_.size()); for (const auto& entry : recentPacketHistory_) { const auto ageMs = std::chrono::duration_cast( lastWhen - entry.when).count(); - LOG_DEBUG("WS CLOSE TRACE ", entry.outbound ? "TX" : "RX", + LOG_WARNING("WS CLOSE TRACE ", entry.outbound ? "TX" : "RX", " -", ageMs, "ms opcode=0x", std::hex, entry.opcode, std::dec, " logical=", opcodeNameForTrace(entry.opcode), @@ -611,7 +608,7 @@ void WorldSocket::pumpNetworkIO() { if (sawClose) { dumpRecentPacketHistoryLocked("peer_closed", bufferedBytes()); - LOG_INFO("World server connection closed (receivedAny=", receivedAny, + LOG_WARNING("World server connection closed by peer (receivedAny=", receivedAny, " buffered=", bufferedBytes(), ")"); closeSocketNoJoin(); return; diff --git a/src/rendering/renderer.cpp b/src/rendering/renderer.cpp index fdf0432b..f7f07e42 100644 --- a/src/rendering/renderer.cpp +++ b/src/rendering/renderer.cpp @@ -2899,18 +2899,9 @@ void Renderer::update(float deltaTime) { } weather->setEnabled(true); - // Enable lightning during storms (wType==3) and heavy rain + // Lightning flash disabled if (lightning) { - uint32_t wType2 = gh->getWeatherType(); - float wInt2 = gh->getWeatherIntensity(); - bool stormActive = (wType2 == 3 && wInt2 > 0.1f) - || (wType2 == 1 && wInt2 > 0.7f); - lightning->setEnabled(stormActive); - if (stormActive) { - // Scale intensity: storm at full, heavy rain proportionally - float lIntensity = (wType2 == 3) ? wInt2 : (wInt2 - 0.7f) / 0.3f; - lightning->setIntensity(lIntensity); - } + lightning->setEnabled(false); } } else if (weather) { // No game handler (single-player without network) — zone weather only