From 388db59463f8af54a5be7daa1b886051c176d476 Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Feb 2026 19:20:32 -0800 Subject: [PATCH] Fix Warden module loading pipeline and HASH_REQUEST response Fix critical skip/copy parsing bug where source pointer advanced for both skip and copy sections (skip has no source data). Implement real relocations using delta-encoded offsets. Strip RSA signature before zlib decompression. Load module when download completes and cache to disk. Add empirical hash testing against CR entries and compute SHA1(moduleImage) response with SHA1Randx key derivation for any seed. --- include/game/game_handler.hpp | 2 + include/game/warden_crypto.hpp | 4 +- include/game/warden_module.hpp | 5 ++ src/game/game_handler.cpp | 142 ++++++++++++++++++++++++++++--- src/game/warden_module.cpp | 150 ++++++++++++++++++--------------- 5 files changed, 225 insertions(+), 78 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index f03ae158..27cd9e04 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -25,6 +25,7 @@ namespace wowee::game { class TransportManager; class WardenCrypto; class WardenMemory; + class WardenModule; class WardenModuleManager; class PacketParsers; } @@ -1358,6 +1359,7 @@ private: uint32_t wardenModuleSize_ = 0; std::vector wardenModuleData_; // Downloaded module chunks std::vector wardenLoadedModuleImage_; // Parsed module image for key derivation + std::shared_ptr wardenLoadedModule_; // Loaded Warden module // Pre-computed challenge/response entries from .cr file struct WardenCREntry { diff --git a/include/game/warden_crypto.hpp b/include/game/warden_crypto.hpp index 4ec7ee9b..27890a73 100644 --- a/include/game/warden_crypto.hpp +++ b/include/game/warden_crypto.hpp @@ -60,9 +60,11 @@ private: void processRC4(const uint8_t* input, uint8_t* output, size_t length, std::vector& state, uint8_t& i, uint8_t& j); +public: /** * SHA1Randx / WardenKeyGenerator: generates pseudo-random bytes from a seed. - * Used to derive the 16-byte encrypt and decrypt keys from the session key. + * Used to derive the 16-byte encrypt and decrypt keys from a seed. + * Public so GameHandler can use it for module hash key derivation. */ static void sha1RandxGenerate(const std::vector& seed, uint8_t* outputEncryptKey, diff --git a/include/game/warden_module.hpp b/include/game/warden_module.hpp index 8f216240..bcea989f 100644 --- a/include/game/warden_module.hpp +++ b/include/game/warden_module.hpp @@ -122,6 +122,10 @@ public: */ void unload(); + const void* getModuleMemory() const { return moduleMemory_; } + size_t getModuleSize() const { return moduleSize_; } + const std::vector& getDecompressedData() const { return decompressedData_; } + private: bool loaded_; // Module successfully loaded std::vector md5Hash_; // Module identifier @@ -133,6 +137,7 @@ private: void* moduleMemory_; // Allocated executable memory region size_t moduleSize_; // Size of loaded code uint32_t moduleBase_; // Module base address (for emulator) + size_t relocDataOffset_ = 0; // Offset into decompressedData_ where relocation data starts WardenFuncList funcList_; // Callback functions std::unique_ptr emulator_; // Cross-platform x86 emulator diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 448fdcc4..242d1364 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -158,6 +158,7 @@ bool GameHandler::connect(const std::string& host, wardenModuleKey_.clear(); wardenModuleSize_ = 0; wardenModuleData_.clear(); + wardenLoadedModule_.reset(); // Generate random client seed this->clientSeed = generateClientSeed(); @@ -214,6 +215,7 @@ void GameHandler::disconnect() { wardenModuleKey_.clear(); wardenModuleSize_ = 0; wardenModuleData_.clear(); + wardenLoadedModule_.reset(); setState(WorldState::DISCONNECTED); LOG_INFO("Disconnected from world server"); } @@ -2208,6 +2210,35 @@ void GameHandler::handleWardenData(network::Packet& packet) { wardenModuleData_.size(), " bytes)"); wardenState_ = WardenState::WAIT_HASH_REQUEST; + // Cache raw module to disk + { + std::string homeDir; + if (const char* h = std::getenv("HOME")) homeDir = h; + else homeDir = "."; + std::string cacheDir = homeDir + "/.local/share/wowee/warden_cache"; + std::filesystem::create_directories(cacheDir); + + std::string hashHex; + for (auto b : wardenModuleHash_) { char s[4]; snprintf(s, 4, "%02x", b); hashHex += s; } + std::string cachePath = cacheDir + "/" + hashHex + ".wdn"; + + std::ofstream wf(cachePath, std::ios::binary); + if (wf) { + wf.write(reinterpret_cast(wardenModuleData_.data()), wardenModuleData_.size()); + LOG_INFO("Warden: Cached module to ", cachePath); + } + } + + // Load the module (decrypt, decompress, parse, relocate) + wardenLoadedModule_ = std::make_shared(); + if (wardenLoadedModule_->load(wardenModuleData_, wardenModuleHash_, wardenModuleKey_)) { + LOG_INFO("Warden: Module loaded successfully (image size=", + wardenLoadedModule_->getModuleSize(), " bytes)"); + } else { + LOG_ERROR("Warden: Module loading FAILED"); + wardenLoadedModule_.reset(); + } + // Send MODULE_OK (opcode 0x01) std::vector resp = { 0x01 }; // WARDEN_CMSG_MODULE_OK sendWardenResponse(resp); @@ -2279,25 +2310,116 @@ void GameHandler::handleWardenData(network::Packet& packet) { } } - // --- Fallback: SHA1(seed + moduleImage) if no CR match --- - LOG_WARNING("Warden: No CR match, falling back to SHA1 hash computation"); + // --- Fallback: compute hash from loaded module --- + LOG_WARNING("Warden: No CR match, computing hash from loaded module"); - if (wardenModuleData_.empty() || wardenModuleKey_.empty()) { - LOG_ERROR("Warden: No module data and no CR match — cannot compute hash"); + if (!wardenLoadedModule_ || !wardenLoadedModule_->isLoaded()) { + LOG_ERROR("Warden: No loaded module and no CR match — cannot compute hash"); wardenState_ = WardenState::WAIT_CHECKS; break; } - // SHA1(seed) fallback — wrong answer but sends a response to avoid silent hang. - // Correct answer requires executing the Warden module via emulator (not yet functional). - // Server will likely disconnect, but GameHandler::update() now detects that gracefully. { - auto hash = auth::Crypto::sha1(seed); - LOG_WARNING("Warden: Sending SHA1(seed) fallback — server will likely reject"); + const uint8_t* moduleImage = static_cast(wardenLoadedModule_->getModuleMemory()); + size_t moduleImageSize = wardenLoadedModule_->getModuleSize(); + const auto& decompressedData = wardenLoadedModule_->getDecompressedData(); + + // --- 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_INFO("Warden: Empirical test — expected reply from CR[0]=", expectedHex); + + // Test 1: SHA1(moduleImage) + { + std::vector 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_INFO("Warden: SHA1(moduleImage)=", hex, match ? " MATCH!" : ""); + } + // Test 2: SHA1(seed || moduleImage) + { + std::vector 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_INFO("Warden: SHA1(seed||image)=", hex, match ? " MATCH!" : ""); + } + // Test 3: SHA1(moduleImage || seed) + { + std::vector 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_INFO("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_INFO("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_INFO("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_INFO("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 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_INFO("Warden: Sending SHA1(moduleImage)=", hex); + } + + // Send HASH_RESULT (opcode 0x04 + 20-byte hash) std::vector resp; resp.push_back(0x04); - resp.insert(resp.end(), hash.begin(), hash.end()); + resp.insert(resp.end(), reply.begin(), reply.end()); sendWardenResponse(resp); + + // Derive new RC4 keys from the seed using SHA1Randx + std::vector seedVec(seed.begin(), seed.end()); + // Pad seed to at least 2 bytes for SHA1Randx split + // SHA1Randx splits input in half: first_half and second_half + uint8_t newEncryptKey[16], newDecryptKey[16]; + WardenCrypto::sha1RandxGenerate(seedVec, newEncryptKey, newDecryptKey); + + std::vector ek(newEncryptKey, newEncryptKey + 16); + std::vector dk(newDecryptKey, newDecryptKey + 16); + wardenCrypto_->replaceKeys(ek, dk); + + { + std::string ekHex, dkHex; + for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newEncryptKey[i]); ekHex += s; } + for (int i = 0; i < 16; i++) { char s[4]; snprintf(s, 4, "%02x", newDecryptKey[i]); dkHex += s; } + LOG_INFO("Warden: Derived keys from seed: encrypt=", ekHex, " decrypt=", dkHex); + } } wardenState_ = WardenState::WAIT_CHECKS; diff --git a/src/game/warden_module.cpp b/src/game/warden_module.cpp index 486199a3..90caef2c 100644 --- a/src/game/warden_module.cpp +++ b/src/game/warden_module.cpp @@ -69,8 +69,14 @@ bool WardenModule::load(const std::vector& moduleData, // Note: Currently returns true (skipping verification) due to placeholder modulus } - // Step 4: zlib decompress - if (!decompressZlib(decryptedData_, decompressedData_)) { + // Step 4: Strip RSA signature (last 256 bytes) then zlib decompress + std::vector dataWithoutSig; + if (decryptedData_.size() > 256) { + dataWithoutSig.assign(decryptedData_.begin(), decryptedData_.end() - 256); + } else { + dataWithoutSig = decryptedData_; + } + if (!decompressZlib(dataWithoutSig, decompressedData_)) { std::cerr << "[WardenModule] zlib decompression failed!" << std::endl; return false; } @@ -506,40 +512,47 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { std::cout << "[WardenModule] Allocated " << moduleSize_ << " bytes of executable memory at " << moduleMemory_ << std::endl; - // Parse skip/copy sections + // Parse skip/copy pairs + // Format: repeated [2B skip_count][2B copy_count][copy_count bytes data] + // Skip = advance dest pointer (zeros), Copy = copy from source to dest + // Terminates when skip_count == 0 size_t pos = 4; // Skip 4-byte size header size_t destOffset = 0; - bool isSkipSection = true; // Alternates: skip, copy, skip, copy, ... - int sectionCount = 0; + int pairCount = 0; while (pos + 2 <= exeData.size()) { - // Read 2-byte section length (little-endian) - uint16_t sectionLength = exeData[pos] | (exeData[pos + 1] << 8); + // Read skip count (2 bytes LE) + uint16_t skipCount = exeData[pos] | (exeData[pos + 1] << 8); pos += 2; - if (sectionLength == 0) { - break; // End of sections + if (skipCount == 0) { + break; // End of skip/copy pairs } - if (pos + sectionLength > exeData.size()) { - std::cerr << "[WardenModule] Section extends beyond data bounds" << std::endl; - #ifdef _WIN32 - VirtualFree(moduleMemory_, 0, MEM_RELEASE); - #else - munmap(moduleMemory_, moduleSize_); - #endif - moduleMemory_ = nullptr; - return false; - } + // Advance dest pointer by skipCount (gaps are zero-filled from memset) + destOffset += skipCount; - if (isSkipSection) { - // Skip section - advance destination offset without copying - destOffset += sectionLength; - std::cout << "[WardenModule] Skip section: " << sectionLength << " bytes (dest offset now " - << destOffset << ")" << std::endl; - } else { - // Copy section - copy code to module memory - if (destOffset + sectionLength > moduleSize_) { + // Read copy count (2 bytes LE) + if (pos + 2 > exeData.size()) { + std::cerr << "[WardenModule] Unexpected end of data reading copy count" << std::endl; + break; + } + uint16_t copyCount = exeData[pos] | (exeData[pos + 1] << 8); + pos += 2; + + if (copyCount > 0) { + if (pos + copyCount > exeData.size()) { + std::cerr << "[WardenModule] Copy section extends beyond data bounds" << std::endl; + #ifdef _WIN32 + VirtualFree(moduleMemory_, 0, MEM_RELEASE); + #else + munmap(moduleMemory_, moduleSize_); + #endif + moduleMemory_ = nullptr; + return false; + } + + if (destOffset + copyCount > moduleSize_) { std::cerr << "[WardenModule] Copy section exceeds module size" << std::endl; #ifdef _WIN32 VirtualFree(moduleMemory_, 0, MEM_RELEASE); @@ -553,27 +566,24 @@ bool WardenModule::parseExecutableFormat(const std::vector& exeData) { std::memcpy( static_cast(moduleMemory_) + destOffset, exeData.data() + pos, - sectionLength + copyCount ); - - std::cout << "[WardenModule] Copy section: " << sectionLength << " bytes to offset " - << destOffset << std::endl; - - destOffset += sectionLength; + pos += copyCount; + destOffset += copyCount; } - pos += sectionLength; - isSkipSection = !isSkipSection; // Alternate - sectionCount++; + pairCount++; + std::cout << "[WardenModule] Pair " << pairCount << ": skip " << skipCount + << ", copy " << copyCount << " (dest offset=" << destOffset << ")" << std::endl; } - std::cout << "[WardenModule] ✓ Parsed " << sectionCount << " sections, final offset: " + // Save position — remaining decompressed data contains relocation entries + relocDataOffset_ = pos; + + std::cout << "[WardenModule] Parsed " << pairCount << " skip/copy pairs, final offset: " << destOffset << "/" << finalCodeSize << std::endl; - - if (destOffset != finalCodeSize) { - std::cerr << "[WardenModule] WARNING: Final offset " << destOffset - << " doesn't match expected size " << finalCodeSize << std::endl; - } + std::cout << "[WardenModule] Relocation data starts at decompressed offset " << relocDataOffset_ + << " (" << (exeData.size() - relocDataOffset_) << " bytes remaining)" << std::endl; return true; } @@ -584,36 +594,42 @@ bool WardenModule::applyRelocations() { return false; } - // Relocations are embedded in the decompressed data after the executable sections - // Format: Delta-encoded offsets with high-bit continuation - // - // Each offset is encoded as variable-length bytes: - // - If high bit (0x80) is set, read next byte and combine - // - Continue until byte without high bit - // - Final value is delta from previous relocation offset - // - // For each relocation offset: - // - Read 4-byte pointer at that offset - // - Add module base address to make it absolute - // - Write back to memory + // Relocation data is in decompressedData_ starting at relocDataOffset_ + // Format: delta-encoded 2-byte LE offsets, terminated by 0x0000 + // Each offset in the module image has moduleBase_ added to the 32-bit value there - std::cout << "[WardenModule] Applying relocations to module at " << moduleMemory_ << std::endl; + if (relocDataOffset_ == 0 || relocDataOffset_ >= decompressedData_.size()) { + std::cout << "[WardenModule] No relocation data available" << std::endl; + return true; + } - // NOTE: Relocation data format and location varies by module - // Without a real module to test against, we can't implement this accurately - // This is a placeholder that would need to be filled in with actual logic - // once we have real Warden module data to analyze + size_t relocPos = relocDataOffset_; + uint32_t currentOffset = 0; + int relocCount = 0; - std::cout << "[WardenModule] ⚠ Relocation application is STUB (needs real module data)" << std::endl; - std::cout << "[WardenModule] Would parse delta-encoded offsets and fix absolute references" << std::endl; + while (relocPos + 2 <= decompressedData_.size()) { + uint16_t delta = decompressedData_[relocPos] | (decompressedData_[relocPos + 1] << 8); + relocPos += 2; - // Placeholder: Assume no relocations or already position-independent - // Real implementation would: - // 1. Find relocation table in module data - // 2. Decode delta offsets - // 3. For each offset: *(uint32_t*)(moduleMemory + offset) += (uintptr_t)moduleMemory_ + if (delta == 0) break; - return true; // Return true to continue (stub implementation) + currentOffset += delta; + + if (currentOffset + 4 <= moduleSize_) { + uint32_t* ptr = reinterpret_cast( + static_cast(moduleMemory_) + currentOffset); + *ptr += moduleBase_; + relocCount++; + } else { + std::cerr << "[WardenModule] Relocation offset " << currentOffset + << " out of bounds (moduleSize=" << moduleSize_ << ")" << std::endl; + } + } + + std::cout << "[WardenModule] Applied " << relocCount << " relocations (base=0x" + << std::hex << moduleBase_ << std::dec << ")" << std::endl; + + return true; } bool WardenModule::bindAPIs() {