/* * This file is part of the TrinityCore 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 "Common.h" #include "ByteBuffer.h" #include "Containers.h" #include "CryptoRandom.h" #include "DatabaseEnv.h" #include "GameTime.h" #include "HMAC.h" #include "Log.h" #include "Opcodes.h" #include "Player.h" #include "SessionKeyGenerator.h" #include "SmartEnum.h" #include "Util.h" #include "WardenModuleWin.h" #include "WardenCheckMgr.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); WardenWin::WardenWin() : Warden(), _serverTicks(0) { for (WardenCheckCategory category : EnumUtils::Iterate()) { auto& [checks, checksIt] = _checks[category]; checks = sWardenCheckMgr->GetAvailableChecks(category); Trinity::Containers::RandomShuffle(checks); checksIt = checks.begin(); } } void WardenWin::Init(WorldSession* session, SessionKey const& K) { _session = session; // Generate Warden Key SessionKeyGenerator WK(K); WK.Generate(_inputKey.data(), _inputKey.size()); WK.Generate(_outputKey.data(), _outputKey.size()); _seed = Module.Seed; _inputCrypto.Init(_inputKey); _outputCrypto.Init(_outputKey); TC_LOG_DEBUG("warden", "Server side warden for client {} initializing...", session->GetAccountId()); TC_LOG_DEBUG("warden", "C->S Key: {}", ByteArrayToHexStr(_inputKey)); TC_LOG_DEBUG("warden", "S->C Key: {}", ByteArrayToHexStr(_outputKey)); TC_LOG_DEBUG("warden", " Seed: {}", ByteArrayToHexStr(_seed)); TC_LOG_DEBUG("warden", "Loading Module..."); MakeModuleForClient(); TC_LOG_DEBUG("warden", "Module Key: {}", ByteArrayToHexStr(_module->Key)); TC_LOG_DEBUG("warden", "Module ID: {}", ByteArrayToHexStr(_module->Id)); RequestModule(); } void WardenWin::InitializeModuleForClient(ClientWardenModule& module) { // data assign module.CompressedData = Module.Module.data(); module.CompressedSize = Module.Module.size(); module.Key = Module.ModuleKey; } void WardenWin::InitializeModule() { TC_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, 20); 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_WARDEN3_DATA, sizeof(WardenInitModuleRequest)); pkt.append(reinterpret_cast(&Request), sizeof(WardenInitModuleRequest)); _session->SendPacket(&pkt); } void WardenWin::RequestHash() { TC_LOG_DEBUG("warden", "Request hash"); // Create packet structure WardenHashRequest Request; Request.Command = WARDEN_SMSG_HASH_REQUEST; Request.Seed = _seed; // Encrypt with warden RC4 key. EncryptData(reinterpret_cast(&Request), sizeof(WardenHashRequest)); WorldPacket pkt(SMSG_WARDEN3_DATA, sizeof(WardenHashRequest)); pkt.append(reinterpret_cast(&Request), sizeof(WardenHashRequest)); _session->SendPacket(&pkt); } void WardenWin::HandleHashResult(ByteBuffer &buff) { // Verify key Trinity::Crypto::SHA1::Digest response; buff.read(response); if (response != Module.ClientKeySeedHash) { char const* penalty = ApplyPenalty(nullptr); TC_LOG_WARN("warden", "{} failed hash reply. Action: {}", _session->GetPlayerInfo(), penalty); return; } TC_LOG_DEBUG("warden", "Request hash reply: succeed"); // Change keys here _inputKey = Module.ClientKeySeed; _outputKey = Module.ServerKeySeed; _inputCrypto.Init(_inputKey); _outputCrypto.Init(_outputKey); _initialized = true; } static constexpr uint8 GetCheckPacketBaseSize(WardenCheckType type) { switch (type) { case DRIVER_CHECK: return 1; case LUA_EVAL_CHECK: return 1 + sizeof(_luaEvalPrefix)-1 + sizeof(_luaEvalMidfix)-1 + 4 + sizeof(_luaEvalPostfix)-1; case MPQ_CHECK: return 1; case PAGE_CHECK_A: return (4 + 1); case PAGE_CHECK_B: return (4 + 1); case MODULE_CHECK: return (4 + Trinity::Crypto::HMAC_SHA1::DIGEST_LENGTH); case MEM_CHECK: return (1 + 4 + 1); default: return 0; } } static uint16 GetCheckPacketSize(WardenCheck const& check) { uint16 size = 1 + GetCheckPacketBaseSize(check.Type); // 1 byte check type if (!check.Str.empty()) size += (check.Str.length() + 1); // 1 byte string length if (!check.Data.empty()) size += check.Data.size(); return size; } void WardenWin::RequestChecks() { TC_LOG_DEBUG("warden", "Request data from {} (account {}) - loaded: {}", _session->GetPlayerName(), _session->GetAccountId(), _session->GetPlayer() && !_session->PlayerLoading()); // If all checks for a category are done, fill its todo list again for (WardenCheckCategory category : EnumUtils::Iterate()) { auto& [checks, checksIt] = _checks[category]; if ((checksIt == checks.end()) && !checks.empty()) { TC_LOG_DEBUG("warden", "Finished all {} checks, re-shuffling", EnumUtils::ToConstant(category)); Trinity::Containers::RandomShuffle(checks); checksIt = checks.begin(); } } _serverTicks = GameTime::GetGameTimeMS(); _currentChecks.clear(); // Build check request ByteBuffer buff; buff << uint8(WARDEN_SMSG_CHEAT_CHECKS_REQUEST); for (WardenCheckCategory category : EnumUtils::Iterate()) { if (IsWardenCategoryInWorldOnly(category) && !_session->GetPlayer()) continue; auto& [checks, checksIt] = _checks[category]; for (uint32 i = 0, n = sWorld->getIntConfig(GetWardenCategoryCountConfig(category)); i < n; ++i) { if (checksIt == checks.end()) // all checks were already sent, list will be re-filled on next Update() run break; _currentChecks.push_back(*(checksIt++)); } } Trinity::Containers::RandomShuffle(_currentChecks); uint16 expectedSize = 4; Trinity::Containers::EraseIf(_currentChecks, [&expectedSize](uint16 id) { uint8 const thisSize = GetCheckPacketSize(sWardenCheckMgr->GetCheckData(id)); if ((expectedSize + thisSize) > 450) // warden packets are truncated to 512 bytes clientside return true; expectedSize += thisSize; return false; } ); for (uint16 const id : _currentChecks) { WardenCheck const& check = sWardenCheckMgr->GetCheckData(id); if (check.Type == 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); } else if (!check.Str.empty()) { buff << uint8(check.Str.size()); buff.append(check.Str.data(), check.Str.size()); } } uint8 xorByte = _inputKey[0]; // Add TIMING_CHECK buff << uint8(0x00); buff << uint8(TIMING_CHECK ^ xorByte); uint8 index = 1; for (uint16 const id : _currentChecks) { WardenCheck const& check = sWardenCheckMgr->GetCheckData(id); WardenCheckType const type = check.Type; buff << uint8(type ^ xorByte); switch (type) { case MEM_CHECK: { buff << uint8(0x00); buff << uint32(check.Address); buff << uint8(sWardenCheckMgr->GetCheckResult(id).size()); break; } case PAGE_CHECK_A: case PAGE_CHECK_B: { buff.append(check.Data.data(), check.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: { buff.append(check.Data.data(), check.Data.size()); buff << uint8(index++); break; } case MODULE_CHECK: { std::array seed = Trinity::Crypto::GetRandomBytes<4>(); buff.append(seed); buff.append(Trinity::Crypto::HMAC_SHA1::GetDigestOf(seed, check.Str)); break; } /*case PROC_CHECK: { buff.append(check->i.AsByteArray(0, false).get(), check->i.GetNumBytes()); buff << uint8(index++); buff << uint8(index++); buff << uint32(check->Address); buff << uint8(check->Length); break; }*/ default: break; // Should never happen } } buff << uint8(xorByte); buff.hexlike(); auto idstring = [this]() -> std::string { std::stringstream stream; for (uint16 const id : _currentChecks) stream << id << " "; return stream.str(); }; if (buff.size() == expectedSize) { TC_LOG_DEBUG("warden", "Finished building warden packet, size is {} bytes", buff.size()); TC_LOG_DEBUG("warden", "Sent checks: {}", idstring()); } else { TC_LOG_WARN("warden", "Finished building warden packet, size is {} bytes, but expected {} bytes!", buff.size(), expectedSize); TC_LOG_WARN("warden", "Sent checks: {}", idstring()); } // Encrypt with warden RC4 key EncryptData(buff.data(), buff.size()); WorldPacket pkt(SMSG_WARDEN3_DATA, buff.size()); pkt.append(buff); _session->SendPacket(&pkt); _dataSent = true; } void WardenWin::HandleCheckResult(ByteBuffer &buff) { TC_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(); char const* penalty = ApplyPenalty(nullptr); TC_LOG_WARN("warden", "{} sends manipulated warden packet. Action: {}", _session->GetPlayerInfo(), penalty); return; } if (!IsValidCheckSum(Checksum, buff.data() + buff.rpos(), Length)) { buff.rfinish(); char const* penalty = ApplyPenalty(nullptr); TC_LOG_WARN("warden", "{} failed checksum. Action: {}", _session->GetPlayerInfo(), penalty); return; } // TIMING_CHECK { uint8 result; buff >> result; /// @todo test it. if (result == 0x00) { char const* penalty = ApplyPenalty(nullptr); TC_LOG_WARN("warden", "{} failed timing check. Action: {}", _session->GetPlayerInfo(), penalty); return; } uint32 newClientTicks; buff >> newClientTicks; uint32 ticksNow = GameTime::GetGameTimeMS(); uint32 ourTicks = newClientTicks + (ticksNow - _serverTicks); TC_LOG_DEBUG("warden", "Server tick count now: {}", ticksNow); TC_LOG_DEBUG("warden", "Server tick count at req: {}", _serverTicks); TC_LOG_DEBUG("warden", "Client ticks in response: {}", newClientTicks); TC_LOG_DEBUG("warden", "Round trip response time: {} ms", ourTicks - newClientTicks); } uint16 checkFailed = 0; for (uint16 const id : _currentChecks) { WardenCheck const& check = sWardenCheckMgr->GetCheckData(id); switch (check.Type) { case MEM_CHECK: { uint8 Mem_Result; buff >> Mem_Result; if (Mem_Result != 0) { TC_LOG_DEBUG("warden", "RESULT MEM_CHECK not 0x00, CheckId {} account Id {}", id, _session->GetAccountId()); checkFailed = id; continue; } WardenCheckResult const& expected = sWardenCheckMgr->GetCheckResult(id); std::vector response; response.resize(expected.size()); buff.read(response.data(), response.size()); if (response != expected) { TC_LOG_DEBUG("warden", "RESULT MEM_CHECK fail CheckId {} account Id {}", id, _session->GetAccountId()); TC_LOG_DEBUG("warden", "Expected: {}", ByteArrayToHexStr(expected)); TC_LOG_DEBUG("warden", "Got: {}", ByteArrayToHexStr(response)); checkFailed = id; continue; } TC_LOG_DEBUG("warden", "RESULT MEM_CHECK passed CheckId {} account Id {}", id, _session->GetAccountId()); break; } case PAGE_CHECK_A: case PAGE_CHECK_B: case DRIVER_CHECK: case MODULE_CHECK: { if (buff.read() != 0xE9) { TC_LOG_DEBUG("warden", "RESULT {} fail, CheckId {} account Id {}", EnumUtils::ToConstant(check.Type), id, _session->GetAccountId()); checkFailed = id; continue; } TC_LOG_DEBUG("warden", "RESULT {} passed CheckId {} account Id {}", EnumUtils::ToConstant(check.Type), id, _session->GetAccountId()); break; } case LUA_EVAL_CHECK: { uint8 const result = buff.read(); if (result == 0) buff.read_skip(buff.read()); // discard attached string TC_LOG_DEBUG("warden", "LUA_EVAL_CHECK CheckId {} account Id {} got in-warden dummy response ({})", id, _session->GetAccountId(), result); break; } case MPQ_CHECK: { uint8 Mpq_Result; buff >> Mpq_Result; if (Mpq_Result != 0) { TC_LOG_DEBUG("warden", "RESULT MPQ_CHECK not 0x00 account id {}", _session->GetAccountId()); checkFailed = id; continue; } std::vector result; result.resize(Trinity::Crypto::SHA1::DIGEST_LENGTH); buff.read(result.data(), result.size()); if (result != sWardenCheckMgr->GetCheckResult(id)) // SHA1 { TC_LOG_DEBUG("warden", "RESULT MPQ_CHECK fail, CheckId {} account Id {}", id, _session->GetAccountId()); checkFailed = id; continue; } TC_LOG_DEBUG("warden", "RESULT MPQ_CHECK passed, CheckId {} account Id {}", id, _session->GetAccountId()); break; } default: // Should never happen break; } } if (checkFailed > 0) { WardenCheck const& check = sWardenCheckMgr->GetCheckData(checkFailed); char const* penalty = ApplyPenalty(&check); TC_LOG_WARN("warden", "{} failed Warden check {} ({}). Action: {}", _session->GetPlayerInfo(), checkFailed, EnumUtils::ToConstant(check.Type), penalty); } // Set hold off timer, minimum timer should at least be 1 second uint32 holdOff = sWorld->getIntConfig(CONFIG_WARDEN_CLIENT_CHECK_HOLDOFF); _checkTimer = (holdOff < 1 ? 1 : holdOff) * IN_MILLISECONDS; } size_t WardenWin::DEBUG_ForceSpecificChecks(std::vector const& checks) { std::array::iterator, NUM_CHECK_CATEGORIES> swapPositions; for (WardenCheckCategory category : EnumUtils::Iterate()) swapPositions[category] = _checks[category].first.begin(); size_t n = 0; for (uint16 check : checks) { for (WardenCheckCategory category : EnumUtils::Iterate()) { std::vector& checks = _checks[category].first; std::vector::iterator& swapPos = swapPositions[category]; if (auto it = std::find(swapPos, checks.end(), check); it != checks.end()) { std::iter_swap(swapPos, it); ++swapPos; ++n; break; } } } for (WardenCheckCategory category : EnumUtils::Iterate()) _checks[category].second = _checks[category].first.begin(); return n; }