Aller au contenu principal

Décisions techniques

Cette page résume les choix structurants du projet. Les détails bas niveau sont dans Références techniques.

Environnement

Le projet utilise QEMU/KVM. Ce choix donne un environnement Linux proche du réel, scriptable et facile à remettre à zéro.

Les deux VMs tournent sur Ubuntu Desktop 22.04. La distribution fournit les paquets kernel Jammy, une session graphique pour la page victime, la capture d'écran et les outils nécessaires au dashboard.

Le noyau cible est 5.15.0-171-generic. Les hooks ftrace, les symboles __x64_sys_*, les sockets kernel et call_usermodehelper dépendent de détails internes du noyau. Fixer la version évite les écarts d'ABI pendant les tests. Les notions concernées sont détaillées dans ftrace, Kprobes et symboles, Sockets kernel et call_usermodehelper. Plus de détails dans Choix du noyaux

Build du module

wlkom.ko est compilé côté attaquant. La victime n'a donc pas besoin de headers, de gcc, de make ni des sources kernel.

La contrepartie est le vermagic. Le module doit être compilé pour le même noyau que celui de la victime, sinon insmod refuse le chargement.

Voir Kbuild et vermagic pour le lien entre headers, version du noyau et chargement du .ko.

Réseau

Le bridge privé br-wlkom donne des IPs fixes: 192.168.100.1 pour l'attaquant et 192.168.100.2 pour la victime.

Le NAT QEMU reste séparé. Il sert aux paquets apt et au serveur host accessible depuis les VMs via 10.0.2.2:8000.

Cette séparation rend le trafic C2 prévisible sans couper l'accès aux dépendances.

Canal C2

Le module ouvre une connexion reverse vers 192.168.100.1:4444. La victime n'expose donc pas de port d'écoute.

Si le listener n'est pas prêt, le thread réseau attend puis retente. Après une déconnexion, il ferme la socket et recommence.

À l'arrêt du module, le code ne se limite pas à kthread_stop(). Il garde la socket active en mémoire et appelle kernel_sock_shutdown(..., SHUT_RDWR) avant d'attendre le thread. Ce choix évite qu'un kernel_recvmsg() bloqué garde module_exit suspendu.

Ce modèle simplifie l'état côté dashboard: absent, détecté, en attente du mot de passe, puis connecté.

Authentification

Le mot de passe est fourni au chargement avec le paramètre password=<secret>. Il n'est pas hardcodé dans les sources C.

Pour survivre au reboot, le même secret est écrit dans /etc/modprobe.d/wlkom.conf. Quand le module est actif, le hook read masque les lignes sensibles. Une inspection offline peut toutefois retrouver le fichier.

Chiffrement

Le canal utilise une clé dérivée du mot de passe par FNV-1a 64-bit, puis un XOR de flux avec offsets séparés en émission et réception.

Ce choix reste volontairement simple. Il est court à maintenir côté C, Python et React, mais il ne fournit pas les garanties de TLS ou d'un chiffrement authentifié.

Hooks kernel

Les hooks sont posés avec ftrace sur les syscalls utilisés pour les listings, les lectures de persistance et les accès par chemin: getdents*, read, pread64, readv, mmap, openat, openat2, newfstatat, statx, access, faccessat, readlinkat et unlinkat.

ftrace évite de patcher directement une table read-only. Les hooks sont aussi retirables proprement avec unregister_ftrace_function.

La limite est claire: le mécanisme dépend des options du noyau, des symboles exportés et du comportement exact de la version cible.

Pour comprendre ce que FTRACE_OPS_FL_IPMODIFY, pt_regs, notrace et within_module font concrètement, voir ftrace, pt_regs et Macros kernel utiles dans les hooks.

Masquage

getdents et getdents64 filtrent les noms liés à wlkom, au dossier runtime et aux artefacts temporaires.

Le hook read ne cache pas des fichiers entiers. Il retire seulement les lignes sensibles dans les fichiers de persistance. Cela limite les effets de bord sur les outils système.

Les hooks par chemin bloquent aussi les accès directs vers /opt/wlkom_data, /sys/module/wlkom et le module persistant wlkom.ko. Cette couche existe parce qu'un listing filtré n'empêche pas quelqu'un de taper un chemin connu à la main.

Le module se retire aussi de la liste utilisée par lsmod. Ce masquage reste partiel: les logs, sysfs ou une inspection mémoire peuvent encore révéler des indices.

Les détails sont dans getdents, Résolution de chemins kernel, Masquage du module et Logs kernel.

Commandes

Le module n'implémente pas un shell dans le noyau. Il lance /bin/sh -c via call_usermodehelper.

La sortie standard et les erreurs sont redirigées dans /opt/wlkom_data/.cmd.out, puis relues avec kernel_read. C'est plus simple qu'un pipe complet géré côté kernel.

Transferts

Les transferts utilisent un protocole texte avec base64 et chunks. Ce format reste compatible avec la boucle commande/réponse existante.

Un protocole binaire serait plus efficace pour les gros fichiers, mais il demanderait plus de logique dans le module et dans le backend.

Persistance

La persistance repose sur les mécanismes Linux standard: /etc/modules-load.d/wlkom.conf pour charger le module et /etc/modprobe.d/wlkom.conf pour appliquer les options.

Le script d'installation valide d'abord le vermagic, prépare ensuite la persistance, puis lance insmod. Si le chargement échoue, il supprime les fichiers préparés. Cet ordre évite deux pièges: une configuration de boot incompatible, et le blocage provoqué par le rootkit déjà chargé qui protège /lib/modules/.../wlkom.ko.

Interface attaquante

Le backend Flask maintient la socket C2, applique la crypto et expose l'API. Le frontend React affiche l'état, l'authentification, les commandes, les transferts et les captures.

Une CLI seule serait plus courte, mais elle rendrait moins visibles les états importants: socket pending, erreurs d'authentification, offsets crypto et historique.

Choix écartés

Compiler côté victime aurait supprimé naturellement le problème de vermagic, mais ça ajoutait une étape lente et pénible au mauvais endroit. Pour une démo, attendre une compilation, gérer les headers et expliquer les erreurs de build côté victime n'apportait pas grand-chose. On a donc choisi la solution la plus pratique: compiler côté attaquant, puis vérifier la compatibilité avant le chargement.

Utiliser DHCP partout aurait réduit la configuration réseau. Le module aurait alors dû découvrir l'attaquant ou recevoir l'adresse dynamiquement, ce qui complique les tests et les captures.

Un bind shell côté victime aurait été plus direct. La connexion reverse est préférable ici, car elle ne nécessite aucun port ouvert côté victime.

TLS directement dans le noyau aurait ajouté beaucoup de complexité. Le XOR/FNV-1a garde le protocole court, tout en laissant ses limites visibles.

Cacher des fichiers entiers aurait été plus agressif. Le filtrage ciblé préserve davantage le comportement normal du système.

Bloquer mmap sur les fichiers de persistance a été gardé, car sinon un programme pourrait éviter read et voir les lignes brutes.

Tentatives abandonnées

Cette section liste les essais qui ont influencé la version finale, même quand ils n'ont pas été gardés. L'objectif n'était pas seulement d'empiler des fonctionnalités, mais de stabiliser une démonstration reproductible.

TentativeProblème observéDécision finale
Autoinstall UbuntuTrop lent et fragile selon les machines de test. Les échecs étaient difficiles à diagnostiquer pendant l'installation.Remplacé par une installation graphique Ubuntu Desktop, plus prévisible et plus facile à vérifier visuellement.
Compiler côté victimeTechniquement faisable et pratique pour le vermagic, mais trop lourd pour la démo: installation des headers, toolchain, compilation plus lente et erreurs de build à gérer côté victime. Le gain ne valait pas l'étape en plus.Compilation côté attaquant par pragmatisme: build dans l'environnement préparé, téléchargement du .ko par la victime, puis vérification vermagic avant insmod.
Upload/download non chunkésLes commandes dépassaient rapidement WLKOM_RX_SIZE, surtout après encodage base64.Protocole chunké avec upload-append et download-chunk.
Screenshot directTrop dépendant de Wayland, X11, des variables d'environnement et de l'outil disponible.Stratégie best-effort multi-outils: gnome-screenshot, scrot, import, puis xwd + convert.
Crypto sans offsetTCP fragmentait ou regroupait les messages. Réinitialiser le XOR à chaque lecture cassait le déchiffrement.Offsets séparés en émission et réception pour traiter TCP comme un flux continu.
Persistance écrite après insmodLe module actif protège /lib/modules/.../wlkom.ko. L'installateur pouvait se faire refuser l'écriture de son propre fichier persistant.freecloude.sh valide vermagic, prépare la persistance avant insmod, puis rollback si le chargement échoue.