diff --git a/CMakeLists.txt b/CMakeLists.txt index f6571f5f..7590d80c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -89,6 +89,7 @@ set(WOWEE_SOURCES # Game src/game/game_handler.cpp + src/game/warden_crypto.cpp src/game/transport_manager.cpp src/game/world.cpp src/game/player.cpp diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index 34ad2d2d..c211f034 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -21,6 +21,7 @@ namespace wowee::game { class TransportManager; + class WardenCrypto; } namespace wowee { @@ -744,6 +745,22 @@ private: */ void handleLoginVerifyWorld(network::Packet& packet); + /** + * Handle SMSG_CLIENTCACHE_VERSION from server + */ + void handleClientCacheVersion(network::Packet& packet); + + /** + * Handle SMSG_TUTORIAL_FLAGS from server + */ + void handleTutorialFlags(network::Packet& packet); + + /** + * Handle SMSG_WARDEN_DATA gate packet from server. + * We do not implement anti-cheat exchange for third-party realms. + */ + void handleWardenData(network::Packet& packet); + /** * Handle SMSG_ACCOUNT_DATA_TIMES from server */ @@ -1164,6 +1181,13 @@ private: bool pendingCharCreateResult_ = false; bool pendingCharCreateSuccess_ = false; std::string pendingCharCreateMsg_; + bool requiresWarden_ = false; + bool wardenGateSeen_ = false; + float wardenGateElapsed_ = 0.0f; + float wardenGateNextStatusLog_ = 2.0f; + uint32_t wardenPacketsAfterGate_ = 0; + bool wardenCharEnumBlockedLogged_ = false; + std::unique_ptr wardenCrypto_; // ---- XP tracking ---- uint32_t playerXp_ = 0; diff --git a/include/game/warden_crypto.hpp b/include/game/warden_crypto.hpp new file mode 100644 index 00000000..d193b121 --- /dev/null +++ b/include/game/warden_crypto.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include +#include +#include + +namespace wowee { +namespace game { + +/** + * Warden anti-cheat crypto handler for WoW 3.3.5a + * Handles RC4 encryption/decryption of Warden packets + */ +class WardenCrypto { +public: + WardenCrypto(); + ~WardenCrypto(); + + /** + * Initialize Warden crypto with module seed + * @param moduleData The SMSG_WARDEN_DATA payload containing seed + * @return true if initialization succeeded + */ + bool initialize(const std::vector& moduleData); + + /** + * Decrypt an incoming Warden packet + * @param data Encrypted data from server + * @return Decrypted data + */ + std::vector decrypt(const std::vector& data); + + /** + * Encrypt an outgoing Warden response + * @param data Plaintext response data + * @return Encrypted data + */ + std::vector encrypt(const std::vector& data); + + /** + * Check if crypto has been initialized + */ + bool isInitialized() const { return initialized_; } + +private: + bool initialized_; + std::vector inputKey_; + std::vector outputKey_; + + // RC4 state for incoming packets + std::vector inputRC4State_; + uint8_t inputRC4_i_; + uint8_t inputRC4_j_; + + // RC4 state for outgoing packets + std::vector outputRC4State_; + uint8_t outputRC4_i_; + uint8_t outputRC4_j_; + + void initRC4(const std::vector& key, + std::vector& state, + uint8_t& i, uint8_t& j); + + void processRC4(const uint8_t* input, uint8_t* output, size_t length, + std::vector& state, uint8_t& i, uint8_t& j); +}; + +} // namespace game +} // namespace wowee diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index c60197ed..96db7f9f 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -1,5 +1,6 @@ #include "game/game_handler.hpp" #include "game/transport_manager.hpp" +#include "game/warden_crypto.hpp" #include "game/opcodes.hpp" #include "network/world_socket.hpp" #include "network/packet.hpp" @@ -1819,7 +1820,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { wardenPacketsAfterGate_ = 0; } - // Log the full packet for analysis + // Log the raw encrypted packet std::string hex; hex.reserve(data.size() * 3); for (size_t i = 0; i < data.size(); ++i) { @@ -1827,94 +1828,173 @@ void GameHandler::handleWardenData(network::Packet& packet) { snprintf(b, sizeof(b), "%02x ", data[i]); hex += b; } - LOG_INFO("Received SMSG_WARDEN_DATA (len=", data.size(), ", bytes: ", hex, ")"); + LOG_INFO("Received SMSG_WARDEN_DATA (len=", data.size(), ", raw: ", hex, ")"); - // Prepare response packet - network::Packet response(static_cast(Opcode::CMSG_WARDEN_DATA)); + // Initialize crypto on first packet (usually module load) + if (!wardenCrypto_) { + wardenCrypto_ = std::make_unique(); + if (!wardenCrypto_->initialize(data)) { + LOG_ERROR("Warden: Failed to initialize crypto"); + wardenCrypto_.reset(); + return; + } + LOG_INFO("Warden: Crypto initialized, sending module ACK with checksum"); + + // Build module acknowledgment response + // Format: [0x00 opcode][16-byte MD5 hash of module][0x01 result = success] + std::vector moduleResponse; + + // Opcode 0x00 = module info response + moduleResponse.push_back(0x00); + + // Compute simple checksum of module data (16 bytes) + // For a proper implementation, this would be MD5, but we'll use a simpler hash + uint8_t checksum[16] = {0}; + for (size_t i = 0; i < data.size(); ++i) { + checksum[i % 16] ^= data[i]; + } + + // Add checksum to response + for (int i = 0; i < 16; ++i) { + moduleResponse.push_back(checksum[i]); + } + + // Result code: 0x01 = module loaded successfully + moduleResponse.push_back(0x01); + + // Log plaintext module response + std::string respHex; + respHex.reserve(moduleResponse.size() * 3); + for (uint8_t byte : moduleResponse) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", byte); + respHex += b; + } + LOG_INFO("Warden: Module ACK plaintext (", moduleResponse.size(), " bytes): ", respHex); + + // Encrypt the response + std::vector encryptedResponse = wardenCrypto_->encrypt(moduleResponse); + + // Log encrypted response + std::string encHex; + encHex.reserve(encryptedResponse.size() * 3); + for (uint8_t byte : encryptedResponse) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", byte); + encHex += b; + } + LOG_INFO("Warden: Module ACK encrypted (", encryptedResponse.size(), " bytes): ", encHex); + + // Send encrypted module ACK + network::Packet response(static_cast(Opcode::CMSG_WARDEN_DATA)); + for (uint8_t byte : encryptedResponse) { + response.writeUInt8(byte); + } + + if (socket && socket->isConnected()) { + socket->send(response); + LOG_INFO("Sent CMSG_WARDEN_DATA module ACK (", encryptedResponse.size(), " bytes encrypted)"); + } + return; + } + + // Decrypt the packet + std::vector decrypted = wardenCrypto_->decrypt(data); + + // Log decrypted data + std::string decHex; + decHex.reserve(decrypted.size() * 3); + for (size_t i = 0; i < decrypted.size(); ++i) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", decrypted[i]); + decHex += b; + } + LOG_INFO("Warden: Decrypted (", decrypted.size(), " bytes): ", decHex); + + // Prepare response data std::vector responseData; - if (data.empty()) { - LOG_INFO("Warden: Empty packet - sending empty response"); + if (decrypted.empty()) { + LOG_INFO("Warden: Empty decrypted packet"); } else { - uint8_t opcode = data[0]; + uint8_t opcode = decrypted[0]; - // Warden packet types (from WoW 3.3.5a protocol) + // Warden check opcodes (from WoW 3.3.5a protocol) switch (opcode) { case 0x00: // Module info request LOG_INFO("Warden: Module info request"); - // Response: [0x00] = module not loaded / not available responseData.push_back(0x00); break; case 0x01: // Hash request LOG_INFO("Warden: Hash request"); - // Response: [0x01][result] where 0x00 = pass responseData.push_back(0x01); - responseData.push_back(0x00); // Hash matches (legitimate) + responseData.push_back(0x00); // Pass break; case 0x02: // Lua string check LOG_INFO("Warden: Lua string check"); - // Response: [0x02][length][string_result] or [0x02][0x00] for empty responseData.push_back(0x02); - responseData.push_back(0x00); // Empty result = no detection + responseData.push_back(0x00); // Empty = no detection break; - case 0x05: // Memory/page check request - LOG_INFO("Warden: Memory check request"); - // Parse number of checks and respond with all passing results - if (data.size() >= 2) { - uint8_t numChecks = data[1]; - LOG_INFO("Warden: Memory check has ", (int)numChecks, " checks"); - + case 0x05: // Memory check + LOG_INFO("Warden: Memory check"); + if (decrypted.size() >= 2) { + uint8_t numChecks = decrypted[1]; + LOG_INFO("Warden: ", (int)numChecks, " memory checks"); responseData.push_back(0x05); responseData.push_back(numChecks); - - // For each check, respond with 0x00 (no violation) for (uint8_t i = 0; i < numChecks; ++i) { - responseData.push_back(0x00); + responseData.push_back(0x00); // All pass } } else { - // Malformed packet, send minimal response responseData.push_back(0x05); responseData.push_back(0x00); } break; default: - // Unknown opcode - could be module transfer (0x14), seed, or encrypted - LOG_INFO("Warden: Unknown opcode 0x", std::hex, (int)opcode, std::dec); - - if (data.size() > 20) { - LOG_INFO("Warden: Large packet (", data.size(), " bytes) - likely module transfer or seed"); - // Module transfers often don't require immediate response - // or require just an empty ACK - } - - // For unknown opcodes, try echoing the opcode with success status + LOG_INFO("Warden: Unknown check opcode 0x", std::hex, (int)opcode, std::dec); + // Send minimal response responseData.push_back(opcode); - responseData.push_back(0x00); // Generic success/ACK + responseData.push_back(0x00); break; } } - // Build and send response + // Log plaintext response + std::string respPlainHex; + respPlainHex.reserve(responseData.size() * 3); for (uint8_t byte : responseData) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", byte); + respPlainHex += b; + } + LOG_INFO("Warden: Response plaintext (", responseData.size(), " bytes): ", respPlainHex); + + // Encrypt response + std::vector encrypted = wardenCrypto_->encrypt(responseData); + + // Log encrypted response + std::string respEncHex; + respEncHex.reserve(encrypted.size() * 3); + for (uint8_t byte : encrypted) { + char b[4]; + snprintf(b, sizeof(b), "%02x ", byte); + respEncHex += b; + } + LOG_INFO("Warden: Response encrypted (", encrypted.size(), " bytes): ", respEncHex); + + // Build and send response packet + network::Packet response(static_cast(Opcode::CMSG_WARDEN_DATA)); + for (uint8_t byte : encrypted) { response.writeUInt8(byte); } if (socket && socket->isConnected()) { socket->send(response); - - // Log response - std::string respHex; - respHex.reserve(responseData.size() * 3); - for (uint8_t byte : responseData) { - char b[4]; - snprintf(b, sizeof(b), "%02x ", byte); - respHex += b; - } - LOG_INFO("Sent CMSG_WARDEN_DATA response (", responseData.size(), " bytes: ", respHex, ")"); + LOG_INFO("Sent CMSG_WARDEN_DATA encrypted response"); } } diff --git a/src/game/warden_crypto.cpp b/src/game/warden_crypto.cpp new file mode 100644 index 00000000..a13b3a43 --- /dev/null +++ b/src/game/warden_crypto.cpp @@ -0,0 +1,124 @@ +#include "game/warden_crypto.hpp" +#include "core/logger.hpp" +#include +#include + +namespace wowee { +namespace game { + +// Warden module keys for WoW 3.3.5a (from client analysis) +// These are the standard keys used by most 3.3.5a servers +static const uint8_t WARDEN_MODULE_KEY[16] = { + 0xC5, 0x35, 0xB2, 0x1E, 0xF8, 0xE7, 0x9F, 0x4B, + 0x91, 0xB6, 0xD1, 0x34, 0xA7, 0x2F, 0x58, 0x8C +}; + +WardenCrypto::WardenCrypto() + : initialized_(false) + , inputRC4_i_(0) + , inputRC4_j_(0) + , outputRC4_i_(0) + , outputRC4_j_(0) { +} + +WardenCrypto::~WardenCrypto() { +} + +bool WardenCrypto::initialize(const std::vector& moduleData) { + if (moduleData.empty()) { + LOG_ERROR("Warden: Cannot initialize with empty module data"); + return false; + } + + LOG_INFO("Warden: Initializing crypto with ", moduleData.size(), " byte module"); + + // Warden 3.3.5a module format (typically): + // [1 byte opcode][16 bytes seed/key][remaining bytes = encrypted module data] + + if (moduleData.size() < 17) { + LOG_WARNING("Warden: Module too small (", moduleData.size(), " bytes), using default keys"); + // Use default keys + inputKey_.assign(WARDEN_MODULE_KEY, WARDEN_MODULE_KEY + 16); + outputKey_.assign(WARDEN_MODULE_KEY, WARDEN_MODULE_KEY + 16); + } else { + // Extract seed from module (skip first opcode byte) + inputKey_.assign(moduleData.begin() + 1, moduleData.begin() + 17); + outputKey_ = inputKey_; + + // XOR with module key for output + for (size_t i = 0; i < 16; ++i) { + outputKey_[i] ^= WARDEN_MODULE_KEY[i]; + } + + LOG_INFO("Warden: Extracted 16-byte seed from module"); + } + + // Initialize RC4 ciphers + inputRC4State_.resize(256); + outputRC4State_.resize(256); + + initRC4(inputKey_, inputRC4State_, inputRC4_i_, inputRC4_j_); + initRC4(outputKey_, outputRC4State_, outputRC4_i_, outputRC4_j_); + + initialized_ = true; + LOG_INFO("Warden: Crypto initialized successfully"); + return true; +} + +std::vector WardenCrypto::decrypt(const std::vector& data) { + if (!initialized_) { + LOG_WARNING("Warden: Attempted to decrypt without initialization"); + return data; + } + + std::vector result(data.size()); + processRC4(data.data(), result.data(), data.size(), + inputRC4State_, inputRC4_i_, inputRC4_j_); + return result; +} + +std::vector WardenCrypto::encrypt(const std::vector& data) { + if (!initialized_) { + LOG_WARNING("Warden: Attempted to encrypt without initialization"); + return data; + } + + std::vector result(data.size()); + processRC4(data.data(), result.data(), data.size(), + outputRC4State_, outputRC4_i_, outputRC4_j_); + return result; +} + +void WardenCrypto::initRC4(const std::vector& key, + std::vector& state, + uint8_t& i, uint8_t& j) { + // Initialize permutation + for (int idx = 0; idx < 256; ++idx) { + state[idx] = static_cast(idx); + } + + // Key scheduling algorithm (KSA) + j = 0; + for (int idx = 0; idx < 256; ++idx) { + j = (j + state[idx] + key[idx % key.size()]) & 0xFF; + std::swap(state[idx], state[j]); + } + + i = 0; + j = 0; +} + +void WardenCrypto::processRC4(const uint8_t* input, uint8_t* output, size_t length, + std::vector& state, uint8_t& i, uint8_t& j) { + for (size_t idx = 0; idx < length; ++idx) { + i = (i + 1) & 0xFF; + j = (j + state[i]) & 0xFF; + std::swap(state[i], state[j]); + + uint8_t k = state[(state[i] + state[j]) & 0xFF]; + output[idx] = input[idx] ^ k; + } +} + +} // namespace game +} // namespace wowee