5.8 KiB
The Cycle: Frontier — serveur dédié gameplay (R&D netcode)
Branche game-server. Objectif : un serveur de jeu autoritatif écrit from scratch
(Prospect.Unreal, réimplémentation C# du netcode Unreal) pour du vrai co-op, sans
passer par un client-hôte P2P. Statut : bloqué au chiffrement (voir plus bas).
Client cible : UE4 build R3.5.0 (4.27.2 netcode), Steam depot 868271.
Ce qui fonctionne (murs franchis)
La connexion d'un vrai client va jusqu'à l'entrée du login :
Handshake stateless UDP (cookie/challenge) ✅
Reconstruction du paquet (PacketHandler) ✅
Séquençage des paquets (adopt des seq client) ✅
Alignement des bunches ✅ ← percée
Canal de contrôle ouvert ✅
NMT_Hello reçu et parsé ✅
Transition login Hello → Login ✅
Chiffrement DTLS-PSK ❌ ← mur final
Percée : le bit de header spécifique R3.5.0
Le client écrit un bit de plus entre l'historique d'ack du FNetPacketNotify et
le payload packet-info, que l'UE 4.27 stock (EngineNetVer 16) n'a pas. Décodage
bit-à-bit d'un vrai paquet : l'en-tête fait 65 bits, pas 64. En consommant ce bit
(bCycleExtraHeaderBit dans UNetConnection.ReceivedPacket), tout se réaligne :
bHasPacketInfoPayload, l'horloge jitter (10 bits) et bHasServerFrameTime tombent
juste, et le premier bunch du canal de contrôle parse proprement (ChIndex 0, bOpen,
bReliable = NMT_Hello). Sans ce fix, le ChIndex sortait en vrac (~1 049 000) et le
serveur droppait/plantait.
Le mur final : chiffrement DTLS-PSK
Le client exige le chiffrement. Établi par reverse-engineering :
NMT_HelloporteEncryptionToken= le PlayFab user_id du joueur (ex.92EBCFE8C3EAF3AC). Vu dans l'URL de connexion du client :...?EntityToken=<JWT>?EncryptionToken=92EBCFE8C3EAF3AC.- Pile PacketHandler du client (ses propres logs) :
[DTLSHandlerComponent, StatelessConnectHandlerComponent]. - Le exe embarque les suites
ECDHE-PSK-AES256-*,DHE-PSK-AES256-GCM-SHA384, la cvarDTLS.PreSharedKeys, etDTLSPSKClientCallback/DTLSPSKServerCallback→ DTLS en mode PSK (clé pré-partagée 32 octets, identité = user_id). - Compression : OodleNetwork compilé, mais aucun dictionnaire
.udic→ pass-through (les paquets ne sont ni compressés ni chiffrés au niveau paquet ; entropie faible + longues suites de zéros le confirment).
Proposer un challenge en clair (sans NMT_EncryptionAck) ne marche pas : le client
ferme le canal de contrôle juste après.
Pour finir il faudrait : (1) un serveur DTLS-PSK collé au framing du
DTLSHandlerComponent d'UE, et (2) la PSK de 32 octets dérivée par le client à
partir du user_id/EntityToken.
Pourquoi la clé est inaccessible (statique)
L'exe Prospect-Win64-Shipping.exe est packé/chiffré (protection anti-triche,
BattlEye) :
- Entropie de
.text= 8.000 (maximum = aléatoire/chiffré ; du code normal ≈ 6.3). .rdata= 4.96 (normal → les strings restent lisibles, d'où les découvertes ci-dessus).- Un scan brut du
.text(76 Mo) ne trouve que ~76 instructions → niveau du bruit : ce n'est pas du code sur disque, c'est du chiffré déchiffré au runtime.
Conséquence : Ghidra / radare2 n'analysent que du ciphertext ; aucune référence aux fonctions de chiffrement n'est trouvable statiquement. La dérivation de la PSK vit dans ce code chiffré.
Seule voie restante (non tentée) : dump mémoire au runtime. Le jeu tourne sous
Proton/Linux (BattlEye n'a pas de driver kernel sous Linux) → un autre process Linux
peut lire /proc/<pid>/mem et récupérer le .text déchiffré, puis l'analyser dans
Ghidra pour retrouver la dérivation. Zone grise ToS, plusieurs étapes.
Fichiers clés
src/Prospect.Server.Game/Program.cs— hôte du serveur de jeu (map Station, GameSession).src/Prospect.Unreal/Net/UNetConnection.cs—ReceivedPacket: fix du bit de header (bCycleExtraHeaderBit), adopt des séquences client, drop gracieux des bunches.src/Prospect.Unreal/Runtime/UWorld.cs—NotifyControlMessage: Hello/Login ; leelsedu blocNMT.Hellodocumente le mur DTLS-PSK.
Verdict
On a amené un serveur dédié gameplay The Cycle plus loin qu'aucun projet public connu (le projet communautaire deiteris/Prospect n'émule que les services en ligne, pas le netcode de jeu). Le mur restant — DTLS-PSK dont la clé est derrière un packer anti-triche — est un chantier crypto + RE dynamique d'un autre ordre de grandeur.
MAJ (2026-07-16) — mur DTLS franchi (côté serveur)
- PSK récupérée : la clé 32 octets a été obtenue au runtime (dump du
.textdéchiffré du client via/proc/<pid>/memsous Proton — BattlEye n'a pas de driver kernel sous Linux). Stockée dans Vault (secret/the-cycle, cléGAMESERVER_DTLS_PSKS, formatuser_id:hex32), jamais dans le repo. - Serveur DTLS-PSK implémenté (BouncyCastle) :
DtlsPskStore(envPROSPECT_DTLS_PSKS),DtlsPacketTransport(pont bloquant→paquet),ProspectPskTlsServer(DTLS 1.2, identité=user_id),DTLSHandlerComponent(pipeline).UWorldsurNMT_Hellochiffré : PSK connue →NMT_EncryptionAck+ handshake.- Compile (0 erreur). Non validé en live.
- Reste (itération live obligatoire, invalidable hors client) :
- ordre exact du pipeline
[DTLS, Stateless]et routage des records de handshake (émis viaLowLevelSend— doivent porter le framing stateless attendu) ; - framing DTLS-sur-PacketHandler d'UE (record layer, cookie DTLS éventuel) ;
- bascule handshake→données applicatives.
Méthode : lancer
Prospect.Server.Gameavec la PSK, diriger le client dessus, itérer sur les logs serveur.
- ordre exact du pipeline