Anti-Cheat-architectuur: serverautoriteit en gedragsanalyse
Valsspelen in videogames is een probleem van $30 miljard per jaar voor de game-industrie. Niet het draait allemaal om eerlijkheid: valsspelers vernietigen de ervaring van andere spelers, vergroten churn, verminder de inkomsten en beschadig de reputatie van het spel. Een onderzoek uit 2024 heeft dat wel gedaan ontdekte dat 77% van de spelers een multiplayer-titel verliet vanwege valsspelers.
Traditionele anti-cheat-oplossingen - Valve's VAC, Easy Anti-Cheat, BattlEye - zijn gebaseerd op vooral over Detectie aan de clientzijde: een driver die de processen van de besturingssysteem om softwarecheats te detecteren. Deze aanpak heeft een fundamenteel probleem: en een kat-en-muisspel dat valsspelers consequent winnen op aangepaste hardware, hypervisor en kernel-bypass. De moderne tegenmaatregel en deserver-gezaghebbende architectuur gecombineerd met gedragsanalyse ML.
In dit artikel zullen we een complete anti-cheat-architectuur verkennen: vanaf de gezaghebbende server valideert elke actie, tot statistische validatie van uitschieters, tot op ML gebaseerde systemen transformator voor gedragsdetectie (96,94% nauwkeurigheid, 98,36% AUC in zoekopdrachten in 2025).
Wat je gaat leren
- Soorten cheats: speedhack, aim-bot, wallhack, ESP, economy-exploit
- Server-gezaghebbende architectuur: wat te delegeren aan de klant en wat niet
- Validatie aan de serverzijde: fysica, botsingsdetectie, zichtlijn
- Statistische uitbijterdetectie: Z-score, k-sigma voor doelanalyse
- Gedragsanalyse ML: feature-engineering voor anti-cheat
- Transformer-gebaseerde cheatdetectie (AntiCheatPT-aanpak)
- Herhalingsanalyse: post-hoc detectie via telemetrie
- Vals positief management: onschuldige spelers beschermen
1. Cheat-taxonomie: waar je tegen vecht
Voordat we verdedigingsmechanismen ontwerpen, is het essentieel om te begrijpen wat moet worden tegengegaan. De cheats zijn verdeeld in twee macrocategorieën: macrocategorieën die het gedrag van de cliënt wijzigen (invoermanipulatie) en degenen die kwetsbaarheden in het protocol of de spellogica misbruiken (protocolexploitatie).
Taxonomie Cheats en tegenmaatregelen
| Bedrog | Beschrijving | Detectie | Preventie |
|---|---|---|---|
| Snelheidhack | Verander de systeemklok om sneller te bewegen | Snelheidscontroleserver | Server-gezaghebbende natuurkunde |
| Teleport-hack | Stel een willekeurige positie in, sla bewegingsvalidatie over | Positie deltacontrole | Server-gezaghebbende positie |
| Richt Bot | Richt automatisch op spelers met bovenmenselijke nauwkeurigheid | Statistische doelanalyse | ML-gedragsdetectie |
| Muurhack / ESP | Hij ziet spelers door muren heen | Frustum ruimen aan de serverzijde | Stuur geen onzichtbare vijandelijke locaties |
| Geen terugslag | Elimineert terugslag om met consistente precisie te schieten | Schotpatroonanalyse | Terugslagsimulatie aan de serverzijde |
| Economie uitbuiting | Dubbele valuta of items via racevoorwaarde | Transactiecontrole | Idempotente transacties + tariefbeperking |
| Pakketmanipulatie | Wijzig netwerkpakketten om de spelstatus te wijzigen | Berichtvalidatie | DTLS/TLS + schemavalidatie |
2. Server-gezaghebbende architectuur: de basis van beveiliging
Het belangrijkste principe van moderne anti-cheat is: de server is de bron van absolute waarheid. De klant verzendt alleen de invoer (wat de speler wil doen), nooit de resultaten (wat is er gebeurd). De server berekent de spelstatus en communiceert deze met de clients. Dit elimineert categorisch snelheidshacks, teleporthacks, economische exploits en packet wall-hacks.
// Server-Authoritative Game Loop - Go
// Il server calcola TUTTO: posizione, danno, risultati
type AuthoritativeServer struct {
players map[string]*PlayerState
world *WorldState
physics *PhysicsEngine // Server-side physics simulation
lineOfSight *LOSCalculator // Calcolo visibilità server-side
}
// ProcessInput: l'unica cosa che il client invia e l'input
// Il server valida e calcola il risultato
func (s *AuthoritativeServer) ProcessInput(playerID string, input PlayerInput) *GameStateUpdate {
player, ok := s.players[playerID]
if !ok {
return nil
}
// === VALIDAZIONE INPUT ===
// 1. Rate limiting: un player non può inviare input più di N/tick
if !s.rateLimiter.Allow(playerID) {
return &GameStateUpdate{Error: "input_rate_exceeded"}
}
// 2. Validazione movimento: fisica server-side
if input.Type == InputTypeMove {
newPos := player.Position.Add(input.MoveDelta)
// Verifica velocità massima (impossibile con speed hack se server calcola)
maxSpeed := player.GetMaxSpeed() // Dipende da buff, terreno, etc.
actualSpeed := input.MoveDelta.Length() / s.tickDeltaTime
if actualSpeed > maxSpeed*1.1 { // 10% tolerance per jitter di rete
s.flagSuspicious(playerID, "speed_violation",
fmt.Sprintf("speed=%.2f max=%.2f", actualSpeed, maxSpeed))
return &GameStateUpdate{Position: player.Position} // Ignora il movimento
}
// Verifica collisioni server-side
if !s.world.IsPositionValid(newPos, player.Size) {
s.flagSuspicious(playerID, "wall_clip_attempt",
fmt.Sprintf("pos=%v", newPos))
return &GameStateUpdate{Position: player.Position}
}
// Aggiorna posizione SOLO dopo validazione
player.Position = newPos
}
// 3. Validazione attacco: server-side hit detection
if input.Type == InputTypeShoot {
shot := s.validateShot(player, input)
if shot != nil {
s.applyDamage(shot)
}
}
// 4. Visibility culling: non invia posizioni di nemici non visibili
// Previene wall hack via packet sniffing
visiblePlayers := s.lineOfSight.GetVisiblePlayers(player)
return &GameStateUpdate{
Position: player.Position,
VisiblePlayers: visiblePlayers, // Solo chi il player PUO vedere
// Non include mai posizioni di giocatori non visibili!
}
}
// validateShot: hit detection server-side con lag compensation
func (s *AuthoritativeServer) validateShot(shooter *PlayerState, input PlayerInput) *ShotResult {
// Lag compensation: ricostruisci lo stato del mondo al momento dello sparo lato client
// Il client ha inviato l'input con timestamp: usa quello per trovare lo stato server passato
pastState := s.history.GetStateAt(input.ClientTimestamp - shooter.Latency)
// Verifica line-of-sight al momento dello sparo
if !s.lineOfSight.HasLoS(shooter.Position, input.TargetPosition, pastState) {
return nil // Muro tra shooter e target: no shot
}
// Verifica distanza massima dell'arma
weapon := shooter.GetEquippedWeapon()
dist := shooter.Position.DistanceTo(input.TargetPosition)
if dist > weapon.MaxRange {
return nil // Fuori portata
}
// Verifica che il target esista e sia effettivamente alla posizione indicata
// Con tolerance per il lag (lagCompensation)
target := pastState.GetPlayerAt(input.TargetPosition, lagCompensationRadius(shooter.Latency))
if target == nil {
return nil // Nessun target in quella posizione
}
return &ShotResult{
ShooterID: shooter.ID,
TargetID: target.ID,
Damage: weapon.CalculateDamage(dist),
Headshot: input.IsHeadshot && s.validateHeadshot(shooter, target, pastState),
}
}
3. Doelbotdetectie: statistische analyse
Een richtbot produceert richtpatronen die statistisch gezien onmogelijk zijn voor een mens: rotatiehoeken instant, snelle shots met 100% precisie, perfecte tracking. Detectie is gebaseerd op analyse statistieken van muis-/stickbewegingen in de loop van de tijd, waarbij de speler wordt vergeleken met de verdeling van de bevolking.
// aim_analysis.go - Statistical aim bot detection
package anticheat
import (
"math"
"time"
)
// AimSample: campione di movimento del mouse/stick in un singolo frame
type AimSample struct {
DeltaYaw float64 // Angolo orizzontale (gradi/frame)
DeltaPitch float64 // Angolo verticale
OnTarget bool // Se punta verso un nemico
SnapToTarget float64 // Distanza di snap verso il target più vicino
Timestamp time.Time
}
// PlayerAimProfile: profilo cumulativo dei movimenti di mira
type PlayerAimProfile struct {
PlayerID string
Samples []AimSample
SnapRates []float64 // Storico snap-to-target rates
FlickAngles []float64 // Angoli dei flick shots
}
// AnalyzeAim: restituisce un aim suspicion score (0.0 - 1.0)
func AnalyzeAim(profile *PlayerAimProfile) AimAnalysisResult {
if len(profile.Samples) < 100 {
return AimAnalysisResult{Score: 0, Insufficient: true}
}
// Feature 1: Snap rate analysis
// Un aimbot "snappa" al target con velocità sovrumana
snapsToTarget := 0
for _, s := range profile.Samples {
if s.OnTarget && s.SnapToTarget > 50 { // 50 gradi snap in un frame = impossibile
snapsToTarget++
}
}
snapRate := float64(snapsToTarget) / float64(len(profile.Samples))
// Feature 2: Micro-correction analysis
// Gli aimbot mostrano pattern di micro-correzione innaturali dopo ogni sparo
corrections := extractMicroCorrections(profile.Samples)
correctionMean := mean(corrections)
correctionStd := stddev(corrections, correctionMean)
// Feature 3: Jitter analysis
// Il mouse umano ha jitter naturale. Zero jitter = aimbot
jitter := calculateJitter(profile.Samples)
humanJitterRange := [2]float64{0.3, 3.0} // Range tipico umano (gradi/frame)
// Feature 4: FOV tracking efficiency
// Aimbot = efficienza quasi perfetta nel FOV del target
trackingEfficiency := calculateTrackingEfficiency(profile.Samples)
// Calcola score combinato (threshold empirici da dati reali)
score := 0.0
if snapRate > 0.05 { // > 5% snap shots = sospetto
score += 0.4 * math.Min(snapRate/0.05, 1.0)
}
if jitter < humanJitterRange[0] { // Jitter troppo basso = aimbot
score += 0.3 * (1.0 - jitter/humanJitterRange[0])
}
if trackingEfficiency > 0.92 { // > 92% tracking efficiency = sovrumano
score += 0.3 * math.Min((trackingEfficiency-0.92)/0.08, 1.0)
}
return AimAnalysisResult{
Score: score,
SnapRate: snapRate,
CorrectionStd: correctionStd,
Jitter: jitter,
TrackingEfficiency: trackingEfficiency,
Suspicious: score > 0.7,
}
}
func mean(data []float64) float64 {
sum := 0.0
for _, v := range data { sum += v }
return sum / float64(len(data))
}
func stddev(data []float64, m float64) float64 {
variance := 0.0
for _, v := range data { variance += (v - m) * (v - m) }
return math.Sqrt(variance / float64(len(data)))
}
4. Machine Learning: Transformers voor gedragsdetectie
Statistische analyse legt grovere cheats vast, maar geavanceerde cheats (bijvoorbeeld richtbots met jitter kunstmatig) vereisen ML-benaderingen. Het meest recente onderzoek (AntiCheatPT, 2025) laat zien hoe i Transformers toegepast op spelactiereeksen bereiken een nauwkeurigheid van 96,94% en AUC van 98,36% bij detectie, beter dan LSTM en traditioneel CNN.
// Feature engineering per ML anti-cheat
// Estrae feature da una finestra temporale di azioni di gioco
from typing import List, Dict
import numpy as np
from dataclasses import dataclass
@dataclass
class GameAction:
timestamp: float
action_type: str # "move", "shoot", "reload", "ability"
delta_x: float # Movimento mouse X
delta_y: float # Movimento mouse Y
aim_x: float # Angolo mira
aim_y: float
on_target: bool # True se mira verso un nemico
result: str # "hit", "miss", "kill"
def extract_features(actions: List[GameAction], window_size: int = 100) -> np.ndarray:
"""
Estrae feature dalla finestra di azioni per classificazione ML.
Output: array di shape (window_size, feature_dim) per Transformer
"""
features = []
for i in range(min(len(actions), window_size)):
a = actions[i]
# Feature cinematiche (movimento)
aim_speed = np.sqrt(a.delta_x**2 + a.delta_y**2)
aim_accel = 0.0
if i > 0:
prev_speed = np.sqrt(actions[i-1].delta_x**2 + actions[i-1].delta_y**2)
dt = a.timestamp - actions[i-1].timestamp
aim_accel = (aim_speed - prev_speed) / max(dt, 0.001)
# Feature di target acquisition
snap_magnitude = 0.0
if a.on_target and i > 0 and not actions[i-1].on_target:
snap_magnitude = aim_speed # Velocita di snap al target
# Feature di shooting behavior
is_shoot = 1.0 if a.action_type == "shoot" else 0.0
is_hit = 1.0 if a.result == "hit" else 0.0
is_kill = 1.0 if a.result == "kill" else 0.0
# Feature inter-azione
time_since_last_shoot = 0.0
for j in range(i-1, max(0, i-10), -1):
if actions[j].action_type == "shoot":
time_since_last_shoot = a.timestamp - actions[j].timestamp
break
feature_vector = np.array([
aim_speed, # Velocita di mira
aim_accel, # Accelerazione mira
a.delta_x, # Movimento X raw
a.delta_y, # Movimento Y raw
snap_magnitude, # Magnitudine snap
float(a.on_target), # On target flag
is_shoot, # E uno sparo?
is_hit, # Ha colpito?
is_kill, # Ha ucciso?
time_since_last_shoot, # Tempo dall'ultimo sparo
])
features.append(feature_vector)
# Padding se la finestra e più corta di window_size
while len(features) < window_size:
features.append(np.zeros(10))
return np.array(features, dtype=np.float32)
# Modello Transformer per classificazione comportamentale (PyTorch)
import torch
import torch.nn as nn
class AntiCheatTransformer(nn.Module):
def __init__(self, feature_dim=10, d_model=64, nhead=4, num_layers=3, window_size=100):
super().__init__()
self.input_projection = nn.Linear(feature_dim, d_model)
self.positional_encoding = nn.Embedding(window_size, d_model)
encoder_layer = nn.TransformerEncoderLayer(
d_model=d_model, nhead=nhead, dim_feedforward=256,
dropout=0.1, batch_first=True
)
self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers)
self.classifier = nn.Sequential(
nn.Linear(d_model, 32),
nn.ReLU(),
nn.Dropout(0.1),
nn.Linear(32, 2) # 2 classi: legittimo, cheater
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
# x: (batch, window_size, feature_dim)
batch_size, seq_len, _ = x.shape
positions = torch.arange(seq_len, device=x.device).unsqueeze(0).expand(batch_size, -1)
x = self.input_projection(x) + self.positional_encoding(positions)
x = self.transformer(x)
# Usa il CLS token (prima posizione) per classificazione
x = self.classifier(x[:, 0, :])
return x # logits: (batch, 2)
5. Vals positief management: bescherming van onschuldige spelers
De grootste fout van een anti-cheatsysteem en het verbieden van onschuldige spelers: een VPN die simuleert hoge latentie, een zeer bekwame speler met uitzonderlijke reacties, een defecte controller genereert afwijkende invoer. Een false positive vernietigt het vertrouwen van de speler en is bijna onmogelijk herstel het. Het systeem moet vóór elk verbod worden ontworpen met meerdere bevestigingslagen.
// sanction_pipeline.go - Multi-layer sanction decision pipeline
package anticheat
type SuspicionReport struct {
PlayerID string
ReportType string // "speed_hack", "aim_bot", etc.
Score float64 // 0.0 - 1.0
Evidence []Evidence
Timestamp time.Time
}
type SanctionPipeline struct {
suspicionDB *SuspicionDatabase
accountAge *AccountAgeService
humanReview *HumanReviewQueue
}
// ProcessSuspicion: decide cosa fare con una segnalazione sospetta
func (p *SanctionPipeline) ProcessSuspicion(report SuspicionReport) SanctionDecision {
// Layer 1: Accumulo prove nel tempo
// Una singola violazione non e sufficiente per agire
history := p.suspicionDB.GetHistory(report.PlayerID, 30*24*time.Hour) // 30 giorni
history = append(history, report)
// Calcola score cumulativo pesato per recency
cumulativeScore := 0.0
for i, h := range history {
age := time.Since(h.Timestamp).Hours() / 24 // giorni
weight := math.Exp(-age / 7) // Decadimento esponenziale su 7 giorni
cumulativeScore += h.Score * weight * (float64(i+1) / float64(len(history)))
}
// Layer 2: Account age factor
// Account nuovi con alto suspicion score = più probabile cheater
accountAgeDays := p.accountAge.GetAgeDays(report.PlayerID)
if accountAgeDays < 7 {
cumulativeScore *= 1.3 // Boost per account nuovi
} else if accountAgeDays > 365 {
cumulativeScore *= 0.8 // Discount per account vecchi e stabiliti
}
// Layer 3: Decision tree
switch {
case cumulativeScore >= 0.95:
// Auto-ban: evidenze schiaccianti, molto difficile falso positivo
return SanctionDecision{
Action: "permanent_ban",
AutoApply: true,
Reason: fmt.Sprintf("cumulative_score=%.3f", cumulativeScore),
}
case cumulativeScore >= 0.80:
// Soft ban temporaneo + review umana obbligatoria
return SanctionDecision{
Action: "temp_ban_24h",
AutoApply: true,
SendToReview: true,
Reason: "high_suspicion_pending_review",
}
case cumulativeScore >= 0.60:
// Solo monitoraggio aumentato, nessuna sanzione automatica
p.suspicionDB.SetMonitoringLevel(report.PlayerID, MonitoringHigh)
return SanctionDecision{
Action: "monitor_only",
AutoApply: false,
}
default:
// Falso positivo probabile: solo log
return SanctionDecision{Action: "log_only", AutoApply: false}
}
}
6. Herhalingsanalyse: post-hocdetectie
Niet alle cheats zijn in realtime detecteerbaar. Herhalingsanalyse - mogelijk dankzij volledige telemetrie opgenomen door de server - hiermee kunt u wedstrijden bekijken na een player is gerapporteerd door anderen, waarbij een meer computationeel zware analyse is toegepast.
-- ClickHouse: Query per identificare candidati sospetti da analisi replay
-- Identifica giocatori con statistiche outlier negli ultimi 7 giorni
SELECT
player_id,
count() AS total_matches,
avg(toFloat64OrZero(payload['headshot_rate'])) AS avg_headshot_rate,
avg(toFloat64OrZero(payload['kda'])) AS avg_kda,
avg(toFloat64OrZero(payload['avg_ttk_ms'])) AS avg_ttk_ms,
-- Accuracy Z-Score rispetto alla media della regione
-- Z > 3: più di 3 sigma sopra la media = outlier statistico
(avg(toFloat64OrZero(payload['headshot_rate'])) -
avg(avg(toFloat64OrZero(payload['headshot_rate'])))
OVER (PARTITION BY toStartOfDay(server_ts))) /
stddevPop(toFloat64OrZero(payload['headshot_rate']))
OVER (PARTITION BY toStartOfDay(server_ts)) AS headshot_zscore,
-- Ratio di report ricevuti da altri player
countIf(payload['was_reported'] = 'true') / count() AS report_rate
FROM game_analytics.events_all
WHERE event_type = 'gameplay.match_end'
AND server_ts >= now() - INTERVAL 7 DAY
GROUP BY player_id
HAVING
total_matches >= 10 -- Minimo partite per avere dati significativi
AND (
headshot_zscore > 3 -- Statistically impossible accuracy
OR avg_ttk_ms < 100 -- Kills troppo veloci
OR report_rate > 0.3 -- > 30% partite con segnalazioni
)
ORDER BY headshot_zscore DESC, report_rate DESC
LIMIT 100;
Veel voorkomende fouten bij anti-cheat
- Verbod alleen voor hoge K/D: Een zeer goede speler heeft een hoge K/D. Analyseer altijd in combinatie met meerdere signalen (richtpatronen, snelheid, rapporten) alvorens te handelen.
- Negeer latentie in besturingselementen: Met lagcompensatie, een speler met Een latentie van 100 ms kan aanvoelen als "door muren heen schieten". Kalibreer validatietoleranties gebaseerd op de werkelijke latentie van de speler.
- Duidelijk anti-cheatsysteem: Geef nooit systeemdetails vrij anti-cheat: valsspelers analyseren reacties om te begrijpen wat ze moeten activeren en wat ze moeten vermijden. Gebruik dubbelzinnige antwoorden en willekeurige vertragingen vóór een verbod.
- Geen beroepsprocedure: Zelfs het meest nauwkeurige systeem maakt fouten. Bied een een duidelijke en humane beroepsprocedure voor betwiste verboden.
Conclusies
Een effectief anti-cheatsysteem in 2025 vereist een meerlaagse aanpak: dearchitectuur server-gezaghebbend als een onaantastbare basis, de statistische validatie voor afwijkende patronen, en de machinaal leren (vooral Transformers) voor geavanceerde gedragsdetectie. Geen van de drie alleen is voldoende.
Het beheer van valse positieven is net zo belangrijk als de detectie: een systeem dat velen verbiedt onschuldig en erger dan geen anti-cheatsysteem. De meerlaagse sanctiepijplijn met evaluatie verplichte mensenrechten voor grensgevallen en de open beroepsprocedure zijn niet onderhandelbaar.
Volgende stappen in de Game Backend-serie
- Vorig artikel: Matchmakingsysteem: ELO, Glicko-2 en wachtrijbeheer
- Volgend artikel: Open Match en Nakama: open source game-backend
- Gerelateerde serie: Webbeveiliging - API-beveiliging en kwetsbaarheidsbeoordeling







