From e8bcfe06f0e1c347b32d701b95a8df3dea5a9527 Mon Sep 17 00:00:00 2001 From: server Date: Thu, 16 Apr 2026 11:18:39 +0200 Subject: [PATCH] Add server-side anti-cheat evidence scoring --- src/game/battle.cpp | 15 ++++++- src/game/char.cpp | 65 ++++++++++++++++++++++++++++++ src/game/char.h | 6 +++ src/game/char_battle.cpp | 4 +- src/game/input_main.cpp | 87 +++++++++++++++++++++++++++++++++------- 5 files changed, 160 insertions(+), 17 deletions(-) diff --git a/src/game/battle.cpp b/src/game/battle.cpp index 7d2db7a..3451378 100644 --- a/src/game/battle.cpp +++ b/src/game/battle.cpp @@ -801,6 +801,13 @@ bool IS_SPEED_HACK(LPCHARACTER ch, LPCHARACTER victim, DWORD current_time) if (current_time - ch->m_kAttackLog.dwTime < GET_ATTACK_SPEED(ch)) { INCREASE_SPEED_HACK_COUNT(ch); + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "delta=%u limit=%u victim=%u count=%d", + current_time - ch->m_kAttackLog.dwTime, + GET_ATTACK_SPEED(ch), + victim->GetVID(), + ch->m_speed_hack_count); + ch->RecordAntiCheatViolation("ATTACK_SPEED", 6, szDetail); if (test_server) { @@ -830,6 +837,13 @@ bool IS_SPEED_HACK(LPCHARACTER ch, LPCHARACTER victim, DWORD current_time) if (current_time - victim->m_AttackedLog.dwAttackedTime < GET_ATTACK_SPEED(ch)) { INCREASE_SPEED_HACK_COUNT(ch); + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "delta=%u limit=%u victim=%u count=%d", + current_time - victim->m_AttackedLog.dwAttackedTime, + GET_ATTACK_SPEED(ch), + victim->GetVID(), + ch->m_speed_hack_count); + ch->RecordAntiCheatViolation("ATTACK_SPEED", 6, szDetail); if (test_server) { @@ -854,4 +868,3 @@ bool IS_SPEED_HACK(LPCHARACTER ch, LPCHARACTER victim, DWORD current_time) return false; } - diff --git a/src/game/char.cpp b/src/game/char.cpp index 7969ea1..fe4a686 100644 --- a/src/game/char.cpp +++ b/src/game/char.cpp @@ -347,6 +347,9 @@ void CHARACTER::Initialize() m_dwLastComboTime = 0; m_bComboIndex = 0; m_iComboHackCount = 0; + m_iAntiCheatScore = 0; + m_dwLastAntiCheatScoreTime = 0; + m_dwLastAntiCheatPersistTime = 0; m_dwSkipComboAttackByTime = 0; m_dwMountTime = 0; @@ -7238,6 +7241,68 @@ void CHARACTER::ResetComboHackCount() m_iComboHackCount = 0; } +namespace +{ + constexpr int kAntiCheatMaxScore = 1000; + constexpr int kAntiCheatDisconnectScore = 100; + constexpr DWORD kAntiCheatDecayIntervalMs = 15000; + constexpr DWORD kAntiCheatPersistIntervalMs = 5000; +} + +void CHARACTER::DecayAntiCheatScore(DWORD dwNow) +{ + if (0 == m_dwLastAntiCheatScoreTime) + { + m_dwLastAntiCheatScoreTime = dwNow; + return; + } + + const DWORD dwElapsed = dwNow - m_dwLastAntiCheatScoreTime; + if (dwElapsed < kAntiCheatDecayIntervalMs) + return; + + const int iDecaySteps = dwElapsed / kAntiCheatDecayIntervalMs; + m_iAntiCheatScore = MAX(0, m_iAntiCheatScore - iDecaySteps); + m_dwLastAntiCheatScoreTime += iDecaySteps * kAntiCheatDecayIntervalMs; +} + +void CHARACTER::RecordAntiCheatViolation(const char* category, int score, const char* detail, bool forcePersist) +{ + const DWORD dwNow = get_dword_time(); + DecayAntiCheatScore(dwNow); + + if (score <= 0) + score = 1; + + m_iAntiCheatScore = MIN(kAntiCheatMaxScore, m_iAntiCheatScore + score); + + char szReason[512]; + snprintf(szReason, + sizeof(szReason), + "ANTI_CHEAT[%s] score=%d map=%ld pos=(%ld,%ld) detail=%s", + category ? category : "UNKNOWN", + m_iAntiCheatScore, + static_cast(GetMapIndex()), + static_cast(GetX()), + static_cast(GetY()), + detail ? detail : "-"); + + sys_log(0, "%s pid=%u name=%s", szReason, GetPlayerID(), GetName()); + + const bool shouldPersist = forcePersist || (GetDesc() && dwNow - m_dwLastAntiCheatPersistTime >= kAntiCheatPersistIntervalMs); + if (shouldPersist && GetDesc()) + { + LogManager::instance().HackLog(szReason, this); + m_dwLastAntiCheatPersistTime = dwNow; + } + + if (GetDesc() && m_iAntiCheatScore >= kAntiCheatDisconnectScore) + { + if (GetDesc()->DelayedDisconnect(number(3, 8))) + sys_log(0, "ANTI_CHEAT_DISCONNECT: %s score=%d", GetName(), m_iAntiCheatScore); + } +} + void CHARACTER::SkipComboAttackByTime(int interval) { m_dwSkipComboAttackByTime = get_dword_time() + interval; diff --git a/src/game/char.h b/src/game/char.h index be31b9f..0c56987 100644 --- a/src/game/char.h +++ b/src/game/char.h @@ -1304,14 +1304,20 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider void ResetComboHackCount(); void SkipComboAttackByTime(int interval); DWORD GetSkipComboAttackByTime() const; + void RecordAntiCheatViolation(const char* category, int score, const char* detail = nullptr, bool forcePersist = false); + int GetAntiCheatScore() const { return m_iAntiCheatScore; } protected: + void DecayAntiCheatScore(DWORD dwNow); BYTE m_bComboSequence; DWORD m_dwLastComboTime; int m_iValidComboInterval; BYTE m_bComboIndex; int m_iComboHackCount; DWORD m_dwSkipComboAttackByTime; + int m_iAntiCheatScore; + DWORD m_dwLastAntiCheatScoreTime; + DWORD m_dwLastAntiCheatPersistTime; protected: void UpdateAggrPointEx(LPCHARACTER ch, EDamageType type, int dam, TBattleInfo & info); diff --git a/src/game/char_battle.cpp b/src/game/char_battle.cpp index c9fd26b..8187bac 100644 --- a/src/game/char_battle.cpp +++ b/src/game/char_battle.cpp @@ -254,6 +254,9 @@ bool CHARACTER::Attack(LPCHARACTER pkVictim, BYTE bType) { if (dwCurrentTime - m_dwLastSkillTime > 1500) { + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "skill=%u delta=%u victim=%s", bType, dwCurrentTime - m_dwLastSkillTime, pkVictim ? pkVictim->GetName() : "-"); + RecordAntiCheatViolation("SKILL_TERM", 10, szDetail, true); sys_log(1, "HACK: Too long skill using term. Name(%s) PID(%u) delta(%u)", GetName(), GetPlayerID(), (dwCurrentTime - m_dwLastSkillTime)); return false; @@ -3755,4 +3758,3 @@ void CHARACTER::ChangeVictimByAggro(int iNewAggro, LPCHARACTER pNewVictim) } } } - diff --git a/src/game/input_main.cpp b/src/game/input_main.cpp index df4ace2..b758d6d 100644 --- a/src/game/input_main.cpp +++ b/src/game/input_main.cpp @@ -1539,7 +1539,12 @@ bool CheckComboHack(LPCHARACTER ch, BYTE bArg, DWORD dwTime, bool CheckSpeedHack { // 말에 타거나 내렸을 때 1.5초간 공격은 핵으로 간주하지 않되 공격력은 없게 하는 처리 if (get_dword_time() - ch->GetLastMountTime() > 1500) + { + char szDetail[128]; + snprintf(szDetail, sizeof(szDetail), "arg=%u interval=%d valid=%d scalar=%d", bArg, ComboInterval, ch->GetValidComboInterval(), HackScalar); + ch->RecordAntiCheatViolation("COMBO", MIN(20, 2 + HackScalar), szDetail, HackScalar >= 5); ch->IncreaseComboHackCount(1 + HackScalar); + } ch->SkipComboAttackByTime(ch->GetValidComboInterval()); } @@ -1556,6 +1561,9 @@ void CInputMain::Move(LPCHARACTER ch, const char * data) if (pinfo->bFunc >= FUNC_MAX_NUM && !(pinfo->bFunc & 0x80)) { + char szDetail[64]; + snprintf(szDetail, sizeof(szDetail), "func=%u arg=%u", pinfo->bFunc, pinfo->bArg); + ch->RecordAntiCheatViolation("MOVE_TYPE", 12, szDetail, true); sys_err("invalid move type: %s", ch->GetName()); return; } @@ -1580,13 +1588,17 @@ void CInputMain::Move(LPCHARACTER ch, const char * data) if (((false == ch->IsRiding() && fDist > 25) || fDist > 40) && OXEVENT_MAP_INDEX != ch->GetMapIndex()) { - if( false == LC_IsEurope() ) - { - const PIXEL_POSITION & warpPos = ch->GetWarpPosition(); - - if (warpPos.x == 0 && warpPos.y == 0) - LogManager::instance().HackLog("Teleport", ch); // 부정확할 수 있음 - } + char szDetail[160]; + snprintf(szDetail, + sizeof(szDetail), + "dist=%.1f riding=%d cur=(%ld,%ld) dst=(%ld,%ld)", + fDist, + ch->IsRiding() ? 1 : 0, + static_cast(ch->GetX()), + static_cast(ch->GetY()), + static_cast(pinfo->lX), + static_cast(pinfo->lY)); + ch->RecordAntiCheatViolation("MOVE_DISTANCE", 15, szDetail, true); sys_log(0, "MOVE: %s trying to move too far (dist: %.1fm) Riding(%d)", ch->GetName(), fDist, ch->IsRiding()); @@ -1659,7 +1671,7 @@ void CInputMain::Move(LPCHARACTER ch, const char * data) char szBuf[256]; snprintf(szBuf, sizeof(szBuf), "SKILL_HACK: name=%s, job=%d, group=%d, motion=%d", name, job, group, motion); - LogManager::instance().HackLog(szBuf, ch->GetDesc()->GetAccountTable().login, ch->GetName(), ch->GetDesc()->GetHostName()); + ch->RecordAntiCheatViolation("SKILL_MOTION", 40, szBuf, true); sys_log(0, "%s", szBuf); if (test_server) @@ -1869,6 +1881,9 @@ int CInputMain::SyncPosition(LPCHARACTER ch, const char * c_pcData, size_t uiByt if( iCount > nCountLimit ) { //LogManager::instance().HackLog( "SYNC_POSITION_HACK", ch ); + char szDetail[64]; + snprintf(szDetail, sizeof(szDetail), "count=%d limit=%d", iCount, nCountLimit); + ch->RecordAntiCheatViolation("SYNC_POSITION_COUNT", 3, szDetail); sys_err( "Too many SyncPosition Count(%d) from Name(%s)", iCount, ch->GetName() ); //ch->GetDesc()->SetPhase(PHASE_CLOSE); //return -1; @@ -1918,12 +1933,29 @@ int CInputMain::SyncPosition(LPCHARACTER ch, const char * c_pcData, size_t uiByt if (ch->GetSyncHackCount() < g_iSyncHackLimitCount) { ch->SetSyncHackCount(ch->GetSyncHackCount() + 1); + char szDetail[160]; + snprintf(szDetail, + sizeof(szDetail), + "owner_dist=%.1f limit=%.1f victim=%s count=%d", + fDistWithSyncOwner, + fLimitDistWithSyncOwner, + victim->GetName(), + ch->GetSyncHackCount()); + ch->RecordAntiCheatViolation("SYNC_OWNER_DISTANCE", 8, szDetail); continue; } else { - LogManager::instance().HackLog( "SYNC_POSITION_HACK", ch ); - + char szDetail[192]; + snprintf(szDetail, + sizeof(szDetail), + "owner_dist=%.1f limit=%.1f victim=%s sync=(%ld,%ld)", + fDistWithSyncOwner, + fLimitDistWithSyncOwner, + victim->GetName(), + static_cast(e->lX), + static_cast(e->lY)); + ch->RecordAntiCheatViolation("SYNC_OWNER_DISTANCE", 35, szDetail, true); sys_err( "Too far SyncPosition DistanceWithSyncOwner(%f)(%s) from Name(%s) CH(%d,%d) VICTIM(%d,%d) SYNC(%d,%d)", fDistWithSyncOwner, victim->GetName(), ch->GetName(), ch->GetX(), ch->GetY(), victim->GetX(), victim->GetY(), e->lX, e->lY ); @@ -1947,12 +1979,29 @@ int CInputMain::SyncPosition(LPCHARACTER ch, const char * c_pcData, size_t uiByt if (ch->GetSyncHackCount() < g_iSyncHackLimitCount) { ch->SetSyncHackCount(ch->GetSyncHackCount() + 1); + char szDetail[160]; + snprintf(szDetail, + sizeof(szDetail), + "interval_ms=%ld limit_us=%ld victim=%s count=%d", + tvDiff->tv_sec * 1000 + tvDiff->tv_usec / 1000, + static_cast(g_lValidSyncInterval), + victim->GetName(), + ch->GetSyncHackCount()); + ch->RecordAntiCheatViolation("SYNC_INTERVAL", 8, szDetail); continue; } else { - LogManager::instance().HackLog( "SYNC_POSITION_HACK", ch ); - + char szDetail[192]; + snprintf(szDetail, + sizeof(szDetail), + "interval_ms=%ld limit_us=%ld victim=%s sync=(%ld,%ld)", + tvDiff->tv_sec * 1000 + tvDiff->tv_usec / 1000, + static_cast(g_lValidSyncInterval), + victim->GetName(), + static_cast(e->lX), + static_cast(e->lY)); + ch->RecordAntiCheatViolation("SYNC_INTERVAL", 35, szDetail, true); sys_err( "Too often SyncPosition Interval(%ldms)(%s) from Name(%s) VICTIM(%d,%d) SYNC(%d,%d)", tvDiff->tv_sec * 1000 + tvDiff->tv_usec / 1000, victim->GetName(), ch->GetName(), victim->GetX(), victim->GetY(), e->lX, e->lY ); @@ -1964,8 +2013,17 @@ int CInputMain::SyncPosition(LPCHARACTER ch, const char * c_pcData, size_t uiByt } else if( fDist > 25.0f ) { - LogManager::instance().HackLog( "SYNC_POSITION_HACK", ch ); - + char szDetail[192]; + snprintf(szDetail, + sizeof(szDetail), + "dist=%.1f victim=%s cur=(%ld,%ld) sync=(%ld,%ld)", + fDist, + victim->GetName(), + static_cast(victim->GetX()), + static_cast(victim->GetY()), + static_cast(e->lX), + static_cast(e->lY)); + ch->RecordAntiCheatViolation("SYNC_DISTANCE", 30, szDetail, true); sys_err( "Too far SyncPosition Distance(%f)(%s) from Name(%s) CH(%d,%d) VICTIM(%d,%d) SYNC(%d,%d)", fDist, victim->GetName(), ch->GetName(), ch->GetX(), ch->GetY(), victim->GetX(), victim->GetY(), e->lX, e->lY ); @@ -3492,4 +3550,3 @@ int CInputDead::Analyze(LPDESC d, uint16_t wHeader, const char * c_pData) return (this->*(it->second.handler))(d, c_pData); } -