diff --git a/contrib/warden/pointer_chain_examples.sql b/contrib/warden/pointer_chain_examples.sql new file mode 100644 index 000000000..23e347aaf --- /dev/null +++ b/contrib/warden/pointer_chain_examples.sql @@ -0,0 +1,213 @@ +-- ============================================================================ +-- POINTER_CHAIN_CHECK (type 244 / 0xF4) example seeds for the `warden` table. +-- +-- Wire format on the client side is identical to MEM_CHECK (243 / 0xF3); the +-- server walks a multi-hop pointer dereference chain across consecutive Warden +-- cycles and `memcmp`-validates the bytes at the final resolved address. +-- +-- Schema reminder (see WardenCheckMgr::LoadWardenChecks): +-- id uint16 -- check id +-- build uint16 -- client build (vanilla 1.12.1 enUS = 5875) +-- type uint8 -- 244 for POINTER_CHAIN_CHECK +-- data string -- unused for this type (leave empty) +-- result string -- hex of the EXPECTED final-hop bytes (length must +-- match the `length` column) +-- address uint32 -- chain base address (32-bit, x86) +-- length uint8 -- bytes to read at the final hop (1..N) +-- str string -- comma-separated hex offsets, e.g. '0x10,0x24,0x8'. +-- Empty string = zero hops (degenerate single-read). +-- Each offset is added to the pointer dereferenced at +-- the previous hop to produce the next hop's address. +-- A leading '!' flips the terminal compare: instead of +-- "fail on mismatch" (verify expected bytes), the row +-- becomes "fail on match" (detect a forbidden cheat +-- signature, e.g. PQR landing in a dynamically +-- resolved memory region). The '!' is consumed before +-- the offsets are parsed, so '!0x4,0x8' is a 2-hop +-- chain in signature-detect mode. +-- comment string -- free text +-- +-- Walk semantics for offsets `o1, o2, ..., oN` and base `B`: +-- hop 0 (intermediate): read 4 bytes at B -> P0 +-- hop 1 (intermediate): read 4 bytes at P0 + o1 -> P1 +-- hop 2 (intermediate): read 4 bytes at P1 + o2 -> P2 +-- ... +-- hop N (terminal): read `length` bytes at P_{N-1} + oN +-- +-- IMPORTANT — addresses below are templates calibrated for the canonical +-- vanilla 1.12.1 enUS WoW.exe (build 5875, +-- MD5 5fea0d4eed95002f436200a16a4f4795). Image base 0x00400000. They are +-- consistent with the function addresses already used in WardenWin.cpp's +-- module-init block (SFileOpenFile = 0x002485F0 + image base, etc.) and with +-- offsets widely documented in vanilla emulation/cheat communities (e.g. +-- ownedcore vanilla reverse-engineering threads, public vanilla bot/cheat +-- repos). +-- +-- BEFORE GOING TO PRODUCTION: +-- 1. Re-confirm every address against your actual binary disassembly. A +-- different localisation (frFR, deDE, etc.) shifts addresses. +-- 2. Capture the real expected bytes at the resolved address from a known +-- clean client and paste them into the `result` column. Placeholder +-- values are noted with `-- TODO` below. +-- 3. Pick check ids that don't collide with your existing rows. +-- ============================================================================ + + +-- ---------------------------------------------------------------------------- +-- Example 1 — Vtable hook detection on the Client Object Manager +-- +-- Detects: cheats that hook a virtual function on the global Object Manager +-- singleton (a common technique for "object dumper" cheats that swap +-- a vtable slot for a thunk that filters returned objects, leaks +-- GUIDs, or injects fake updates). +-- +-- Chain (3 hops, terminal reads 5 bytes of the first virtual function's +-- prologue): +-- base = 0x00B41414 ; s_curMgr — pointer to ClntObjMgr instance +-- hop 0: read [0x00B41414] -> objMgrInstance +-- hop 1: read [objMgrInstance + 0x00] -> vtable +-- hop 2: read [vtable + 0x00] -> first virtual function pointer +-- hop 3 (terminal): read 5 bytes at that function -> expected prologue +-- +-- A `jmp` detour is 5 bytes (E9 XX XX XX XX), which guarantees the prologue +-- bytes change if the function is hooked. +-- ---------------------------------------------------------------------------- +INSERT INTO `warden` + (`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`) +VALUES + (10001, 5875, 244, '', + '0000000000', -- TODO: replace with the real first 5 prologue bytes from clean WoW.exe + 0x00B41414, 5, '0x0,0x0,0x0', + 'Pointer chain: ClntObjMgr -> vtable -> vtable[0] -> prologue (detect vtable hook)'); + + +-- ---------------------------------------------------------------------------- +-- Example 2 — Hook detection on import-resolved GetTickCount +-- +-- Detects: cheats that patch the WoW import for kernel32!GetTickCount to a +-- thunk returning bogus timestamps, defeating the TIMING_CHECK +-- (87/0x57). The IAT slot lives in WoW's `.idata`; following it +-- lands inside kernel32.dll's loaded copy. +-- +-- Chain (1 hop, terminal reads 5 bytes of the resolved function prologue): +-- base = 0x00C2D154 ; IAT slot for kernel32!GetTickCount +-- ; (TODO: confirm RVA on your binary) +-- hop 0 (terminal): read 5 bytes at [0x00C2D154] +-- +-- Caveat: kernel32.dll is part of the OS, so the prologue bytes vary across +-- Windows versions. In practice you'd seed `result` per-OS or use a small +-- whitelist via multiple check rows. Listed here as a textbook IAT-hook +-- pattern; consider it a template, not a drop-in. +-- ---------------------------------------------------------------------------- +INSERT INTO `warden` + (`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`) +VALUES + (10002, 5875, 244, '', + '0000000000', -- TODO: per-OS captured bytes of kernel32!GetTickCount prologue + 0x00C2D154, 5, '', + 'Pointer chain: WoW IAT[GetTickCount] -> kernel32 prologue (detect IAT detour)'); + + +-- ---------------------------------------------------------------------------- +-- Example 3 — Sanity check on Local Player object type field +-- +-- Detects: object-replace cheats that swap the local player's object type at +-- its UnitFields descriptor block to confuse server-side validation. +-- Reads the object type byte, expected to be 4 (TYPEID_PLAYER) for +-- the local player object. +-- +-- Chain (3 hops, terminal reads 1 byte): +-- base = 0x00B41414 ; s_curMgr +-- hop 0: read [0x00B41414] -> objMgrInstance +-- hop 1: read [objMgrInstance + 0xAC] -> first object in linked list +-- (publicly documented offset) +-- hop 2: read [object + 0x14] -> object type id field +-- (TYPEID layout is documented +-- in Object.h) +-- +-- Note: the linked list head at +0xAC is not always the local player; it +-- iterates by +0x3C until matching local GUID. For a simple template we use +-- the head — adjust if you want strict local-player semantics. This is +-- primarily useful as a presence/structural-integrity check. +-- ---------------------------------------------------------------------------- +INSERT INTO `warden` + (`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`) +VALUES + (10003, 5875, 244, '', + '04', -- TYPEID_PLAYER + 0x00B41414, 1, '0x0,0xAC,0x14', + 'Pointer chain: ObjMgr -> first object -> typeId byte (detect object spoof)'); + + +-- ---------------------------------------------------------------------------- +-- Example 4 — Zero-hop sanity (degenerate chain) +-- +-- Equivalent in semantics to a plain MEM_CHECK, but routed through the +-- POINTER_CHAIN_CHECK code path. Useful as a smoke test when bringing the +-- feature up: pick a known stable byte run in WoW.exe's `.text` (any +-- function whose prologue you already trust) and seed the expected bytes. +-- +-- Reads 5 bytes of the SFileOpenFile prologue at 0x006485F0 +-- (image base 0x00400000 + RVA 0x002485F0, taken straight from the +-- WardenInitModuleRequest in src/game/Warden/WardenWin.cpp). +-- ---------------------------------------------------------------------------- +INSERT INTO `warden` + (`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`) +VALUES + (10004, 5875, 244, '', + '0000000000', -- TODO: replace with real first 5 bytes of SFileOpenFile prologue + 0x006485F0, 5, '', + 'Zero-hop pointer-chain smoke test (SFileOpenFile prologue)'); + + +-- ---------------------------------------------------------------------------- +-- Example 5 — Signature-detect mode (third-party allocation scan) +-- +-- Inspired by Krilliac/AdvancedWarden's MEM2_CHECK / GAGARIN pair pattern, +-- which targets cheats that allocate executable memory in well-known +-- dynamic regions and place their payload at a fixed offset within that +-- region. Approach there: one MEM_CHECK reads the dynamic base address and +-- caches it on the session; a paired MEM_CHECK then scans `base + small +-- offset` and fails when bytes != 0 (i.e. the region is not empty as it +-- should be on a clean client). +-- +-- Our generalised equivalent: a single POINTER_CHAIN_CHECK row that walks +-- to the suspect address and uses signature-detect mode (leading `!`) to +-- fail when a known cheat-signature pattern appears. +-- +-- Chain (1 hop, terminal reads 4 bytes; fails if pattern found): +-- base = 0x009F348 ; static slot that holds the dynamic +-- ; allocation pointer for this cheat +-- ; family. (TODO: confirm against your +-- ; binary; the AdvancedWarden seed used +-- ; address=652040=0x9F348, length=4.) +-- hop 0 (terminal): read 4 bytes at *base + 2 (or other small offset) +-- expected = the cheat's 4-byte signature +-- fail when read == expected (signature present) +-- +-- The leading '!' on the offset string flips the terminal compare into +-- signature-detect mode. Pick the offset value (here 0x2) to match the +-- byte-window where the cheat is known to land. Length must equal the +-- length of the signature in `result`. +-- ---------------------------------------------------------------------------- +INSERT INTO `warden` + (`id`, `build`, `type`, `data`, `result`, `address`, `length`, `str`, `comment`) +VALUES + (10005, 5875, 244, '', + '00003000', -- TODO: replace with the real cheat signature bytes + 0x0009F348, 4, '!0x2', + 'Signature detect: dynamic 3rd-party allocation scan (PQR-class)'); + + +-- ---------------------------------------------------------------------------- +-- Optional: action override per check id (only if you want non-default +-- penalty for these). Default action comes from +-- CONFIG_UINT32_WARDEN_CLIENT_FAIL_ACTION (0=LOG, 1=KICK, 2=BAN). The +-- override lives in the characters DB. +-- ---------------------------------------------------------------------------- +-- INSERT INTO `warden_action` (`wardenId`, `action`) VALUES +-- (10001, 1), -- kick on vtable-hook detection +-- (10002, 0), -- log only on IAT check (more false-positive prone) +-- (10003, 1), -- kick on object-type spoof +-- (10004, 0), -- log only on smoke test +-- (10005, 2); -- ban on confirmed signature match diff --git a/src/game/Warden/WardenCheckMgr.cpp b/src/game/Warden/WardenCheckMgr.cpp index 2b7d29093..3891871b6 100644 --- a/src/game/Warden/WardenCheckMgr.cpp +++ b/src/game/Warden/WardenCheckMgr.cpp @@ -1,323 +1,323 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * Copyright (C) 2008-2015 TrinityCore - * - * 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, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * World of Warcraft, and all World of Warcraft or Warcraft art, images, - * and lore are copyrighted by Blizzard Entertainment, Inc. - */ - -/** - * @file WardenCheckMgr.cpp - * @brief Warden check manager implementation - * - * This file implements WardenCheckMgr which manages the database - * of Warden anti-cheat checks. It loads checks from the database - * and stores check results for analysis. - * - * Check types supported: - * - Memory checks (PAGE_CHECK_A, PAGE_CHECK_B) - * - Driver checks (DRIVER_CHECK) - * - Lua checks (LUA_CHECK) - * - Timing checks (TIMING_CHECK) - * - * @see WardenCheckMgr for the manager class - * @see WardenCheck for individual check definitions - */ - -#include "Common.h" -#include "World.h" -#include "WorldPacket.h" -#include "WorldSession.h" -#include "Log.h" -#include "Database/DatabaseEnv.h" -#include "WardenCheckMgr.h" -#include "Warden.h" - -/** - * @brief WardenCheckMgr constructor - * - * Initializes the check manager with empty stores. - */ -WardenCheckMgr::WardenCheckMgr() : m_lock(0), CheckStore(), CheckResultStore() {} - -/** - * @brief WardenCheckMgr destructor - * - * Cleans up all check and check result objects. - */ -WardenCheckMgr::~WardenCheckMgr() -{ - for (CheckMap::iterator it = CheckStore.begin(); it != CheckStore.end(); ++it) - { - delete it->second; - } - - for (CheckResultMap::iterator it = CheckResultStore.begin(); it != CheckResultStore.end(); ++it) - { - delete it->second; - } - - CheckStore.clear(); - CheckResultStore.clear(); -} - -/** - * @brief Load Warden checks from database - * - * Loads all Warden checks from the `warden` table in the - * world database. Checks are organized by client build number. - * Skips loading if Warden is disabled in config. - */ -void WardenCheckMgr::LoadWardenChecks() -{ - // Check if Warden is enabled by config before loading anything - if (!sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED) && !sWorld.getConfig(CONFIG_BOOL_WARDEN_OSX_ENABLED)) - { - 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"); - - if (!result) - { - sLog.outString("[Warden]: >> Loaded 0 warden data and results"); - return; - } - - uint32 count = 0; - Field* fields; - - do - { - fields = result->Fetch(); - - uint16 id = fields[0].GetUInt16(); - uint16 build = fields[1].GetUInt16(); - uint8 checkType = fields[2].GetUInt8(); - std::string data = fields[3].GetString(); - std::string checkResult = fields[4].GetString(); - uint32 address = fields[5].GetUInt32(); - uint8 length = fields[6].GetUInt8(); - std::string str = fields[7].GetString(); - std::string comment = fields[8].GetString(); - - WardenCheck* wardenCheck = new WardenCheck(); - wardenCheck->Type = checkType; - wardenCheck->CheckId = id; - - // Initialize action with default action from config - wardenCheck->Action = WardenActions(sWorld.getConfig(CONFIG_UINT32_WARDEN_CLIENT_FAIL_ACTION)); - - if (checkType == PAGE_CHECK_A || checkType == PAGE_CHECK_B || checkType == DRIVER_CHECK) - { - wardenCheck->Data.SetHexStr(data.c_str()); - int len = data.size() / 2; - - if (wardenCheck->Data.GetNumBytes() < len) - { - uint8 temp[24]; - memset(temp, 0, len); - memcpy(temp, wardenCheck->Data.AsByteArray(), wardenCheck->Data.GetNumBytes()); - std::reverse(temp, temp + len); - wardenCheck->Data.SetBinary((uint8*)temp, len); - } - } - - if (checkType == MEM_CHECK || checkType == PAGE_CHECK_A || checkType == PAGE_CHECK_B || checkType == PROC_CHECK) - { - wardenCheck->Address = address; - wardenCheck->Length = length; - } - - // PROC_CHECK support missing - if (checkType == MEM_CHECK || checkType == MPQ_CHECK || checkType == LUA_STR_CHECK || checkType == DRIVER_CHECK || checkType == MODULE_CHECK) - { - wardenCheck->Str = str; - } - - CheckStore.insert(std::pair(build, wardenCheck)); - - if (checkType == MPQ_CHECK || checkType == MEM_CHECK) - { - WardenCheckResult* wr = new WardenCheckResult(); - wr->Id = id; - wr->Result.SetHexStr(checkResult.c_str()); - int len = checkResult.size() / 2; - if (wr->Result.GetNumBytes() < len) - { - uint8 *temp = new uint8[len]; - memset(temp, 0, len); - memcpy(temp, wr->Result.AsByteArray(), wr->Result.GetNumBytes()); - std::reverse(temp, temp + len); - wr->Result.SetBinary((uint8*)temp, len); - delete[] temp; - } - CheckResultStore.insert(std::pair(build, wr)); - } - - if (comment.empty()) - { - wardenCheck->Comment = ""; - } - else - { - wardenCheck->Comment = comment; - } - - ++count; - } while (result->NextRow()); - - sLog.outString(">> Loaded %u warden checks.", count); - - delete result; -} - -/** - * @brief Loads per-check Warden action overrides from the character database. - */ -void WardenCheckMgr::LoadWardenOverrides() -{ - // Check if Warden is enabled by config before loading anything - if (!sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED) && !sWorld.getConfig(CONFIG_BOOL_WARDEN_OSX_ENABLED)) - { - sLog.outString(">> Warden disabled, loading check overrides skipped."); - return; - } - - // 0 1 - QueryResult* result = CharacterDatabase.Query("SELECT `wardenId`, `action` FROM `warden_action`"); - - if (!result) - { - sLog.outString(">> Loaded 0 Warden action overrides. DB table `warden_action` is empty!"); - return; - } - - uint32 count = 0; - - ACE_WRITE_GUARD(LOCK, g, m_lock) - - do - { - Field* fields = result->Fetch(); - - uint16 checkId = fields[0].GetUInt16(); - uint8 action = fields[1].GetUInt8(); - - // Check if action value is in range (0-2, see WardenActions enum) - if (action > WARDEN_ACTION_BAN) - { - sLog.outWarden("Warden check override action out of range (ID: %u, action: %u)", checkId, action); - } - else - { - bool found = false; - for (CheckMap::iterator it = CheckStore.begin(); it != CheckStore.end(); ++it) - { - if (it->second->CheckId == checkId) - { - it->second->Action = WardenActions(action); - ++count; - found = true; - } - } - if (!found) - sLog.outWarden("Warden check action override for non-existing check (ID: %u, action: %u), skipped", checkId, action); - } - } - while (result->NextRow()); - - sLog.outString(">> Loaded %u warden action overrides.", count); -} - -/** - * @brief Finds a Warden check definition by client build and check id. - * - * @param build The client build. - * @param id The Warden check id. - * @return WardenCheck* The matching Warden check, or NULL if not found. - */ -WardenCheck* WardenCheckMgr::GetWardenDataById(uint16 build, uint16 id) -{ - WardenCheck* result = NULL; - - ACE_READ_GUARD_RETURN(LOCK, g, m_lock, result) - for (CheckMap::iterator it = CheckStore.lower_bound(build); it != CheckStore.upper_bound(build); ++it) - { - if (it->second->CheckId == id) - { - result = it->second; - } - } - - return result; -} - -/** - * @brief Finds an expected Warden check result by client build and check id. - * - * @param build The client build. - * @param id The Warden result id. - * @return WardenCheckResult* The matching expected result, or NULL if not found. - */ -WardenCheckResult* WardenCheckMgr::GetWardenResultById(uint16 build, uint16 id) -{ - WardenCheckResult* result = NULL; - - ACE_READ_GUARD_RETURN(LOCK, g, m_lock, result) - for (CheckResultMap::iterator it = CheckResultStore.lower_bound(build); it != CheckResultStore.upper_bound(build); ++it) - { - if (it->second->Id == id) - { - result = it->second; - } - } - - return result; -} - -/** - * @brief Collects Warden check ids for a client build by memory or non-memory category. - * - * @param isMemCheck True to collect memory-related checks; false for other checks. - * @param build The client build. - * @param idl The list that receives matching check ids. - */ -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) - { - if (isMemCheck) - { - if ((it->second->Type == MEM_CHECK) || (it->second->Type == MODULE_CHECK)) - { - idl.push_back(it->second->CheckId); - } - } - else - { - idl.push_back(it->second->CheckId); - } - } -} +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * Copyright (C) 2008-2015 TrinityCore + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/** + * @file WardenCheckMgr.cpp + * @brief Warden check manager implementation + * + * This file implements WardenCheckMgr which manages the database + * of Warden anti-cheat checks. It loads checks from the database + * and stores check results for analysis. + * + * Check types supported: + * - Memory checks (PAGE_CHECK_A, PAGE_CHECK_B) + * - Driver checks (DRIVER_CHECK) + * - Lua checks (LUA_CHECK) + * - Timing checks (TIMING_CHECK) + * + * @see WardenCheckMgr for the manager class + * @see WardenCheck for individual check definitions + */ + +#include "Common.h" +#include "World.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include "Log.h" +#include "Database/DatabaseEnv.h" +#include "WardenCheckMgr.h" +#include "Warden.h" + +/** + * @brief WardenCheckMgr constructor + * + * Initializes the check manager with empty stores. + */ +WardenCheckMgr::WardenCheckMgr() : m_lock(0), CheckStore(), CheckResultStore() {} + +/** + * @brief WardenCheckMgr destructor + * + * Cleans up all check and check result objects. + */ +WardenCheckMgr::~WardenCheckMgr() +{ + for (CheckMap::iterator it = CheckStore.begin(); it != CheckStore.end(); ++it) + { + delete it->second; + } + + for (CheckResultMap::iterator it = CheckResultStore.begin(); it != CheckResultStore.end(); ++it) + { + delete it->second; + } + + CheckStore.clear(); + CheckResultStore.clear(); +} + +/** + * @brief Load Warden checks from database + * + * Loads all Warden checks from the `warden` table in the + * world database. Checks are organized by client build number. + * Skips loading if Warden is disabled in config. + */ +void WardenCheckMgr::LoadWardenChecks() +{ + // Check if Warden is enabled by config before loading anything + if (!sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED) && !sWorld.getConfig(CONFIG_BOOL_WARDEN_OSX_ENABLED)) + { + 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"); + + if (!result) + { + sLog.outString("[Warden]: >> Loaded 0 warden data and results"); + return; + } + + uint32 count = 0; + Field* fields; + + do + { + fields = result->Fetch(); + + uint16 id = fields[0].GetUInt16(); + uint16 build = fields[1].GetUInt16(); + uint8 checkType = fields[2].GetUInt8(); + std::string data = fields[3].GetString(); + std::string checkResult = fields[4].GetString(); + uint32 address = fields[5].GetUInt32(); + uint8 length = fields[6].GetUInt8(); + std::string str = fields[7].GetString(); + std::string comment = fields[8].GetString(); + + WardenCheck* wardenCheck = new WardenCheck(); + wardenCheck->Type = checkType; + wardenCheck->CheckId = id; + + // Initialize action with default action from config + wardenCheck->Action = WardenActions(sWorld.getConfig(CONFIG_UINT32_WARDEN_CLIENT_FAIL_ACTION)); + + if (checkType == PAGE_CHECK_A || checkType == PAGE_CHECK_B || checkType == DRIVER_CHECK) + { + wardenCheck->Data.SetHexStr(data.c_str()); + int len = data.size() / 2; + + if (wardenCheck->Data.GetNumBytes() < len) + { + uint8 temp[24]; + memset(temp, 0, len); + memcpy(temp, wardenCheck->Data.AsByteArray(), wardenCheck->Data.GetNumBytes()); + std::reverse(temp, temp + len); + wardenCheck->Data.SetBinary((uint8*)temp, len); + } + } + + if (checkType == MEM_CHECK || checkType == PAGE_CHECK_A || checkType == PAGE_CHECK_B || checkType == PROC_CHECK) + { + wardenCheck->Address = address; + wardenCheck->Length = length; + } + + // PROC_CHECK support missing + if (checkType == MEM_CHECK || checkType == MPQ_CHECK || checkType == LUA_STR_CHECK || checkType == DRIVER_CHECK || checkType == MODULE_CHECK) + { + wardenCheck->Str = str; + } + + CheckStore.insert(std::pair(build, wardenCheck)); + + if (checkType == MPQ_CHECK || checkType == MEM_CHECK) + { + WardenCheckResult* wr = new WardenCheckResult(); + wr->Id = id; + wr->Result.SetHexStr(checkResult.c_str()); + int len = checkResult.size() / 2; + if (wr->Result.GetNumBytes() < len) + { + uint8 *temp = new uint8[len]; + memset(temp, 0, len); + memcpy(temp, wr->Result.AsByteArray(), wr->Result.GetNumBytes()); + std::reverse(temp, temp + len); + wr->Result.SetBinary((uint8*)temp, len); + delete[] temp; + } + CheckResultStore.insert(std::pair(build, wr)); + } + + if (comment.empty()) + { + wardenCheck->Comment = ""; + } + else + { + wardenCheck->Comment = comment; + } + + ++count; + } while (result->NextRow()); + + sLog.outString(">> Loaded %u warden checks.", count); + + delete result; +} + +/** + * @brief Loads per-check Warden action overrides from the character database. + */ +void WardenCheckMgr::LoadWardenOverrides() +{ + // Check if Warden is enabled by config before loading anything + if (!sWorld.getConfig(CONFIG_BOOL_WARDEN_WIN_ENABLED) && !sWorld.getConfig(CONFIG_BOOL_WARDEN_OSX_ENABLED)) + { + sLog.outString(">> Warden disabled, loading check overrides skipped."); + return; + } + + // 0 1 + QueryResult* result = CharacterDatabase.Query("SELECT `wardenId`, `action` FROM `warden_action`"); + + if (!result) + { + sLog.outString(">> Loaded 0 Warden action overrides. DB table `warden_action` is empty!"); + return; + } + + uint32 count = 0; + + ACE_WRITE_GUARD(LOCK, g, m_lock) + + do + { + Field* fields = result->Fetch(); + + uint16 checkId = fields[0].GetUInt16(); + uint8 action = fields[1].GetUInt8(); + + // Check if action value is in range (0-2, see WardenActions enum) + if (action > WARDEN_ACTION_BAN) + { + sLog.outWarden("Warden check override action out of range (ID: %u, action: %u)", checkId, action); + } + else + { + bool found = false; + for (CheckMap::iterator it = CheckStore.begin(); it != CheckStore.end(); ++it) + { + if (it->second->CheckId == checkId) + { + it->second->Action = WardenActions(action); + ++count; + found = true; + } + } + if (!found) + sLog.outWarden("Warden check action override for non-existing check (ID: %u, action: %u), skipped", checkId, action); + } + } + while (result->NextRow()); + + sLog.outString(">> Loaded %u warden action overrides.", count); +} + +/** + * @brief Finds a Warden check definition by client build and check id. + * + * @param build The client build. + * @param id The Warden check id. + * @return WardenCheck* The matching Warden check, or NULL if not found. + */ +WardenCheck* WardenCheckMgr::GetWardenDataById(uint16 build, uint16 id) +{ + WardenCheck* result = NULL; + + ACE_READ_GUARD_RETURN(LOCK, g, m_lock, result) + for (CheckMap::iterator it = CheckStore.lower_bound(build); it != CheckStore.upper_bound(build); ++it) + { + if (it->second->CheckId == id) + { + result = it->second; + } + } + + return result; +} + +/** + * @brief Finds an expected Warden check result by client build and check id. + * + * @param build The client build. + * @param id The Warden result id. + * @return WardenCheckResult* The matching expected result, or NULL if not found. + */ +WardenCheckResult* WardenCheckMgr::GetWardenResultById(uint16 build, uint16 id) +{ + WardenCheckResult* result = NULL; + + ACE_READ_GUARD_RETURN(LOCK, g, m_lock, result) + for (CheckResultMap::iterator it = CheckResultStore.lower_bound(build); it != CheckResultStore.upper_bound(build); ++it) + { + if (it->second->Id == id) + { + result = it->second; + } + } + + return result; +} + +/** + * @brief Collects Warden check ids for a client build by memory or non-memory category. + * + * @param isMemCheck True to collect memory-related checks; false for other checks. + * @param build The client build. + * @param idl The list that receives matching check ids. + */ +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) + { + if (isMemCheck) + { + if ((it->second->Type == MEM_CHECK) || (it->second->Type == MODULE_CHECK)) + { + idl.push_back(it->second->CheckId); + } + } + else + { + idl.push_back(it->second->CheckId); + } + } +} diff --git a/src/game/Warden/WardenWin.cpp b/src/game/Warden/WardenWin.cpp index 0cb1f5b53..e3e106591 100644 --- a/src/game/Warden/WardenWin.cpp +++ b/src/game/Warden/WardenWin.cpp @@ -1,567 +1,567 @@ -/** - * MaNGOS is a full featured server for World of Warcraft, supporting - * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 - * - * Copyright (C) 2005-2025 MaNGOS - * Copyright (C) 2008-2015 TrinityCore - * - * 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, write to the Free Software - * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA - * - * World of Warcraft, and all World of Warcraft or Warcraft art, images, - * and lore are copyrighted by Blizzard Entertainment, Inc. - */ - -/** - * @file WardenWin.cpp - * @brief Windows-specific Warden implementation - * - * This file implements WardenWin which provides the Windows-specific - * implementation of the Warden anti-cheat system for Windows clients. - * - * Key features: - * - Module loading and initialization - * - Memory scanning checks - * - Timing-based checks - * - Server tick tracking - * - * @see WardenWin for the Windows-specific class - * @see Warden for the base class - */ - -#include "HMACSHA1.h" -#include "WardenKeyGeneration.h" -#include "Common.h" -#include "WorldPacket.h" -#include "WorldSession.h" -#include "Log.h" -#include "Opcodes.h" -#include "ByteBuffer.h" -#include -#include "Database/DatabaseEnv.h" -#include "World.h" -#include "Player.h" -#include "Util.h" -#include "WardenWin.h" -#include "WardenModuleWin.h" -#include "WardenCheckMgr.h" -#include "GameTime.h" - -/** - * @brief WardenWin constructor - * - * Initializes the Windows-specific Warden implementation. - */ -WardenWin::WardenWin() : Warden(), _serverTicks(0) {} - -/** - * @brief WardenWin destructor - */ -WardenWin::~WardenWin() {} - -/** - * @brief Initialize Windows Warden - * @param session Client session - * @param k Session key - * - * Initializes the Windows-specific Warden with: - * - SHA1-based key generation from session key - * - Module-specific seed - * - Windows-specific module loading - */ -void WardenWin::Init(WorldSession* session, BigNumber* k) -{ - _session = session; - // Generate Warden Key - SHA1Randx WK(k->AsByteArray(), k->GetNumBytes()); - WK.Generate(_inputKey, 16); - WK.Generate(_outputKey, 16); - - memcpy(_seed, Module.Seed, 16); - - _inputCrypto.Init(_inputKey); - _outputCrypto.Init(_outputKey); - sLog.outWarden("Server side warden for client %u (build %u) initializing...", session->GetAccountId(), _session->GetClientBuild()); - sLog.outWarden("C->S Key: %s", ByteArrayToHexStr(_inputKey, 16).c_str()); - sLog.outWarden("S->C Key: %s", ByteArrayToHexStr(_outputKey, 16).c_str()); - sLog.outWarden(" Seed: %s", ByteArrayToHexStr(_seed, 16).c_str()); - sLog.outWarden("Loading Module..."); - - _module = GetModuleForClient(); - - sLog.outWarden("Module Key: %s", ByteArrayToHexStr(_module->Key, 16).c_str()); - sLog.outWarden("Module ID: %s", ByteArrayToHexStr(_module->Id, 16).c_str()); - RequestModule(); -} - -/** - * @brief Get Windows Warden module - * @return Client Warden module - * - * Returns the Windows-specific Warden module with its - * compressed data, key, and MD5 hash. - */ -ClientWardenModule* WardenWin::GetModuleForClient() -{ - ClientWardenModule *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, Module.ModuleKey, 16); - - // md5 hash - MD5_CTX ctx; - MD5_Init(&ctx); - MD5_Update(&ctx, mod->CompressedData, length); - MD5_Final((uint8*)&mod->Id, &ctx); - - return mod; -} - -/** - * @brief Initialize Windows Warden module - * - * Initializes the Windows-specific Warden module on the client. - */ -void WardenWin::InitializeModule() -{ - sLog.outWarden("Initialize module"); - - // Create packet structure - WardenInitModuleRequest Request; - Request.Command1 = WARDEN_SMSG_MODULE_INITIALIZE; - Request.Size1 = 20; - Request.CheckSumm1 = BuildChecksum(&Request.Unk1, 20); - Request.Unk1 = 1; - Request.Unk2 = 0; - Request.Type = 1; - Request.String_library1 = 0; - Request.Function1[0] = 0x002485F0; // 0x00400000 + 0x002485F0 SFileOpenFile - Request.Function1[1] = 0x002487F0; // 0x00400000 + 0x002487F0 SFileGetFileSize - Request.Function1[2] = 0x00248460; // 0x00400000 + 0x00248460 SFileReadFile - Request.Function1[3] = 0x00248730; // 0x00400000 + 0x00248730 SFileCloseFile - - 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; - - 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; - - // Encrypt with warden RC4 key. - EncryptData((uint8*)&Request, sizeof(WardenInitModuleRequest)); - - WorldPacket pkt(SMSG_WARDEN_DATA, sizeof(WardenInitModuleRequest)); - pkt.append((uint8*)&Request, sizeof(WardenInitModuleRequest)); - _session->SendPacket(&pkt); - - Warden::InitializeModule(); -} - -/** - * @brief Validates the Windows client hash reply and initializes the session crypto keys. - * - * @param buff The received Warden payload buffer. - */ -void WardenWin::HandleHashResult(ByteBuffer &buff) -{ - buff.rpos(buff.wpos()); - - // Verify key - if (memcmp(buff.contents() + 1, Module.ClientKeySeedHash, sizeof(Module.ClientKeySeedHash)) != 0) - { - sLog.outWarden("%s failed hash reply. Action: %s", _session->GetPlayerName(), Penalty().c_str()); - return; - } - - sLog.outWarden("Request hash reply: succeed"); - - // Change keys here - memcpy(_inputKey, Module.ClientKeySeed, 16); - memcpy(_outputKey, Module.ServerKeySeed, 16); - - _inputCrypto.Init(_inputKey); - _outputCrypto.Init(_outputKey); - - _previousTimestamp = GameTime::GetGameTimeMS(); -} - -/** - * @brief Builds and sends the current batch of Windows Warden checks. - */ -void WardenWin::RequestData() -{ - sLog.outWarden("Request data"); - - uint16 build = _session->GetClientBuild(); - uint16 id = 0; - uint8 type = 0; - WardenCheck* wd = NULL; - - // If all checks were done, fill the todo list again - if (_memChecksTodo.empty()) - { - sWardenCheckMgr->GetWardenCheckIds(true, build, _memChecksTodo); - } - - if (_otherChecksTodo.empty()) - { - sWardenCheckMgr->GetWardenCheckIds(false, build, _otherChecksTodo); - } - - _serverTicks = GameTime::GetGameTimeMS(); - - _currentChecks.clear(); - - // Build check request - 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 - _currentChecks.push_back(id); - } - - ByteBuffer buff; - buff << uint8(WARDEN_SMSG_CHEAT_CHECKS_REQUEST); - - 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 - _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) - { - 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; - } - } - } - - uint8 xorByte = _inputKey[0]; - - // Add TIMING_CHECK - buff << uint8(0x00); - buff << uint8(TIMING_CHECK ^ xorByte); - - uint8 index = 1; - - for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) - { - wd = sWardenCheckMgr->GetWardenDataById(build, *itr); - - type = wd->Type; - buff << uint8(type ^ xorByte); - switch (type) - { - case MEM_CHECK: - { - buff << uint8(0x00); - buff << uint32(wd->Address); - buff << uint8(wd->Length); - break; - } - case PAGE_CHECK_A: - case PAGE_CHECK_B: - { - buff.append(wd->Data.AsByteArray(0, false), wd->Data.GetNumBytes()); - buff << uint32(wd->Address); - buff << uint8(wd->Length); - break; - } - case MPQ_CHECK: - case LUA_STR_CHECK: - { - buff << uint8(index++); - break; - } - case DRIVER_CHECK: - { - buff.append(wd->Data.AsByteArray(0, false), wd->Data.GetNumBytes()); - buff << uint8(index++); - break; - } - case MODULE_CHECK: - { - uint32 seed = rand32(); - buff << uint32(seed); - HMACSHA1 hmac(4, (uint8*)&seed); - hmac.UpdateData(wd->Str); - hmac.Finalize(); - buff.append(hmac.GetDigest(), hmac.GetLength()); - 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; - }*/ - default: - break; // Should never happen - } - } - buff << uint8(xorByte); - buff.hexlike(); - - // Encrypt with warden RC4 key - EncryptData((uint8*)buff.contents(), buff.size()); - - WorldPacket pkt(SMSG_WARDEN_DATA, buff.size()); - pkt.append(buff); - _session->SendPacket(&pkt); - - std::stringstream stream; - stream << "Sent check id's: "; - for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) - { - stream << *itr << " "; - } - - sLog.outWarden("%s", stream.str().c_str()); - - Warden::RequestData(); -} - -/** - * @brief Processes a Windows Warden data response and validates the reported checks. - * - * @param buff The received Warden payload buffer. - */ -void WardenWin::HandleData(ByteBuffer &buff) -{ - sLog.outWarden("Handle data"); - - uint16 Length; - buff >> Length; - uint32 Checksum; - buff >> Checksum; - - if (!IsValidCheckSum(Checksum, buff.contents() + buff.rpos(), Length)) - { - buff.rpos(buff.wpos()); - sLog.outWarden("%s failed checksum. Action: %s", _session->GetPlayerName(), Penalty().c_str()); - return; - } - - // TIMING_CHECK - { - uint8 result; - buff >> result; - /// @todo test it. - if (result == 0x00) - { - sLog.outWarden("%s failed timing check. Action: %s", _session->GetPlayerName(), Penalty().c_str()); - return; - } - - uint32 newClientTicks; - buff >> newClientTicks; - - uint32 ticksNow = GameTime::GetGameTimeMS(); - uint32 ourTicks = newClientTicks + (ticksNow - _serverTicks); - - 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; - - for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) - { - rd = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), *itr); - rs = sWardenCheckMgr->GetWardenResultById(_session->GetClientBuild(), *itr); - - type = rd->Type; - switch (type) - { - case MEM_CHECK: - { - uint8 Mem_Result; - buff >> Mem_Result; - - 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()); - break; - } - case PAGE_CHECK_A: - case PAGE_CHECK_B: - case DRIVER_CHECK: - case MODULE_CHECK: - { - 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()); - } - 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()); - } - 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; - } - case LUA_STR_CHECK: - { - 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; - } - - 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; - } - 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) - { - sLog.outWarden("RESULT MPQ_CHECK not 0x00 account id %u", _session->GetAccountId()); - checkFailed = *itr; - continue; - } - - 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; - } - - buff.rpos(buff.rpos() + 20); // 20 bytes SHA1 - sLog.outWarden("RESULT MPQ_CHECK passed, CheckId %u account Id %u", *itr, _session->GetAccountId()); - break; - } - default: // Should never happen - break; - } - } - - if (checkFailed > 0) - { - 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); - } - - Warden::HandleData(buff); -} +/** + * MaNGOS is a full featured server for World of Warcraft, supporting + * the following clients: 1.12.x, 2.4.3, 3.3.5a, 4.3.4a and 5.4.8 + * + * Copyright (C) 2005-2025 MaNGOS + * Copyright (C) 2008-2015 TrinityCore + * + * 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + * World of Warcraft, and all World of Warcraft or Warcraft art, images, + * and lore are copyrighted by Blizzard Entertainment, Inc. + */ + +/** + * @file WardenWin.cpp + * @brief Windows-specific Warden implementation + * + * This file implements WardenWin which provides the Windows-specific + * implementation of the Warden anti-cheat system for Windows clients. + * + * Key features: + * - Module loading and initialization + * - Memory scanning checks + * - Timing-based checks + * - Server tick tracking + * + * @see WardenWin for the Windows-specific class + * @see Warden for the base class + */ + +#include "HMACSHA1.h" +#include "WardenKeyGeneration.h" +#include "Common.h" +#include "WorldPacket.h" +#include "WorldSession.h" +#include "Log.h" +#include "Opcodes.h" +#include "ByteBuffer.h" +#include +#include "Database/DatabaseEnv.h" +#include "World.h" +#include "Player.h" +#include "Util.h" +#include "WardenWin.h" +#include "WardenModuleWin.h" +#include "WardenCheckMgr.h" +#include "GameTime.h" + +/** + * @brief WardenWin constructor + * + * Initializes the Windows-specific Warden implementation. + */ +WardenWin::WardenWin() : Warden(), _serverTicks(0) {} + +/** + * @brief WardenWin destructor + */ +WardenWin::~WardenWin() {} + +/** + * @brief Initialize Windows Warden + * @param session Client session + * @param k Session key + * + * Initializes the Windows-specific Warden with: + * - SHA1-based key generation from session key + * - Module-specific seed + * - Windows-specific module loading + */ +void WardenWin::Init(WorldSession* session, BigNumber* k) +{ + _session = session; + // Generate Warden Key + SHA1Randx WK(k->AsByteArray(), k->GetNumBytes()); + WK.Generate(_inputKey, 16); + WK.Generate(_outputKey, 16); + + memcpy(_seed, Module.Seed, 16); + + _inputCrypto.Init(_inputKey); + _outputCrypto.Init(_outputKey); + sLog.outWarden("Server side warden for client %u (build %u) initializing...", session->GetAccountId(), _session->GetClientBuild()); + sLog.outWarden("C->S Key: %s", ByteArrayToHexStr(_inputKey, 16).c_str()); + sLog.outWarden("S->C Key: %s", ByteArrayToHexStr(_outputKey, 16).c_str()); + sLog.outWarden(" Seed: %s", ByteArrayToHexStr(_seed, 16).c_str()); + sLog.outWarden("Loading Module..."); + + _module = GetModuleForClient(); + + sLog.outWarden("Module Key: %s", ByteArrayToHexStr(_module->Key, 16).c_str()); + sLog.outWarden("Module ID: %s", ByteArrayToHexStr(_module->Id, 16).c_str()); + RequestModule(); +} + +/** + * @brief Get Windows Warden module + * @return Client Warden module + * + * Returns the Windows-specific Warden module with its + * compressed data, key, and MD5 hash. + */ +ClientWardenModule* WardenWin::GetModuleForClient() +{ + ClientWardenModule *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, Module.ModuleKey, 16); + + // md5 hash + MD5_CTX ctx; + MD5_Init(&ctx); + MD5_Update(&ctx, mod->CompressedData, length); + MD5_Final((uint8*)&mod->Id, &ctx); + + return mod; +} + +/** + * @brief Initialize Windows Warden module + * + * Initializes the Windows-specific Warden module on the client. + */ +void WardenWin::InitializeModule() +{ + sLog.outWarden("Initialize module"); + + // Create packet structure + WardenInitModuleRequest Request; + Request.Command1 = WARDEN_SMSG_MODULE_INITIALIZE; + Request.Size1 = 20; + Request.CheckSumm1 = BuildChecksum(&Request.Unk1, 20); + Request.Unk1 = 1; + Request.Unk2 = 0; + Request.Type = 1; + Request.String_library1 = 0; + Request.Function1[0] = 0x002485F0; // 0x00400000 + 0x002485F0 SFileOpenFile + Request.Function1[1] = 0x002487F0; // 0x00400000 + 0x002487F0 SFileGetFileSize + Request.Function1[2] = 0x00248460; // 0x00400000 + 0x00248460 SFileReadFile + Request.Function1[3] = 0x00248730; // 0x00400000 + 0x00248730 SFileCloseFile + + 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; + + 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; + + // Encrypt with warden RC4 key. + EncryptData((uint8*)&Request, sizeof(WardenInitModuleRequest)); + + WorldPacket pkt(SMSG_WARDEN_DATA, sizeof(WardenInitModuleRequest)); + pkt.append((uint8*)&Request, sizeof(WardenInitModuleRequest)); + _session->SendPacket(&pkt); + + Warden::InitializeModule(); +} + +/** + * @brief Validates the Windows client hash reply and initializes the session crypto keys. + * + * @param buff The received Warden payload buffer. + */ +void WardenWin::HandleHashResult(ByteBuffer &buff) +{ + buff.rpos(buff.wpos()); + + // Verify key + if (memcmp(buff.contents() + 1, Module.ClientKeySeedHash, sizeof(Module.ClientKeySeedHash)) != 0) + { + sLog.outWarden("%s failed hash reply. Action: %s", _session->GetPlayerName(), Penalty().c_str()); + return; + } + + sLog.outWarden("Request hash reply: succeed"); + + // Change keys here + memcpy(_inputKey, Module.ClientKeySeed, 16); + memcpy(_outputKey, Module.ServerKeySeed, 16); + + _inputCrypto.Init(_inputKey); + _outputCrypto.Init(_outputKey); + + _previousTimestamp = GameTime::GetGameTimeMS(); +} + +/** + * @brief Builds and sends the current batch of Windows Warden checks. + */ +void WardenWin::RequestData() +{ + sLog.outWarden("Request data"); + + uint16 build = _session->GetClientBuild(); + uint16 id = 0; + uint8 type = 0; + WardenCheck* wd = NULL; + + // If all checks were done, fill the todo list again + if (_memChecksTodo.empty()) + { + sWardenCheckMgr->GetWardenCheckIds(true, build, _memChecksTodo); + } + + if (_otherChecksTodo.empty()) + { + sWardenCheckMgr->GetWardenCheckIds(false, build, _otherChecksTodo); + } + + _serverTicks = GameTime::GetGameTimeMS(); + + _currentChecks.clear(); + + // Build check request + 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 + _currentChecks.push_back(id); + } + + ByteBuffer buff; + buff << uint8(WARDEN_SMSG_CHEAT_CHECKS_REQUEST); + + 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 + _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) + { + 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; + } + } + } + + uint8 xorByte = _inputKey[0]; + + // Add TIMING_CHECK + buff << uint8(0x00); + buff << uint8(TIMING_CHECK ^ xorByte); + + uint8 index = 1; + + for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) + { + wd = sWardenCheckMgr->GetWardenDataById(build, *itr); + + type = wd->Type; + buff << uint8(type ^ xorByte); + switch (type) + { + case MEM_CHECK: + { + buff << uint8(0x00); + buff << uint32(wd->Address); + buff << uint8(wd->Length); + break; + } + case PAGE_CHECK_A: + case PAGE_CHECK_B: + { + buff.append(wd->Data.AsByteArray(0, false), wd->Data.GetNumBytes()); + buff << uint32(wd->Address); + buff << uint8(wd->Length); + break; + } + case MPQ_CHECK: + case LUA_STR_CHECK: + { + buff << uint8(index++); + break; + } + case DRIVER_CHECK: + { + buff.append(wd->Data.AsByteArray(0, false), wd->Data.GetNumBytes()); + buff << uint8(index++); + break; + } + case MODULE_CHECK: + { + uint32 seed = rand32(); + buff << uint32(seed); + HMACSHA1 hmac(4, (uint8*)&seed); + hmac.UpdateData(wd->Str); + hmac.Finalize(); + buff.append(hmac.GetDigest(), hmac.GetLength()); + 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; + }*/ + default: + break; // Should never happen + } + } + buff << uint8(xorByte); + buff.hexlike(); + + // Encrypt with warden RC4 key + EncryptData((uint8*)buff.contents(), buff.size()); + + WorldPacket pkt(SMSG_WARDEN_DATA, buff.size()); + pkt.append(buff); + _session->SendPacket(&pkt); + + std::stringstream stream; + stream << "Sent check id's: "; + for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) + { + stream << *itr << " "; + } + + sLog.outWarden("%s", stream.str().c_str()); + + Warden::RequestData(); +} + +/** + * @brief Processes a Windows Warden data response and validates the reported checks. + * + * @param buff The received Warden payload buffer. + */ +void WardenWin::HandleData(ByteBuffer &buff) +{ + sLog.outWarden("Handle data"); + + uint16 Length; + buff >> Length; + uint32 Checksum; + buff >> Checksum; + + if (!IsValidCheckSum(Checksum, buff.contents() + buff.rpos(), Length)) + { + buff.rpos(buff.wpos()); + sLog.outWarden("%s failed checksum. Action: %s", _session->GetPlayerName(), Penalty().c_str()); + return; + } + + // TIMING_CHECK + { + uint8 result; + buff >> result; + /// @todo test it. + if (result == 0x00) + { + sLog.outWarden("%s failed timing check. Action: %s", _session->GetPlayerName(), Penalty().c_str()); + return; + } + + uint32 newClientTicks; + buff >> newClientTicks; + + uint32 ticksNow = GameTime::GetGameTimeMS(); + uint32 ourTicks = newClientTicks + (ticksNow - _serverTicks); + + 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; + + for (std::list::iterator itr = _currentChecks.begin(); itr != _currentChecks.end(); ++itr) + { + rd = sWardenCheckMgr->GetWardenDataById(_session->GetClientBuild(), *itr); + rs = sWardenCheckMgr->GetWardenResultById(_session->GetClientBuild(), *itr); + + type = rd->Type; + switch (type) + { + case MEM_CHECK: + { + uint8 Mem_Result; + buff >> Mem_Result; + + 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()); + break; + } + case PAGE_CHECK_A: + case PAGE_CHECK_B: + case DRIVER_CHECK: + case MODULE_CHECK: + { + 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()); + } + 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()); + } + 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; + } + case LUA_STR_CHECK: + { + 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; + } + + 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; + } + 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) + { + sLog.outWarden("RESULT MPQ_CHECK not 0x00 account id %u", _session->GetAccountId()); + checkFailed = *itr; + continue; + } + + 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; + } + + buff.rpos(buff.rpos() + 20); // 20 bytes SHA1 + sLog.outWarden("RESULT MPQ_CHECK passed, CheckId %u account Id %u", *itr, _session->GetAccountId()); + break; + } + default: // Should never happen + break; + } + } + + if (checkFailed > 0) + { + 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); + } + + Warden::HandleData(buff); +} diff --git a/src/game/Warden/WardenWin.h b/src/game/Warden/WardenWin.h index 2570c08e6..7958f0534 100644 --- a/src/game/Warden/WardenWin.h +++ b/src/game/Warden/WardenWin.h @@ -27,6 +27,7 @@ #define _WARDEN_WIN_H #include "Warden.h" +#include #if defined(__GNUC__) #pragma pack(1)