Add server-side anti-cheat evidence scoring
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled

This commit is contained in:
server
2026-04-16 11:18:39 +02:00
parent b4ee8aa5d7
commit e8bcfe06f0
5 changed files with 160 additions and 17 deletions

View File

@@ -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;
}

View File

@@ -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<long>(GetMapIndex()),
static_cast<long>(GetX()),
static_cast<long>(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;

View File

@@ -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);

View File

@@ -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)
}
}
}

View File

@@ -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<long>(ch->GetX()),
static_cast<long>(ch->GetY()),
static_cast<long>(pinfo->lX),
static_cast<long>(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<long>(e->lX),
static_cast<long>(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<long>(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<long>(g_lValidSyncInterval),
victim->GetName(),
static_cast<long>(e->lX),
static_cast<long>(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<long>(victim->GetX()),
static_cast<long>(victim->GetY()),
static_cast<long>(e->lX),
static_cast<long>(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);
}