Vue d'ensemble du rootkit
wlkom.ko est un module kernel Linux découpé par responsabilité. L'objectif de cette page est d'expliquer le chemin principal du module et pourquoi chaque étape existe.
Organisation des sources
| Fichier ou dossier | Responsabilité |
|---|---|
src/core/main.c | module_init, module_exit, ordre de démarrage, masquage module |
src/core/module_parameters.c | paramètres attacker_ip, command_port, password |
src/core/symbols.c | résolution de symboles kernel via kallsyms_lookup_name récupéré par kprobe |
src/crypto/xor_stream.c | FNV-1a 64-bit, clé 16 octets, XOR de flux |
src/hooks/ftrace_hooks.c | installation des hooks ftrace |
src/hooks/dirent_hooks.c | hooks getdents et getdents64 |
src/hooks/dirent_filter.c | suppression des records linux_dirent masqués |
src/hooks/hidden_names.c | règles de noms et chemins à masquer |
src/hooks/read_filter.c | filtrage read, pread64, readv et refus mmap |
src/hooks/access_hooks.c | blocage d'accès direct par chemin |
src/network/ | socket kernel, handshake, chiffrement réseau, routage de commandes |
src/commands/ | exécution shell, upload/download, screenshot |
src/storage/hidden_directory.c | création de /opt/wlkom_data |
src/userland/ | call_usermodehelper, suppression et lecture de fichiers |
include/ | headers, tailles de buffers et prototypes |
Paramètres module
| Paramètre | Défaut | Permission | Usage |
|---|---|---|---|
attacker_ip | 192.168.100.1 | 0444 | IP que le module contacte |
command_port | 4444 | 0444 | port TCP du listener |
password | aucun | 0000 | secret obligatoire, non exposé via sysfs |
Le module refuse de démarrer si password est vide:
wlkom: password parameter is required
Ce refus est volontaire. Sans secret, le module ne peut ni authentifier l'opérateur ni dériver la clé réseau.
Init du module
Code réel dans src/core/main.c:
Pourquoi cet ordre:
- la clé est dérivée avant tout échange réseau
- les hooks sont installés avant l'usage intensif des artefacts
wlkom - le dossier de travail est créé avant les commandes qui écrivent dedans
- le thread réseau démarre seulement quand les prérequis locaux sont prêts
- le module se masque à la fin, après succès, pour éviter de cacher un chargement partiel.
Si l'installation des hooks échoue, le module refuse de charger. Si le thread réseau ne démarre pas, les hooks sont retirés avant de retourner l'erreur.
Résolution des symboles
Référence à garder ouverte pour cette partie: Kprobes et symboles, Syscalls et pt_regs.
src/core/symbols.c fait deux choses:
- pose un kprobe temporaire sur
kallsyms_lookup_name - utilise cette fonction pour résoudre les symboles demandés.
Si kallsyms_lookup_name n'est pas récupérable, le code tente un kprobe temporaire directement sur le symbole cible.
Ce mécanisme sert au code ftrace pour trouver:
__x64_sys_getdents64
__x64_sys_getdents
__x64_sys_read
__x64_sys_pread64
__x64_sys_readv
__x64_sys_mmap
__x64_sys_openat
__x64_sys_openat2
__x64_sys_newfstatat
__x64_sys_statx
__x64_sys_access
__x64_sys_faccessat
__x64_sys_readlinkat
__x64_sys_unlinkat
Pourquoi ne pas hardcoder les adresses? Elles changent avec la version du noyau, KASLR, la configuration et les symboles disponibles. Le lookup dynamique rend le projet reproductible tant que le noyau cible expose encore les symboles nécessaires.
Hooks ftrace
Les détails bas niveau sont dans ftrace, Macros kernel utiles dans les hooks et Mémoire userland et uaccess.
wlkom_install_syscall_hooks installe les hooks déclarés dans wlkom_syscall_hooks[]:
| Symbole | Hook | Effet |
|---|---|---|
__x64_sys_getdents64 | wlkom_hook_getdents64 | filtre les entrées 64-bit de répertoire |
__x64_sys_getdents | wlkom_hook_getdents | filtre les entrées compat/anciennes |
__x64_sys_read | wlkom_hook_read | filtre les lectures simples de persistance |
__x64_sys_pread64 | wlkom_hook_pread64 | filtre une lecture avec offset explicite |
__x64_sys_readv | wlkom_hook_readv | filtre une lecture répartie dans plusieurs buffers |
__x64_sys_mmap | wlkom_hook_mmap | refuse le mapping direct des fichiers de persistance |
__x64_sys_openat | wlkom_hook_openat | bloque l'ouverture des chemins protégés |
__x64_sys_openat2 | wlkom_hook_openat2 | bloque la variante moderne de openat |
__x64_sys_newfstatat | wlkom_hook_newfstatat | bloque la lecture de métadonnées par chemin |
__x64_sys_statx | wlkom_hook_statx | bloque la variante moderne de stat |
__x64_sys_access | wlkom_hook_access | bloque les tests d'existence/accessibilité |
__x64_sys_faccessat | wlkom_hook_faccessat | bloque les tests d'accès avec dirfd |
__x64_sys_readlinkat | wlkom_hook_readlinkat | bloque les symlinks vers des zones protégées |
__x64_sys_unlinkat | wlkom_hook_unlinkat | bloque la suppression directe des chemins protégés |
Chaque hook conserve l'adresse originale. Le thunk ftrace vérifie aussi:
if (!within_module(parent_ip, THIS_MODULE))
registers->ip = hook_function
Cette condition évite de rediriger les appels internes venus du module lui-même et limite les boucles de récursion.
Image mentale:
programme -> syscall Linux -> ftrace -> hook WLKOM -> syscall original si autorisé
Le hook n'est donc pas obligé de remplacer tout Linux. Il peut juste vérifier, filtrer, puis rappeler le comportement normal.
Masquage de répertoires
Le hook getdents* prépare d'abord un buffer de filtrage, appelle le syscall
original seulement si ce buffer existe, puis filtre le contenu retourné.
Règles de masquage:
nom contenant "wlkom"
nom égal à "wlkom_data"
nom commençant par ".wlkom"
artefacts runtime: .cmd.out, .screen.png, .screen.txt
chemin situé sous /opt/wlkom_data
Pourquoi cela fonctionne? ls, find, beaucoup d'explorateurs et de bibliothèques lisent les dossiers via getdents ou getdents64. En retirant les records avant leur retour en espace utilisateur, on masque les fichiers pour de nombreux outils à la fois.
Comment le buffer est modifié:
- si l'entrée masquée est la première,
memmovetasse le buffer - sinon, le record précédent augmente
d_reclenpour sauter l'entrée masquée - le buffer filtré est recopié vers l'espace utilisateur.
Masquage de lignes
wlkom_hook_read, wlkom_hook_pread64 et wlkom_hook_readv ne filtrent pas toutes les lectures. Ils vérifient d'abord que le descripteur pointe vers un fichier de persistance:
wlkom.conf
/etc/modules
/etc/modules-load.d/*
/etc/modprobe.d/*
Puis il retire seulement les lignes contenant:
wlkom
/opt/wlkom_data
Pourquoi ne pas cacher tout le fichier? Un fichier vide ou illisible serait suspect et casserait des outils normaux. Le filtre conserve le reste du contenu et masque uniquement les lignes ajoutées par l'installation.
mmap est traité à part. Si un programme mappe directement un fichier de persistance, il peut lire son contenu sans passer par read. Le module refuse donc mmap sur ces fichiers avec -EACCES.
Blocage des accès directs
Le listing ne suffit pas. Même si ls /opt ne montre pas wlkom_data, quelqu'un peut essayer:
cat /opt/wlkom_data/.cmd.out
stat /lib/modules/$(uname -r)/kernel/drivers/wlkom.ko
readlink /sys/module/wlkom
rm /lib/modules/$(uname -r)/kernel/drivers/wlkom.ko
access_hooks.c ajoute donc une deuxième couche. Le hook récupère le chemin utilisateur, vérifie le chemin brut, le dirfd éventuel, puis le chemin résolu avec user_path_at() et d_path(). Si une de ces vues touche une zone protégée, le syscall retourne -EACCES.
Les chemins protégés principaux sont:
/opt/wlkom_data/sys/module/wlkom- tout chemin contenant un segment
wlkom_data - le fichier
wlkom.kodans/lib/modules/.
Le compteur atomique wlkom_bypass_access désactive temporairement ce filtre pendant les opérations internes du module. Sans ce bypass, le module pourrait se bloquer lui-même en lisant .cmd.out ou en supprimant un fichier temporaire.
Masquage du module
Après initialisation réussie:
list_del_init(&THIS_MODULE->list)
lsmod et /proc/modules s'appuient sur les vues classiques des modules chargés. Retirer l'entrée de cette liste suffit à perturber ces outils.
Limite importante: cela ne supprime pas toutes les traces. Selon le moment et l'outil, il peut rester des logs dmesg, des entrées sysfs ou des références internes. Le masquage reste partiel.
Thread réseau
Pour cette partie, les notions kernel utiles sont Kthreads, Credentials kernel, Sockets kernel et kvec/msghdr.
wlkom_network_thread tourne dans un kthread séparé pour que module_init puisse rendre la main au noyau.
Il fait:
- prépare des credentials privilégiés avec
prepare_kernel_cred(NULL)puiscommit_creds - alloue
receive_bufferde160 KiB - alloue
send_bufferde4 MiB - tente
sock_create_kern+kernel_connect - mémorise la socket active pour permettre un arrêt forcé
- remet les offsets crypto à zéro pour chaque nouvelle socket
- lance le handshake
- route les commandes jusqu'à déconnexion ou arrêt du module
- oublie la socket active, la ferme et recommence.
La boucle respecte kthread_should_stop() pour sortir quand le module est arrêté. Avant d'attendre le thread avec kthread_stop, wlkom_stop_network_thread appelle kernel_sock_shutdown(..., SHUT_RDWR) sur la socket active. Cela force un kernel_recvmsg() bloqué à revenir, au lieu de laisser module_exit attendre indéfiniment une ligne réseau.
Le thread est séparé de module_init. C'est essentiel: module_init doit rendre la main au noyau. Il ne doit pas rester coincé dans une boucle de connexion TCP.
Exécution userland
Les commandes ne sont pas interprétées dans le noyau. Le module lance:
/bin/sh -c "<commande>"
avec call_usermodehelper(..., UMH_WAIT_PROC).
La sortie est capturée dans:
/opt/wlkom_data/.cmd.out
Puis relue avec kernel_read. Le module cherche le dernier marqueur:
[exit:N]
Il en extrait le code de retour, retire le marqueur de la sortie et renvoie le résultat chiffré au backend.
Le routeur ajoute ensuite un marqueur de fin de réponse:
__KOZACI_RESPONSE_END_8F8F6A0E6D4A41C2__
Le backend attend ce marqueur, puis consomme le prompt suivant. Cela évite de confondre une sortie utilisateur avec la fin de la réponse.
Dossier de travail
Le dossier:
/opt/wlkom_data
sert à stocker:
.cmd.out
.screen.png
.screen.txt
Il est créé par un helper userland:
mkdir -p /opt/wlkom_data && chmod 700 /opt/wlkom_data
Pourquoi utiliser un helper? Ici, c'est plus simple et lisible que réimplémenter toute la création de répertoire via les primitives VFS noyau.
Arrêt
wlkom_stop_module:
- demande l'arrêt de la socket active avec
kernel_sock_shutdown(..., SHUT_RDWR) - attend la fin du thread réseau avec
kthread_stop - retire les hooks
ftrace - loggue le déchargement.
En pratique, comme le module se retire de la liste des modules, un rmmod classique peut être difficile. Le script de désinstallation supprime surtout la persistance et recommande un reboot.
Pour approfondir
- Fonctionnalités rootkit pour chaque fonction visible côté dashboard.
- Canal reverse et commandes pour le thread réseau, le handshake et la boucle commande/réponse.
- Hooks et masquage pour
ftrace,getdents,read,openat,mmapet les règles de filtrage. - Actions userland pour
/bin/sh, les fichiers temporaires, les transferts et les screenshots. - Références techniques pour
ftrace, kprobes, kthreads, credentials et sockets kernel. - Choix du noyau pour comprendre le lien entre hooks internes,
vermagicet noyau fixe.