치트 방지 아키텍처: 서버 권한 및 행동 분석
비디오 게임에서의 부정행위는 게임 산업에서 연간 300억 달러 규모의 문제입니다. 아니다 그것은 공정성에 관한 것입니다. 사기꾼은 다른 플레이어의 경험을 파괴하고, 이탈, 수익 감소, 게임 평판 손상 등의 행위를 할 수 있습니다. 2024년 조사에서는 플레이어의 77%가 부정행위로 인해 멀티플레이어 타이틀을 포기한 것으로 나타났습니다.
기존의 치트 방지 솔루션(Valve's VAC, Easy Anti-Cheat, BattlEye)은 주로 약 클라이언트 측 감지: 프로세스를 모니터링하는 드라이버 소프트웨어 치트를 탐지하는 운영 체제. 이 접근 방식에는 근본적인 문제가 있습니다. 맞춤형 하드웨어에서 부정행위자가 지속적으로 승리하는 고양이와 쥐 게임, 하이퍼바이저 및 커널 우회. 현대적 대책과서버 권한 아키텍처 와 결합 행동 분석 ML.
이 기사에서는 완전한 치트 방지 아키텍처를 살펴보겠습니다. 이상값의 통계적 검증, ML 기반 시스템에 이르기까지 모든 작업을 검증합니다. 행동 탐지용 변환기(96.94% 정확도, 2025년 검색에서 98.36% AUC).
무엇을 배울 것인가
- 치트 유형: 스피드 해킹, 조준 봇, 월 해킹, ESP, 경제 착취
- 서버 권한 아키텍처: 클라이언트에 위임할 것과 위임하지 않을 것
- 서버 측 검증: 물리, 충돌 감지, 시선
- 통계적 이상치 탐지: 목표 분석을 위한 Z-점수, k-시그마
- 행동 분석 ML: 치트 방지를 위한 기능 엔지니어링
- Transformer 기반 치트 감지(AntiCheatPT 접근 방식)
- 재생 분석: 원격 측정을 통한 사후 감지
- 오탐지 관리: 무고한 플레이어 보호
1. 치트 분류: 당신이 맞서 싸우는 대상
방어를 설계하기 전에 무엇에 대응해야 하는지 이해하는 것이 중요합니다. 사기꾼들이 나누어져 있어요 클라이언트의 동작(입력 조작)을 수정하는 매크로 범주와 프로토콜이나 게임 로직의 취약점을 악용하는 것(프로토콜 악용).
분류 요령 및 대책
| 치트 | 설명 | 발각 | 방지 |
|---|---|---|---|
| 스피드 해킹 | 더 빠르게 움직이도록 시스템 시계 변경 | 속도 확인 서버 | 서버 권한 물리학 |
| 텔레포트 해킹 | 임의 위치 설정, 모션 검증 건너뛰기 | 위치 델타 확인 | 서버 권한 위치 |
| 조준 봇 | 초인적인 정확도로 플레이어를 자동 조준합니다. | 통계적 목표 분석 | ML 행동 감지 |
| 월 해킹 / ESP | 그는 벽 너머로 선수들을 본다 | 서버측 절두체 컬링 | 보이지 않는 적의 위치를 보내지 마세요 |
| 무반동 | 반동을 제거하여 일관된 정밀도로 촬영 | 샷 패턴 분석 | 서버측 반동 시뮬레이션 |
| 경제 착취 | 경쟁 조건으로 인해 통화 또는 항목이 중복되었습니다. | 거래 감사 | 멱등성 트랜잭션 + 속도 제한 |
| 패킷 조작 | 게임 상태를 변경하기 위해 네트워크 패킷 수정 | 메시지 검증 | DTLS/TLS + 스키마 검증 |
2. 서버 권한 아키텍처: 보안의 기초
최신 부정행위 방지의 핵심 원칙은 다음과 같습니다. 서버는 절대적인 진실의 원천입니다. 클라이언트는 단지 입력 (플레이어가 하고 싶은 것), 결코 결과가 아니다 (무슨 일이 있었는지). 서버는 게임 상태를 계산하고 이를 클라이언트에 전달합니다. 이를 통해 스피드 해킹, 텔레포트 해킹, 경제적 악용 및 패킷 월 해킹이 확실하게 제거됩니다.
// 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. 에임봇 탐지: 통계 분석
조준 봇은 인간에게는 통계적으로 불가능한 조준 패턴, 즉 회전 각도를 생성합니다. 100% 정밀도, 완벽한 추적 기능을 갖춘 즉각적인 플릭 샷. 탐지는 분석을 기반으로 합니다. 시간에 따른 마우스/스틱 움직임 통계, 플레이어와 분포 비교 인구의.
// 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. 기계 학습: 행동 탐지를 위한 변환기
통계 분석은 더 거친 치트를 포착하지만 고급 치트(예: 지터가 있는 조준 봇)를 포착합니다. 인공)에는 ML 접근 방식이 필요합니다. 가장 최근의 연구(AntiCheatPT, 2025)는 내가 어떻게 게임 액션 시퀀스에 적용된 트랜스포머는 96.94%의 정확도에 도달하며, 탐지율 98.36%의 AUC로 LSTM과 기존 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. 오탐지 관리: 무고한 플레이어 보호
치트 방지 시스템과 무고한 플레이어 차단의 최악의 실수: 시뮬레이션하는 VPN 높은 레이턴시, 예외적인 반응을 보이는 고기술 플레이어, 오작동하는 컨트롤러 비정상적인 입력을 생성합니다. 거짓 긍정은 플레이어의 자신감을 파괴하고 거의 불가능합니다. 그것을 회복하십시오. 시스템은 각 금지 전에 여러 단계의 확인을 거쳐 설계되어야 합니다.
// 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. 재생 분석: 사후 탐지
모든 치트가 실시간으로 감지되는 것은 아닙니다. 재생 분석 - 덕분에 가능 서버에 의해 기록된 전체 원격 측정 - 이후 일치 항목을 검토할 수 있습니다. 플레이어는 더 많은 계산을 필요로 하는 분석을 적용하여 다른 사람들에 의해 보고되었습니다.
-- 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;
치트 방지의 일반적인 실수
- K/D가 높은 경우에만 금지: 아주 좋은 선수는 K/D가 높습니다. 분석하다 행동하기 전에 항상 여러 신호(조준 패턴, 속도, 보고)와 결합됩니다.
- 컨트롤의 대기 시간 무시: 지연 보상이 있는 플레이어는 100ms의 지연 시간은 "벽을 뚫고 쏘는 것"처럼 느껴질 수 있습니다. 검증 공차 교정 플레이어의 실제 대기 시간을 기준으로 합니다.
- 명확한 치트 방지 시스템: 시스템 세부정보를 절대 노출하지 마세요. 부정행위 방지: 부정행위자는 응답을 분석하여 트리거할 항목과 피해야 할 항목을 이해합니다. 차단 전 모호한 답변과 무작위 지연을 사용하세요.
- 항소 절차 없음: 가장 정밀한 시스템이라도 실수는 있습니다. 제안 이의가 있는 금지 조치에 대한 명확하고 인도적인 항소 절차.
결론
2025년의 효과적인 치트 방지 시스템에는 다층적인 접근 방식이 필요합니다.건축 서버 권한 난공불락의 기초로서, 통계적 검증 비정상적인 패턴에 대해서는 기계 학습 (특히 트랜스포머) 정교한 행동 탐지. 세 가지 중 어느 것만으로는 충분하지 않습니다.
오탐지 관리는 탐지만큼 중요합니다. 많은 것을 금지하는 시스템입니다. 순진하고 치트 방지 시스템이 없는 것보다 더 나쁩니다. 검토가 포함된 다층 제재 파이프라인 경계선에 있는 사건에 대한 의무적 인권과 공개 항소 절차는 협상할 수 없습니다.
게임 백엔드 시리즈의 다음 단계
- 이전 기사: 매치메이킹 시스템: ELO, Glicko-2 및 대기열 관리
- 다음 기사: 오픈 매치와 나카마: 오픈 소스 게임 백엔드
- 관련 시리즈: 웹 보안 - API 보안 및 취약성 평가







