diff --git a/src/game/Warden/Warden.cpp b/src/game/Warden/Warden.cpp index 76edb996c..2a1a7e766 100644 --- a/src/game/Warden/Warden.cpp +++ b/src/game/Warden/Warden.cpp @@ -212,6 +212,35 @@ void Warden::SetNewState(WardenState::Value state) _clientResponseTimer = 0; } +bool Warden::IsValidIncomingOpcode(uint8 opcode) const +{ + switch (opcode) + { + case WARDEN_CMSG_MODULE_MISSING: + case WARDEN_CMSG_MODULE_OK: + // The client tells us whether it has the module after we sent + // WARDEN_SMSG_MODULE_USE. + return _state == WardenState::STATE_REQUESTED_MODULE; + + case WARDEN_CMSG_MODULE_FAILED: + // Sent if loading the module we transferred fails on the client. + return _state == WardenState::STATE_REQUESTED_MODULE + || _state == WardenState::STATE_SENT_MODULE; + + case WARDEN_CMSG_HASH_RESULT: + // Reply to WARDEN_SMSG_HASH_REQUEST. + return _state == WardenState::STATE_REQUESTED_HASH; + + case WARDEN_CMSG_CHEAT_CHECKS_RESULT: + case WARDEN_CMSG_MEM_CHECKS_RESULT: + // Reply to WARDEN_SMSG_CHEAT_CHECKS_REQUEST. + return _state == WardenState::STATE_REQUESTED_DATA; + + default: + return false; + } +} + bool Warden::IsValidCheckSum(uint32 checksum, const uint8* data, const uint16 length) { uint32 newChecksum = BuildChecksum(data, length); @@ -314,6 +343,16 @@ void WorldSession::HandleWardenDataOpcode(WorldPacket& recvData) sLog.outWarden("Got packet, opcode %02X, size %u", opcode, uint32(recvData.size())); recvData.hexlike(); + if (!_warden->IsValidIncomingOpcode(opcode)) + { + sLog.outWarden("Account %u sent Warden opcode %02X in unexpected state %s (latency %u, IP %s)", + GetAccountId(), opcode, WardenState::to_string(_warden->GetState()), + GetLatency(), GetRemoteAddress().c_str()); + // Drain the packet so partial reads don't bleed into later handlers. + recvData.rpos(recvData.wpos()); + return; + } + switch (opcode) { case WARDEN_CMSG_MODULE_MISSING: @@ -326,17 +365,27 @@ void WorldSession::HandleWardenDataOpcode(WorldPacket& recvData) _warden->HandleData(recvData); break; case WARDEN_CMSG_MEM_CHECKS_RESULT: - sLog.outWarden("NYI WARDEN_CMSG_MEM_CHECKS_RESULT received!"); + // Sent by the client when a MEM_CHECK byte sequence does not match. + // We treat that as a failed check and apply the configured penalty. + sLog.outWarden("Account %u (%s) reported MEM_CHECK mismatch via WARDEN_CMSG_MEM_CHECKS_RESULT. Action: %s", + GetAccountId(), GetPlayerName(), _warden->Penalty().c_str()); + recvData.rpos(recvData.wpos()); break; case WARDEN_CMSG_HASH_RESULT: _warden->HandleHashResult(recvData); _warden->InitializeModule(); break; case WARDEN_CMSG_MODULE_FAILED: - sLog.outWarden("NYI WARDEN_CMSG_MODULE_FAILED received!"); + // Module failed to load on the client side - usually a cache fail. + // Log explicitly and apply penalty if configured. + sLog.outWarden("Account %u (%s) reported MODULE_FAILED. Action: %s", + GetAccountId(), GetPlayerName(), _warden->Penalty().c_str()); + recvData.rpos(recvData.wpos()); break; default: - sLog.outWarden("Got unknown warden opcode %02X of size %u.", opcode, uint32(recvData.size() - 1)); + sLog.outWarden("Got unknown warden opcode %02X of size %u from account %u.", + opcode, uint32(recvData.size() - 1), GetAccountId()); + recvData.rpos(recvData.wpos()); break; } } diff --git a/src/game/Warden/Warden.h b/src/game/Warden/Warden.h index 4521c54e4..97c92a14f 100644 --- a/src/game/Warden/Warden.h +++ b/src/game/Warden/Warden.h @@ -168,6 +168,10 @@ class Warden void EncryptData(uint8* buffer, uint32 length); void SetNewState(WardenState::Value state); + WardenState::Value GetState() const { return _state; } + + // Returns true if the given client opcode is valid given the current state. + bool IsValidIncomingOpcode(uint8 opcode) const; static bool IsValidCheckSum(uint32 checksum, const uint8 *data, const uint16 length); static uint32 BuildChecksum(const uint8 *data, uint32 length); diff --git a/src/game/Warden/WardenCheckMgr.cpp b/src/game/Warden/WardenCheckMgr.cpp index fda9aaf96..b256447aa 100644 --- a/src/game/Warden/WardenCheckMgr.cpp +++ b/src/game/Warden/WardenCheckMgr.cpp @@ -58,8 +58,28 @@ void WardenCheckMgr::LoadWardenChecks() sLog.outString(">> Warden disabled, loading checks skipped."); return; } - // 0 1 2 3 4 5 6 7 8 - QueryResult *result = WorldDatabase.Query("SELECT `id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment` FROM `warden` ORDER BY `build` ASC, `id` ASC"); + + // Detect whether the optional `groupid` column exists in this deployment's + // schema. If it does, include it in the SELECT so rotation can use it. + bool hasGroupId = false; + { + QueryResult* probe = WorldDatabase.Query("SHOW COLUMNS FROM `warden` LIKE 'groupid'"); + if (probe) + { + hasGroupId = true; + delete probe; + } + } + + QueryResult *result = NULL; + if (hasGroupId) + { // 0 1 2 3 4 5 6 7 8 9 + result = WorldDatabase.Query("SELECT `id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`, `groupid` FROM `warden` ORDER BY `build` ASC, `id` ASC"); + } + else + { // 0 1 2 3 4 5 6 7 8 + result = WorldDatabase.Query("SELECT `id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment` FROM `warden` ORDER BY `build` ASC, `id` ASC"); + } if (!result) { @@ -83,10 +103,12 @@ void WardenCheckMgr::LoadWardenChecks() uint8 length = fields[6].GetUInt8(); std::string str = fields[7].GetString(); std::string comment = fields[8].GetString(); + uint16 groupId = hasGroupId ? fields[9].GetUInt16() : 0; WardenCheck* wardenCheck = new WardenCheck(); wardenCheck->Type = checkType; wardenCheck->CheckId = id; + wardenCheck->GroupId = groupId; // Initialize action with default action from config wardenCheck->Action = WardenActions(sWorld.getConfig(CONFIG_UINT32_WARDEN_CLIENT_FAIL_ACTION)); @@ -242,23 +264,80 @@ WardenCheckResult* WardenCheckMgr::GetWardenResultById(uint16 build, uint16 id) return result; } +// MEM_CHECK and MODULE_CHECK go through the dedicated memory queue. Every other +// check type goes through the "other" queue. The two queues must be disjoint so +// the same check id is never sent twice in the same cycle. +static bool IsMemoryQueueCheck(uint8 type) +{ + return type == MEM_CHECK || type == MODULE_CHECK; +} + void WardenCheckMgr::GetWardenCheckIds(bool isMemCheck, uint16 build, std::list& idl) { idl.clear(); //just to be sure - ACE_READ_GUARD(LOCK, g, m_lock) - for (CheckMap::iterator it = CheckStore.lower_bound(build); it != CheckStore.upper_bound(build); ++it) + // Bucket by groupid so we can interleave when consuming. groupid 0 is treated + // as "no group" - one bucket per check. + typedef std::map > GroupBuckets; + GroupBuckets buckets; + std::vector ungrouped; + { - if (isMemCheck) + ACE_READ_GUARD(LOCK, g, m_lock) + for (CheckMap::iterator it = CheckStore.lower_bound(build); it != CheckStore.upper_bound(build); ++it) { - if ((it->second->Type == MEM_CHECK) || (it->second->Type == MODULE_CHECK)) + const uint8 type = it->second->Type; + const bool isMem = IsMemoryQueueCheck(type); + + if (isMemCheck) + { + if (!isMem) + { + continue; + } + } + else { - idl.push_back(it->second->CheckId); + // Skip TIMING_CHECK - WardenWin::RequestData() appends a + // synthetic TIMING_CHECK per cycle. + if (isMem || type == TIMING_CHECK) + { + continue; + } + } + + if (it->second->GroupId == 0) + { + ungrouped.push_back(it->second->CheckId); + } + else + { + buckets[it->second->GroupId].push_back(it->second->CheckId); } } - else + } + + // Round-robin across grouped buckets so checks belonging to the same group + // do not all get consumed back-to-back in a single cycle. This minimizes + // repeated coverage of similar checks. + bool madeProgress; + do + { + madeProgress = false; + for (GroupBuckets::iterator it = buckets.begin(); it != buckets.end(); ++it) { - idl.push_back(it->second->CheckId); + if (!it->second.empty()) + { + idl.push_back(it->second.back()); + it->second.pop_back(); + madeProgress = true; + } } + } while (madeProgress); + + // Ungrouped checks tail (insertion order). + for (std::vector::iterator it = ungrouped.begin(); it != ungrouped.end(); ++it) + { + idl.push_back(*it); } } diff --git a/src/game/Warden/WardenCheckMgr.h b/src/game/Warden/WardenCheckMgr.h index 65310134d..7a139377e 100644 --- a/src/game/Warden/WardenCheckMgr.h +++ b/src/game/Warden/WardenCheckMgr.h @@ -45,6 +45,7 @@ struct WardenCheck std::string Str; // LUA, MPQ, DRIVER std::string Comment; uint16 CheckId; + uint16 GroupId; // Optional grouping for rotation; 0 means ungrouped enum WardenActions Action; }; diff --git a/src/game/Warden/WardenWin.cpp b/src/game/Warden/WardenWin.cpp index fdf39714f..d813f0649 100644 --- a/src/game/Warden/WardenWin.cpp +++ b/src/game/Warden/WardenWin.cpp @@ -95,11 +95,13 @@ void WardenWin::InitializeModule() { sLog.outWarden("Initialize module"); - // Create packet structure + // Zero-initialize so checksums never cover uninitialized stack bytes. WardenInitModuleRequest Request; + memset(&Request, 0, sizeof(Request)); + + // Block 1 - SFile* function table Request.Command1 = WARDEN_SMSG_MODULE_INITIALIZE; Request.Size1 = 20; - Request.CheckSumm1 = BuildChecksum(&Request.Unk1, 20); Request.Unk1 = 1; Request.Unk2 = 0; Request.Type = 1; @@ -109,24 +111,29 @@ void WardenWin::InitializeModule() Request.Function1[2] = 0x00248460; // 0x00400000 + 0x00248460 SFileReadFile Request.Function1[3] = 0x00248730; // 0x00400000 + 0x00248730 SFileCloseFile + // Block 2 - FrameScript::GetText Request.Command2 = WARDEN_SMSG_MODULE_INITIALIZE; Request.Size2 = 8; - Request.CheckSumm2 = BuildChecksum(&Request.Unk2, 8); Request.Unk3 = 4; Request.Unk4 = 0; Request.String_library2 = 0; Request.Function2 = 0x00419D40; // 0x00400000 + 0x00419D40 FrameScript::GetText Request.Function2_set = 1; + // Block 3 - PerformanceCounter Request.Command3 = WARDEN_SMSG_MODULE_INITIALIZE; Request.Size3 = 8; - Request.CheckSumm3 = BuildChecksum(&Request.Unk5, 8); Request.Unk5 = 1; Request.Unk6 = 1; Request.String_library3 = 0; Request.Function3 = 0x0046AE20; // 0x00400000 + 0x0046AE20 PerformanceCounter Request.Function3_set = 1; + // Compute checksums AFTER all covered fields have been assigned. + Request.CheckSumm1 = BuildChecksum(&Request.Unk1, 20); + Request.CheckSumm2 = BuildChecksum(&Request.Unk3, 8); + Request.CheckSumm3 = BuildChecksum(&Request.Unk5, 8); + // Encrypt with warden RC4 key. EncryptData((uint8*)&Request, sizeof(WardenInitModuleRequest)); @@ -180,24 +187,38 @@ void WardenWin::RequestData() sWardenCheckMgr->GetWardenCheckIds(false, build, _otherChecksTodo); } + // No checks defined for this build at all - controlled disable rather than + // sending an empty request that the client may reject. + if (_memChecksTodo.empty() && _otherChecksTodo.empty()) + { + sLog.outWarden("No Warden checks loaded for build %u (account %u). Skipping request.", + build, _session->GetAccountId()); + Warden::RequestData(); + return; + } + _serverTicks = GameTime::GetGameTimeMS(); _currentChecks.clear(); - // Build check request + // Build check request - memory checks for (uint16 i = 0; i < sWorld.getConfig(CONFIG_UINT32_WARDEN_NUM_MEM_CHECKS); ++i) { - // If todo list is done break loop (will be filled on next Update() run) if (_memChecksTodo.empty()) { break; } - // Get check id from the end and remove it from todo id = _memChecksTodo.back(); _memChecksTodo.pop_back(); - // Add the id to the list sent in this cycle + // Skip checks whose metadata could not be resolved (missing/malformed DB row). + if (!sWardenCheckMgr->GetWardenDataById(build, id)) + { + sLog.outWarden("Skipping unknown memory check id %u for build %u", id, build); + continue; + } + _currentChecks.push_back(id); } @@ -206,35 +227,33 @@ void WardenWin::RequestData() for (uint16 i = 0; i < sWorld.getConfig(CONFIG_UINT32_WARDEN_NUM_OTHER_CHECKS); ++i) { - // If todo list is done break loop (will be filled on next Update() run) if (_otherChecksTodo.empty()) { break; } - // Get check id from the end and remove it from todo id = _otherChecksTodo.back(); _otherChecksTodo.pop_back(); - // Add the id to the list sent in this cycle + wd = sWardenCheckMgr->GetWardenDataById(build, id); + if (!wd) + { + sLog.outWarden("Skipping unknown check id %u for build %u", id, build); + continue; + } + _currentChecks.push_back(id); - // if we are here, the function is guaranteed to not return NULL - // but ... who knows - wd = sWardenCheckMgr->GetWardenDataById(build, id); - if (wd) + switch (wd->Type) { - switch (wd->Type) - { - case MPQ_CHECK: - case LUA_STR_CHECK: - case DRIVER_CHECK: - buff << uint8(wd->Str.size()); - buff.append(wd->Str.c_str(), wd->Str.size()); - break; - default: - break; - } + case MPQ_CHECK: + case LUA_STR_CHECK: + case DRIVER_CHECK: + buff << uint8(wd->Str.size()); + buff.append(wd->Str.c_str(), wd->Str.size()); + break; + default: + break; } } @@ -246,9 +265,17 @@ void WardenWin::RequestData() uint8 index = 1; - for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) + for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ) { wd = sWardenCheckMgr->GetWardenDataById(build, *itr); + if (!wd) + { + // Defensive: should never trip because we filtered above, but if a row + // disappears between filter and use, drop the id from the cycle. + sLog.outWarden("Warden check id %u disappeared mid-cycle (build %u), removing", *itr, build); + itr = _currentChecks.erase(itr); + continue; + } type = wd->Type; buff << uint8(type ^ xorByte); @@ -303,6 +330,7 @@ void WardenWin::RequestData() default: break; // Should never happen } + ++itr; } buff << uint8(xorByte); buff.hexlike(); @@ -330,11 +358,40 @@ void WardenWin::HandleData(ByteBuffer &buff) { sLog.outWarden("Handle data"); + // Reject responses we never asked for instead of trying to parse against + // a stale (or empty) _currentChecks list. + if (_state != WardenState::STATE_REQUESTED_DATA) + { + sLog.outWarden("Account %u sent CHEAT_CHECKS_RESULT in unexpected state %s", + _session->GetAccountId(), WardenState::to_string(_state)); + buff.rpos(buff.wpos()); + return; + } + + // We need at least Length(2) + Checksum(4) + Timing(1) + Ticks(4) = 11 bytes + // before we can safely start reading. + if (buff.size() - buff.rpos() < 11) + { + sLog.outWarden("%s sent truncated Warden response (size %u). Action: %s", + _session->GetPlayerName(), uint32(buff.size() - buff.rpos()), Penalty().c_str()); + buff.rpos(buff.wpos()); + return; + } + uint16 Length; buff >> Length; uint32 Checksum; buff >> Checksum; + // Length must fit within the buffer or memcmp/checksum will read OOB. + if (Length > buff.size() - buff.rpos()) + { + sLog.outWarden("%s sent invalid Warden length %u (remaining %u). Action: %s", + _session->GetPlayerName(), Length, uint32(buff.size() - buff.rpos()), Penalty().c_str()); + buff.rpos(buff.wpos()); + return; + } + if (!IsValidCheckSum(Checksum, buff.contents() + buff.rpos(), Length)) { buff.rpos(buff.wpos()); @@ -361,146 +418,235 @@ void WardenWin::HandleData(ByteBuffer &buff) sLog.outWarden("ServerTicks %u, RequestTicks %u, ClientTicks %u", ticksNow, _serverTicks, newClientTicks); // Now, At request, At response sLog.outWarden("Waittime %u", ourTicks - newClientTicks); - } WardenCheckResult* rs; WardenCheck *rd; uint8 type; - uint16 checkFailed = 0; + std::list failedChecks; - for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) + try { - rd = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), *itr); - rs = sWardenCheckMgr->GetWardenResultById(_session->GetClientBuild(), *itr); - - type = rd->Type; - switch (type) + for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) { - case MEM_CHECK: - { - uint8 Mem_Result; - buff >> Mem_Result; + rd = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), *itr); + rs = sWardenCheckMgr->GetWardenResultById(_session->GetClientBuild(), *itr); - if (Mem_Result != 0) - { - sLog.outWarden("RESULT MEM_CHECK not 0x00, CheckId %u account Id %u", *itr, _session->GetAccountId()); - checkFailed = *itr; - continue; - } - if (memcmp(buff.contents() + buff.rpos(), rs->Result.AsByteArray(0, false), rd->Length) != 0) - { - sLog.outWarden("RESULT MEM_CHECK fail CheckId %u account Id %u", *itr, _session->GetAccountId()); - checkFailed = *itr; - buff.rpos(buff.rpos() + rd->Length); - continue; - } - - buff.rpos(buff.rpos() + rd->Length); - sLog.outWarden("RESULT MEM_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); + // Without metadata we cannot know how many bytes this check consumes; + // any further parsing would desynchronize us from the rest of the buffer. + if (!rd) + { + sLog.outWarden("Warden response for unknown check id %u (account %u) - aborting parse", + *itr, _session->GetAccountId()); + buff.rpos(buff.wpos()); break; } - case PAGE_CHECK_A: - case PAGE_CHECK_B: - case DRIVER_CHECK: - case MODULE_CHECK: + + type = rd->Type; + switch (type) { - const uint8 byte = 0xE9; - if (memcmp(buff.contents() + buff.rpos(), &byte, sizeof(uint8)) != 0) + case MEM_CHECK: { - if (type == PAGE_CHECK_A || type == PAGE_CHECK_B) + uint8 Mem_Result; + buff >> Mem_Result; + + if (Mem_Result != 0) { - sLog.outWarden("RESULT PAGE_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + sLog.outWarden("RESULT MEM_CHECK not 0x00, CheckId %u account Id %u", *itr, _session->GetAccountId()); + failedChecks.push_back(*itr); + continue; } - if (type == MODULE_CHECK) + + if (!rs) { - sLog.outWarden("RESULT MODULE_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + sLog.outWarden("MEM_CHECK id %u missing expected result row - skipping", *itr); + buff.read_skip(rd->Length); + continue; } - if (type == DRIVER_CHECK) + + if (buff.rpos() + rd->Length > buff.size()) { - sLog.outWarden("RESULT DRIVER_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + sLog.outWarden("MEM_CHECK id %u response truncated - aborting parse", *itr); + buff.rpos(buff.wpos()); + break; } - checkFailed = *itr; - buff.rpos(buff.rpos() + 1); - continue; - } - buff.rpos(buff.rpos() + 1); - if (type == PAGE_CHECK_A || type == PAGE_CHECK_B) - { - sLog.outWarden("RESULT PAGE_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); + if (memcmp(buff.contents() + buff.rpos(), rs->Result.AsByteArray(0, false), rd->Length) != 0) + { + sLog.outWarden("RESULT MEM_CHECK fail CheckId %u account Id %u", *itr, _session->GetAccountId()); + failedChecks.push_back(*itr); + buff.read_skip(rd->Length); + continue; + } + + buff.read_skip(rd->Length); + sLog.outWarden("RESULT MEM_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); + break; } - else if (type == MODULE_CHECK) + case PAGE_CHECK_A: + case PAGE_CHECK_B: + case DRIVER_CHECK: + case MODULE_CHECK: { - sLog.outWarden("RESULT MODULE_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); + if (buff.rpos() + 1 > buff.size()) + { + sLog.outWarden("PAGE/DRIVER/MODULE check id %u response truncated - aborting", *itr); + buff.rpos(buff.wpos()); + break; + } + + const uint8 byte = 0xE9; + if (memcmp(buff.contents() + buff.rpos(), &byte, sizeof(uint8)) != 0) + { + if (type == PAGE_CHECK_A || type == PAGE_CHECK_B) + { + sLog.outWarden("RESULT PAGE_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + } + if (type == MODULE_CHECK) + { + sLog.outWarden("RESULT MODULE_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + } + if (type == DRIVER_CHECK) + { + sLog.outWarden("RESULT DRIVER_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + } + failedChecks.push_back(*itr); + buff.read_skip(1); + continue; + } + + buff.read_skip(1); + if (type == PAGE_CHECK_A || type == PAGE_CHECK_B) + { + sLog.outWarden("RESULT PAGE_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); + } + else if (type == MODULE_CHECK) + { + sLog.outWarden("RESULT MODULE_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); + } + else if (type == DRIVER_CHECK) + { + sLog.outWarden("RESULT DRIVER_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); + } + break; } - else if (type == DRIVER_CHECK) + case LUA_STR_CHECK: { - sLog.outWarden("RESULT DRIVER_CHECK passed CheckId %u account Id %u", *itr, _session->GetAccountId()); - } - break; - } - case LUA_STR_CHECK: - { - uint8 Lua_Result; - buff >> Lua_Result; + uint8 Lua_Result; + buff >> Lua_Result; - if (Lua_Result != 0) - { - sLog.outWarden("RESULT LUA_STR_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); - checkFailed = *itr; - continue; - } + if (Lua_Result != 0) + { + sLog.outWarden("RESULT LUA_STR_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + failedChecks.push_back(*itr); + continue; + } - uint8 luaStrLen; - buff >> luaStrLen; + uint8 luaStrLen; + buff >> luaStrLen; - if (luaStrLen != 0) - { - char *str = new char[luaStrLen + 1]; - memcpy(str, buff.contents() + buff.rpos(), luaStrLen); - str[luaStrLen] = '\0'; // null terminator - sLog.outWarden("Lua string: %s", str); - delete[] str; + if (luaStrLen != 0) + { + if (buff.rpos() + luaStrLen > buff.size()) + { + sLog.outWarden("LUA_STR_CHECK id %u length %u exceeds buffer - aborting", *itr, luaStrLen); + buff.rpos(buff.wpos()); + break; + } + + std::vector str(luaStrLen + 1); + memcpy(str.data(), buff.contents() + buff.rpos(), luaStrLen); + str[luaStrLen] = '\0'; + sLog.outWarden("Lua string: %s", str.data()); + } + buff.read_skip(luaStrLen); + sLog.outWarden("RESULT LUA_STR_CHECK passed, CheckId %u account Id %u", *itr, _session->GetAccountId()); + break; } - buff.rpos(buff.rpos() + luaStrLen); // Skip string - sLog.outWarden("RESULT LUA_STR_CHECK passed, CheckId %u account Id %u", *itr, _session->GetAccountId()); - break; - } - case MPQ_CHECK: - { - uint8 Mpq_Result; - buff >> Mpq_Result; - - if (Mpq_Result != 0) + case MPQ_CHECK: { - sLog.outWarden("RESULT MPQ_CHECK not 0x00 account id %u", _session->GetAccountId()); - checkFailed = *itr; - continue; - } + uint8 Mpq_Result; + buff >> Mpq_Result; - if (memcmp(buff.contents() + buff.rpos(), rs->Result.AsByteArray(0, false), 20) != 0) // SHA1 - { - sLog.outWarden("RESULT MPQ_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); - checkFailed = *itr; - buff.rpos(buff.rpos() + 20); // 20 bytes SHA1 - continue; - } + if (Mpq_Result != 0) + { + sLog.outWarden("RESULT MPQ_CHECK not 0x00 account id %u", _session->GetAccountId()); + failedChecks.push_back(*itr); + continue; + } - buff.rpos(buff.rpos() + 20); // 20 bytes SHA1 - sLog.outWarden("RESULT MPQ_CHECK passed, CheckId %u account Id %u", *itr, _session->GetAccountId()); - break; + if (!rs) + { + sLog.outWarden("MPQ_CHECK id %u missing expected result row - skipping", *itr); + buff.read_skip(20); + continue; + } + + if (buff.rpos() + 20 > buff.size()) + { + sLog.outWarden("MPQ_CHECK id %u response truncated - aborting", *itr); + buff.rpos(buff.wpos()); + break; + } + + if (memcmp(buff.contents() + buff.rpos(), rs->Result.AsByteArray(0, false), 20) != 0) // SHA1 + { + sLog.outWarden("RESULT MPQ_CHECK fail, CheckId %u account Id %u", *itr, _session->GetAccountId()); + failedChecks.push_back(*itr); + buff.read_skip(20); + continue; + } + + buff.read_skip(20); + sLog.outWarden("RESULT MPQ_CHECK passed, CheckId %u account Id %u", *itr, _session->GetAccountId()); + break; + } + default: // Should never happen + sLog.outWarden("Unhandled Warden check type %u (CheckId %u) - aborting parse", + uint32(type), *itr); + buff.rpos(buff.wpos()); + break; } - default: // Should never happen - break; } } + catch (ByteBufferException&) + { + // ByteBuffer reads throw on underflow; treat as malformed packet. + sLog.outWarden("%s sent malformed Warden response (buffer underflow). Action: %s", + _session->GetPlayerName(), Penalty().c_str()); + buff.rpos(buff.wpos()); + return; + } - if (checkFailed > 0) + if (!failedChecks.empty()) { - WardenCheck* check = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), checkFailed); //note it IS NOT NULL here - sLog.outWarden("%s failed Warden check %u. Action: %s", _session->GetPlayerName(), checkFailed, Penalty(check).c_str()); - LogPositiveToDB(check); + // Log every failure so audits aren't limited to the last failed id, then + // apply the strictest penalty configured for any failed check. + WardenCheck* worst = NULL; + for (std::list::iterator itr = failedChecks.begin(); itr != failedChecks.end(); ++itr) + { + WardenCheck* check = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), *itr); + if (!check) + { + continue; + } + + sLog.outWarden("%s failed Warden check %u (%s)", + _session->GetPlayerName(), *itr, + check->Comment.empty() ? "Undocumented Check" : check->Comment.c_str()); + LogPositiveToDB(check); + + if (!worst || check->Action > worst->Action) + { + worst = check; + } + } + + if (worst) + { + sLog.outWarden("%s Warden penalty action: %s", + _session->GetPlayerName(), Penalty(worst).c_str()); + } } Warden::HandleData(buff); diff --git a/src/mangosd/mangosd.conf.dist.in b/src/mangosd/mangosd.conf.dist.in index 480ccdce3..fc9200649 100644 --- a/src/mangosd/mangosd.conf.dist.in +++ b/src/mangosd/mangosd.conf.dist.in @@ -1721,8 +1721,11 @@ QuestTracker.Enable= 0 # Description: Default action being taken if a client check failed. Actions can be # overwritten for each single check via warden_action table in characters # database. -# Default: 1 - (Kick) -# 0 - (Disabled, Logging only) +# Recommendation: leave at 0 (log-only) until Warden has been validated +# against your real client population. Switch to 1 (kick) or 2 (ban) +# only after the Warden log shows no false positives. +# Default: 0 - (Disabled, Logging only) +# 1 - (Kick) # 2 - (Ban) # # Warden.BanDuration @@ -1746,7 +1749,7 @@ Warden.NumMemChecks = 3 Warden.NumOtherChecks = 7 Warden.ClientResponseDelay = 600 Warden.ClientCheckHoldOff = 30 -Warden.ClientCheckFailAction = 1 +Warden.ClientCheckFailAction = 0 Warden.BanDuration = 86400 Warden.DBLogLevel = 0