From eea3784976922ffad06cf3978a515a4ffff347de Mon Sep 17 00:00:00 2001 From: Kelsi Date: Sat, 14 Mar 2026 22:18:28 -0700 Subject: [PATCH] fix: harden turtle movement parsing and warden fallback --- include/game/game_handler.hpp | 2 + src/game/game_handler.cpp | 85 +++++++++++++++++++---------- src/game/packet_parsers_classic.cpp | 33 +++++++++++ 3 files changed, 91 insertions(+), 29 deletions(-) diff --git a/include/game/game_handler.hpp b/include/game/game_handler.hpp index b1ddeb97..c308dadb 100644 --- a/include/game/game_handler.hpp +++ b/include/game/game_handler.hpp @@ -2434,6 +2434,8 @@ private: std::chrono::steady_clock::time_point movementClockStart_ = std::chrono::steady_clock::now(); uint32_t lastMovementTimestampMs_ = 0; bool serverMovementAllowed_ = true; + uint32_t monsterMovePacketsThisTick_ = 0; + uint32_t monsterMovePacketsDroppedThisTick_ = 0; // Fall/jump tracking for movement packet correctness. // fallTime must be the elapsed ms since the FALLING flag was set; the server diff --git a/src/game/game_handler.cpp b/src/game/game_handler.cpp index 072944e0..f0acaeb3 100644 --- a/src/game/game_handler.cpp +++ b/src/game/game_handler.cpp @@ -757,6 +757,10 @@ void GameHandler::update(float deltaTime) { return; } + // Reset per-tick monster-move budget tracking (Classic/Turtle flood protection). + monsterMovePacketsThisTick_ = 0; + monsterMovePacketsDroppedThisTick_ = 0; + // Update socket (processes incoming data and triggers callbacks) if (socket) { auto socketStart = std::chrono::steady_clock::now(); @@ -7960,7 +7964,7 @@ void GameHandler::handlePacket(network::Packet& packet) { uint64_t kickerGuid = packet.readUInt64(); uint32_t reasonType = packet.readUInt32(); std::string reason; - if (packet.getSize() - packet.getReadPos() > 0) + if (packet.getReadPos() < packet.getSize()) reason = packet.readString(); (void)kickerGuid; (void)reasonType; @@ -8006,14 +8010,14 @@ void GameHandler::handlePacket(network::Packet& packet) { uint32_t ticketId = packet.readUInt32(); std::string subject; std::string body; - if (packet.getSize() - packet.getReadPos() > 0) subject = packet.readString(); - if (packet.getSize() - packet.getReadPos() > 0) body = packet.readString(); + if (packet.getReadPos() < packet.getSize()) subject = packet.readString(); + if (packet.getReadPos() < packet.getSize()) body = packet.readString(); uint32_t responseCount = 0; if (packet.getSize() - packet.getReadPos() >= 4) responseCount = packet.readUInt32(); std::string responseText; for (uint32_t i = 0; i < responseCount && i < 10; ++i) { - if (packet.getSize() - packet.getReadPos() > 0) { + if (packet.getReadPos() < packet.getSize()) { std::string t = packet.readString(); if (i == 0) responseText = t; } @@ -9034,6 +9038,18 @@ void GameHandler::handleWardenData(network::Packet& packet) { } std::vector seed(decrypted.begin() + 1, decrypted.begin() + 17); + auto applyWardenSeedRekey = [&](const std::vector& rekeySeed) { + // Derive new RC4 keys from the seed using SHA1Randx. + uint8_t newEncryptKey[16], newDecryptKey[16]; + WardenCrypto::sha1RandxGenerate(rekeySeed, newEncryptKey, newDecryptKey); + + std::vector ek(newEncryptKey, newEncryptKey + 16); + std::vector dk(newDecryptKey, newDecryptKey + 16); + wardenCrypto_->replaceKeys(ek, dk); + for (auto& b : newEncryptKey) b = 0; + for (auto& b : newDecryptKey) b = 0; + LOG_DEBUG("Warden: Derived and applied key update from seed"); + }; // --- Try CR lookup (pre-computed challenge/response entries) --- if (!wardenCREntries_.empty()) { @@ -9082,7 +9098,24 @@ void GameHandler::handleWardenData(network::Packet& packet) { LOG_WARNING("Warden: No CR match, computing hash from loaded module"); if (!wardenLoadedModule_ || !wardenLoadedModule_->isLoaded()) { - LOG_ERROR("Warden: No loaded module and no CR match — cannot compute hash"); + LOG_WARNING("Warden: No loaded module and no CR match — using raw module fallback hash"); + + // Never skip HASH_RESULT: some realms disconnect quickly if this response is missing. + std::vector fallbackReply; + if (!wardenModuleData_.empty()) { + fallbackReply = auth::Crypto::sha1(wardenModuleData_); + } else if (!wardenModuleHash_.empty()) { + fallbackReply = auth::Crypto::sha1(wardenModuleHash_); + } else { + fallbackReply.assign(20, 0); + } + + std::vector 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; } @@ -9171,19 +9204,7 @@ void GameHandler::handleWardenData(network::Packet& packet) { 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); - for (auto& b : newEncryptKey) b = 0; - for (auto& b : newDecryptKey) b = 0; - LOG_DEBUG("Warden: Derived and applied key update from seed"); + applyWardenSeedRekey(seed); } wardenState_ = WardenState::WAIT_CHECKS; @@ -15560,9 +15581,9 @@ void GameHandler::handleLfgBootProposalUpdate(network::Packet& packet) { lfgBootNeeded_ = votesNeeded; // Optional: reason string and target name (null-terminated) follow the fixed fields - if (packet.getSize() - packet.getReadPos() > 0) + if (packet.getReadPos() < packet.getSize()) lfgBootReason_ = packet.readString(); - if (packet.getSize() - packet.getReadPos() > 0) + if (packet.getReadPos() < packet.getSize()) lfgBootTargetName_ = packet.readString(); if (inProgress) { @@ -16282,6 +16303,21 @@ void GameHandler::handleCompressedMoves(network::Packet& packet) { } void GameHandler::handleMonsterMove(network::Packet& packet) { + if (isActiveExpansion("classic") || isActiveExpansion("turtle")) { + constexpr uint32_t kMaxMonsterMovesPerTick = 256; + ++monsterMovePacketsThisTick_; + if (monsterMovePacketsThisTick_ > kMaxMonsterMovesPerTick) { + ++monsterMovePacketsDroppedThisTick_; + if (monsterMovePacketsDroppedThisTick_ <= 3 || + (monsterMovePacketsDroppedThisTick_ % 100) == 0) { + LOG_WARNING("SMSG_MONSTER_MOVE: per-tick cap exceeded, dropping packet", + " (processed=", monsterMovePacketsThisTick_, + " dropped=", monsterMovePacketsDroppedThisTick_, ")"); + } + return; + } + } + MonsterMoveData data; auto logMonsterMoveParseFailure = [&](const std::string& msg) { static uint32_t failCount = 0; @@ -16290,14 +16326,6 @@ void GameHandler::handleMonsterMove(network::Packet& packet) { LOG_WARNING(msg, " (occurrence=", failCount, ")"); } }; - auto logWrappedFallbackUsed = [&]() { - static uint32_t wrappedFallbackCount = 0; - ++wrappedFallbackCount; - if (wrappedFallbackCount <= 10 || (wrappedFallbackCount % 100) == 0) { - LOG_WARNING("SMSG_MONSTER_MOVE parsed via wrapped-subpacket fallback", - " (occurrence=", wrappedFallbackCount, ")"); - } - }; auto logWrappedUncompressedFallbackUsed = [&]() { static uint32_t wrappedUncompressedFallbackCount = 0; ++wrappedUncompressedFallbackCount; @@ -16352,7 +16380,6 @@ void GameHandler::handleMonsterMove(network::Packet& packet) { network::Packet wrappedPacket(packet.getOpcode(), stripped); if (packetParsers_->parseMonsterMove(wrappedPacket, data)) { parsed = true; - logWrappedFallbackUsed(); } } if (!parsed) { diff --git a/src/game/packet_parsers_classic.cpp b/src/game/packet_parsers_classic.cpp index 74935162..4a28e556 100644 --- a/src/game/packet_parsers_classic.cpp +++ b/src/game/packet_parsers_classic.cpp @@ -1818,6 +1818,39 @@ bool TurtlePacketParsers::parseMonsterMove(network::Packet& packet, MonsterMoveD return true; } + auto looksLikeWotlkMonsterMove = [&](network::Packet& probe) -> bool { + const size_t probeStart = probe.getReadPos(); + uint64_t guid = UpdateObjectParser::readPackedGuid(probe); + if (guid == 0) { + probe.setReadPos(probeStart); + return false; + } + if (probe.getReadPos() >= probe.getSize()) { + probe.setReadPos(probeStart); + return false; + } + uint8_t unk = probe.readUInt8(); + if (unk > 1) { + probe.setReadPos(probeStart); + return false; + } + if (probe.getReadPos() + 12 + 4 + 1 > probe.getSize()) { + probe.setReadPos(probeStart); + return false; + } + probe.readFloat(); probe.readFloat(); probe.readFloat(); // xyz + probe.readUInt32(); // splineId + uint8_t moveType = probe.readUInt8(); + probe.setReadPos(probeStart); + return moveType >= 1 && moveType <= 4; + }; + + packet.setReadPos(start); + if (!looksLikeWotlkMonsterMove(packet)) { + packet.setReadPos(start); + return false; + } + packet.setReadPos(start); if (MonsterMoveParser::parse(packet, data)) { LOG_DEBUG("[Turtle] SMSG_MONSTER_MOVE parsed via WotLK fallback layout");