From 7b069dcf2036b54d107ab6ce701e400e37328b35 Mon Sep 17 00:00:00 2001 From: Dencelle Date: Tue, 31 Aug 2021 01:08:31 -0500 Subject: [PATCH] [Cheat Detection] Anti-Cheat reimplementation (#1434) * [Cheat Detection] Anti-Cheat reimplementation * minor patch fixes * ceiling to server side runspeed Warp(LT) was picking up a bunch of expected 6.2 but it was reported back as 6.5, this should help reduce the amount of false positives we get * use ceil instead of std::ceilf for linux * boat false positive fix * stopping the double detection * fixes and cleanup * auto merge tricked me... * dummy divide by 0 checks this should prevent anyone from setting Zone:MQWarpDetectionDistanceFactor to 0 and causing a crash. * Formatting * encapsulation to its own class and clean up * more detections * typo * OP_UnderWorld implmentation * Update client_packet.h * Syntax changes, formatting, cleanup * preventing crashes due to invalid packet size * typos and clearer logic * seperated the catagory for cheats * Updated MQGhost for more detail Co-authored-by: Akkadius --- common/emu_oplist.h | 1 + common/eq_constants.h | 1 + common/eq_packet_structs.h | 17 ++ common/eqemu_logsys.cpp | 1 + common/eqemu_logsys.h | 2 + common/eqemu_logsys_log_aliases.h | 10 + common/ruletypes.h | 15 ++ zone/CMakeLists.txt | 8 +- zone/cheat_manager.cpp | 386 ++++++++++++++++++++++++++++++ zone/cheat_manager.h | 88 +++++++ zone/client.cpp | 9 +- zone/client.h | 15 +- zone/client_packet.cpp | 44 +++- zone/client_packet.h | 2 + zone/client_process.cpp | 2 + zone/embparser.cpp | 8 + zone/event_codes.h | 1 + zone/lua_general.cpp | 1 + zone/lua_parser.cpp | 2 + zone/lua_parser_events.cpp | 12 + zone/lua_parser_events.h | 2 + zone/mob.cpp | 1 + zone/spells.cpp | 30 ++- zone/zone.cpp | 6 +- zone/zoning.cpp | 26 +- 25 files changed, 664 insertions(+), 26 deletions(-) create mode 100644 zone/cheat_manager.cpp create mode 100644 zone/cheat_manager.h diff --git a/common/emu_oplist.h b/common/emu_oplist.h index 46fd564e6..d81ed3a55 100644 --- a/common/emu_oplist.h +++ b/common/emu_oplist.h @@ -567,4 +567,5 @@ N(OP_ZoneServerReady), N(OP_ZoneSpawns), N(OP_ZoneUnavail), N(OP_ResetAA), +N(OP_UnderWorld), // mail and chat opcodes located in ../mail_oplist.h diff --git a/common/eq_constants.h b/common/eq_constants.h index 988ac7e60..fb6f0c557 100644 --- a/common/eq_constants.h +++ b/common/eq_constants.h @@ -65,6 +65,7 @@ #define AT_FindBits 46 // set FindBits, whatever those are! #define AT_TextureType 48 // TextureType #define AT_FacePick 49 // Turns off face pick window? maybe ... +#define AT_AntiCheat 51 // sent by the client randomly telling the server how long since last action has occured #define AT_GuildShow 52 // this is what MQ2 call sit, not sure #define AT_Offline 53 // Offline mode diff --git a/common/eq_packet_structs.h b/common/eq_packet_structs.h index ac08fe8d1..308ba22e9 100644 --- a/common/eq_packet_structs.h +++ b/common/eq_packet_structs.h @@ -5530,6 +5530,23 @@ struct SayLinkBodyFrame_Struct { /*056*/ }; +struct UpdateMovementEntry { + /* 00 */ float Y; + /* 04 */ float X; + /* 08 */ float Z; + /* 12 */ uint8 type; + /* 13 */ unsigned int timestamp; + /* 17 */ +}; + +struct UnderWorld { + /* 00 */ int spawn_id; + /* 04 */ float y; + /* 08 */ float x; + /* 12 */ float z; + /* 16 */ +}; + // Restore structure packing to default #pragma pack() diff --git a/common/eqemu_logsys.cpp b/common/eqemu_logsys.cpp index 950c17107..3666735e4 100644 --- a/common/eqemu_logsys.cpp +++ b/common/eqemu_logsys.cpp @@ -129,6 +129,7 @@ EQEmuLogSys *EQEmuLogSys::LoadLogSettingsDefaults() log_settings[Logs::HotReload].log_to_console = static_cast(Logs::General); log_settings[Logs::Loot].log_to_gmsay = static_cast(Logs::General); log_settings[Logs::Scheduler].log_to_console = static_cast(Logs::General); + log_settings[Logs::Cheat].log_to_console = static_cast(Logs::General); /** * RFC 5424 diff --git a/common/eqemu_logsys.h b/common/eqemu_logsys.h index 4db6cedb2..6e881eecc 100644 --- a/common/eqemu_logsys.h +++ b/common/eqemu_logsys.h @@ -122,6 +122,7 @@ namespace Logs { Expeditions, DynamicZones, Scheduler, + Cheat, MaxCategoryID /* Don't Remove this */ }; @@ -202,6 +203,7 @@ namespace Logs { "Expeditions", "DynamicZones", "Scheduler", + "Cheat" }; } diff --git a/common/eqemu_logsys_log_aliases.h b/common/eqemu_logsys_log_aliases.h index ddd27335f..b683d9111 100644 --- a/common/eqemu_logsys_log_aliases.h +++ b/common/eqemu_logsys_log_aliases.h @@ -646,6 +646,16 @@ OutF(LogSys, Logs::Detail, Logs::Scheduler, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ } while (0) +#define LogCheat(message, ...) do {\ + if (LogSys.log_settings[Logs::Cheat].is_category_enabled == 1)\ + OutF(LogSys, Logs::General, Logs::Cheat, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + +#define LogCheatDetail(message, ...) do {\ + if (LogSys.log_settings[Logs::Cheat].is_category_enabled == 1)\ + OutF(LogSys, Logs::Detail, Logs::Cheat, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ +} while (0) + #define Log(debug_level, log_category, message, ...) do {\ if (LogSys.log_settings[log_category].is_category_enabled == 1)\ LogSys.Out(debug_level, log_category, __FILE__, __func__, __LINE__, message, ##__VA_ARGS__);\ diff --git a/common/ruletypes.h b/common/ruletypes.h index a3ec71a47..a86764570 100644 --- a/common/ruletypes.h +++ b/common/ruletypes.h @@ -762,6 +762,21 @@ RULE_BOOL(DynamicZone, EnableInDynamicZoneStatus, false, "Enables the 'In Dynami RULE_INT(DynamicZone, WorldProcessRate, 6000, "Timer interval (milliseconds) that systems check their dynamic zone states") RULE_CATEGORY_END() +RULE_CATEGORY(Cheat) +RULE_REAL(Cheat, MQWarpDetectionDistanceFactor, 9.0, "clients move at 4.4 about if in a straight line but with movement and to acct for lag we raise it a bit") +RULE_INT(Cheat, MQWarpExemptStatus, -1, "Required status level to exempt the MQWarpDetector. Set to -1 to disable this feature.") +RULE_INT(Cheat, MQZoneExemptStatus, -1, "Required status level to exempt the MQZoneDetector. Set to -1 to disable this feature.") +RULE_INT(Cheat, MQGateExemptStatus, -1, "Required status level to exempt the MQGateDetector. Set to -1 to disable this feature.") +RULE_INT(Cheat, MQGhostExemptStatus, -1, "Required status level to exempt the MQGhostDetector. Set to -1 to disable this feature.") +RULE_INT(Cheat, MQFastMemExemptStatus, -1, "Required status level to exempt the MQFastMemDetector. Set to -1 to disable this feature.") +RULE_BOOL(Cheat, EnableMQWarpDetector, true, "Enable the MQWarp Detector. Set to False to disable this feature.") +RULE_BOOL(Cheat, EnableMQZoneDetector, true, "Enable the MQZone Detector. Set to False to disable this feature.") +RULE_BOOL(Cheat, EnableMQGateDetector, true, "Enable the MQGate Detector. Set to False to disable this feature.") +RULE_BOOL(Cheat, EnableMQGhostDetector, true, "Enable the MQGhost Detector. Set to False to disable this feature.") +RULE_BOOL(Cheat, EnableMQFastMemDetector, true, "Enable the MQFastMem Detector. Set to False to disable this feature.") +RULE_BOOL(Cheat, MarkMQWarpLT, false, "Mark clients makeing smaller warps") +RULE_CATEGORY_END() + #undef RULE_CATEGORY #undef RULE_INT #undef RULE_REAL diff --git a/zone/CMakeLists.txt b/zone/CMakeLists.txt index 5f00e689a..554d3e717 100644 --- a/zone/CMakeLists.txt +++ b/zone/CMakeLists.txt @@ -14,6 +14,7 @@ SET(zone_sources bot_command.cpp bot_database.cpp botspellsai.cpp + cheat_manager.cpp client.cpp client_mods.cpp client_packet.cpp @@ -157,7 +158,8 @@ SET(zone_sources zone_event_scheduler.cpp zone_reload.cpp zone_store.cpp - zoning.cpp) + zoning.cpp +) SET(zone_headers aa.h @@ -171,6 +173,7 @@ SET(zone_headers bot_command.h bot_database.h bot_structs.h + cheat_manager.h client.h client_packet.h command.h @@ -274,7 +277,8 @@ SET(zone_headers zonedb.h zonedump.h zone_reload.h - zone_store.h) + zone_store.h +) ADD_EXECUTABLE(zone ${zone_sources} ${zone_headers}) diff --git a/zone/cheat_manager.cpp b/zone/cheat_manager.cpp new file mode 100644 index 000000000..10269286d --- /dev/null +++ b/zone/cheat_manager.cpp @@ -0,0 +1,386 @@ +#include "cheat_manager.h" +#include "client.h" +#include "quest_parser_collection.h" + +void CheatManager::SetClient(Client *cli) +{ + m_target = cli; +} + +void CheatManager::SetExemptStatus(ExemptionType type, bool v) +{ + if (v == true) { + MovementCheck(); + } + m_exemption[type] = v; +} + +bool CheatManager::GetExemptStatus(ExemptionType type) +{ + return m_exemption[type]; +} + +void CheatManager::CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position2) +{ + switch (type) { + case MQWarp: + if (m_time_since_last_warp_detection.GetRemainingTime() == 0 && RuleB(Cheat, EnableMQWarpDetector) && + ((m_target->Admin() < RuleI(Cheat, MQWarpExemptStatus) || (RuleI(Cheat, MQWarpExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQWarp (large warp detection) with location from x [{:.2f}] y [{:.2f}] z [{:.2f}] to x [{:.2f}] y [{:.2f}] z [{:.2f}] Distance [{:.2f}]", + position1.x, + position1.y, + position1.z, + position2.x, + position2.y, + position2.z, + Distance(position1, position2) + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + std::string export_string = fmt::format("{} {} {}", position1.x, position1.y, position1.z); + parse->EventPlayer(EVENT_WARP, m_target, export_string, 0); + } + break; + case MQWarpAbsolute: + if (RuleB(Cheat, EnableMQWarpDetector) && + ((m_target->Admin() < RuleI(Cheat, MQWarpExemptStatus) || (RuleI(Cheat, MQWarpExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQWarp (Absolute) with location from x [{:.2f}] y [{:.2f}] z [{:.2f}] to x [{:.2f}] y [{:.2f}] z [{:.2f}] Distance [{:.2f}]", + position1.x, + position1.y, + position1.z, + position2.x, + position2.y, + position2.z, + Distance(position1, position2) + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + std::string export_string = fmt::format("{} {} {}", position1.x, position1.y, position1.z); + parse->EventPlayer(EVENT_WARP, m_target, export_string, 0); + m_time_since_last_warp_detection.Start(2500); + } + break; + case MQWarpShadowStep: + if (RuleB(Cheat, EnableMQWarpDetector) && + ((m_target->Admin() < RuleI(Cheat, MQWarpExemptStatus) || (RuleI(Cheat, MQWarpExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQWarp(ShadowStep) with location from x [{:.2f}] y [{:.2f}] z [{:.2f}] the target was shadow step exempt but we still found this suspicious.", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + } + break; + case MQWarpKnockBack: + if (RuleB(Cheat, EnableMQWarpDetector) && + ((m_target->Admin() < RuleI(Cheat, MQWarpExemptStatus) || (RuleI(Cheat, MQWarpExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQWarp(Knockback) with location from x [{:.2f}] y [{:.2f}] z [{:.2f}] the target was Knock Back exempt but we still found this suspicious.", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + } + break; + + case MQWarpLight: + if (RuleB(Cheat, EnableMQWarpDetector) && + ((m_target->Admin() < RuleI(Cheat, MQWarpExemptStatus) || (RuleI(Cheat, MQWarpExemptStatus)) == -1))) { + if (RuleB(Cheat, MarkMQWarpLT)) { + std::string message = fmt::format( + "/MQWarp(Knockback) with location from x [{:.2f}] y [{:.2f}] z [{:.2f}] running fast but not fast enough to get killed, possibly: small warp, speed hack, excessive lag, marked as suspicious.", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + } + } + break; + + case MQZone: + if (RuleB(Cheat, EnableMQZoneDetector) && + ((m_target->Admin() < RuleI(Cheat, MQZoneExemptStatus) || (RuleI(Cheat, MQZoneExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQZone used at x [{:.2f}] y [{:.2f}] z [{:.2f}]", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + } + break; + case MQZoneUnknownDest: + if (RuleB(Cheat, EnableMQZoneDetector) && + ((m_target->Admin() < RuleI(Cheat, MQZoneExemptStatus) || (RuleI(Cheat, MQZoneExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQZone used at x [{:.2f}] y [{:.2f}] z [{:.2f}] with Unknown Destination", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName()); + LogCheat(message); + } + break; + case MQGate: + if (RuleB(Cheat, EnableMQGateDetector) && + ((m_target->Admin() < RuleI(Cheat, MQGateExemptStatus) || (RuleI(Cheat, MQGateExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQGate used at x [{:.2f}] y [{:.2f}] z [{:.2f}]", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + } + break; + case MQGhost: + // this isn't just for ghost, its also for if a person isn't sending their MovementHistory packet also. + if (RuleB(Cheat, EnableMQGhostDetector) && + ((m_target->Admin() < RuleI(Cheat, MQGhostExemptStatus) || (RuleI(Cheat, MQGhostExemptStatus)) == -1))) { + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + "Packet blocking detected.", + zone->GetShortName()); + LogCheat("{} was caught not sending the proper packets as regularly as they were suppose to."); + } + break; + case MQFastMem: + if (RuleB(Cheat, EnableMQFastMemDetector) && + ((m_target->Admin() < RuleI(Cheat, MQFastMemExemptStatus) || + (RuleI(Cheat, MQFastMemExemptStatus)) == -1))) { + std::string message = fmt::format( + "/MQFastMem used at x [{:.2f}] y [{:.2f}] z [{:.2f}]", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + } + break; + default: + std::string message = fmt::format( + "Unhandled HackerDetection flag with location from x [{:.2f}] y [{:.2f}] z [{:.2f}]", + position1.x, + position1.y, + position1.z + ); + database.SetMQDetectionFlag( + m_target->AccountName(), + m_target->GetName(), + message.c_str(), + zone->GetShortName() + ); + LogCheat(message); + break; + } +} + +void CheatManager::MovementCheck(glm::vec3 updated_position) +{ + if (m_time_since_last_movement_history.GetRemainingTime() == 0) { + CheatDetected(MQGhost, updated_position); + } + + float dist = DistanceNoZ(m_target->GetPosition(), updated_position); + uint32 cur_time = Timer::GetCurrentTime(); + if (dist == 0) { + if (m_distance_since_last_position_check > 0.0f) { + MovementCheck(0); + } + else { + m_time_since_last_position_check = cur_time; + m_cheat_detect_moved = false; + } + } + else { + m_distance_since_last_position_check += dist; + m_cheat_detect_moved = true; + if (m_time_since_last_position_check == 0) { + m_time_since_last_position_check = cur_time; + } + else { + MovementCheck(2500); + } + } +} + +void CheatManager::MovementCheck(uint32 time_between_checks) +{ + uint32 cur_time = Timer::GetCurrentTime(); + if ((cur_time - m_time_since_last_position_check) > time_between_checks) { + float estimated_speed = + (m_distance_since_last_position_check * 100) / (float) (cur_time - m_time_since_last_position_check); + float run_speed = m_target->GetRunspeed() / + std::min(RuleR(Cheat, MQWarpDetectionDistanceFactor), 1.0f); // MQWarpDetection shouldn't go below 1.0f so we can't end up dividing by 0. + if (estimated_speed > run_speed) { + bool using_gm_speed = m_target->GetGMSpeed(); + bool is_immobile = m_target->GetRunspeed() == 0; // this covers stuns, roots, mez, and pseudorooted. + if (!using_gm_speed && !is_immobile) { + if (GetExemptStatus(ShadowStep)) { + if (m_distance_since_last_position_check > 800) { + CheatDetected( + MQWarpShadowStep, + glm::vec3( + m_target->GetX(), + m_target->GetY(), + m_target->GetZ() + ) + ); + } + } + else if (GetExemptStatus(KnockBack)) { + if (estimated_speed > 30.0f) { + CheatDetected(MQWarpKnockBack, glm::vec3(m_target->GetX(), m_target->GetY(), m_target->GetZ())); + } + } + else if (!GetExemptStatus(Port)) { + if (estimated_speed > (run_speed * 1.5)) { + CheatDetected(MQWarp, glm::vec3(m_target->GetX(), m_target->GetY(), m_target->GetZ())); + m_time_since_last_position_check = cur_time; + m_distance_since_last_position_check = 0.0f; + } + else { + CheatDetected(MQWarpLight, glm::vec3(m_target->GetX(), m_target->GetY(), m_target->GetZ())); + } + } + } + } + if (time_between_checks != 1000) { + SetExemptStatus(ShadowStep, false); + SetExemptStatus(KnockBack, false); + SetExemptStatus(Port, false); + } + m_time_since_last_position_check = cur_time; + m_distance_since_last_position_check = 0.0f; + } +} + +void CheatManager::CheckMemTimer() +{ + if (m_target == nullptr) { + return; + } + if (m_time_since_last_memorization - Timer::GetCurrentTime() <= 1) { + glm::vec3 pos = m_target->GetPosition(); + CheatDetected(MQFastMem, pos); + } + m_time_since_last_memorization = Timer::GetCurrentTime(); +} + +void CheatManager::ProcessMovementHistory(const EQApplicationPacket *app) +{ + // if they haven't sent sent the packet within this time... they are probably spoofing... + // linux users reported that they don't send this packet at all but i can't prove they don't so i'm not sure if thats a fake or not. + m_time_since_last_movement_history.Start(70000); + if (GetExemptStatus(Port)) { + return; + } + auto *m_MovementHistory = (UpdateMovementEntry *) app->pBuffer; + if (app->size < sizeof(UpdateMovementEntry)) + { + LogDebug("Size mismatch in OP_MovementHistoryList, expected {}, got [{}]", sizeof(UpdateMovementEntry), app->size); + DumpPacket(app); + return; + } + + for (int index = 0; index < (app->size) / sizeof(UpdateMovementEntry); index++) { + glm::vec3 to = glm::vec3(m_MovementHistory[index].X, m_MovementHistory[index].Y, m_MovementHistory[index].Z); + switch (m_MovementHistory[index].type) { + case UpdateMovementType::ZoneLine: + SetExemptStatus(Port, true); + break; + case UpdateMovementType::TeleportA: + if (index != 0) { + glm::vec3 from = glm::vec3( + m_MovementHistory[index - 1].X, + m_MovementHistory[index - 1].Y, + m_MovementHistory[index - 1].Z + ); + CheatDetected(MQWarpAbsolute, from, to); + } + SetExemptStatus(Port, false); + break; + } + } +} + +void CheatManager::ProcessSpawnApperance(uint16 spawn_id, uint16 type, uint32 parameter) +{ + if (type == AT_Anim && parameter == ANIM_SIT) { + m_time_since_last_memorization = Timer::GetCurrentTime(); + } + else if (spawn_id == 0 && type == AT_AntiCheat) { + m_time_since_last_action = parameter; + } +} + +void CheatManager::ProcessItemVerifyRequest(int32 slot_id, uint32 target_id) +{ + if (slot_id == -1 && m_warp_counter != target_id) { + m_warp_counter = target_id; + } +} + +void CheatManager::ClientProcess() +{ + if (!m_cheat_detect_moved) { + m_time_since_last_position_check = Timer::GetCurrentTime(); + } +} diff --git a/zone/cheat_manager.h b/zone/cheat_manager.h new file mode 100644 index 000000000..43efe55af --- /dev/null +++ b/zone/cheat_manager.h @@ -0,0 +1,88 @@ +#ifndef ANTICHEAT_H +#define ANTICHEAT_H +class CheatManager; +class Client; + +#include "../common/timer.h" +#include "../common/rulesys.h" +#include +#include "../common/eq_packet_structs.h" +#include "../common/eq_packet.h" + +typedef enum { + Collision = 1, + TeleportB, + TeleportA, + ZoneLine, + Unknown0x5, + Unknown0x6, + SpellA, // Titanium - UF + Unknown0x8, + SpellB // Used in RoF+ +} UpdateMovementType; + +typedef enum { + ShadowStep, + KnockBack, + Port, + Assist, + Sense, + MAX_EXEMPTIONS +} ExemptionType; + +typedef enum { + MQWarp, + MQWarpShadowStep, + MQWarpKnockBack, + MQWarpLight, + MQZone, + MQZoneUnknownDest, + MQGate, + MQGhost, + MQFastMem, + MQWarpAbsolute +} CheatTypes; + +class CheatManager { +public: + CheatManager() + { + SetExemptStatus(ShadowStep, false); + SetExemptStatus(KnockBack, false); + SetExemptStatus(Port, false); + SetExemptStatus(Assist, false); + SetExemptStatus(Sense, false); + m_distance_since_last_position_check = 0.0f; + m_cheat_detect_moved = false; + m_target = nullptr; + m_time_since_last_memorization = 0; + m_time_since_last_position_check = 0; + m_time_since_last_warp_detection.Start(); + m_time_since_last_movement_history.Start(70000); + m_warp_counter = 0; + } + void SetClient(Client *cli); + void SetExemptStatus(ExemptionType type, bool v); + bool GetExemptStatus(ExemptionType type); + void CheatDetected(CheatTypes type, glm::vec3 position1, glm::vec3 position2 = glm::vec3(0, 0, 0)); + void MovementCheck(glm::vec3 updated_position); + void MovementCheck(uint32 time_between_checks = 1000); + void CheckMemTimer(); + void ProcessMovementHistory(const EQApplicationPacket *app); + void ProcessSpawnApperance(uint16 spawn_id, uint16 type, uint32 parameter); + void ProcessItemVerifyRequest(int32 slot_id, uint32 target_id); + void ClientProcess(); +private: + bool m_exemption[ExemptionType::MAX_EXEMPTIONS]{}; + float m_distance_since_last_position_check; + bool m_cheat_detect_moved; + + Client *m_target; + uint32 m_time_since_last_position_check; + uint32 m_time_since_last_memorization; + uint32 m_time_since_last_action{}; + Timer m_time_since_last_warp_detection; + Timer m_time_since_last_movement_history; + uint32 m_warp_counter; +}; +#endif ANTICHEAT_H diff --git a/zone/client.cpp b/zone/client.cpp index a90d424fa..f221c5fd9 100644 --- a/zone/client.cpp +++ b/zone/client.cpp @@ -61,6 +61,7 @@ extern volatile bool RunLoops; #include "mob_movement_manager.h" #include "../common/content/world_content_service.h" #include "../common/expedition_lockout_timer.h" +#include "cheat_manager.h" extern QueryServ* QServ; extern EntityList entity_list; @@ -177,7 +178,7 @@ Client::Client(EQStreamInterface* ieqs) for (int client_filter = 0; client_filter < _FilterCount; client_filter++) ClientFilters[client_filter] = FilterShow; - + cheat_manager.SetClient(this); mMovementManager->AddClient(this); character_id = 0; conn_state = NoPacketsReceived; @@ -10258,7 +10259,7 @@ void Client::ApplyWeaponsStance() - From spells, just remove the Primary buff that contains the WeaponStance effect in it. - For items with worn effect, unequip the item. - For AA abilities, a hotkey is used to Enable and Disable the effect. See. Client::TogglePassiveAlternativeAdvancement in aa.cpp for extensive details. - + Rank - Most important for AA, but if you have more than one of WeaponStance effect for a given type, the spell trigger buff will apply whatever has the highest 'rank' value from the spells table. AA's on live for this effect naturally do this. Be awere of this if making custom spells/worn effects/AA. @@ -10270,7 +10271,7 @@ void Client::ApplyWeaponsStance() if (!IsWeaponStanceEnabled()) { return; } - + bool enabled = false; bool item_bonus_exists = false; bool aa_bonus_exists = false; @@ -10326,7 +10327,7 @@ void Client::ApplyWeaponsStance() if (itembonuses.WeaponStance[WEAPON_STANCE_TYPE_2H] || itembonuses.WeaponStance[WEAPON_STANCE_TYPE_SHIELD] || itembonuses.WeaponStance[WEAPON_STANCE_TYPE_DUAL_WIELD]) { - + enabled = true; item_bonus_exists = true; diff --git a/zone/client.h b/zone/client.h index 004e905da..f9d0e724c 100644 --- a/zone/client.h +++ b/zone/client.h @@ -66,6 +66,7 @@ namespace EQ #include "zone_store.h" #include "task_manager.h" #include "task_client_state.h" +#include "cheat_manager.h" #ifdef _WINDOWS // since windows defines these within windef.h (which windows.h include) @@ -120,17 +121,6 @@ typedef enum { EvacToSafeCoords } ZoneMode; -typedef enum { - MQWarp, - MQWarpShadowStep, - MQWarpKnockBack, - MQWarpLight, - MQZone, - MQZoneUnknownDest, - MQGate, - MQGhost -} CheatTypes; - enum { HideCorpseNone = 0, HideCorpseAll = 1, @@ -604,7 +594,7 @@ public: inline double GetEXPModifier(uint32 zone_id) const { return database.GetEXPModifier(CharacterID(), zone_id); }; inline void SetAAEXPModifier(uint32 zone_id, double aa_modifier) { database.SetAAEXPModifier(CharacterID(), zone_id, aa_modifier); }; inline void SetEXPModifier(uint32 zone_id, double exp_modifier) { database.SetEXPModifier(CharacterID(), zone_id, exp_modifier); }; - + bool UpdateLDoNPoints(uint32 theme_id, int points); void SetPVPPoints(uint32 Points) { m_pp.PVPCurrentPoints = Points; } uint32 GetPVPPoints() { return m_pp.PVPCurrentPoints; } @@ -1598,6 +1588,7 @@ public: Raid *p_raid_instance; void ShowDevToolsMenu(); + CheatManager cheat_manager; protected: friend class Mob; diff --git a/zone/client_packet.cpp b/zone/client_packet.cpp index 64d45bb06..a1e3eb9c0 100644 --- a/zone/client_packet.cpp +++ b/zone/client_packet.cpp @@ -209,7 +209,7 @@ void MapOpcodes() ConnectedOpcodes[OP_FeignDeath] = &Client::Handle_OP_FeignDeath; ConnectedOpcodes[OP_FindPersonRequest] = &Client::Handle_OP_FindPersonRequest; ConnectedOpcodes[OP_Fishing] = &Client::Handle_OP_Fishing; - ConnectedOpcodes[OP_FloatListThing] = &Client::Handle_OP_Ignore; + ConnectedOpcodes[OP_FloatListThing] = &Client::Handle_OP_MovementHistoryList; ConnectedOpcodes[OP_Forage] = &Client::Handle_OP_Forage; ConnectedOpcodes[OP_FriendsWho] = &Client::Handle_OP_FriendsWho; ConnectedOpcodes[OP_GetGuildMOTD] = &Client::Handle_OP_GetGuildMOTD; @@ -413,6 +413,7 @@ void MapOpcodes() ConnectedOpcodes[OP_YellForHelp] = &Client::Handle_OP_YellForHelp; ConnectedOpcodes[OP_ZoneChange] = &Client::Handle_OP_ZoneChange; ConnectedOpcodes[OP_ResetAA] = &Client::Handle_OP_ResetAA; + ConnectedOpcodes[OP_UnderWorld] = &Client::Handle_OP_UnderWorld; } void ClearMappedOpcode(EmuOpcode op) @@ -2922,6 +2923,7 @@ void Client::Handle_OP_Assist(const EQApplicationPacket *app) Mob *new_target = assistee->GetTarget(); if (new_target && (GetGM() || Distance(m_Position, assistee->GetPosition()) <= TARGETING_RANGE)) { + cheat_manager.SetExemptStatus(Assist, true); eid->entity_id = new_target->GetID(); } else { eid->entity_id = 0; @@ -4488,7 +4490,7 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) { double cosine = std::cos(thetar); double sine = std::sin(thetar); - double normalizedx, normalizedy; + double normalizedx, normalizedy; normalizedx = cx * cosine - -cy * sine; normalizedy = -cx * sine + cy * cosine; @@ -4500,6 +4502,8 @@ void Client::Handle_OP_ClientUpdate(const EQApplicationPacket *app) { } } + cheat_manager.MovementCheck(glm::vec3(cx, cy, cz)); + if (IsDraggingCorpse()) DragCorpses(); @@ -8765,6 +8769,7 @@ void Client::Handle_OP_ItemVerifyRequest(const EQApplicationPacket *app) slot_id = request->slot; target_id = request->target; + cheat_manager.ProcessItemVerifyRequest(request->slot, request->target); EQApplicationPacket *outapp = nullptr; outapp = new EQApplicationPacket(OP_ItemVerifyReply, sizeof(ItemVerifyReply_Struct)); @@ -9614,6 +9619,7 @@ return; void Client::Handle_OP_MemorizeSpell(const EQApplicationPacket *app) { + cheat_manager.CheckMemTimer(); OPMemorizeSpell(app); return; } @@ -13465,6 +13471,8 @@ void Client::Handle_OP_SpawnAppearance(const EQApplicationPacket *app) } SpawnAppearance_Struct* sa = (SpawnAppearance_Struct*)app->pBuffer; + cheat_manager.ProcessSpawnApperance(sa->spawn_id, sa->type, sa->parameter); + if (sa->spawn_id != GetID()) return; @@ -13917,6 +13925,11 @@ void Client::Handle_OP_TargetCommand(const EQApplicationPacket *app) GetTarget()->IsTargeted(1); return; } + else if (cheat_manager.GetExemptStatus(Assist)) { + GetTarget()->IsTargeted(1); + cheat_manager.SetExemptStatus(Assist, false); + return; + } else if (GetTarget()->IsClient()) { //make sure this client is in our raid/group @@ -13932,6 +13945,15 @@ void Client::Handle_OP_TargetCommand(const EQApplicationPacket *app) SetTarget((Mob*)nullptr); return; } + else if (cheat_manager.GetExemptStatus(Port)) { + GetTarget()->IsTargeted(1); + return; + } + else if (cheat_manager.GetExemptStatus(Sense)) { + GetTarget()->IsTargeted(1); + cheat_manager.SetExemptStatus(Sense, false); + return; + } else if (IsXTarget(GetTarget())) { GetTarget()->IsTargeted(1); @@ -15173,3 +15195,21 @@ void Client::Handle_OP_ResetAA(const EQApplicationPacket *app) } return; } + +void Client::Handle_OP_MovementHistoryList(const EQApplicationPacket* app) { + cheat_manager.ProcessMovementHistory(app); +} + +void Client::Handle_OP_UnderWorld(const EQApplicationPacket* app) { + UnderWorld* m_UnderWorld = (UnderWorld*)app->pBuffer; + if (app->size != sizeof(UnderWorld)) + { + LogDebug("Size mismatch in OP_UnderWorld, expected {}, got [{}]", sizeof(UnderWorld), app->size); + DumpPacket(app); + return; + } + auto dist = Distance(glm::vec3(m_UnderWorld->x, m_UnderWorld->y, zone->newzone_data.underworld), glm::vec3(m_UnderWorld->x, m_UnderWorld->y, m_UnderWorld->z)); + cheat_manager.MovementCheck(glm::vec3(m_UnderWorld->x, m_UnderWorld->y, m_UnderWorld->z)); + if (m_UnderWorld->spawn_id == GetID() && dist <= 5.0f && zone->newzone_data.underworld_teleport_index != 0) + cheat_manager.SetExemptStatus(Port, true); +} diff --git a/zone/client_packet.h b/zone/client_packet.h index 3951a1761..e04d43483 100644 --- a/zone/client_packet.h +++ b/zone/client_packet.h @@ -313,3 +313,5 @@ void Handle_OP_YellForHelp(const EQApplicationPacket *app); void Handle_OP_ZoneChange(const EQApplicationPacket *app); void Handle_OP_ResetAA(const EQApplicationPacket *app); + void Handle_OP_MovementHistoryList(const EQApplicationPacket* app); + void Handle_OP_UnderWorld(const EQApplicationPacket* app); diff --git a/zone/client_process.cpp b/zone/client_process.cpp index 6106ee8ae..2739db30f 100644 --- a/zone/client_process.cpp +++ b/zone/client_process.cpp @@ -203,6 +203,8 @@ bool Client::Process() { if (IsStunned() && stunned_timer.Check()) Mob::UnStun(); + cheat_manager.ClientProcess(); + if (bardsong_timer.Check() && bardsong != 0) { //NOTE: this is kinda a heavy-handed check to make sure the mob still exists before //doing the next pulse on them... diff --git a/zone/embparser.cpp b/zone/embparser.cpp index 22e4b3234..f34208eff 100644 --- a/zone/embparser.cpp +++ b/zone/embparser.cpp @@ -120,6 +120,7 @@ const char *QuestEventSubroutines[_LargestEventID] = { "EVENT_USE_SKILL", "EVENT_COMBINE_VALIDATE", "EVENT_BOT_COMMAND", + "EVENT_WARP", "EVENT_TEST_BUFF" }; @@ -1636,6 +1637,13 @@ void PerlembParser::ExportEventVariables( ExportVar(package_name.c_str(), "langid", extradata); break; } + case EVENT_WARP: { + Seperator sep(data); + ExportVar(package_name.c_str(), "from_x", sep.arg[0]); + ExportVar(package_name.c_str(), "from_y", sep.arg[1]); + ExportVar(package_name.c_str(), "from_z", sep.arg[2]); + break; + } default: { break; diff --git a/zone/event_codes.h b/zone/event_codes.h index 5765c56ff..186b4cc80 100644 --- a/zone/event_codes.h +++ b/zone/event_codes.h @@ -88,6 +88,7 @@ typedef enum { EVENT_USE_SKILL, EVENT_COMBINE_VALIDATE, EVENT_BOT_COMMAND, + EVENT_WARP, EVENT_TEST_BUFF, _LargestEventID } QuestEventID; diff --git a/zone/lua_general.cpp b/zone/lua_general.cpp index 29c3ee0d2..c1e72890f 100644 --- a/zone/lua_general.cpp +++ b/zone/lua_general.cpp @@ -3225,6 +3225,7 @@ luabind::scope lua_register_events() { luabind::value("spawn_zone", static_cast(EVENT_SPAWN_ZONE)), luabind::value("death_zone", static_cast(EVENT_DEATH_ZONE)), luabind::value("use_skill", static_cast(EVENT_USE_SKILL)), + luabind::value("warp", static_cast(EVENT_WARP)), luabind::value("test_buff", static_cast(EVENT_TEST_BUFF)) ]; } diff --git a/zone/lua_parser.cpp b/zone/lua_parser.cpp index 1342c719e..148230550 100644 --- a/zone/lua_parser.cpp +++ b/zone/lua_parser.cpp @@ -131,6 +131,7 @@ const char *LuaEvents[_LargestEventID] = { "event_use_skill", "event_combine_validate", "event_bot_command", + "event_warp", "event_test_buff" }; @@ -217,6 +218,7 @@ LuaParser::LuaParser() { PlayerArgumentDispatch[EVENT_TEST_BUFF] = handle_test_buff; PlayerArgumentDispatch[EVENT_COMBINE_VALIDATE] = handle_player_combine_validate; PlayerArgumentDispatch[EVENT_BOT_COMMAND] = handle_player_bot_command; + PlayerArgumentDispatch[EVENT_WARP] = handle_player_warp; ItemArgumentDispatch[EVENT_ITEM_CLICK] = handle_item_click; ItemArgumentDispatch[EVENT_ITEM_CLICK_CAST] = handle_item_click; diff --git a/zone/lua_parser_events.cpp b/zone/lua_parser_events.cpp index 73acd9232..f0940b761 100644 --- a/zone/lua_parser_events.cpp +++ b/zone/lua_parser_events.cpp @@ -561,6 +561,18 @@ void handle_player_bot_command(QuestInterface* parse, lua_State* L, Client* clie lua_setfield(L, -2, "args"); } +void handle_player_warp(QuestInterface* parse, lua_State* L, Client* client, std::string data, uint32 extra_data, std::vector* extra_pointers) { + Seperator sep(data.c_str()); + lua_pushnumber(L, std::stof(sep.arg[0])); + lua_setfield(L, -2, "from_x"); + + lua_pushnumber(L, std::stof(sep.arg[1])); + lua_setfield(L, -2, "from_y"); + + lua_pushnumber(L, std::stof(sep.arg[2])); + lua_setfield(L, -2, "from_z"); +} + //Item void handle_item_click(QuestInterface *parse, lua_State* L, Client* client, EQ::ItemInstance* item, Mob *mob, std::string data, uint32 extra_data, std::vector *extra_pointers) { diff --git a/zone/lua_parser_events.h b/zone/lua_parser_events.h index 47e7883ae..64dfdb5d9 100644 --- a/zone/lua_parser_events.h +++ b/zone/lua_parser_events.h @@ -103,6 +103,8 @@ void handle_player_combine_validate(QuestInterface* parse, lua_State* L, Client* std::vector* extra_pointers); void handle_player_bot_command(QuestInterface *parse, lua_State* L, Client* client, std::string data, uint32 extra_data, std::vector *extra_pointers); +void handle_player_warp(QuestInterface* parse, lua_State* L, Client* client, std::string data, uint32 extra_data, + std::vector* extra_pointers); //Item void handle_item_click(QuestInterface *parse, lua_State* L, Client* client, EQ::ItemInstance* item, Mob *mob, std::string data, uint32 extra_data, diff --git a/zone/mob.cpp b/zone/mob.cpp index 2bbb15c30..f749e89cc 100644 --- a/zone/mob.cpp +++ b/zone/mob.cpp @@ -4657,6 +4657,7 @@ void Mob::DoKnockback(Mob *caster, uint32 pushback, uint32 pushup) { if(IsClient()) { + CastToClient()->cheat_manager.SetExemptStatus(KnockBack, true); auto outapp_push = new EQApplicationPacket(OP_ClientUpdate, sizeof(PlayerPositionUpdateServer_Struct)); PlayerPositionUpdateServer_Struct* spu = (PlayerPositionUpdateServer_Struct*)outapp_push->pBuffer; diff --git a/zone/spells.cpp b/zone/spells.cpp index f5a756f79..567d239c6 100644 --- a/zone/spells.cpp +++ b/zone/spells.cpp @@ -2743,6 +2743,22 @@ void Mob::BardPulse(uint16 spell_id, Mob *caster) { action->effect_flag = 4; + if (spells[spell_id].pushback != 0.0f || spells[spell_id].pushup != 0.0f) + { + if (IsClient()) + { + if (!IsBuffSpell(spell_id)) + { + CastToClient()->cheat_manager.SetExemptStatus(KnockBack, true); + } + } + } + + if (IsClient() && IsEffectInSpell(spell_id, SE_ShadowStep)) + { + CastToClient()->cheat_manager.SetExemptStatus(ShadowStep, true); + } + if(!IsEffectInSpell(spell_id, SE_BindAffinity)) { CastToClient()->QueuePacket(packet); @@ -4028,7 +4044,14 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, bool reflect, bool use_r if(spells[spell_id].pushback != 0.0f || spells[spell_id].pushup != 0.0f) { - if (RuleB(Spells, NPCSpellPush) && !spelltar->IsRooted() && spelltar->ForcedMovement == 0) { + if (spelltar->IsClient()) + { + if (!IsBuffSpell(spell_id)) + { + spelltar->CastToClient()->cheat_manager.SetExemptStatus(KnockBack, true); + } + } + else if (RuleB(Spells, NPCSpellPush) && !spelltar->IsRooted() && spelltar->ForcedMovement == 0) { spelltar->m_Delta.x += action->force * g_Math.FastSin(action->hit_heading); spelltar->m_Delta.y += action->force * g_Math.FastCos(action->hit_heading); spelltar->m_Delta.z += action->hit_pitch; @@ -4036,6 +4059,11 @@ bool Mob::SpellOnTarget(uint16 spell_id, Mob *spelltar, bool reflect, bool use_r } } + if (spelltar->IsClient() && IsEffectInSpell(spell_id, SE_ShadowStep)) + { + spelltar->CastToClient()->cheat_manager.SetExemptStatus(ShadowStep, true); + } + if(!IsEffectInSpell(spell_id, SE_BindAffinity)) { if(spelltar != this && spelltar->IsClient()) // send to target diff --git a/zone/zone.cpp b/zone/zone.cpp index 3dceb1250..aae61ad23 100755 --- a/zone/zone.cpp +++ b/zone/zone.cpp @@ -1911,7 +1911,11 @@ ZonePoint* Zone::GetClosestZonePoint(const glm::vec3& location, uint32 to, Clien // this shouldn't open up any exploits since those situations are detected later on if ((zone->HasWaterMap() && !zone->watermap->InZoneLine(glm::vec3(client->GetPosition()))) || (!zone->HasWaterMap() && closest_dist > 400.0f && closest_dist < max_distance2)) { - //TODO cheat detection + if (client) { + if (!client->cheat_manager.GetExemptStatus(Port)) { + client->cheat_manager.CheatDetected(MQZoneUnknownDest, location); + } + } LogInfo("WARNING: Closest zone point for zone id [{}] is [{}], you might need to update your zone_points table if you dont arrive at the right spot", to, closest_dist); LogInfo(". [{}]", to_string(location).c_str()); } diff --git a/zone/zoning.cpp b/zone/zoning.cpp index 0d088aadf..300e9d1bf 100644 --- a/zone/zoning.cpp +++ b/zone/zoning.cpp @@ -99,9 +99,14 @@ void Client::Handle_OP_ZoneChange(const EQApplicationPacket *app) { //unable to find a zone point... is there anything else //that can be a valid un-zolicited zone request? - //Todo cheat detection Message(Chat::Red, "Invalid unsolicited zone request."); LogError("Zoning [{}]: Invalid unsolicited zone request to zone id [{}]", GetName(), target_zone_id); + if (GetBindZoneID() == target_zone_id) { + cheat_manager.CheatDetected(MQGate, glm::vec3(zc->x, zc->y, zc->z)); + } + else { + cheat_manager.CheatDetected(MQZone, glm::vec3(zc->x, zc->y, zc->z)); + } SendZoneCancel(zc); return; } @@ -134,7 +139,12 @@ void Client::Handle_OP_ZoneChange(const EQApplicationPacket *app) { //then we assume this is invalid. if(!zone_point || zone_point->target_zone_id != target_zone_id) { LogError("Zoning [{}]: Invalid unsolicited zone request to zone id [{}]", GetName(), target_zone_id); - //todo cheat detection + if (GetBindZoneID() == target_zone_id) { + cheat_manager.CheatDetected(MQGate, glm::vec3(zc->x, zc->y, zc->z)); + } + else { + cheat_manager.CheatDetected(MQZone, glm::vec3(zc->x, zc->y, zc->z)); + } SendZoneCancel(zc); return; } @@ -282,7 +292,12 @@ void Client::Handle_OP_ZoneChange(const EQApplicationPacket *app) { //for now, there are no other cases... //could not find a valid reason for them to be zoning, stop it. - //todo cheat detection + if (GetBindZoneID() == target_zone_id) { + cheat_manager.CheatDetected(MQGate, glm::vec3(zc->x, zc->y, zc->z)); + } + else { + cheat_manager.CheatDetected(MQZone, glm::vec3(zc->x, zc->y, zc->z)); + } LogError("Zoning [{}]: Invalid unsolicited zone request to zone id [{}]. Not near a zone point", GetName(), target_zone_name); SendZoneCancel(zc); return; @@ -379,6 +394,7 @@ void Client::Handle_OP_ZoneChange(const EQApplicationPacket *app) { void Client::SendZoneCancel(ZoneChange_Struct *zc) { //effectively zone them right back to where they were //unless we find a better way to stop the zoning process. + cheat_manager.SetExemptStatus(Port, true); EQApplicationPacket *outapp = nullptr; outapp = new EQApplicationPacket(OP_ZoneChange, sizeof(ZoneChange_Struct)); ZoneChange_Struct *zc2 = (ZoneChange_Struct*)outapp->pBuffer; @@ -397,7 +413,7 @@ void Client::SendZoneCancel(ZoneChange_Struct *zc) { void Client::SendZoneError(ZoneChange_Struct *zc, int8 err) { LogError("Zone [{}] is not available because target wasn't found or character insufficent level", zc->zoneID); - + cheat_manager.SetExemptStatus(Port, true); EQApplicationPacket *outapp = nullptr; outapp = new EQApplicationPacket(OP_ZoneChange, sizeof(ZoneChange_Struct)); ZoneChange_Struct *zc2 = (ZoneChange_Struct*)outapp->pBuffer; @@ -667,6 +683,8 @@ void Client::ZonePC(uint32 zoneID, uint32 instance_id, float x, float y, float z pShortZoneName = ZoneName(zoneID); content_db.GetZoneLongName(pShortZoneName, &pZoneName); + cheat_manager.SetExemptStatus(Port, true); + if(!pZoneName) { Message(Chat::Red, "Invalid zone number specified"); safe_delete_array(pZoneName);