Le modèle de processus et de session Linux dans le cadre de l'alerting et du monitoring de sécurité

illustration-radar-security.png

Le modèle de processus Linux, disponible au sein d'Elastic, permet aux utilisateurs de rédiger des règles d'alerting très ciblées et de mieux comprendre ce qui se passe exactement sur leurs serveurs et ordinateurs de bureau Linux.

Dans ce blog, nous vous fournirons le contexte du modèle de processus Linux, un aspect clé de la façon dont les charges de travail Linux sont représentées.

Linux suit le modèle de processus Unix des années 1970, renforcé par les sessions de concept des années 1980, à en juger par le moment où l'appel système setsid() a été introduit par les tout premiers documents POSIX

Le modèle de processus Linux est une bonne option d'enregistrement des charges de travail informatiques (les programmes exécutés) et de rédaction des règles visant à réagir à ces événements. Il offre une représentation claire de qui a fait quoi, quand et sur quel serveur pour l'alerting, la conformité et la recherche de menaces.

La capture des créations de processus, de l'escalade des privilèges et des durées de vie offre des informations détaillées sur la façon dont les applications et services sont implémentés dans leurs modèles normaux d'exécution de programmes. Une fois les modèles d'exécution normaux identifiés, des règles peuvent être rédigées pour envoyer des alertes en cas de modèles d'exécutions anormaux.

Des informations détaillées sur les processus permettent de rédiger des règles très ciblées pour les alertes, ce qui réduit les faux positifs et les fausses alertes.  Cela permet également aux sessions Linux d'être catégorisées dans les listes suivantes :

    • services autonomes lancés au démarrage (par exemple, cron)
    • services fournissant un accès à distance (par exemple, sshd)
    • accès à distance interactif (probablement humain) (par exemple, terminal bash démarré via ssh)
    • accès à distance non interactif (par exemple Ansible qui installe un logiciel via ssh)

Ces catégorisations permettent des règles et une évaluation très précises. On pourra par exemple évaluer toutes les sessions interactives sur des serveurs spécifiques dans un délai sélectionné.

Cet article décrit la façon dont le modèle de processus Linux fonctionne, et vous aidera à rédiger des règles d'alerting et de réponse pour des événements de charge de travail. Pour comprendre les conteneurs ainsi que les espaces de noms et les cgroups qui les composent, il est également essentiel de comprendre d'abord le modèle de processus Linux.

Comparaison de la capture de modèle de processus et des logs d'appel système

Il est plus facile et plus clair de capturer les modifications apportées à un modèle de session en termes de nouveaux processus, de nouvelles sessions, de processus de sortie, etc., que de capturer les appels système utilisés pour décréter ces modifications. Linux possède environ 400 appels système, et ne les restructure pas une fois publiés. Cette approche conserve une interface binaire-programme (ABI) stable, ce qui signifie que les programmes compilés pour être exécutés sur Linux plusieurs années auparavant doivent continuer à s'exécuter sur Linux aujourd'hui sans devoir être reconstruits à partir du code source.

De nombreux appels système sont ajoutés pour améliorer les capacités ou la sécurité plutôt que de restructurer les appels système existants (ce qui évite de briser l'ABI). Résultat : le mappage d'une liste d'appels système classés par ordre chronologique et de leurs paramètres sur les actions logiques qu'ils exécutent nécessite une très forte expertise. De plus, des appels système plus récents, comme ceux de io_uring, rendent possibles la lecture et l'écriture de fichiers et de sockets sans appel système supplémentaire, en utilisant la mémoire mappée entre le noyau et l'espace utilisateur.

En revanche, le modèle de processus est stable (il n'a pas beaucoup changé depuis les années 1970), mais il couvre encore de manière exhaustive les actions réalisées sur un système lorsque l'on inclut un accès fichier, du réseautage et d'autres opérations logiques.

Formation des processus : l'initialisation est le premier processus après le démarrage

Une fois le noyau Linux démarré, il crée un processus spécial appelé "le processus d'initialisation." Un processus incarne l'exécution d'un ou plusieurs programmes. Le processus d'initialisation possède toujours l'id de processus (PID) de 1, et il est exécuté avec un id utilisateur de 0 (racine). La plupart des distributions Linux modernes utilisent systemd comme programme exécutable de leur processus d'initialisation.

Le rôle de l'initialisation est de démarrer les services configurés tels que les bases de données, les serveurs Web et les services d'accès à distance, tels que sshd. Ces services sont généralement contenus dans leurs propres sessions, ce qui simplifie les services de démarrage et d'arrêt en regroupant tous les processus de chaque service sous un id de session (SID) unique.

L'accès à distance, comme via le protocole SSH vers un service sshd, créera une nouvelle session Linux pour l'utilisateur qui y accède. Cette session exécutera initialement le programme que l'utilisateur à distance a demandé (la plupart du temps un shell interactif) et le ou les processus associé(s) auront tous le même SID.

Les mécanismes de création d'un processus

Chaque processus, excepté le processus d'initialisation, possède un seul processus parent. Chaque processus possède un PPID, l'id de processus de son processus parent (0/aucun parent dans le cas de l'initialisation). L'attribution d'un nouveau parent peut avoir lieu si un processus parent se ferme d'une façon qui ne termine pas également le ou les processus enfant(s).

Le processus d'attribution d'un nouveau parent choisit généralement le processus d'initialisation en tant que nouveau processus parent, qui possède un code spécial pour nettoyer derrière ses processus enfants adoptés lorsqu'ils se ferment. Sans cette option et ce code de nettoyage, les processus enfants orphelins deviendraient des processus "zombie" (sérieusement !). Ils traînent en attendant que leur parent les recueille, pour que le processus parent puisse examiner leur code de sortie, qui indique si le programme enfant a bien terminé ses tâches.

L'avènement des "conteneurs", les espaces de noms pid en particulier, a nécessité de pouvoir désigner des processus autres que l'initialisation comme "adoptants secondaires" (des processus disposés à adopter des processus orphelins). Les adoptants secondaires sont généralement le premier processus d'un conteneur. Cela se passe comme ça, car les processus du conteneur ne peuvent pas "voir" les processus des anciens espaces de noms pid (c'est-à-dire que leur valeur PPID n'aurait aucun sens si le processus parent se trouvait dans un ancien espace de nom pid).

Pour créer un processus enfant, le processus parent se clone via l'appel système fork() ou clone(). Après l'appel fork/clone, l'exécution continue immédiatement aussi bien dans le processus parent que dans le processus enfant (en ignorant l'option CLONE_VFORK de vfork() et clone()), mais sur différents chemins de code en vertu de la valeur de code de retour de fork()/clone().

Vous avez bien lu : un appel système fork()/clone() fournit un code de retour dans deux processus différents ! Le processus parent reçoit le PID du processus enfant comme code de retour, et l'enfant reçoit 0, afin que le code partagé du parent et de l'enfant puisse se diviser sur la base de cette valeur. Il existe certaines nuances de clonage avec les processus parents multithread et la mémoire copy-on-write pour l'efficacité, que nous n'avons pas besoin d'élaborer ici. Le processus enfant hérite de l'état de la mémoire du processus parent et de ses fichiers ouverts, ses sockets réseau ainsi que le terminal de contrôle, le cas échéant.

Le processus parent capturera généralement le PID du processus enfant pour monitorer son cycle de vie (voir l'adoption ci-dessus). Le comportement du processus enfant dépend du programme qui s'est cloné (il fournit un chemin d'exécution à suivre sur la base du code de retour de fork()).

Il est possible qu'un serveur Web tel que nginx se clone, créant ainsi un processus enfant pour gérer les connexions http. Dans ces cas-là, le processus enfant n'exécute pas de nouveau programme, mais exécute simplement un chemin de code différent dans le même programme pour gérer les connexions http. Souvenez-vous que la valeur de retour d'un clone ou d'un fork dit au processus enfant qu'il est un enfant afin qu'il puisse choisir ce chemin de code.

Les processus shell interactifs (par exemple, un bash, sh, fish, zsh, etc. avec un terminal de contrôle), qui proviennent probablement d'une session ssh, se clonent lorsqu'une commande est saisie. Le processus enfant, qui exécute toujours un chemin de code à partir du parent/shell, travaille d'arrache-pied à configurer des descripteurs de fichiers pour une redirection IO, à configurer le groupe de processus, et bien plus encore, avant que le chemin de code du processus enfant ne passe l'appel système execve() ou similaire pour exécuter un programme différent au sein de ce processus.

Si vous tapez ls dans votre shell, cela duplique votre shell, la configuration décrite ci-dessous est effectuée par le shell/enfant, puis le programme ls (généralement à partir du fichier /fr/usr/bin/ls) s'exécute pour remplacer les contenus de ce processus par le code machine pour ls. Cet article concernant l'implémentation d'un contrôle de tâche shell fournit de précieuses informations sur les fonctionnements internes des shells et groupes de processus.

Il est important de noter qu'un processus peut appeler execve() plus d'une fois, et que par conséquent, les modèles de données de capture de charge de travail doivent également gérer ces appels. Cela signifie qu'un processus peut se transformer en plusieurs programmes différents avant de sortir, pas seulement son programme de processus parent suivi éventuellement par un programme. Consultez la commande shell exec intégrée pour savoir comment faire cela dans un shell (c'est-à-dire remplacer le programme shell par un autre dans le même processus).

Un autre aspect de l'exécution d'un programme dans un processus est que certains descripteurs de fichiers ouverts (ceux marqués comme close-on-exec) peuvent être fermés avant l'exécution du nouveau programme, pendant que d'autres peuvent rester disponibles pour le nouveau programme. Souvenez-vous qu'un seul appel fork()/clone() fournit un code de retour dans deux processus : le parent et l'enfant. L'appel système execve() est également étrange, dans le sens où un appel execve() qui a abouti ne reçoit aucun code de retour indiquant la réussite, car il provoque l'exécution d'un nouveau programme, de sorte qu'il n'y a nulle part où revenir, sauf si l'appel execve() échoue.

Création de nouvelles sessions

Linux crée actuellement de nouvelles sessions avec un seul appel système, setsid(), qui est appelé par le processus qui devient le nouveau leader de la session. Cet appel système fait souvent partie de l'exécution du chemin de code du processus enfant cloné avant d'exécuter un autre programme dans ce processus (c'est-à-dire qu'il est programmé par, et inclus dans, le code du processus parent). Tous les processus d'une session partagent le même SID, qui est le même que le PID du processus appelé setsid(), également connu sous le nom de leader de la session. En d'autres termes, un leader de session est n'importe quel processus dont le PID correspond au SID. La fermeture d'un processus de leader de session déclenchera la fin de ses groupes de processus enfants immédiats.

Création de nouveaux groupes de processus

Linux utilise des groupes de processus pour identifier un groupe de processus qui collaborent au sein d'une session. Ils partageront tous les mêmes SID et id de groupe de processus (PGID). Le PGID est le PID du leader du groupe de processus. Il n'existe aucun statut spécial pour le leader du groupe de processus ; il peut se fermer sans avoir d'effet sur les autres membres du groupe de processus, qui conserveront le même PGID, même si le processus possédant ce PID n'existe plus.

Veuillez noter que même avec pid-wrap (réutilisation d'un pid récemment utilisé sur des systèmes occupés), le noyau Linux garantit que le pid d'un leader de groupe de processus fermé n'est pas réutilisé avant que tous les membres de ce groupe de processus soient fermés (c'est-à-dire qu'il est impossible que leur PGID fasse accidentellement référence à un nouveau processus).

Les groupes de processus sont précieux pour les commandes de pipeline shell comme :

cat foo.txt | grep bar | wc -l

Cela crée trois processus pour trois programmes différents (cat, grep and wc) et les connecte avec des canaux. Les shells créeront un nouveau groupe de processus même pour des commandes de programme uniques comme Is. L'objectif des groupes de processus est de permettre le ciblage des signaux vers un ensemble de processus et d'identifier un ensemble de processus (le groupe de processus de premier plan) qui bénéficient d'un accès complet en lecture et en écriture au terminal de contrôle de leur session, le cas échéant.

En d'autres termes, Ctrl+C dans votre shell enverra un signal d'interruption à tous les processus dans le groupe de processus de premier plan (la valeur PGID négative en tant que cible pid du signal fait une différence entre le groupe et le processus du leader de groupe de processus lui-même). L'association du terminal de contrôle garantit que les processus qui lisent l'entrée depuis le terminal ne se concurrencent pas et ne causent pas de problème (la sortie du terminal peut être autorisée à partir des groupes de processus qui ne sont pas au premier plan).

Utilisateurs et groupes

Comme indiqué ci-dessus, le processus d'initialisation possède l'id utilisateur 0 (racine). Chaque processus possède un utilisateur et un groupe associés qui peuvent être utilisés pour restreindre l'accès aux appels système et aux fichiers. Les utilisateurs et les groupes possèdent des ids numériques et peuvent avoir un nom associé comme root ou ms. L'utilisateur racine est le superutilisateur qui peut tout faire, et ne doit être utilisé que lorsque c'est absolument nécessaire pour des raisons de sécurité.

Le noyau Linux ne se soucie que des id. Les noms sont facultatifs et fournis uniquement pour un aspect pratique par les fichiers /etc/passwd et /etc/group. Le Name Service Switch (NSS) autorise ces fichiers a bénéficier d'extensions avec des utilisateurs et des groupes issues du protocole LDAP et d'autres répertoires (utilisez getent passwd si vous souhaitez voir l'association de /fr/etc/passwd et d'utilisateurs fournis par NSS).

Chaque processus peut avoir plusieurs utilisateurs et groupes associés (groupes réels, en vigueur, enregistrés et supplémentaires). Consultez les identifiants man 7 pour en savoir plus.

L'utilisation de plus en plus importante de conteneurs dont les systèmes fichiers racines sont définis par des images de conteneurs a augmenté la probabilité que /fr/etc/passwd et /fr/etc/group soient absents ou qu'il leur manque certains noms d'utilisateurs et id de groupe en cours d'utilisation. Puisque le noyau Linux ne se soucie pas des noms, mais uniquement des id, ce n'est pas un problème.

Résumé

Le modèle de processus Linux fournit un moyen précis et succinct de représenter les charges de travail du serveur, ce qui permet ensuite d'avoir des règles et évaluations d'alerting très ciblées. Une interprétation par session facile à comprendre du modèle de processus de votre navigateur fournirait de précieuses informations aux charges de travail de votre serveur.

Vous pouvez vous lancer aujourd'hui avec un essai gratuit d'Elastic Cloud pendant 14 jours. Et si vous préférez une expérience autogérée, vous pouvez choisir de télécharger gratuitement la Suite Elastic.

En savoir plus

Les pages de documentation de Linux sont une excellente source d'information. Les pages de documentation ci-dessous contiennent les informations du modèle de processus Linux décrit ci-dessus :