Canal reverse et commandes
Cette page explique la grosse brique réseau du rootkit: le module ne lance pas un shell TTY complet, il maintient un canal reverse chiffré qui transporte des lignes de commandes et renvoie leurs sorties.
À garder en tête: on est sur TCP. TCP transporte des octets dans l'ordre, mais il ne garde pas les frontières "un send = un message". C'est pour cela que la crypto utilise des offsets, et que les réponses utilisent un marqueur de fin.
Les primitives kernel derrière cette page sont regroupées dans Kthreads, Credentials kernel, Sockets kernel, kvec/msghdr et call_usermodehelper.
Fichiers concernés
| Fichier | Rôle |
|---|---|
src/network/connection.c | thread réseau, connexion reverse, buffers |
src/network/socket_io.c | envoi/réception chiffrés |
src/network/protocol.c | handshake, prompt, boucle de session |
src/network/command_router.c | routage des commandes reçues |
src/crypto/xor_stream.c | clé FNV-1a et XOR de flux |
src/commands/command_capture.c | exécution shell et capture de sortie |
attacking_program/backend/rootkit/transport.py | listener TCP côté attaquant |
attacking_program/backend/rootkit/controller.py | état de session et authentification |
Pourquoi une connexion reverse?
La victime n'ouvre pas de port serveur. Le module contacte l'attaquant:
attacker_ip:command_port
192.168.100.1:4444 par défaut
Ce modèle est plus pratique dans le projet:
- le dashboard attaquant peut écouter passivement
- la victime n'a pas besoin d'être joignable en inbound
- le module peut retenter la connexion après une erreur
- le backend sait afficher un état clair: détecté, en attente de mot de passe, connecté.
Boucle du thread réseau
Le thread wlkom_network_thread démarre après l'installation des hooks et la création du dossier runtime.
Deux pauses existent:
| Cas | Pause |
|---|---|
| connexion impossible | 10 secondes |
| session terminée | 5 secondes |
Pourquoi ces pauses? Sans pause, un échec réseau déclencherait une boucle CPU bruyante. Avec une pause courte, le module reste réactif quand le backend revient.
Le thread utilise kthread_should_stop() pour s'arrêter proprement. Quand wlkom_stop_module() décharge le module, il ne se contente pas d'appeler kthread_stop(): wlkom_stop_network_thread() fait d'abord un kernel_sock_shutdown(..., SHUT_RDWR) sur la socket active. Ce shutdown réveille une réception bloquée dans kernel_recvmsg(), puis kthread_stop() peut attendre un thread qui est réellement en train de sortir.
Pour relire pourquoi module_init ne doit pas rester bloqué et pourquoi ssleep est utilisé entre deux essais, voir Kthreads.
Buffers et tailles
Les tailles principales viennent de wlkom_config.h:
| Constante | Valeur | Usage |
|---|---|---|
WLKOM_RX_SIZE | 160 KiB | ligne entrante maximum |
WLKOM_TX_SIZE | 4 MiB | réponse sortante maximum |
WLKOM_CRYPTO_KEY_LEN | 16 | clé XOR dérivée |
La réception lit une ligne jusqu'à \n. Si la ligne dépasse WLKOM_RX_SIZE - 1, le module retourne:
COMMAND TOO LONG
Maximum accepted input line: 163839 bytes.
[exit:1]
Cette limite explique pourquoi les uploads sont chunkés: chaque commande upload-append doit tenir dans une seule ligne.
Chiffrement du flux
Le mot de passe sert à dériver une clé de 16 octets avec FNV-1a. Chaque octet envoyé ou reçu est XORé avec:
key[(offset + i) % 16]
Le module garde deux offsets:
| Offset module | Offset backend correspondant | Sens |
|---|---|---|
send_crypto_offset | rx_offset | module vers backend |
receive_crypto_offset | tx_offset | backend vers module |
Pourquoi les offsets sont indispensables? TCP ne respecte pas les frontières de messages. Une bannière, un prompt ou une réponse peut être fragmenté ou regroupé. Les offsets font du XOR un flux continu.
Limite: ce chiffrement est pédagogique. Il masque le trafic en clair dans Wireshark, mais il n'offre ni intégrité, ni authentification cryptographique forte.
Handshake
La bannière est chiffrée dès le premier octet.
Pourquoi une socket pending côté backend? Le module se connecte avant que l'opérateur ait entré le mot de passe. Le backend garde donc la socket dans un état non authentifié, puis la promeut en session active seulement si le déchiffrement et la réponse sont cohérents.
Protocole de commandes
Après authentification, le module envoie un prompt:
wlkom>
Puis il lit une ligne chiffrée et la route selon son préfixe:
| Ligne reçue | Traitement |
|---|---|
exit | ferme la session |
upload-begin <path> | crée le parent et tronque le fichier |
upload-append <base64> <path> | décode et ajoute un chunk |
download-chunk <offset> <length> <path> | lit une plage du fichier |
screenshot | lance la capture graphique |
| autre texte | exécute comme commande shell |
Les anciens raccourcis upload <...> et download <...> ne sont plus routés. Les transferts fichier passent uniquement par les commandes chunkées.
Commandes shell
Pour une commande normale, le routeur appelle wlkom_execute_command.
Le module construit:
(<commande>) > /opt/wlkom_data/.cmd.out 2>&1; echo "[exit:$?]" >> /opt/wlkom_data/.cmd.out
Puis il lance /bin/sh -c via call_usermodehelper, relit .cmd.out, extrait le dernier marqueur [exit:N], supprime le fichier temporaire et renvoie:
stdout/stderr
[exit:N]
__KOZACI_RESPONSE_END_8F8F6A0E6D4A41C2__
Le backend attend le marqueur __KOZACI_RESPONSE_END_8F8F6A0E6D4A41C2__ et non plus le prompt wlkom>. Ainsi, une commande qui affiche elle-même wlkom> ne coupe plus la réponse trop tôt.
Pourquoi ce n'est pas un vrai terminal interactif? Chaque action est une commande isolée. Le backend simule un cwd logique en préfixant les commandes par cd <cwd> &&, mais il n'y a pas de pseudo-TTY, pas de job control et pas d'état shell interactif durable.
Transferts sur le même canal
Les transferts fichier réutilisent le canal de commandes:
upload-begin <path>
upload-append <base64> <path>
download-chunk <offset> <length> <path>
Le backend découpe les fichiers pour respecter WLKOM_RX_SIZE côté upload et WLKOM_TX_SIZE côté download. L'upload garde un plafond plus bas, 120 KiB base64, car le contenu passe par un argument shell avant d'être décodé. Le module répond aux downloads avec:
FILE-CHUNK:<offset>:<length>:<path>
<base64>
[exit:0]
__KOZACI_RESPONSE_END_8F8F6A0E6D4A41C2__
Le choix base64 garde le protocole lisible et évite de mélanger des octets binaires bruts avec les prompts et les marqueurs [exit:N].
Les constantes WLKOM_RX_SIZE, WLKOM_TX_SIZE, WLKOM_UPLOAD_BASE64_CHUNK_MAX et le lien avec les buffers kernel sont reprises dans Transferts chunkés et Allocations mémoire kernel.
États côté backend
Le dashboard distingue:
| État | Sens |
|---|---|
listener_bound | le backend écoute sur le port C2 |
detected | une socket victime est arrivée |
needs_password | la socket attend l'authentification |
connected | la session est prête |
last_error | dernier problème réseau, crypto ou protocole |
Ces états viennent du fait que le module initie la connexion, mais que le mot de passe est fourni plus tard dans l'interface.
Limites à retenir
- Une seule session active est gérée à la fois.
- Une seule commande est traitée à la fois côté backend.
- Les commandes entrantes doivent tenir dans
WLKOM_RX_SIZE, soit160 KiBcôté kernel. - Le canal n'est pas un TTY interactif complet.
- Les réponses sont terminées par un marqueur dédié, pas par le prompt.
- La crypto XOR protège seulement contre la lecture directe, pas contre une analyse active.
- Si le backend redémarre, le module se reconnecte après la prochaine tentative réseau.
Liens liés
- Actions userland pour comprendre
/bin/sh, les captures et les transferts. - Hooks et masquage pour comprendre pourquoi les artefacts du canal sont moins visibles.
- Chiffrement pour le détail FNV-1a/XOR.
- Références techniques pour
kthread, sockets kernel,kvec,msghdr,GFP_KERNELet codes d'erreur.