fix: harden turtle movement parsing and warden fallback

This commit is contained in:
Kelsi 2026-03-14 22:18:28 -07:00
parent f44ef7b9ea
commit eea3784976
3 changed files with 91 additions and 29 deletions

View file

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

View file

@ -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<uint8_t> seed(decrypted.begin() + 1, decrypted.begin() + 17);
auto applyWardenSeedRekey = [&](const std::vector<uint8_t>& rekeySeed) {
// Derive new RC4 keys from the seed using SHA1Randx.
uint8_t newEncryptKey[16], newDecryptKey[16];
WardenCrypto::sha1RandxGenerate(rekeySeed, newEncryptKey, newDecryptKey);
std::vector<uint8_t> ek(newEncryptKey, newEncryptKey + 16);
std::vector<uint8_t> 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<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);
}
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;
}
@ -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<uint8_t> 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<uint8_t> ek(newEncryptKey, newEncryptKey + 16);
std::vector<uint8_t> 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) {

View file

@ -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");