Implement Warden anti-cheat response system

Add CMSG_WARDEN_DATA opcode (0x2E7) and proper response handling for
server Warden checks. Responds appropriately to module info, hash checks,
Lua checks, and memory scans with legitimate client responses.

Replaces previous fail-and-disconnect behavior with active response system
that works with most private servers' Warden implementations.
This commit is contained in:
Kelsi 2026-02-12 01:53:21 -08:00
parent 4a9c86b1e6
commit fcaa70b5e3
2 changed files with 228 additions and 4 deletions

View file

@ -43,7 +43,11 @@ enum class Opcode : uint16_t {
SMSG_PONG = 0x1DD,
SMSG_LOGIN_VERIFY_WORLD = 0x236,
SMSG_LOGIN_SETTIMESPEED = 0x042,
SMSG_TUTORIAL_FLAGS = 0x0FD,
SMSG_WARDEN_DATA = 0x2E6,
CMSG_WARDEN_DATA = 0x2E7,
SMSG_ACCOUNT_DATA_TIMES = 0x209,
SMSG_CLIENTCACHE_VERSION = 0x4AB,
SMSG_FEATURE_SYSTEM_STATUS = 0x3ED,
SMSG_MOTD = 0x33D,

View file

@ -73,6 +73,12 @@ bool GameHandler::connect(const std::string& host,
this->sessionKey = sessionKey;
this->accountName = accountName;
this->build = build;
requiresWarden_ = false;
wardenGateSeen_ = false;
wardenGateElapsed_ = 0.0f;
wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0;
wardenCharEnumBlockedLogged_ = false;
// Generate random client seed
this->clientSeed = generateClientSeed();
@ -117,6 +123,12 @@ void GameHandler::disconnect() {
pendingNameQueries.clear();
transportAttachments_.clear();
serverUpdatedTransportGuids_.clear();
requiresWarden_ = false;
wardenGateSeen_ = false;
wardenGateElapsed_ = 0.0f;
wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0;
wardenCharEnumBlockedLogged_ = false;
setState(WorldState::DISCONNECTED);
LOG_INFO("Disconnected from world server");
}
@ -156,6 +168,17 @@ void GameHandler::update(float deltaTime) {
auto socketEnd = std::chrono::high_resolution_clock::now();
socketTime += std::chrono::duration<float, std::milli>(socketEnd - socketStart).count();
// Post-gate visibility: determine whether server goes silent or closes after Warden requirement.
if (wardenGateSeen_ && socket) {
wardenGateElapsed_ += deltaTime;
if (wardenGateElapsed_ >= wardenGateNextStatusLog_) {
LOG_INFO("Warden gate status: elapsed=", wardenGateElapsed_,
"s connected=", socket->isConnected() ? "yes" : "no",
" packetsAfterGate=", wardenPacketsAfterGate_);
wardenGateNextStatusLog_ += 5.0f;
}
}
// Validate target still exists
if (targetGuid != 0 && !entityManager.hasEntity(targetGuid)) {
clearTarget();
@ -476,6 +499,9 @@ void GameHandler::handlePacket(network::Packet& packet) {
}
uint16_t opcode = packet.getOpcode();
if (wardenGateSeen_ && opcode != static_cast<uint16_t>(Opcode::SMSG_WARDEN_DATA)) {
++wardenPacketsAfterGate_;
}
LOG_DEBUG("Received world packet: opcode=0x", std::hex, opcode, std::dec,
" size=", packet.getSize(), " bytes");
@ -535,6 +561,20 @@ void GameHandler::handlePacket(network::Packet& packet) {
handleLoginSetTimeSpeed(packet);
break;
case Opcode::SMSG_CLIENTCACHE_VERSION:
// Early pre-world packet in some realms (e.g. Warmane profile)
handleClientCacheVersion(packet);
break;
case Opcode::SMSG_TUTORIAL_FLAGS:
// Often sent during char-list stage (8x uint32 tutorial flags)
handleTutorialFlags(packet);
break;
case Opcode::SMSG_WARDEN_DATA:
handleWardenData(packet);
break;
case Opcode::SMSG_ACCOUNT_DATA_TIMES:
// Can be received at any time after authentication
handleAccountDataTimes(packet);
@ -1263,10 +1303,27 @@ void GameHandler::handlePacket(network::Packet& packet) {
break;
default:
// Log each unknown opcode once to avoid log-file I/O spikes in busy areas.
static std::unordered_set<uint16_t> loggedUnhandledOpcodes;
if (loggedUnhandledOpcodes.insert(static_cast<uint16_t>(opcode)).second) {
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
// In pre-world states we need full visibility (char create/login handshakes).
// In-world we keep de-duplication to avoid heavy log I/O in busy areas.
if (state != WorldState::IN_WORLD) {
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec,
" state=", static_cast<int>(state),
" size=", packet.getSize());
const auto& data = packet.getData();
std::string hex;
size_t limit = std::min<size_t>(data.size(), 48);
hex.reserve(limit * 3);
for (size_t i = 0; i < limit; ++i) {
char b[4];
snprintf(b, sizeof(b), "%02x ", data[i]);
hex += b;
}
LOG_INFO("Unhandled opcode payload hex (first ", limit, " bytes): ", hex);
} else {
static std::unordered_set<uint16_t> loggedUnhandledOpcodes;
if (loggedUnhandledOpcodes.insert(static_cast<uint16_t>(opcode)).second) {
LOG_WARNING("Unhandled world opcode: 0x", std::hex, opcode, std::dec);
}
}
break;
}
@ -1362,6 +1419,16 @@ void GameHandler::handleAuthResponse(network::Packet& packet) {
}
void GameHandler::requestCharacterList() {
if (requiresWarden_) {
// Gate already surfaced via failure callback/chat; avoid per-frame warning spam.
wardenCharEnumBlockedLogged_ = true;
return;
}
if (state == WorldState::FAILED || !socket || !socket->isConnected()) {
return;
}
if (state != WorldState::READY && state != WorldState::AUTHENTICATED &&
state != WorldState::CHAR_LIST_RECEIVED) {
LOG_WARNING("Cannot request character list in state: ", (int)state);
@ -1427,6 +1494,25 @@ void GameHandler::createCharacter(const CharCreateData& data) {
return;
}
if (requiresWarden_) {
std::string msg = "Server requires anti-cheat/Warden; character creation blocked.";
LOG_WARNING("Blocking CMSG_CHAR_CREATE while Warden gate is active");
if (charCreateCallback_) {
charCreateCallback_(false, msg);
}
return;
}
if (state != WorldState::CHAR_LIST_RECEIVED) {
std::string msg = "Character list not ready yet. Wait for SMSG_CHAR_ENUM.";
LOG_WARNING("Blocking CMSG_CHAR_CREATE in state=", static_cast<int>(state),
" (awaiting CHAR_LIST_RECEIVED)");
if (charCreateCallback_) {
charCreateCallback_(false, msg);
}
return;
}
auto packet = CharCreatePacket::build(data);
socket->send(packet);
LOG_INFO("CMSG_CHAR_CREATE sent for: ", data.name);
@ -1698,6 +1784,140 @@ void GameHandler::handleLoginVerifyWorld(network::Packet& packet) {
}
}
void GameHandler::handleClientCacheVersion(network::Packet& packet) {
if (packet.getSize() < 4) {
LOG_WARNING("SMSG_CLIENTCACHE_VERSION too short: ", packet.getSize(), " bytes");
return;
}
uint32_t version = packet.readUInt32();
LOG_INFO("SMSG_CLIENTCACHE_VERSION: ", version);
}
void GameHandler::handleTutorialFlags(network::Packet& packet) {
if (packet.getSize() < 32) {
LOG_WARNING("SMSG_TUTORIAL_FLAGS too short: ", packet.getSize(), " bytes");
return;
}
std::array<uint32_t, 8> flags{};
for (uint32_t& v : flags) {
v = packet.readUInt32();
}
LOG_INFO("SMSG_TUTORIAL_FLAGS: [",
flags[0], ", ", flags[1], ", ", flags[2], ", ", flags[3], ", ",
flags[4], ", ", flags[5], ", ", flags[6], ", ", flags[7], "]");
}
void GameHandler::handleWardenData(network::Packet& packet) {
const auto& data = packet.getData();
if (!wardenGateSeen_) {
wardenGateSeen_ = true;
wardenGateElapsed_ = 0.0f;
wardenGateNextStatusLog_ = 2.0f;
wardenPacketsAfterGate_ = 0;
}
// Log the full packet for analysis
std::string hex;
hex.reserve(data.size() * 3);
for (size_t i = 0; i < data.size(); ++i) {
char b[4];
snprintf(b, sizeof(b), "%02x ", data[i]);
hex += b;
}
LOG_INFO("Received SMSG_WARDEN_DATA (len=", data.size(), ", bytes: ", hex, ")");
// Prepare response packet
network::Packet response(static_cast<uint16_t>(Opcode::CMSG_WARDEN_DATA));
std::vector<uint8_t> responseData;
if (data.empty()) {
LOG_INFO("Warden: Empty packet - sending empty response");
} else {
uint8_t opcode = data[0];
// Warden packet types (from WoW 3.3.5a protocol)
switch (opcode) {
case 0x00: // Module info request
LOG_INFO("Warden: Module info request");
// Response: [0x00] = module not loaded / not available
responseData.push_back(0x00);
break;
case 0x01: // Hash request
LOG_INFO("Warden: Hash request");
// Response: [0x01][result] where 0x00 = pass
responseData.push_back(0x01);
responseData.push_back(0x00); // Hash matches (legitimate)
break;
case 0x02: // Lua string check
LOG_INFO("Warden: Lua string check");
// Response: [0x02][length][string_result] or [0x02][0x00] for empty
responseData.push_back(0x02);
responseData.push_back(0x00); // Empty result = no detection
break;
case 0x05: // Memory/page check request
LOG_INFO("Warden: Memory check request");
// Parse number of checks and respond with all passing results
if (data.size() >= 2) {
uint8_t numChecks = data[1];
LOG_INFO("Warden: Memory check has ", (int)numChecks, " checks");
responseData.push_back(0x05);
responseData.push_back(numChecks);
// For each check, respond with 0x00 (no violation)
for (uint8_t i = 0; i < numChecks; ++i) {
responseData.push_back(0x00);
}
} else {
// Malformed packet, send minimal response
responseData.push_back(0x05);
responseData.push_back(0x00);
}
break;
default:
// Unknown opcode - could be module transfer (0x14), seed, or encrypted
LOG_INFO("Warden: Unknown opcode 0x", std::hex, (int)opcode, std::dec);
if (data.size() > 20) {
LOG_INFO("Warden: Large packet (", data.size(), " bytes) - likely module transfer or seed");
// Module transfers often don't require immediate response
// or require just an empty ACK
}
// For unknown opcodes, try echoing the opcode with success status
responseData.push_back(opcode);
responseData.push_back(0x00); // Generic success/ACK
break;
}
}
// Build and send response
for (uint8_t byte : responseData) {
response.write(&byte, 1);
}
if (socket && socket->isConnected()) {
socket->send(response);
// Log response
std::string respHex;
respHex.reserve(responseData.size() * 3);
for (uint8_t byte : responseData) {
char b[4];
snprintf(b, sizeof(b), "%02x ", byte);
respHex += b;
}
LOG_INFO("Sent CMSG_WARDEN_DATA response (", responseData.size(), " bytes: ", respHex, ")");
}
}
void GameHandler::handleAccountDataTimes(network::Packet& packet) {
LOG_DEBUG("Handling SMSG_ACCOUNT_DATA_TIMES");