Aller au contenu principal

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 dossierResponsabilité
src/core/main.cmodule_init, module_exit, ordre de démarrage, masquage module
src/core/module_parameters.cparamètres attacker_ip, command_port, password
src/core/symbols.crésolution de symboles kernel via kallsyms_lookup_name récupéré par kprobe
src/crypto/xor_stream.cFNV-1a 64-bit, clé 16 octets, XOR de flux
src/hooks/ftrace_hooks.cinstallation des hooks ftrace
src/hooks/dirent_hooks.chooks getdents et getdents64
src/hooks/dirent_filter.csuppression des records linux_dirent masqués
src/hooks/hidden_names.crègles de noms et chemins à masquer
src/hooks/read_filter.cfiltrage read, pread64, readv et refus mmap
src/hooks/access_hooks.cblocage 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.ccré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ètreDéfautPermissionUsage
attacker_ip192.168.100.10444IP que le module contacte
command_port44440444port TCP du listener
passwordaucun0000secret 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:

  1. pose un kprobe temporaire sur kallsyms_lookup_name
  2. 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[]:

SymboleHookEffet
__x64_sys_getdents64wlkom_hook_getdents64filtre les entrées 64-bit de répertoire
__x64_sys_getdentswlkom_hook_getdentsfiltre les entrées compat/anciennes
__x64_sys_readwlkom_hook_readfiltre les lectures simples de persistance
__x64_sys_pread64wlkom_hook_pread64filtre une lecture avec offset explicite
__x64_sys_readvwlkom_hook_readvfiltre une lecture répartie dans plusieurs buffers
__x64_sys_mmapwlkom_hook_mmaprefuse le mapping direct des fichiers de persistance
__x64_sys_openatwlkom_hook_openatbloque l'ouverture des chemins protégés
__x64_sys_openat2wlkom_hook_openat2bloque la variante moderne de openat
__x64_sys_newfstatatwlkom_hook_newfstatatbloque la lecture de métadonnées par chemin
__x64_sys_statxwlkom_hook_statxbloque la variante moderne de stat
__x64_sys_accesswlkom_hook_accessbloque les tests d'existence/accessibilité
__x64_sys_faccessatwlkom_hook_faccessatbloque les tests d'accès avec dirfd
__x64_sys_readlinkatwlkom_hook_readlinkatbloque les symlinks vers des zones protégées
__x64_sys_unlinkatwlkom_hook_unlinkatbloque 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, memmove tasse le buffer
  • sinon, le record précédent augmente d_reclen pour 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.ko dans /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:

  1. prépare des credentials privilégiés avec prepare_kernel_cred(NULL) puis commit_creds
  2. alloue receive_buffer de 160 KiB
  3. alloue send_buffer de 4 MiB
  4. tente sock_create_kern + kernel_connect
  5. mémorise la socket active pour permettre un arrêt forcé
  6. remet les offsets crypto à zéro pour chaque nouvelle socket
  7. lance le handshake
  8. route les commandes jusqu'à déconnexion ou arrêt du module
  9. 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:

  1. demande l'arrêt de la socket active avec kernel_sock_shutdown(..., SHUT_RDWR)
  2. attend la fin du thread réseau avec kthread_stop
  3. retire les hooks ftrace
  4. 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