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() {