Compare commits

...

4 commits

Author SHA1 Message Date
Kelsi
ad511dad5e fix: correct KUSER_SHARED_DATA field offsets for Warden anticheat
Some checks are pending
Build / Build (arm64) (push) Waiting to run
Build / Build (x86-64) (push) Waiting to run
Build / Build (macOS arm64) (push) Waiting to run
Build / Build (windows-arm64) (push) Waiting to run
Build / Build (windows-x86-64) (push) Waiting to run
Security / CodeQL (C/C++) (push) Waiting to run
Security / Semgrep (push) Waiting to run
Security / Sanitizer Build (ASan/UBSan) (push) Waiting to run
Multiple fields were at wrong offsets causing MEM_CHECK comparison
failures against expected Windows 7 SP1 values. Key fixes:
- LargePageMinimum: 0x248→0x244
- NtProductType at 0x264 was 0, now 1 (VER_NT_WORKSTATION)
- ProductTypeIsValid at 0x268 was missing
- ProcessorFeatures at 0x274 was clobbered by misplaced NtProductType
- NumberOfPhysicalPages: 0x300→0x2E8
- ActiveConsoleId at 0x2D8 was 4, now 1
- Added SuiteMask, NXSupportPolicy, and other missing fields
2026-03-16 20:55:30 -07:00
Kelsi
e3c2269b16 fix: increase descriptor pool sizes to prevent Vulkan crash
Terrain pool 16384→65536, WMO pool 8192→32768. The previous sizes
were too small for the load/unload radii, causing pool exhaustion
and a hard crash when streaming terrain on large maps.
2026-03-16 17:46:32 -07:00
Kelsi
6fd32ecdc6 fix: skip Warden HASH_RESULT on strict servers when no CR match
Sending a wrong hash to AzerothCore/WotLK servers triggers an
account ban. When no pre-computed challenge-response entry matches
the server seed, skip the response entirely so the server times out
with a kick (recoverable) instead of verifying a bad hash and
banning (unrecoverable). Turtle/Classic servers remain unchanged
as they only log Warden failures.

Also adds RX silence detection and fixes Turtle isTurtle flag
propagation in MEM_CHECK path.
2026-03-16 17:38:25 -07:00
Kelsi
a3279ea1ad fix: async Warden PAGE_A/PAGE_B checks to prevent main-loop stalls
Move 5-second brute-force HMAC-SHA1 code pattern searches to a
background thread via std::async. The main loop now detects PAGE_A/B
checks, launches the response builder async, and drains the result
in update() — encrypting and sending on the main thread to keep
wardenCrypto_ RC4 state thread-safe.

Also adds Turtle WoW PE binary support (isTurtle flag, dedicated exe
search, runtime patches), searchCodePattern with result caching,
writeLE32 public API, and Warden scan entry verification.
2026-03-16 16:46:29 -07:00
8 changed files with 1073 additions and 255 deletions

View file

@ -22,6 +22,7 @@
#include <optional>
#include <algorithm>
#include <chrono>
#include <future>
namespace wowee::game {
class TransportManager;
@ -3144,6 +3145,14 @@ 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<std::vector<uint8_t>> wardenPendingEncrypted_; // encrypted response bytes
bool wardenResponsePending_ = false;
// ---- RX silence detection ----
std::chrono::steady_clock::time_point lastRxTime_{};
bool rxSilenceLogged_ = false;
// ---- XP tracking ----
uint32_t playerXp_ = 0;
uint32_t playerNextLevelXp_ = 0;

View file

@ -3,6 +3,7 @@
#include <vector>
#include <cstdint>
#include <string>
#include <unordered_map>
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<uint8_t>& 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<std::string, bool> codePatternCache_;
};
} // namespace game

View file

@ -171,7 +171,7 @@ private:
// Descriptor pool for material sets
VkDescriptorPool materialDescPool = VK_NULL_HANDLE;
static constexpr uint32_t MAX_MATERIAL_SETS = 16384;
static constexpr uint32_t MAX_MATERIAL_SETS = 65536;
// Loaded terrain chunks
std::vector<TerrainChunkGPU> chunks;

View file

@ -656,7 +656,7 @@ private:
// Descriptor pool for material sets
VkDescriptorPool materialDescPool_ = VK_NULL_HANDLE;
static constexpr uint32_t MAX_MATERIAL_SETS = 8192;
static constexpr uint32_t MAX_MATERIAL_SETS = 32768;
// Texture cache (path -> VkTexture)
struct TextureCacheEntry {

View file

@ -836,6 +836,40 @@ 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<uint8_t> 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 RX silence (server stopped sending packets but TCP still open)
if (state == WorldState::IN_WORLD && socket && socket->isConnected() &&
lastRxTime_.time_since_epoch().count() > 0) {
auto silenceMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - lastRxTime_).count();
if (silenceMs > 10000 && !rxSilenceLogged_) {
rxSilenceLogged_ = true;
LOG_WARNING("RX SILENCE: No packets from server for ", silenceMs, "ms — possible soft disconnect");
}
if (silenceMs > 15000 && silenceMs < 15500) {
LOG_WARNING("RX SILENCE: 15s — server appears to have stopped sending");
}
}
// Detect server-side disconnect (socket closed during update)
if (socket && !socket->isConnected() && state != WorldState::DISCONNECTED) {
if (pendingIncomingPackets_.empty() && pendingUpdateObjectWork_.empty()) {
@ -8242,6 +8276,8 @@ void GameHandler::enqueueIncomingPacket(const network::Packet& packet) {
pendingIncomingPackets_.pop_front();
}
pendingIncomingPackets_.push_back(packet);
lastRxTime_ = std::chrono::steady_clock::now();
rxSilenceLogged_ = false;
}
void GameHandler::enqueueIncomingPacketFront(network::Packet&& packet) {
@ -9322,16 +9358,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
}
if (match) {
LOG_DEBUG("Warden: Found matching CR entry for seed");
// Log the reply we're sending
{
std::string replyHex;
for (int i = 0; i < 20; i++) {
char s[4]; snprintf(s, 4, "%02x", match->reply[i]); replyHex += s;
}
LOG_DEBUG("Warden: Sending pre-computed reply=", replyHex);
}
LOG_WARNING("Warden: HASH_REQUEST — CR entry MATCHED, sending pre-computed reply");
// Send HASH_RESULT (opcode 0x04 + 20-byte reply)
std::vector<uint8_t> resp;
@ -9345,7 +9372,7 @@ void GameHandler::handleWardenData(network::Packet& packet) {
std::vector<uint8_t> newDecryptKey(match->serverKey, match->serverKey + 16);
wardenCrypto_->replaceKeys(newEncryptKey, newDecryptKey);
LOG_DEBUG("Warden: Switched to CR key set");
LOG_WARNING("Warden: Switched to CR key set");
wardenState_ = WardenState::WAIT_CHECKS;
break;
@ -9354,129 +9381,51 @@ void GameHandler::handleWardenData(network::Packet& packet) {
}
}
// --- Fallback: compute hash from loaded module ---
LOG_WARNING("Warden: No CR match, computing hash from loaded module");
// --- No CR match: decide strategy based on server strictness ---
{
std::string seedHex;
for (auto b : seed) { char s[4]; snprintf(s, 4, "%02x", b); seedHex += s; }
if (!wardenLoadedModule_ || !wardenLoadedModule_->isLoaded()) {
LOG_WARNING("Warden: No loaded module and no CR match — using raw module fallback hash");
bool isTurtle = isActiveExpansion("turtle");
bool isClassic = (build <= 6005) && !isTurtle;
if (!isTurtle && !isClassic) {
// WotLK/TBC (AzerothCore, etc.): strict servers BAN for wrong HASH_RESULT.
// Without a matching CR entry we cannot compute the correct hash
// (requires executing the module's native init function).
// Safest action: don't respond. Server will time-out and kick (not ban).
LOG_WARNING("Warden: HASH_REQUEST seed=", seedHex,
" — no CR match, SKIPPING response to avoid account ban");
LOG_WARNING("Warden: To fix, provide a .cr file with the correct seed→reply entry for this module");
// Stay in WAIT_HASH_REQUEST — server will eventually kick.
break;
}
// Turtle/Classic: lenient servers (log-only penalties, no bans).
// Send a best-effort fallback hash so we can continue the handshake.
LOG_WARNING("Warden: No CR match (seed=", seedHex,
"), sending fallback hash (lenient server)");
// Never skip HASH_RESULT: some realms disconnect quickly if this response is missing.
std::vector<uint8_t> fallbackReply;
if (!wardenModuleData_.empty()) {
fallbackReply = auth::Crypto::sha1(wardenModuleData_);
} else if (!wardenModuleHash_.empty()) {
fallbackReply = auth::Crypto::sha1(wardenModuleHash_);
} else {
fallbackReply.assign(20, 0);
if (wardenLoadedModule_ && wardenLoadedModule_->isLoaded()) {
const uint8_t* moduleImage = static_cast<const uint8_t*>(wardenLoadedModule_->getModuleMemory());
size_t moduleImageSize = wardenLoadedModule_->getModuleSize();
if (moduleImage && moduleImageSize > 0) {
std::vector<uint8_t> imageData(moduleImage, moduleImage + moduleImageSize);
fallbackReply = auth::Crypto::sha1(imageData);
}
}
if (fallbackReply.empty()) {
if (!wardenModuleData_.empty())
fallbackReply = auth::Crypto::sha1(wardenModuleData_);
else
fallbackReply.assign(20, 0);
}
std::vector<uint8_t> resp;
resp.push_back(0x04); // WARDEN_CMSG_HASH_RESULT
resp.insert(resp.end(), fallbackReply.begin(), fallbackReply.end());
sendWardenResponse(resp);
applyWardenSeedRekey(seed);
wardenState_ = WardenState::WAIT_CHECKS;
break;
}
{
const uint8_t* moduleImage = static_cast<const uint8_t*>(wardenLoadedModule_->getModuleMemory());
size_t moduleImageSize = wardenLoadedModule_->getModuleSize();
const auto& decompressedData = wardenLoadedModule_->getDecompressedData();
if (!moduleImage || moduleImageSize == 0) {
LOG_WARNING("Warden: Loaded module has no executable image — using raw module hash fallback");
std::vector<uint8_t> fallbackReply =
!wardenModuleData_.empty() ? auth::Crypto::sha1(wardenModuleData_) : std::vector<uint8_t>(20, 0);
std::vector<uint8_t> resp;
resp.push_back(0x04); // WARDEN_CMSG_HASH_RESULT
resp.insert(resp.end(), fallbackReply.begin(), fallbackReply.end());
sendWardenResponse(resp);
applyWardenSeedRekey(seed);
wardenState_ = WardenState::WAIT_CHECKS;
break;
}
// --- Empirical test: try multiple SHA1 computations and check against first CR entry ---
if (!wardenCREntries_.empty()) {
const auto& firstCR = wardenCREntries_[0];
std::string expectedHex;
for (int i = 0; i < 20; i++) { char s[4]; snprintf(s, 4, "%02x", firstCR.reply[i]); expectedHex += s; }
LOG_DEBUG("Warden: Empirical test — expected reply from CR[0]=", expectedHex);
// Test 1: SHA1(moduleImage)
{
std::vector<uint8_t> data(moduleImage, moduleImage + moduleImageSize);
auto h = auth::Crypto::sha1(data);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_DEBUG("Warden: SHA1(moduleImage)=", hex, match ? " MATCH!" : "");
}
// Test 2: SHA1(seed || moduleImage)
{
std::vector<uint8_t> data;
data.insert(data.end(), seed.begin(), seed.end());
data.insert(data.end(), moduleImage, moduleImage + moduleImageSize);
auto h = auth::Crypto::sha1(data);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_DEBUG("Warden: SHA1(seed||image)=", hex, match ? " MATCH!" : "");
}
// Test 3: SHA1(moduleImage || seed)
{
std::vector<uint8_t> data(moduleImage, moduleImage + moduleImageSize);
data.insert(data.end(), seed.begin(), seed.end());
auto h = auth::Crypto::sha1(data);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_DEBUG("Warden: SHA1(image||seed)=", hex, match ? " MATCH!" : "");
}
// Test 4: SHA1(decompressedData)
{
auto h = auth::Crypto::sha1(decompressedData);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_DEBUG("Warden: SHA1(decompressed)=", hex, match ? " MATCH!" : "");
}
// Test 5: SHA1(rawModuleData)
{
auto h = auth::Crypto::sha1(wardenModuleData_);
bool match = (std::memcmp(h.data(), firstCR.reply, 20) == 0);
std::string hex; for (auto b : h) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_DEBUG("Warden: SHA1(rawModule)=", hex, match ? " MATCH!" : "");
}
// Test 6: Check if all CR replies are the same (constant hash)
{
bool allSame = true;
for (size_t i = 1; i < wardenCREntries_.size(); i++) {
if (std::memcmp(wardenCREntries_[i].reply, firstCR.reply, 20) != 0) {
allSame = false;
break;
}
}
LOG_DEBUG("Warden: All ", wardenCREntries_.size(), " CR replies identical? ", allSame ? "YES" : "NO");
}
}
// --- Compute the hash: SHA1(moduleImage) is the most likely candidate ---
// The module's hash response is typically SHA1 of the loaded module image.
// This is a constant per module (seed is not used in the hash, only for key derivation).
std::vector<uint8_t> imageData(moduleImage, moduleImage + moduleImageSize);
auto reply = auth::Crypto::sha1(imageData);
{
std::string hex;
for (auto b : reply) { char s[4]; snprintf(s, 4, "%02x", b); hex += s; }
LOG_DEBUG("Warden: Sending SHA1(moduleImage)=", hex);
}
// Send HASH_RESULT (opcode 0x04 + 20-byte hash)
std::vector<uint8_t> resp;
resp.push_back(0x04);
resp.insert(resp.end(), reply.begin(), reply.end());
sendWardenResponse(resp);
applyWardenSeedRekey(seed);
}
@ -9512,6 +9461,309 @@ 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<WardenMemory>();
if (!wardenMemory_->load(static_cast<uint16_t>(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<uint8_t> {
// 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<uint8_t> 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<uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
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<uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now().time_since_epoch()).count());
wardenMemory_->writeLE32(0xCF0BC8, now - 2000);
}
std::vector<uint8_t> 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 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 {
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;
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()) {
const uint8_t* modMem = static_cast<const uint8_t*>(wardenLoadedModule_->getModuleMemory());
size_t modSize = wardenLoadedModule_->getModuleSize();
if (modMem && modSize >= patLen) {
for (size_t i = 0; i < modSize - patLen + 1; i++) {
uint8_t h[20]; unsigned int hl = 0;
HMAC(EVP_sha1(), seed, 4, modMem+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",
turtleFallback ? " (turtle-fallback)" : "");
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<uint8_t> 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<uint8_t> 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<uint16_t>(resultData.size());
std::vector<uint8_t> 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 };
@ -9654,16 +9906,14 @@ void GameHandler::handleWardenData(network::Packet& packet) {
| (uint32_t(decrypted[pos+2])<<16) | (uint32_t(decrypted[pos+3])<<24);
pos += 4;
uint8_t readLen = decrypted[pos++];
LOG_DEBUG("Warden: MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(),
" len=", (int)readLen);
if (!moduleName.empty()) {
LOG_DEBUG("Warden: MEM module=\"", moduleName, "\"");
}
LOG_WARNING("Warden: (sync) MEM offset=0x", [&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}(),
" len=", (int)readLen,
moduleName.empty() ? "" : (" module=\"" + moduleName + "\""));
// Lazy-load WoW.exe PE image on first MEM_CHECK
if (!wardenMemory_) {
wardenMemory_ = std::make_unique<WardenMemory>();
if (!wardenMemory_->load(static_cast<uint16_t>(build))) {
if (!wardenMemory_->load(static_cast<uint16_t>(build), isActiveExpansion("turtle"))) {
LOG_WARNING("Warden: Could not load WoW.exe for MEM_CHECK");
}
}
@ -9672,6 +9922,21 @@ void GameHandler::handleWardenData(network::Packet& packet) {
std::vector<uint8_t> 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);}());
}
} else {
LOG_WARNING("Warden: MEM_CHECK fallback to zeros for 0x",
[&]{char s[12];snprintf(s,12,"%08x",offset);return std::string(s);}());
@ -9721,7 +9986,16 @@ void GameHandler::handleWardenData(network::Packet& packet) {
(uint32_t(p[26]) << 16) | (uint32_t(p[27]) << 24);
uint8_t len = p[28];
if (isKnownWantedCodeScan(seedBytes, reqHash, off, len)) {
pageResult = 0x4A; // PatternFound
pageResult = 0x4A;
} else if (wardenMemory_ && wardenMemory_->isLoaded() && len > 0) {
if (wardenMemory_->searchCodePattern(seedBytes, reqHash, len, true))
pageResult = 0x4A;
}
// Turtle fallback for 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,
@ -9889,7 +10163,18 @@ void GameHandler::handleWardenData(network::Packet& packet) {
}
}
LOG_DEBUG("Warden: Parsed ", checkCount, " checks, result data size=", resultData.size());
// Log synchronous round summary at WARNING level for diagnostics
{
int syncCounts[10] = {};
// Re-count (we don't have per-check counters in sync path yet)
LOG_WARNING("Warden: (sync) Parsed ", checkCount, " checks, 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: (sync) RESPONSE_HEX [", fullHex, "]");
}
// --- Compute checksum: XOR of 5 uint32s from SHA1(resultData) ---
auto resultHash = auth::Crypto::sha1(resultData);

View file

@ -7,6 +7,8 @@
#include <filesystem>
#include <sstream>
#include <iomanip>
#include <openssl/hmac.h>
#include <openssl/evp.h>
namespace wowee {
namespace game {
@ -106,19 +108,181 @@ bool WardenMemory::parsePE(const std::vector<uint8_t>& 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)
uint32_t ntMajor = 6;
std::memcpy(kuserData_ + 0x026C, &ntMajor, 4);
// -------------------------------------------------------------------
// 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.
// -------------------------------------------------------------------
// NtMinorVersion at offset 0x0270 = 1 (Windows 7)
uint32_t ntMinor = 1;
std::memcpy(kuserData_ + 0x0270, &ntMinor, 4);
auto w32 = [&](uint32_t off, uint32_t v) { std::memcpy(kuserData_ + off, &v, 4); };
auto w16 = [&](uint32_t off, uint16_t v) { std::memcpy(kuserData_ + off, &v, 2); };
auto w8 = [&](uint32_t off, uint8_t v) { kuserData_[off] = v; };
// +0x000 TickCountLowDeprecated (ULONG)
w32(0x0000, 0x003F4A00); // ~70 min uptime
// +0x004 TickCountMultiplier (ULONG)
w32(0x0004, 0x0FA00000);
// +0x008 InterruptTime (KSYSTEM_TIME: Low4 + High1_4 + High2_4)
w32(0x0008, 0x6B49D200);
w32(0x000C, 0x00000029);
w32(0x0010, 0x00000029);
// +0x014 SystemTime (KSYSTEM_TIME) — ~2024 epoch FILETIME
w32(0x0014, 0xA0B71B00);
w32(0x0018, 0x01DA5E80);
w32(0x001C, 0x01DA5E80);
// +0x020 TimeZoneBias (KSYSTEM_TIME) — 0 = UTC
// (leave zeros)
// +0x02C ImageNumberLow / ImageNumberHigh (USHORT each)
w16(0x002C, 0x014C); // IMAGE_FILE_MACHINE_I386
w16(0x002E, 0x014C);
// +0x030 NtSystemRoot (WCHAR[260] = 520 bytes, ends at +0x238)
const wchar_t* sysRoot = L"C:\\WINDOWS";
for (size_t i = 0; i < 10; i++) {
w16(0x0030 + static_cast<uint32_t>(i) * 2, static_cast<uint16_t>(sysRoot[i]));
}
// +0x238 MaxStackTraceDepth (ULONG)
w32(0x0238, 0);
// +0x23C CryptoExponent (ULONG) — 65537
w32(0x023C, 0x00010001);
// +0x240 TimeZoneId (ULONG) — TIME_ZONE_ID_UNKNOWN
w32(0x0240, 0);
// +0x244 LargePageMinimum (ULONG) — 2 MB
w32(0x0244, 0x00200000);
// +0x248 Reserved2[7] (28 bytes) — zeros
// (leave zeros)
// +0x264 NtProductType (NT_PRODUCT_TYPE = ULONG) — VER_NT_WORKSTATION
w32(0x0264, 1);
// +0x268 ProductTypeIsValid (BOOLEAN = UCHAR)
w8(0x0268, 1);
// +0x269 Reserved9[3] — padding
// (leave zeros)
// +0x26C NtMajorVersion (ULONG) — 6 (Windows Vista/7/8/10)
w32(0x026C, 6);
// +0x270 NtMinorVersion (ULONG) — 1 (Windows 7)
w32(0x0270, 1);
// +0x274 ProcessorFeatures (BOOLEAN[64] = 64 bytes, ends at +0x2B4)
// Each entry is a single UCHAR (0 or 1).
// Index Name Value
// [0] PF_FLOATING_POINT_PRECISION_ERRATA 0
// [1] PF_FLOATING_POINT_EMULATED 0
// [2] PF_COMPARE_EXCHANGE_DOUBLE 1
// [3] PF_MMX_INSTRUCTIONS_AVAILABLE 1
// [4] PF_PPC_MOVEMEM_64BIT_OK 0
// [5] PF_ALPHA_BYTE_INSTRUCTIONS 0
// [6] PF_XMMI_INSTRUCTIONS_AVAILABLE (SSE) 1
// [7] PF_3DNOW_INSTRUCTIONS_AVAILABLE 0
// [8] PF_RDTSC_INSTRUCTION_AVAILABLE 1
// [9] PF_PAE_ENABLED 1
// [10] PF_XMMI64_INSTRUCTIONS_AVAILABLE(SSE2)1
// [11] PF_SSE_DAZ_MODE_AVAILABLE 0
// [12] PF_NX_ENABLED 1
// [13] PF_SSE3_INSTRUCTIONS_AVAILABLE 1
// [14] PF_COMPARE_EXCHANGE128 0 (x86 typically 0)
// [15] PF_COMPARE64_EXCHANGE128 0
// [16] PF_CHANNELS_ENABLED 0
// [17] PF_XSAVE_ENABLED 0
w8(0x0274 + 2, 1); // PF_COMPARE_EXCHANGE_DOUBLE
w8(0x0274 + 3, 1); // PF_MMX
w8(0x0274 + 6, 1); // PF_SSE
w8(0x0274 + 8, 1); // PF_RDTSC
w8(0x0274 + 9, 1); // PF_PAE_ENABLED
w8(0x0274 + 10, 1); // PF_SSE2
w8(0x0274 + 12, 1); // PF_NX_ENABLED
w8(0x0274 + 13, 1); // PF_SSE3
// +0x2B4 Reserved1 (ULONG)
// +0x2B8 Reserved3 (ULONG)
// +0x2BC TimeSlip (ULONG)
// +0x2C0 AlternativeArchitecture (ULONG) = 0 (StandardDesign)
// +0x2C4 AltArchitecturePad[1] (ULONG)
// +0x2C8 SystemExpirationDate (LARGE_INTEGER = 8 bytes)
// (leave zeros)
// +0x2D0 SuiteMask (ULONG) — VER_SUITE_SINGLEUSERTS | VER_SUITE_TERMINAL
w32(0x02D0, 0x0110); // 0x0100=SINGLEUSERTS, 0x0010=TERMINAL
// +0x2D4 KdDebuggerEnabled (BOOLEAN = UCHAR)
w8(0x02D4, 0);
// +0x2D5 NXSupportPolicy (UCHAR) — 2 = OptIn
w8(0x02D5, 2);
// +0x2D6 Reserved6[2]
// (leave zeros)
// +0x2D8 ActiveConsoleId (ULONG) — session 0 or 1
w32(0x02D8, 1);
// +0x2DC DismountCount (ULONG)
w32(0x02DC, 0);
// +0x2E0 ComPlusPackage (ULONG)
w32(0x02E0, 0);
// +0x2E4 LastSystemRITEventTickCount (ULONG) — recent input tick
w32(0x02E4, 0x003F4900);
// +0x2E8 NumberOfPhysicalPages (ULONG) — 4GB / 4KB ≈ 1M pages
w32(0x02E8, 0x000FF000);
// +0x2EC SafeBootMode (BOOLEAN) — 0 = normal boot
w8(0x02EC, 0);
// +0x2F0 SharedDataFlags / TraceLogging (ULONG)
w32(0x02F0, 0);
// +0x2F8 TestRetInstruction (ULONGLONG = 8 bytes) — RET opcode
w8(0x02F8, 0xC3); // x86 RET instruction
// +0x300 SystemCall (ULONG)
w32(0x0300, 0);
// +0x304 SystemCallReturn (ULONG)
w32(0x0304, 0);
// +0x308 SystemCallPad[3] (24 bytes)
// (leave zeros)
// +0x320 TickCount (KSYSTEM_TIME) — matches TickCountLowDeprecated
w32(0x0320, 0x003F4A00);
// +0x32C TickCountPad[1]
// (leave zeros)
// +0x330 Cookie (ULONG) — stack cookie, random-looking value
w32(0x0330, 0x4A2F8C15);
// +0x334 ConsoleSessionForegroundProcessId (ULONG) — some PID
w32(0x0334, 0x00001234);
// Everything after +0x338 is typically zero on Win7 x86
}
void WardenMemory::writeLE32(uint32_t va, uint32_t value) {
@ -132,56 +296,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 +355,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 +363,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<uint8_t>& bytes) {
if (fileOffset + bytes.size() > imageSize_) {
LOG_WARNING("WardenMemory: Turtle patch at 0x", std::hex, fileOffset,
" exceeds image size, skipping");
return;
}
std::memcpy(image_.data() + fileOffset, bytes.data(), bytes.size());
};
auto patchString = [&](uint32_t fileOffset, const char* str) {
size_t len = std::strlen(str) + 1; // include null terminator
if (fileOffset + len > imageSize_) return;
std::memcpy(image_.data() + fileOffset, str, len);
};
// --- PatchBinary() patches ---
// Patches 1-4: Unknown purpose code patches in .text
patchBytes(0x2F113A, {0xEB, 0x19});
patchBytes(0x2F1158, {0x03});
patchBytes(0x2F11A7, {0x03});
patchBytes(0x2F11F0, {0xEB, 0xB2});
// PvP rank check removal (6x NOP)
patchBytes(0x2093B0, {0x90, 0x90, 0x90, 0x90, 0x90, 0x90});
// Dwarf mage hackfix removal
patchBytes(0x0706E5, {0xFE});
patchBytes(0x0706EB, {0xFE});
patchBytes(0x07075D, {0xFE});
patchBytes(0x070763, {0xFE});
// Emote sound race ID checks (High Elf support)
patchBytes(0x059289, {0x40});
patchBytes(0x057C81, {0x40});
// Nameplate distance (41 yards)
patchBytes(0x40C448, {0x00, 0x00, 0x24, 0x42});
// Large address aware flag in PE header
patchBytes(0x000126, {0x2F, 0x01});
// Sound channel patches
patchBytes(0x05728C, {0x38, 0x5D, 0x83, 0x00}); // software channels
patchBytes(0x057250, {0x38, 0x5D, 0x83, 0x00}); // hardware channels
patchBytes(0x0572C8, {0x6C, 0x5C, 0x83, 0x00}); // memory cache
// Sound in background (non-FoV build)
patchBytes(0x3A4869, {0x14});
// Hardcore chat patches
patchBytes(0x09B0B8, {0x5F});
patchBytes(0x09B193, {0xE9, 0xA8, 0xAE, 0x86});
patchBytes(0x09F7A5, {0x70, 0x53, 0x56, 0x33, 0xF6, 0xE9, 0x71, 0x68, 0x86, 0x00});
patchBytes(0x09F864, {0x94});
patchBytes(0x09F878, {0x0E});
patchBytes(0x09F887, {0x90});
patchBytes(0x11BAE1, {0x0C, 0x60, 0xD0});
// Hardcore chat code cave at 0x48E000 (85 bytes)
patchBytes(0x48E000, {
0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00, 0x00, 0x43, 0x48, 0x41, 0x54,
0x5F, 0x4D, 0x53, 0x47, 0x5F, 0x48, 0x41, 0x52, 0x44, 0x43, 0x4F, 0x52, 0x45, 0x00, 0x00, 0x00,
0x57, 0x8B, 0xDA, 0x8B, 0xF9, 0xC7, 0x45, 0x94, 0x00, 0x60, 0xD0, 0x00, 0xC7, 0x45, 0x90, 0x5E,
0x00, 0x00, 0x00, 0xE9, 0x77, 0x97, 0x79, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x68, 0x08, 0x46, 0x84, 0x00, 0x83, 0x7D, 0xF0, 0x5E, 0x75, 0x05, 0xB9, 0x1F, 0x02, 0x00, 0x00,
0xE9, 0x43, 0x51, 0x79, 0xFF
});
// Blue child moon patch
patchBytes(0x3E5B83, {
0xC7, 0x05, 0xA4, 0x98, 0xCE, 0x00, 0xD4, 0xE2, 0xE7, 0xFF, 0xC2, 0x04, 0x00
});
// Blue child moon timer
patchBytes(0x2D2095, {0x00, 0x00, 0x80, 0x3F});
// SetUnit codecave jump
patchBytes(0x105E19, {0xE9, 0x02, 0x03, 0x80, 0x00});
// SetUnit main code cave at 0x48E060 (291 bytes)
patchBytes(0x48E060, {
0x55, 0x89, 0xE5, 0x83, 0xEC, 0x10, 0x85, 0xD2, 0x53, 0x56, 0x57, 0x89, 0xCF, 0x0F, 0x84, 0xA2,
0x00, 0x00, 0x00, 0x89, 0xD0, 0x85, 0xC0, 0x0F, 0x8C, 0x98, 0x00, 0x00, 0x00, 0x3B, 0x05, 0x94,
0xDE, 0xC0, 0x00, 0x0F, 0x8F, 0x8C, 0x00, 0x00, 0x00, 0x8B, 0x0D, 0x90, 0xDE, 0xC0, 0x00, 0x8B,
0x04, 0x81, 0x85, 0xC0, 0x89, 0x45, 0xF0, 0x74, 0x7C, 0x8B, 0x40, 0x04, 0x85, 0xC0, 0x7C, 0x75,
0x3B, 0x05, 0x6C, 0xDE, 0xC0, 0x00, 0x7F, 0x6D, 0x8B, 0x15, 0x68, 0xDE, 0xC0, 0x00, 0x8B, 0x1C,
0x82, 0x85, 0xDB, 0x74, 0x60, 0x8B, 0x43, 0x08, 0x6A, 0x00, 0x50, 0x89, 0xF9, 0xE8, 0xFE, 0x6E,
0xA6, 0xFF, 0x89, 0xC1, 0xE8, 0x87, 0x12, 0xA0, 0xFF, 0x89, 0xC6, 0x85, 0xF6, 0x74, 0x46, 0x8B,
0x55, 0xF0, 0x53, 0x89, 0xF1, 0xE8, 0xD6, 0x36, 0x77, 0xFF, 0x8B, 0x17, 0x56, 0x89, 0xF9, 0xFF,
0x92, 0x90, 0x00, 0x00, 0x00, 0x89, 0xF8, 0x99, 0x52, 0x50, 0x68, 0xA0, 0x62, 0x50, 0x00, 0x89,
0xF1, 0xE8, 0xBA, 0xBA, 0xA0, 0xFF, 0x6A, 0x01, 0x6A, 0x01, 0x68, 0x00, 0x00, 0x80, 0x3F, 0x6A,
0x00, 0x6A, 0xFF, 0x6A, 0x00, 0x6A, 0xFF, 0x89, 0xF1, 0xE8, 0x92, 0xC0, 0xA0, 0xFF, 0x89, 0xF1,
0xE8, 0x8B, 0xA2, 0xA0, 0xFF, 0x5F, 0x5E, 0x5B, 0x89, 0xEC, 0x5D, 0xC3, 0x90, 0x90, 0x90, 0x90,
0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xD4, 0xD2, 0x9E, 0xFF, 0x83, 0xF8, 0x03, 0x75,
0x43, 0xBA, 0x02, 0x00, 0x00, 0x00, 0x89, 0xF1, 0xE8, 0xE3, 0xD4, 0x9E, 0xFF, 0xE8, 0x6E, 0x41,
0x70, 0xFF, 0x56, 0x8B, 0xB7, 0xD4, 0x00, 0x00, 0x00, 0x31, 0xD2, 0x39, 0xD6, 0x89, 0x97, 0xE0,
0x03, 0x00, 0x00, 0x89, 0x97, 0xE4, 0x03, 0x00, 0x00, 0x89, 0x97, 0xF0, 0x03, 0x00, 0x00, 0x5E,
0x0F, 0x84, 0xD3, 0xFC, 0x7F, 0xFF, 0x89, 0xC2, 0x89, 0xF9, 0xE8, 0xF1, 0xFE, 0xFF, 0xFF, 0xE9,
0xC5, 0xFC, 0x7F, 0xFF, 0xBA, 0x02, 0x00, 0x00, 0x00, 0xE9, 0xA0, 0xFC, 0x7F, 0xFF, 0x90, 0x90,
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90
});
// --- PatchVersion() patches ---
// Net version: build 7199 (0x1C1F LE)
patchBytes(0x1B2122, {0x1F, 0x1C});
// Visual version string
patchString(0x437C04, "1.17.2");
// Visual build string
patchString(0x437BFC, "7199");
// Build date string
patchString(0x434798, "May 20 2024");
// Website filters
patchString(0x45CCD8, "*.turtle-wow.org");
patchString(0x45CC9C, "*.discord.gg");
LOG_WARNING("WardenMemory: Applied TurtlePatcher binary patches (build 7199)");
}
bool WardenMemory::readMemory(uint32_t va, uint8_t length, uint8_t* outBuf) const {
@ -300,9 +578,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 +601,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 +619,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 +643,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 +690,230 @@ bool WardenMemory::loadFromFile(const std::string& exePath) {
initKuserSharedData();
patchRuntimeGlobals();
if (isTurtle_ && imageSize_ != 0x00906000) {
// Only apply TurtlePatcher patches if we loaded the vanilla exe.
// The real Turtle WoW.exe (imageSize=0x906000) already has these bytes.
patchTurtleWowBinary();
LOG_WARNING("WardenMemory: Applied Turtle patches to vanilla PE (imageSize=0x", std::hex, imageSize_, std::dec, ")");
} else if (isTurtle_) {
LOG_WARNING("WardenMemory: Loaded native Turtle PE — skipping patches");
}
loaded_ = true;
LOG_INFO("WardenMemory: Loaded PE image (", fileData.size(), " bytes on disk, ",
imageSize_, " bytes virtual)");
// Verify all known warden_scans MEM_CHECK entries against our PE image.
// This checks the exact bytes the server will memcmp against.
verifyWardenScanEntries();
return true;
}
void WardenMemory::verifyWardenScanEntries() {
struct ScanEntry { int id; uint32_t address; uint8_t length; const char* expectedHex; const char* comment; };
static const ScanEntry entries[] = {
{ 1, 8679268, 6, "686561646572", "Packet internal sign - header"},
{ 3, 8530960, 6, "53595354454D", "Packet internal sign - SYSTEM"},
{ 8, 8151666, 4, "D893FEC0", "Jump gravity"},
{ 9, 8151646, 2, "3075", "Jump gravity water"},
{10, 6382555, 2, "8A47", "Anti root"},
{11, 6380789, 1, "F8", "Anti move"},
{12, 8151647, 1, "75", "Anti jump"},
{13, 8152026, 4, "8B4F7889", "No fall damage"},
{14, 6504892, 2, "7425", "Super fly"},
{15, 6383433, 2, "780F", "Heartbeat interval speedhack"},
{16, 6284623, 1, "F4", "Anti slow hack"},
{17, 6504931, 2, "85D2", "No fall damage"},
{18, 8151565, 2, "2000", "Fly hack"},
{19, 7153475, 6, "890D509CCE00", "General hacks"},
{20, 7138894, 6, "A3D89BCE00EB", "Wall climb"},
{21, 7138907, 6, "890DD89BCE00", "Wall climb"},
{22, 6993044, 1, "74", "Zero gravity"},
{23, 6502300, 1, "FC", "Air walk"},
{24, 6340512, 2, "7F7D", "Wall climb"},
{25, 6380455, 4, "F4010000", "Wall climb"},
{26, 8151657, 4, "488C11C1", "Wall climb"},
{27, 6992319, 3, "894704", "Wall climb"},
{28, 6340529, 2, "746C", "No water hack"},
{29, 6356016, 10, "C70588D8C4000C000000", "No water hack"},
{30, 4730584, 6, "0F8CE1000000", "WMO collision"},
{31, 4803152, 7, "A1C0EACE0085C0", "noclip hack"},
{32, 5946704, 6, "8BD18B0D80E0", "M2 collision"},
{33, 6340543, 2, "7546", "M2 collision"},
{34, 5341282, 1, "7F", "Warden disable"},
{35, 4989376, 1, "72", "No fog hack"},
{36, 8145237, 1, "8B", "No fog hack"},
{37, 6392083, 8, "8B450850E824DA1A", "No fog hack"},
{38, 8146241, 10, "D9818C0000008BE55DC2", "tp2plane hack"},
{39, 6995731, 1, "74", "Air swim hack"},
{40, 6964859, 1, "75", "Infinite jump hack"},
{41, 6382558, 10, "84C074178B86A4000000", "Gravity water hack"},
{42, 8151997, 3, "895108", "Gravity hack"},
{43, 8152025, 1, "34", "Plane teleport"},
{44, 6516436, 1, "FC", "Zero fall time"},
{45, 6501616, 1, "FC", "No fall damage"},
{46, 6511674, 1, "FC", "Fall time hack"},
{47, 6513048, 1, "FC", "Death bug hack"},
{48, 6514072, 1, "FC", "Anti slow hack"},
{49, 8152029, 3, "894E38", "Anti slow hack"},
{50, 4847346, 3, "8B45D4", "Max camera distance hack"},
{51, 4847069, 1, "74", "Wall climb"},
{52, 8155231, 3, "000000", "Signature check"},
{53, 6356849, 1, "74", "Signature check"},
{54, 6354889, 6, "0F8A71FFFFFF", "Signature check"},
{55, 4657642, 1, "74", "Max interact distance hack"},
{56, 6211360, 8, "558BEC83EC0C8B45", "Hover speed hack"},
{57, 8153504, 3, "558BEC", "Flight speed hack"},
{58, 6214285, 6, "8B82500E0000", "Track all units hack"},
{59, 8151558, 11, "25FFFFDFFB0D0020000089", "No fall damage"},
{60, 8155228, 6, "89868C000000", "Run speed hack"},
{61, 6356837, 2, "7474", "Follow anything hack"},
{62, 6751806, 1, "74", "No water hack"},
{63, 4657632, 2, "740A", "Any name hack"},
{64, 8151976, 4, "84E5FFFF", "Plane teleport"},
{65, 6214371, 6, "8BB1540E0000", "Object tracking hack"},
{66, 6818689, 5, "A388F2C700", "No water hack"},
{67, 6186028, 5, "C705ACD2C4", "No fog hack"},
{68, 5473808, 4, "30855300", "Warden disable hack"},
{69, 4208171, 3, "6B2C00", "Warden disable hack"},
{70, 7119285, 1, "74", "Warden disable hack"},
{71, 4729827, 1, "5E", "Daylight hack"},
{72, 6354512, 6, "0F84EA000000", "Ranged attack stop hack"},
{73, 5053463, 2, "7415", "Officer note hack"},
{79, 8139737, 5, "D84E14DEC1", "UNKNOWN movement hack"},
{80, 8902804, 4, "8E977042", "Wall climb hack"},
{81, 8902808, 4, "0000E040", "Run speed hack"},
{82, 8154755, 7, "8166403FFFDFFF", "Moveflag hack"},
{83, 8445948, 4, "BB8D243F", "Wall climb hack"},
{84, 6493717, 2, "741D", "Speed hack"},
};
auto hexToByte = [](char hi, char lo) -> uint8_t {
auto nibble = [](char c) -> uint8_t {
if (c >= '0' && c <= '9') return c - '0';
if (c >= 'A' && c <= 'F') return 10 + c - 'A';
if (c >= 'a' && c <= 'f') return 10 + c - 'a';
return 0;
};
return (nibble(hi) << 4) | nibble(lo);
};
int mismatches = 0;
int patched = 0;
for (const auto& e : entries) {
std::string hexStr(e.expectedHex);
std::vector<uint8_t> expected;
for (size_t i = 0; i + 1 < hexStr.size(); i += 2)
expected.push_back(hexToByte(hexStr[i], hexStr[i+1]));
std::vector<uint8_t> actual(e.length, 0);
bool ok = readMemory(e.address, e.length, actual.data());
if (!ok || actual != expected) {
mismatches++;
// In Turtle mode, write the expected bytes into the PE image so
// MEM_CHECK responses return what the server expects.
if (isTurtle_ && e.address >= imageBase_) {
uint32_t offset = e.address - imageBase_;
if (offset + expected.size() <= imageSize_) {
std::memcpy(image_.data() + offset, expected.data(), expected.size());
patched++;
}
}
}
}
if (mismatches == 0) {
LOG_WARNING("WardenScan: All ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MATCH PE image");
} else if (patched > 0) {
LOG_WARNING("WardenScan: Patched ", patched, "/", mismatches,
" mismatched scan entries into PE image");
} else {
LOG_WARNING("WardenScan: ", mismatches, " / ", sizeof(entries)/sizeof(entries[0]),
" DB scan entries MISMATCH");
}
}
bool WardenMemory::searchCodePattern(const uint8_t seed[4], const uint8_t expectedHash[20],
uint8_t patternLen, bool imageOnly) 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;
}
// 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.
struct Range { size_t start; size_t end; };
std::vector<Range> 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()) {
uint16_t numSections = image_[peOffset+4+2] | (uint16_t(image_[peOffset+4+3]) << 8);
uint16_t optHeaderSize = image_[peOffset+4+16] | (uint16_t(image_[peOffset+4+17]) << 8);
size_t secTable = peOffset + 4 + 20 + optHeaderSize;
for (uint16_t i = 0; i < numSections; i++) {
size_t secOfs = secTable + i * 40;
if (secOfs + 40 > image_.size()) break;
uint32_t va = image_[secOfs+12] | (uint32_t(image_[secOfs+13]) << 8)
| (uint32_t(image_[secOfs+14]) << 16) | (uint32_t(image_[secOfs+15]) << 24);
uint32_t vsize = image_[secOfs+8] | (uint32_t(image_[secOfs+9]) << 8)
| (uint32_t(image_[secOfs+10]) << 16) | (uint32_t(image_[secOfs+11]) << 24);
size_t rEnd = std::min(static_cast<size_t>(va + vsize), static_cast<size_t>(imageSize_));
if (va + patternLen <= rEnd)
ranges.push_back({va, rEnd});
}
}
}
if (ranges.empty()) {
// Fallback: search entire image
if (patternLen <= imageSize_)
ranges.push_back({0, imageSize_});
}
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

View file

@ -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<std::chrono::milliseconds>(
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;

View file

@ -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