Skip to content

Anti-cheat

CTFHive's anti-cheat system is human-in-the-loop: it detects suspicious submissions and raises CRITICAL events in the audit log. Admins review the evidence and decide whether to act. There is no automatic ban.


Layer 1: Cryptographic flag isolation

Every Principal (team or solo player) receives a unique flag for every challenge. The flag is derived with HMAC-SHA3-256:

flag = PREFIX{hmac_sha3_256(ADMIN_KEY, team_secret || challenge_id_bytes)[:32]}

Because each Principal has a different team_secret (32 random bytes, generated once at Principal creation and never exposed), the flag for Principal A is cryptographically distinct from the flag for Principal B — even for the same challenge. A flag shared between two players cannot be submitted by the second player for credit.

See Flag derivation for the full derivation spec and HMAC construction.


Layer 2: Submit-time detection

Implemented in ctfapp/services/anticheat.py, function inspect_cheating_submission. It runs on every wrong submission.

The function performs two independent checks:

Check A — Flag sharing

The submitted flag is compared (constant-time) against the derived flag for every other active Principal for the same challenge. If it matches any of them, the submitter is presenting a flag that belongs to someone else.

Result: a FLAG_SHARE_DETECTED event at CRITICAL severity is written to the audit log, including:

  • shared_from_principal_id — the Principal whose flag was stolen
  • submitting_principal_id — who submitted it

Check B — Flag replay

The submitted flag (hashed with the same algorithm used for Submission storage) is compared against every other Principal's prior submission hashes for the same challenge. If a hash match is found, the same flag has already been submitted by a different player — meaning it was shared and submitted multiple times.

Result: a FLAG_REPLAY_DETECTED event at CRITICAL severity is written, including:

  • replayed_submission_id — the prior submission that matched
  • replayed_from_principal_id — who submitted it before
  • submitting_principal_id — who is submitting it now

Both checks can fire on the same submission (the function accumulates all findings into one dict). Each finding writes its own independent audit event.


Layer 3: Submission rate limiting

Implemented in ctfapp/services/anticheat.py, function check_rate_limit. This is enforced before inspect_cheating_submission runs.

Parameter Default Config key
Wrong attempts before lockout 3 FLAG_WRONG_LIMIT
Lockout duration 30 seconds FLAG_LOCKOUT_SECONDS

Rate limit state is stored in Redis using two keys per (principal_id, challenge_id) pair:

  • wrong_count:<principal_id>:<challenge_id> — rolling wrong attempt counter
  • lockout:<principal_id>:<challenge_id> — TTL key that blocks submissions while the lockout is active

When Redis is unavailable (development without Redis), the rate limiter returns "allowed" to avoid blocking all submissions. Correct submissions call clear_attempts to reset both keys.


How alerts surface to admins

Both FLAG_SHARE_DETECTED and FLAG_REPLAY_DETECTED are written at CRITICAL severity. To review them:

  1. Navigate to /admin/events.
  2. Filter by Severity: CRITICAL or type FLAG in the event type filter.
  3. Each event's JSON payload contains the principal IDs involved. Cross-reference with /admin/users to identify the players.
  4. Decide whether to ban the user (/admin/users/<id>/ban) or apply a score adjustment (/admin/ dashboard, manual score form).

There is no auto-ban. An organizer must review and act.


Roadmap (not yet implemented)

Roadmap — not yet implemented

The following layers are planned but not present in the codebase. Do not expect them to function.

VPN / packet telemetry correlation — The WireGuard gateway has packet capture plumbing (ctfapp/lab/packet_capture.py, ctfapp/models/packet_capture.py) and the dashboard shows VPN peer status. However, this data does not currently feed any anti-cheat scoring. The planned feature would flag submissions from Principals that sent zero packets to the relevant challenge host.

Solve-time z-score — Flag a submission where the time between first challenge view and first correct solve is implausibly short relative to the distribution of all other solvers. The streaming mean/stddev logic and the 5th-percentile threshold are not yet implemented.

Session fingerprint pinning — A sticky (JA4_hash, user_agent_hash, canvas_hash, timezone) tuple compared across sessions. There is no auth/fingerprint.py module and the JA4 or canvas fingerprinting code does not exist.

Cross-tenant correlation — Detecting the same browser fingerprint playing in two simultaneous events under different handles. This requires the CTFHive control plane, which is a separate service not yet deployed.


Summary

Layer Status What it catches
Cryptographic flag isolation Implemented Flags from team A cannot be valid for team B
Submit-time flag sharing Implemented Wrong submission matches another Principal's derived flag
Submit-time flag replay Implemented Wrong submission hash matches a prior submission by a different Principal
Submission rate limiting Implemented Brute-force guessing — 3 wrong attempts triggers a 30-second lockout
VPN packet correlation Roadmap Submissions from Principals with no recorded network activity
Solve-time z-score Roadmap Implausibly fast solves relative to the peer distribution
Session fingerprint pinning Roadmap Same session diverges mid-competition
Cross-tenant correlation Roadmap Same player across multiple simultaneous events