Restreindre les appels système avec seccomp

Seccomp (SECure COMPuting mode) restreint les appels système qu’un processus est autorisé à effectuer. Son objectif est de minimiser la surface d’attaque du noyau en limitant ses points d’entrée, sachant qu’une part significative des bogues exploitables se logent précisément dans les interfaces de syscalls.

Deux modes

Le mode Strict, le plus ancien, limite le processus à quatre appels système seulement — read(), write(), _exit(), sigreturn() — toute autre tentative provoquant une terminaison immédiate par SIGKILL. Le mode Filter (Seccomp-BPF), bien plus flexible, utilise des programmes Berkeley Packet Filter pour analyser chaque appel système selon son numéro et la valeur de ses arguments immédiats (valeurs de registre).

Un filtre peut répondre de plusieurs façons : SECCOMP_RET_KILL_PROCESS termine immédiatement le processus ; SECCOMP_RET_ERRNO refuse l’appel avec un code d’erreur applicatif ; SECCOMP_RET_TRAP envoie un signal SIGSYS que l’application peut gérer elle-même ; SECCOMP_RET_ALLOW autorise l’exécution normale.

Mise en place

La configuration passe par l’appel système prctl(). Avant tout filtrage, un processus non privilégié doit d’abord verrouiller ses privilèges avec prctl(PR_SET_NO_NEW_PRIVS, 1) — une étape obligatoire, qui garantit qu’un processus ne pourra pas utiliser seccomp pour forcer un binaire setuid à s’exécuter de façon dangereuse. Une fois le filtre défini (structure sock_fprog contenant les instructions BPF) et activé via prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, prog), il devient immuable et se transmet à tous les processus enfants créés par fork() ou clone().

Limites

Seccomp ne peut pas inspecter le contenu des pointeurs — le chemin d’un fichier passé à open(), par exemple — car il n’a pas le droit de déréférencer la mémoire de l’espace utilisateur. Les numéros d’appels système variant d’une architecture à l’autre (x86 contre ARM), un filtre doit impérativement vérifier le champ arch des données seccomp_data, sous peine d’autoriser par erreur un appel dangereux sur une architecture non prévue. Enfin, certains appels « virtuels » comme gettimeofday(), gérés directement en espace utilisateur via le mécanisme vDSO, échappent totalement au contrôle de seccomp puisqu’ils ne transitent jamais par le noyau.

TOCTTOU : quand vérifier n’est pas utiliser

TOCTTOU (Time-Of-Check To Time-Of-Use) désigne une vulnérabilité de concurrence qui survient lorsque l’état d’une ressource change entre le moment où le système vérifie une permission et le moment où il exécute réellement l’opération correspondante.

Le mécanisme

L’attaque repose sur une condition de course en trois temps : la vérification, où un programme privilégié contrôle qu’une opération est autorisée (par exemple, vérifier le droit d’ouvrir un fichier via son chemin) ; l’intervalle, durant lequel un processus concurrent modifie la ressource visée juste après cette vérification ; et l’utilisation, où le noyau exécute l’opération sur une ressource qui n’est plus celle qui a été validée.

L’exemple canonique : le lien symbolique substitué

Un processus privilégié vérifie, via access(), les droits d’un utilisateur sur un fichier temporaire (/tmp/X). Juste après cette vérification, mais avant l’appel à open(), l’attaquant remplace /tmp/X par un lien symbolique pointant vers /etc/shadow. Le système ouvre alors le fichier sensible, croyant travailler sur le fichier temporaire qu’il venait de valider.

Un risque particulier pour les sandboxes basées sur ptrace

Les systèmes qui interceptent les appels système — débogueurs, sandboxes fondées sur ptrace — restent particulièrement exposés lorsque les arguments d’un syscall résident en mémoire utilisateur (chaînes de caractères pour un chemin, structures complexes). Un thread malveillant peut modifier cette mémoire après que le moniteur a terminé sa vérification, mais avant que le noyau ne l’exploite réellement.

Les remédiations

Le framework LSM évite le TOCTTOU en plaçant ses points de contrôle (hooks) à l’intérieur même du noyau, après résolution du nom de fichier en un objet concret — l’inode. La vérification porte ainsi sur l’objet réel que le noyau va effectivement utiliser, jamais sur un nom de chemin susceptible d’être détourné entre-temps.

Seccomp-BPF se protège structurellement en interdisant à ses filtres de déréférencer un pointeur : ils n’examinent que les arguments passés directement par les registres du processeur (valeurs immédiates, descripteurs de fichiers), ce qui rend la modification par un thread tiers tout simplement impossible sur ce type d’argument.

Chromium, pour les arguments en mémoire qu’il ne peut vérifier directement, fait copier ces arguments par un processus de confiance dans son propre espace d’adressage privé avant toute vérification — une fois copiés, ils deviennent immunisés contre toute modification ultérieure par du code non fiable.

Les programmes eBPF ne sont pas épargnés : s’exécutant de manière concurrente sur différents cœurs, ils peuvent eux aussi souffrir de TOCTTOU. La recommandation, développée dans la note sur eBPF, consiste à placer les points d’attache immédiatement après que les données utilisateur ont été copiées vers l’espace noyau.