Aller au contenu principal

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

FichierRôle
src/network/connection.cthread réseau, connexion reverse, buffers
src/network/socket_io.cenvoi/réception chiffrés
src/network/protocol.chandshake, prompt, boucle de session
src/network/command_router.croutage des commandes reçues
src/crypto/xor_stream.cclé FNV-1a et XOR de flux
src/commands/command_capture.cexécution shell et capture de sortie
attacking_program/backend/rootkit/transport.pylistener 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:

CasPause
connexion impossible10 secondes
session terminée5 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:

ConstanteValeurUsage
WLKOM_RX_SIZE160 KiBligne entrante maximum
WLKOM_TX_SIZE4 MiBréponse sortante maximum
WLKOM_CRYPTO_KEY_LEN16clé 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 moduleOffset backend correspondantSens
send_crypto_offsetrx_offsetmodule vers backend
receive_crypto_offsettx_offsetbackend 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çueTraitement
exitferme 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
screenshotlance la capture graphique
autre texteexé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:

ÉtatSens
listener_boundle backend écoute sur le port C2
detectedune socket victime est arrivée
needs_passwordla socket attend l'authentification
connectedla session est prête
last_errordernier 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, soit 160 KiB cô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