/* * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for * more details. * * You should have received a copy of the GNU General Public License along * with this program. If not, see . */ #include "WardenWin.h" #include "ByteBuffer.h" #include "Common.h" #include "CryptoRandom.h" #include "GameTime.h" #include "HMAC.h" #include "Log.h" #include "Opcodes.h" #include "Player.h" #include "SessionKeyGenerator.h" #include "Util.h" #include "WardenCheckMgr.h" #include "WardenModuleWin.h" #include "World.h" #include "WorldPacket.h" #include "WorldSession.h" #include // GUILD is the shortest string that has no client validation (RAID only sends if in a raid group) static constexpr char _luaEvalPrefix[] = "local S,T,R=SendAddonMessage,function()"; static constexpr char _luaEvalMidfix[] = " end R=S and T()if R then S('_TW',"; static constexpr char _luaEvalPostfix[] = ",'GUILD')end"; static_assert((sizeof(_luaEvalPrefix)-1 + sizeof(_luaEvalMidfix)-1 + sizeof(_luaEvalPostfix)-1 + WARDEN_MAX_LUA_CHECK_LENGTH) == 255); static constexpr uint8 GetCheckPacketBaseSize(uint8 type) { switch (type) { case DRIVER_CHECK: case MPQ_CHECK: return 1; case LUA_EVAL_CHECK: return 1 + sizeof(_luaEvalPrefix) - 1 + sizeof(_luaEvalMidfix) - 1 + 4 + sizeof(_luaEvalPostfix) - 1; case PAGE_CHECK_A: return (4 + 1); case PAGE_CHECK_B: return (4 + 1); case MODULE_CHECK: return (4 + Acore::Crypto::Constants::SHA1_DIGEST_LENGTH_BYTES); case MEM_CHECK: return (1 + 4 + 1); default: return 0; } } static uint16 GetCheckPacketSize(WardenCheck const* check) { if (!check) { return 0; } uint16 size = 1; if (check->CheckId >= WardenPayloadMgr::WardenPayloadOffsetMin && check->Type == LUA_EVAL_CHECK) { // Custom payload has no prefix, midfix, postfix. size = size + (4 + 1); } else { size = size + GetCheckPacketBaseSize(check->Type); // 1 byte check type } if (!check->Str.empty()) { size += (static_cast(check->Str.length()) + 1); // 1 byte string length } BigNumber tempNumber = check->Data; if (!tempNumber.GetNumBytes()) { size += tempNumber.GetNumBytes(); } return size; } // Returns config id for specific type id static ServerConfigs GetMaxWardenChecksForType(uint8 type) { // Should never be higher type than defined ASSERT(type < MAX_WARDEN_CHECK_TYPES); switch (type) { case WARDEN_CHECK_MEM_TYPE: return CONFIG_WARDEN_NUM_MEM_CHECKS; case WARDEN_CHECK_LUA_TYPE: return CONFIG_WARDEN_NUM_LUA_CHECKS; default: break; } return CONFIG_WARDEN_NUM_OTHER_CHECKS; } WardenWin::WardenWin() : Warden(), _serverTicks(0) { } WardenWin::~WardenWin() = default; void WardenWin::Init(WorldSession* session, SessionKey const& k) { _session = session; // Generate Warden Key SessionKeyGenerator WK(k); WK.Generate(_inputKey, 16); WK.Generate(_outputKey, 16); memcpy(_seed, Module.Seed, 16); _inputCrypto.Init(_inputKey); _outputCrypto.Init(_outputKey); LOG_DEBUG("warden", "Server side warden for client {} initializing...", session->GetAccountId()); LOG_DEBUG("warden", "C->S Key: {}", Acore::Impl::ByteArrayToHexStr(_inputKey, 16)); LOG_DEBUG("warden", "S->C Key: {}", Acore::Impl::ByteArrayToHexStr(_outputKey,16)); LOG_DEBUG("warden", " Seed: {}", Acore::Impl::ByteArrayToHexStr(_seed, 16)); LOG_DEBUG("warden", "Loading Module..."); _module = GetModuleForClient(); LOG_DEBUG("warden", "Module Key: {}", ByteArrayToHexStr(_module->Key)); LOG_DEBUG("warden", "Module ID: {}", ByteArrayToHexStr(_module->Id)); RequestModule(); } ClientWardenModule* WardenWin::GetModuleForClient() { auto mod = new ClientWardenModule; uint32 length = sizeof(Module.Module); // data assign mod->CompressedSize = length; mod->CompressedData = new uint8[length]; memcpy(mod->CompressedData, Module.Module, length); memcpy(mod->Key.data(), Module.ModuleKey, 16); // md5 hash mod->Id = Acore::Crypto::MD5::GetDigestOf(mod->CompressedData, mod->CompressedSize); return mod; } void WardenWin::InitializeModule() { LOG_DEBUG("warden", "Initialize module"); // Create packet structure WardenInitModuleRequest Request{}; Request.Command1 = WARDEN_SMSG_MODULE_INITIALIZE; Request.Size1 = 20; Request.Unk1 = 1; Request.Unk2 = 0; Request.Type = 1; Request.String_library1 = 0; Request.Function1[0] = 0x00024F80; // 0x00400000 + 0x00024F80 SFileOpenFile Request.Function1[1] = 0x000218C0; // 0x00400000 + 0x000218C0 SFileGetFileSize Request.Function1[2] = 0x00022530; // 0x00400000 + 0x00022530 SFileReadFile Request.Function1[3] = 0x00022910; // 0x00400000 + 0x00022910 SFileCloseFile Request.CheckSumm1 = BuildChecksum(&Request.Unk1, Acore::Crypto::Constants::SHA1_DIGEST_LENGTH_BYTES); Request.Command2 = WARDEN_SMSG_MODULE_INITIALIZE; Request.Size2 = 8; Request.Unk3 = 4; Request.Unk4 = 0; Request.String_library2 = 0; Request.Function2 = 0x00419210; // 0x00400000 + 0x00419210 FrameScript::Execute Request.Function2_set = 1; Request.CheckSumm2 = BuildChecksum(&Request.Unk3, 8); Request.Command3 = WARDEN_SMSG_MODULE_INITIALIZE; Request.Size3 = 8; Request.Unk5 = 1; Request.Unk6 = 1; Request.String_library3 = 0; Request.Function3 = 0x0046AE20; // 0x00400000 + 0x0046AE20 PerformanceCounter Request.Function3_set = 1; Request.CheckSumm3 = BuildChecksum(&Request.Unk5, 8); EndianConvert(Request.Size1); EndianConvert(Request.CheckSumm1); EndianConvert(Request.Function1[0]); EndianConvert(Request.Function1[1]); EndianConvert(Request.Function1[2]); EndianConvert(Request.Function1[3]); EndianConvert(Request.Size2); EndianConvert(Request.CheckSumm2); EndianConvert(Request.Function2); EndianConvert(Request.Size3); EndianConvert(Request.CheckSumm3); EndianConvert(Request.Function3); // Encrypt with warden RC4 key. EncryptData(reinterpret_cast(&Request), sizeof(WardenInitModuleRequest)); WorldPacket pkt(SMSG_WARDEN_DATA, sizeof(WardenInitModuleRequest)); pkt.append(reinterpret_cast(&Request), sizeof(WardenInitModuleRequest)); _session->SendPacket(&pkt); } void WardenWin::RequestHash() { LOG_DEBUG("warden", "Request hash"); // Create packet structure WardenHashRequest Request{}; Request.Command = WARDEN_SMSG_HASH_REQUEST; memcpy(Request.Seed, _seed, 16); // Encrypt with warden RC4 key. EncryptData(reinterpret_cast(&Request), sizeof(WardenHashRequest)); WorldPacket pkt(SMSG_WARDEN_DATA, sizeof(WardenHashRequest)); pkt.append(reinterpret_cast(&Request), sizeof(WardenHashRequest)); _session->SendPacket(&pkt); } void WardenWin::HandleHashResult(ByteBuffer& buff) { buff.rpos(buff.wpos()); // Verify key using constant-time comparison if (CRYPTO_memcmp(buff.contents() + 1, Module.ClientKeySeedHash, Acore::Crypto::Constants::SHA1_DIGEST_LENGTH_BYTES) != 0) { LOG_DEBUG("warden", "Request hash reply: failed"); ApplyPenalty(0, "Request hash reply: failed"); return; } LOG_DEBUG("warden", "Request hash reply: succeed"); // Change keys here memcpy(_inputKey, Module.ClientKeySeed, 16); memcpy(_outputKey, Module.ServerKeySeed, 16); _inputCrypto.Init(_inputKey); _outputCrypto.Init(_outputKey); _initialized = true; } /** * @brief Gets the warden check state. * @return The warden check state. */ bool WardenWin::IsCheckInProgress() { return _checkInProgress; } /** * @brief Force call RequestChecks() so they are sent immediately, this interrupts warden and breaks result. */ void WardenWin::ForceChecks() { if (_dataSent) { _interrupted = true; _interruptCounter++; } RequestChecks(); } void WardenWin::RequestChecks() { LOG_DEBUG("warden", "Request data"); _checkInProgress = true; // If all checks were done, fill the todo list again for (uint8 i = 0; i < MAX_WARDEN_CHECK_TYPES; ++i) { if (_ChecksTodo[i].empty()) _ChecksTodo[i].assign(sWardenCheckMgr->CheckIdPool[i].begin(), sWardenCheckMgr->CheckIdPool[i].end()); } _serverTicks = GameTime::GetGameTimeMS().count(); _CurrentChecks.clear(); // Erase any nullptrs. Acore::Containers::EraseIf(_PendingChecks, [this](uint16 id) { WardenCheck const* check = sWardenCheckMgr->GetWardenDataById(id); // Custom payload should be loaded in if equal to over offset. if (!check && id >= WardenPayloadMgr::WardenPayloadOffsetMin) { if (_payloadMgr.CachedChecks.find(id) != _payloadMgr.CachedChecks.end()) { check = &_payloadMgr.CachedChecks.at(id); } } if (!check) { return true; } return false; } ); // No pending checks if (_PendingChecks.empty()) { for (uint8 checkType = 0; checkType < MAX_WARDEN_CHECK_TYPES; ++checkType) { for (uint32 y = 0; y < sWorld->getIntConfig(GetMaxWardenChecksForType(checkType)); ++y) { // If todo list is done break loop (will be filled on next Update() run) if (_ChecksTodo[checkType].empty()) { break; } // Load in any custom payloads if available. if (checkType == WARDEN_CHECK_LUA_TYPE && !_payloadMgr.QueuedPayloads.empty()) { uint16 payloadId = _payloadMgr.QueuedPayloads.front(); LOG_DEBUG("warden", "Adding custom warden payload '{}' to CurrentChecks.", payloadId); _payloadMgr.QueuedPayloads.pop_front(); _CurrentChecks.push_front(payloadId); continue; } // Get check id from the end and remove it from todo uint16 const id = _ChecksTodo[checkType].back(); _ChecksTodo[checkType].pop_back(); // Insert check to queue if (checkType == WARDEN_CHECK_LUA_TYPE) { _CurrentChecks.push_front(id); } else { _CurrentChecks.push_back(id); } } } } else { bool hasLuaChecks = false; for (uint16 const checkId : _PendingChecks) { WardenCheck const* check = sWardenCheckMgr->GetWardenDataById(checkId); // Custom payload should be loaded in if equal to over offset. if (!check && checkId >= WardenPayloadMgr::WardenPayloadOffsetMin) { check = &_payloadMgr.CachedChecks.at(checkId); } if (!hasLuaChecks && check->Type == LUA_EVAL_CHECK) { hasLuaChecks = true; } _CurrentChecks.push_back(checkId); } // Always include lua checks if (!hasLuaChecks) { for (uint32 i = 0; i < sWorld->getIntConfig(GetMaxWardenChecksForType(WARDEN_CHECK_LUA_TYPE)); ++i) { // If todo list is done break loop (will be filled on next Update() run) if (_ChecksTodo[WARDEN_CHECK_LUA_TYPE].empty()) { break; } // Get check id from the end and remove it from todo uint16 const id = _ChecksTodo[WARDEN_CHECK_LUA_TYPE].back(); _ChecksTodo[WARDEN_CHECK_LUA_TYPE].pop_back(); // Lua checks must be always in front _CurrentChecks.push_front(id); } } } // Filter too high checks queue // Filtered checks will get passed in next checks uint16 expectedSize = 4; _PendingChecks.clear(); Acore::Containers::EraseIf(_CurrentChecks, [this, &expectedSize](uint16 id) { WardenCheck const* check = sWardenCheckMgr->GetWardenDataById(id); // Custom payload should be loaded in if equal to over offset. if (!check && id >= WardenPayloadMgr::WardenPayloadOffsetMin) { check = &_payloadMgr.CachedChecks.at(id); } // Remove nullptr if it snuck in from earlier check. if (!check) { return true; } uint16 const thisSize = GetCheckPacketSize(check); if ((expectedSize + thisSize) > 500) // warden packets are truncated to 512 bytes clientside { _PendingChecks.push_back(id); return true; } expectedSize += thisSize; return false; } ); ByteBuffer buff; buff << uint8(WARDEN_SMSG_CHEAT_CHECKS_REQUEST); for (uint16 const checkId : _CurrentChecks) { WardenCheck const* check = sWardenCheckMgr->GetWardenDataById(checkId); // Custom payloads do not have prefix, midfix, postfix. if (!check && checkId >= WardenPayloadMgr::WardenPayloadOffsetMin) { check = &_payloadMgr.CachedChecks.at(checkId); buff << uint8(check->Str.size()); buff.append(check->Str.data(), check->Str.size()); continue; } switch (check->Type) { case LUA_EVAL_CHECK: { buff << uint8(sizeof(_luaEvalPrefix) - 1 + check->Str.size() + sizeof(_luaEvalMidfix) - 1 + check->IdStr.size() + sizeof(_luaEvalPostfix) - 1); buff.append(_luaEvalPrefix, sizeof(_luaEvalPrefix) - 1); buff.append(check->Str.data(), check->Str.size()); buff.append(_luaEvalMidfix, sizeof(_luaEvalMidfix) - 1); buff.append(check->IdStr.data(), check->IdStr.size()); buff.append(_luaEvalPostfix, sizeof(_luaEvalPostfix) - 1); break; } case MPQ_CHECK: case DRIVER_CHECK: { buff << uint8(check->Str.size()); buff.append(check->Str.c_str(), check->Str.size()); break; } } } uint8 const xorByte = _inputKey[0]; // Add TIMING_CHECK buff << uint8(0x00); buff << uint8(TIMING_CHECK ^ xorByte); uint8 index = 1; for (uint16 const checkId : _CurrentChecks) { WardenCheck const* check = sWardenCheckMgr->GetWardenDataById(checkId); // Custom payload should be loaded in if equal to over offset. if (!check && checkId >= WardenPayloadMgr::WardenPayloadOffsetMin) { check = &_payloadMgr.CachedChecks.at(checkId); } buff << uint8(check->Type ^ xorByte); switch (check->Type) { case MEM_CHECK: { buff << uint8(0x00); buff << uint32(check->Address); buff << uint8(check->Length); break; } case PAGE_CHECK_A: case PAGE_CHECK_B: { std::vector data = check->Data.ToByteVector(24, false); buff.append(data.data(), data.size()); buff << uint32(check->Address); buff << uint8(check->Length); break; } case MPQ_CHECK: case LUA_EVAL_CHECK: { buff << uint8(index++); break; } case DRIVER_CHECK: { std::vector data = check->Data.ToByteVector(24, false); buff.append(data.data(), data.size()); buff << uint8(index++); break; } case MODULE_CHECK: { std::array seed = Acore::Crypto::GetRandomBytes<4>(); buff.append(seed); buff.append(Acore::Crypto::HMAC_SHA1::GetDigestOf(seed, check->Str)); break; } /*case PROC_CHECK: { buff.append(wd->i.AsByteArray(0, false).get(), wd->i.GetNumBytes()); buff << uint8(index++); buff << uint8(index++); buff << uint32(wd->Address); buff << uint8(wd->Length); break; }*/ } } buff << uint8(xorByte); buff.hexlike(); // Encrypt with warden RC4 key EncryptData(buff.contents(), buff.size()); WorldPacket pkt(SMSG_WARDEN_DATA, buff.size()); pkt.append(buff); _session->SendPacket(&pkt); _dataSent = true; std::stringstream stream; stream << "Sent check id's: "; for (uint16 checkId : _CurrentChecks) { stream << checkId << " "; } LOG_DEBUG("warden", "{}", stream.str()); } void WardenWin::HandleData(ByteBuffer& buff) { LOG_DEBUG("warden", "Handle data"); _dataSent = false; _clientResponseTimer = 0; uint16 Length; buff >> Length; uint32 Checksum; buff >> Checksum; if (Length != (buff.size() - buff.rpos())) { buff.rfinish(); if (!_interrupted) { ApplyPenalty(0, "Failed size checks in HandleData"); } return; } if (!IsValidCheckSum(Checksum, buff.contents() + buff.rpos(), Length)) { buff.rpos(buff.wpos()); LOG_DEBUG("warden", "CHECKSUM FAIL"); if (!_interrupted) { ApplyPenalty(0, "Failed checksum in HandleData"); } return; } // TIMING_CHECK { uint8 result; buff >> result; /// @todo: test it. if (result == 0x00) { LOG_DEBUG("warden", "TIMING CHECK FAIL result 0x00"); // ApplyPenalty(0, "TIMING CHECK FAIL result"); Commented out because of too many false postives. Mostly caused by client stutter. return; } uint32 newClientTicks; buff >> newClientTicks; uint32 ticksNow = GameTime::GetGameTimeMS().count(); uint32 ourTicks = newClientTicks + (ticksNow - _serverTicks); LOG_DEBUG("warden", "ServerTicks {}", ticksNow); // Now LOG_DEBUG("warden", "RequestTicks {}", _serverTicks); // At request LOG_DEBUG("warden", "Ticks {}", newClientTicks); // At response LOG_DEBUG("warden", "Ticks diff {}", ourTicks - newClientTicks); } uint16 checkFailed = 0; for (uint16 const checkId : _CurrentChecks) { WardenCheck const* rd = sWardenCheckMgr->GetWardenDataById(checkId); // Custom payload should be loaded in if equal to over offset. if (!rd && checkId >= WardenPayloadMgr::WardenPayloadOffsetMin) { rd = &_payloadMgr.CachedChecks.at(checkId); } uint8 const type = rd->Type; switch (type) { case MEM_CHECK: { uint8 Mem_Result; buff >> Mem_Result; if (Mem_Result != 0) { LOG_DEBUG("warden", "RESULT MEM_CHECK not 0x00, CheckId {} account Id {}", checkId, _session->GetAccountId()); checkFailed = checkId; continue; } WardenCheckResult const* rs = sWardenCheckMgr->GetWardenResultById(checkId); std::vector result = rs->Result.ToByteVector(0, false); if (CRYPTO_memcmp(buff.contents() + buff.rpos(), result.data(), rd->Length) != 0) { LOG_DEBUG("warden", "RESULT MEM_CHECK fail CheckId {} account Id {}", checkId, _session->GetAccountId()); checkFailed = checkId; buff.rpos(buff.rpos() + rd->Length); continue; } buff.rpos(buff.rpos() + rd->Length); LOG_DEBUG("warden", "RESULT MEM_CHECK passed CheckId {} account Id {}", checkId, _session->GetAccountId()); break; } case PAGE_CHECK_A: case PAGE_CHECK_B: case DRIVER_CHECK: case MODULE_CHECK: { uint8 const byte = 0xE9; if (CRYPTO_memcmp(buff.contents() + buff.rpos(), &byte, sizeof(uint8)) != 0) { if (type == PAGE_CHECK_A || type == PAGE_CHECK_B) { LOG_DEBUG("warden", "RESULT PAGE_CHECK fail, CheckId {} account Id {}", checkId, _session->GetAccountId()); } if (type == MODULE_CHECK) { LOG_DEBUG("warden", "RESULT MODULE_CHECK fail, CheckId {} account Id {}", checkId, _session->GetAccountId()); } if (type == DRIVER_CHECK) { LOG_DEBUG("warden", "RESULT DRIVER_CHECK fail, CheckId {} account Id {}", checkId, _session->GetAccountId()); } checkFailed = checkId; buff.rpos(buff.rpos() + 1); continue; } buff.rpos(buff.rpos() + 1); if (type == PAGE_CHECK_A || type == PAGE_CHECK_B) { LOG_DEBUG("warden", "RESULT PAGE_CHECK passed CheckId {} account Id {}", checkId, _session->GetAccountId()); } else if (type == MODULE_CHECK) { LOG_DEBUG("warden", "RESULT MODULE_CHECK passed CheckId {} account Id {}", checkId, _session->GetAccountId()); } else if (type == DRIVER_CHECK) { LOG_DEBUG("warden", "RESULT DRIVER_CHECK passed CheckId {} account Id {}", checkId, _session->GetAccountId()); } break; } case LUA_EVAL_CHECK: { uint8 const result = buff.read(); if (result == 0) { buff.read_skip(buff.read()); // discard attached string } LOG_DEBUG("warden", "LUA_EVAL_CHECK CheckId {} account Id {} got in-warden dummy response", checkId, _session->GetAccountId()/* , result */); break; } case MPQ_CHECK: { uint8 Mpq_Result; buff >> Mpq_Result; if (Mpq_Result != 0) { LOG_DEBUG("warden", "RESULT MPQ_CHECK not 0x00 account id {}", _session->GetAccountId()); checkFailed = checkId; continue; } WardenCheckResult const* rs = sWardenCheckMgr->GetWardenResultById(checkId); if (CRYPTO_memcmp(buff.contents() + buff.rpos(), rs->Result.ToByteArray<20>(false).data(), Acore::Crypto::Constants::SHA1_DIGEST_LENGTH_BYTES) != 0) { LOG_DEBUG("warden", "RESULT MPQ_CHECK fail, CheckId {} account Id {}", checkId, _session->GetAccountId()); checkFailed = checkId; buff.rpos(buff.rpos() + Acore::Crypto::Constants::SHA1_DIGEST_LENGTH_BYTES); // 20 bytes SHA1 continue; } buff.rpos(buff.rpos() + Acore::Crypto::Constants::SHA1_DIGEST_LENGTH_BYTES); // 20 bytes SHA1 LOG_DEBUG("warden", "RESULT MPQ_CHECK passed, CheckId {} account Id {}", checkId, _session->GetAccountId()); break; } } } if (checkFailed > 0 && !_interrupted) { ApplyPenalty(checkFailed, ""); } if (_interrupted) { LOG_DEBUG("warden", "Warden was interrupted by ForceChecks, ignoring results."); _interruptCounter--; if (_interruptCounter == 0) { _interrupted = false; } } // Set hold off timer, minimum timer should at least be 1 second uint32 const holdOff = sWorld->getIntConfig(CONFIG_WARDEN_CLIENT_CHECK_HOLDOFF); _checkTimer = (holdOff < 1 ? 1 : holdOff) * IN_MILLISECONDS; _checkInProgress = false; }