Bonjour, membres de Habr! La machine virtuelle BPF est l'un des composants les plus importants du noyau Linux. Son utilisation appropriĂ©e permettra aux ingĂ©nieurs systĂšmes de dĂ©tecter des pannes et de rĂ©soudre mĂȘme les problĂšmes les plus complexes. Vous apprendrez Ă crĂ©er des programmes qui suivent et modifient le comportement du noyau, Ă intĂ©grer du code pour surveiller en toute sĂ©curitĂ© les Ă©vĂ©nements du noyau et bien plus encore. David Calavera et Lorenzo Fontana vous aideront Ă exploiter les capacitĂ©s de BPF. Ălargissez vos connaissances sur l'optimisation des performances, les rĂ©seaux, la sĂ©curitĂ©. â Utilisez BPF pour suivre et modifier le comportement du noyau Linux. â IntĂ©grez du code pour surveiller en toute sĂ©curitĂ© les Ă©vĂ©nements du noyau, sans avoir besoin de recompiler le noyau ou de redĂ©marrer le systĂšme. â Profitez d'exemples de code pratiques en C, Go ou Python. â Prenez le contrĂŽle en maĂźtrisant le cycle de vie du programme BPF.
Sécurité du noyau Linux, ses capacités et Seccomp
BPF offre un moyen puissant d'Ă©tendre le noyau sans compromettre la stabilitĂ©, la sĂ©curitĂ© et la vitesse. Pour cette raison, les dĂ©veloppeurs du noyau ont pensĂ© qu'il serait intĂ©ressant d'utiliser sa polyvalence pour amĂ©liorer l'isolation des processus dans Seccomp en mettant en Ćuvre des filtres Seccomp soutenus par des programmes BPF, connus sous le nom de Seccomp BPF. Dans ce chapitre, nous expliquerons ce qu'est Seccomp et comment il est appliquĂ©. Ensuite, vous apprendrez Ă Ă©crire des filtres Seccomp Ă l'aide de programmes BPF. Nous examinerons ensuite les piĂšges BPF intĂ©grĂ©s qui existent dans le noyau pour les modules de sĂ©curitĂ© Linux.
Les modules de sĂ©curitĂ© Linux (LSM) sont une plateforme offrant un ensemble de fonctionnalitĂ©s qui peuvent ĂȘtre utilisĂ©es pour la mise en Ćuvre standardisĂ©e de divers modĂšles de sĂ©curitĂ©. LSM peut ĂȘtre utilisĂ© directement dans l'arborescence du code source du noyau, comme Apparmor, SELinux et Tomoyo.
Commençons par discuter des capacités de Linux.
Fonctionnalités
L'essence des capacités de Linux repose sur le fait que vous devez donner à un processus non privilégié l'autorisation d'exécuter une tùche spécifique, mais sans utiliser suid à cette fin, ou autrement devenir un processus privilégié, réduisant ainsi les possibilités d'attaque et permettant au processus d'exécuter certaines tùches. Par exemple, si votre application doit ouvrir un port privilégié, disons le 80, au lieu de lancer le processus en tant que root, vous pouvez simplement lui accorder la possibilité CAP_NET_BIND_SERVICE.
Considérons un programme Go nommé main.go :
package main
import (
"net/http"
"log"
)
func main() {
log.Fatalf("%v", http.ListenAndServe(":80", nil))
}Ce programme sert un serveur HTTP sur le port 80 (c'est un port privilégié). Normalement, nous l'exécutons immédiatement aprÚs la compilation :
$ go build -o capabilities main.go
$ ./capabilitiesCependant, comme nous ne fournissons pas de privilĂšges root, ce code renverra une erreur lors de la liaison au port :
2019/04/25 23:17:06 listen tcp :80: bind: permission denied
exit status 1capsh (outil de gestion de shell) est un outil qui lance un shell avec un certain ensemble de capacités.
Dans ce cas, comme mentionné, au lieu de fournir des droits root complets, nous pouvons permettre la liaison des ports privilégiés en accordant la capacité cap_net_bind_service ainsi que toutes les autres déjà présentes dans le programme. Pour cela, nous pouvons envelopper notre programme dans capsh :
# capsh --caps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep'
--keep=1 --user="nobody"
--addamb=cap_net_bind_service -- -c "./capabilities"Décomposons un peu cette commande.
- capsh â nous utilisons capsh comme shell.
- âcaps='cap_net_bind_service+eip cap_setpcap,cap_setuid,cap_setgid+ep' â puisque nous devons changer d'utilisateur (nous ne voulons pas ĂȘtre exĂ©cutĂ©s avec les droits root), nous indiquerons cap_net_bind_service et la capacitĂ© de changer effectivement l'identifiant utilisateur de root Ă nobody, Ă savoir cap_setuid et cap_setgid.
- âkeep=1 â nous voulons conserver les capacitĂ©s Ă©tablies lors de la transition depuis le compte root.
- âuser="nobody" â l'utilisateur final qui exĂ©cutera le programme sera nobody.
- âaddamb=cap_net_bind_service â nous dĂ©finissons le nettoyage des capacitĂ©s associĂ©es aprĂšs le passage en mode root.
- â -c "./capabilities" â nous exĂ©cutons simplement le programme.
Les capacitĂ©s associĂ©es sont un type spĂ©cial de capacitĂ©s hĂ©ritĂ©es par les programmes enfants lorsque le programme actuel les exĂ©cute via execve(). Seules les capacitĂ©s autorisĂ©es comme associĂ©es, ou, en d'autres termes, comme capacitĂ©s d'environnement, peuvent ĂȘtre hĂ©ritĂ©es.
Vous vous demandez probablement ce que signifie +eip aprĂšs avoir indiquĂ© une capacitĂ© dans l'option âcaps. Ces drapeaux sont utilisĂ©s pour dĂ©terminer que la capacitĂ© :
- doit ĂȘtre activĂ©e (p) ;
- est disponible pour l'application (e) ;
- peut ĂȘtre hĂ©ritĂ©e par les processus enfants (i).
Comme nous voulons utiliser cap_net_bind_service, nous devons le faire avec le drapeau e. Ensuite, nous lancerons un shell dans la commande. Cela exécutera le fichier binaire capabilities, et nous devons le marquer avec le drapeau i. Enfin, nous voulons que la capacité soit activée (nous l'avons fait sans changer l'UID) avec p. Cela ressemble à cap_net_bind_service+eip.
Vous pouvez vérifier le résultat avec ss. Nous allons réduire légÚrement la sortie pour qu'elle tienne sur la page, mais elle montrera le port associé et l'identifiant utilisateur différent de 0, dans ce cas précis 65 534 :
# ss -tulpn -e -H | cut -d' ' -f17-
128 *:80 *:*
users:(("capabilities",pid=30040,fd=3)) uid:65534 ino:11311579 sk:2c v6only:0Dans cet exemple, nous avons utilisé capsh, mais vous pouvez écrire un shell avec libcap. Pour plus d'informations, consultez man 3 libcap.
Lors de l'écriture de programmes, le développeur ne connaßt souvent pas à l'avance toutes les capacités nécessaires au programme pendant l'exécution ; de plus, dans les nouvelles versions, ces capacités peuvent changer.
Pour mieux comprendre les capacités de notre programme, nous pouvons prendre l'outil BCC capable, qui installe un kprobe pour la fonction noyau cap_capable :
/usr/share/bcc/tools/capable
TIME UID PID TID COMM CAP NAME AUDIT
10:12:53 0 424 424 systemd-udevd 12 CAP_NET_ADMIN 1
10:12:57 0 1103 1101 timesync 25 CAP_SYS_TIME 1
10:12:57 0 19545 19545 capabilities 10 CAP_NET_BIND_SERVICE 1Nous pouvons obtenir le mĂȘme rĂ©sultat en utilisant bpftrace avec un kprobe en une ligne dans la fonction noyau cap_capable :
bpftrace -e
'kprobe:cap_capable {
time("%H:%M:%S ");
printf("%-6d %-6d %-16s %-4d %dn", uid, pid, comm, arg2, arg3);
}'
| grep -i capabilitiesCela affichera quelque chose comme ce qui suit, si les capacités de notre programme sont activées aprÚs le kprobe :
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 21 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 12 0
12:01:56 1000 13524 capabilities 10 1La cinquiÚme colonne contient les capacités dont le processus a besoin, et comme ces résultats incluent également des événements non audités, nous voyons toutes les vérifications non auditées et enfin la capacité requise avec le drapeau d'audit (le dernier dans la sortie) défini sur 1. La capacité qui nous intéresse est CAP_NET_BIND_SERVICE, elle est définie comme une constante dans le code source du noyau dans le fichier include/uapi/linux/ability.h avec l'identifiant 10 :
/* Allows binding to TCP/UDP sockets below 1024 */
/* Allows binding to ATM VCIs below 32 */
#define CAP_NET_BIND_SERVICE 10<source lang="go">Les capacitĂ©s sont souvent utilisĂ©es lors de l'exĂ©cution de conteneurs, tels que runC ou Docker, pour qu'ils fonctionnent en mode non privilĂ©giĂ©, mais on ne leur accorde que les capacitĂ©s nĂ©cessaires au fonctionnement de la plupart des applications. Lorsque l'application nĂ©cessite des capacitĂ©s spĂ©cifiques, celles-ci peuvent ĂȘtre accordĂ©es dans Docker avec âcap-add :
docker run -it --rm --cap-add=NET_ADMIN ubuntu ip link add dummy0 type dummyCette commande donnera au conteneur la capacité CAP_NET_ADMIN, ce qui lui permettra de configurer le lien réseau pour ajouter l'interface dummy0.
La section suivante montre l'utilisation de capacités telles que le filtrage, mais avec une méthode différente qui nous permettra d'implémenter nos propres filtres par programmation.
Seccomp
Seccomp signifie Secure Computing, c'est un niveau de sĂ©curitĂ© mis en Ćuvre dans le noyau Linux qui permet aux dĂ©veloppeurs de filtrer certains appels systĂšme. Bien que Seccomp soit comparable aux capacitĂ©s Linux, sa capacitĂ© Ă gĂ©rer certains appels systĂšme le rend beaucoup plus flexible par rapport Ă eux.
Seccomp et les capacités Linux ne s'excluent pas mutuellement, ils sont souvent utilisés ensemble pour bénéficier des deux approches. Par exemple, vous pouvez souhaiter accorder au processus la capacité CAP_NET_ADMIN, mais ne pas lui permettre d'accepter des connexions via un socket, en bloquant les appels systÚme accept et accept4.
La mĂ©thode de filtrage Seccomp repose sur des filtres BPF opĂ©rant en mode SECCOMP_MODE_FILTER, et le filtrage des appels systĂšme est effectuĂ© de la mĂȘme maniĂšre que pour les paquets.
Les filtres Seccomp sont chargés à l'aide de prctl via l'opération PR_SET_SECCOMP. Ces filtres prennent la forme d'un programme BPF qui est exécuté pour chaque paquet Seccomp présenté à l'aide de la structure seccomp_data. Cette structure contient l'architecture de référence, le pointeur d'instruction du processeur lors de l'appel systÚme et jusqu'à six arguments d'appel systÚme, exprimés sous forme de uint64.
Voici Ă quoi ressemble la structure seccomp_data dans le code source du noyau dans le fichier linux/seccomp.h :
struct seccomp_data {
int nr;
__u32 arch;
__u64 instruction_pointer;
__u64 args[6];
};Comme on peut le voir dans cette structure, nous pouvons filtrer par appel systĂšme, ses arguments ou leur combinaison.
AprÚs avoir reçu chaque paquet, le filtre Seccomp doit effectuer un traitement pour prendre une décision finale et informer le noyau de la suite à donner. La décision finale est exprimée par l'une des valeurs de retour (codes d'état).
â SECCOMP_RET_KILL_PROCESS â terminaison de l'ensemble du processus immĂ©diatement aprĂšs le filtrage de l'appel systĂšme, qui ne s'exĂ©cute donc pas.
â SECCOMP_RET_KILL_THREAD â terminaison du thread courant immĂ©diatement aprĂšs le filtrage de l'appel systĂšme, qui ne s'exĂ©cute donc pas.
â SECCOMP_RET_KILL â alias pour SECCOMP_RET_KILL_THREAD, laissĂ© pour des raisons de compatibilitĂ©.
â SECCOMP_RET_TRAP â l'appel systĂšme est interdit et le signal SIGSYS (Bad System Call) est envoyĂ© Ă la tĂąche qui l'a appelĂ©.
â SECCOMP_RET_ERRNO â l'appel systĂšme n'est pas exĂ©cutĂ© et une partie de la valeur retournĂ©e par le filtre SECCOMP_RET_DATA est transmise Ă l'espace utilisateur comme valeur errno. DiffĂ©rentes valeurs errno sont retournĂ©es selon la cause de l'erreur. Une liste des numĂ©ros d'erreur est fournie dans la section suivante.
â SECCOMP_RET_TRACE â utilisĂ© pour notifier le traceur ptrace avec â PTRACE_O_TRACESECCOMP afin d'intercepter l'exĂ©cution de l'appel systĂšme pour voir et contrĂŽler ce processus. Si le traceur n'est pas connectĂ©, une erreur est retournĂ©e, errno est dĂ©fini sur -ENOSYS, et l'appel systĂšme n'est pas exĂ©cutĂ©.
â SECCOMP_RET_LOG â l'appel systĂšme est autorisĂ© et enregistrĂ© dans le journal.
â SECCOMP_RET_ALLOW â l'appel systĂšme est simplement autorisĂ©.
ptrace est un appel systĂšme permettant de mettre en Ćuvre des mĂ©canismes de traçage dans un processus appelĂ© tracee, avec la possibilitĂ© d'observer et de contrĂŽler l'exĂ©cution du processus. Le programme de traçage peut influencer efficacement l'exĂ©cution et modifier les registres mĂ©moire de tracee. Dans le contexte de Seccomp, ptrace est utilisĂ© lorsque le code d'Ă©tat SECCOMP_RET_TRACE est dĂ©clenchĂ©, permettant ainsi au traceur d'empĂȘcher l'exĂ©cution de l'appel systĂšme et de mettre en Ćuvre sa propre logique.
Erreurs Seccomp
De temps à autre, en travaillant avec Seccomp, vous rencontrerez différentes erreurs qui sont identifiées par la valeur de retour de type SECCOMP_RET_ERRNO. Pour signaler une erreur, l'appel systÚme seccomp retournera -1 au lieu de 0.
Les erreurs suivantes sont possibles :
â EACCESS â l'appelant n'est pas autorisĂ© Ă effectuer un appel systĂšme. Cela se produit gĂ©nĂ©ralement parce qu'il n'a pas les privilĂšges CAP_SYS_ADMIN ou que no_new_privs n'est pas dĂ©fini via prctl (nous en parlerons plus tard);
â EFAULT â les arguments transfĂ©rĂ©s (args dans la structure seccomp_data) n'ont pas d'adresse valide;
â EINVAL â il peut y avoir quatre raisons ici :
- l'opération demandée est inconnue ou non prise en charge par le noyau dans la configuration actuelle;
- les drapeaux spécifiés sont invalides pour l'opération demandée;
- l'opération implique BPF_ABS, mais il y a des problÚmes avec le décalage spécifié, qui peut dépasser la taille de la structure seccomp_data;
- le nombre d'instructions transmises au filtre dépasse le maximum;
â ENOMEM â mĂ©moire insuffisante pour exĂ©cuter le programme;
â EOPNOTSUPP â l'opĂ©ration a indiquĂ© que l'action SECCOMP_GET_ACTION_AVAIL Ă©tait disponible, cependant le noyau ne prend pas en charge le retour dans les arguments;
â ESRCH â un problĂšme est survenu lors de la synchronisation d'un autre thread;
â ENOSYS â il n'y a pas de traceur attachĂ© Ă l'action SECCOMP_RET_TRACE.
prctl est un appel systÚme qui permet à un programme en espace utilisateur de gérer (définir et obtenir) des aspects spécifiques du processus, tels que le numéro de série des octets, les noms de threads, le mode de calcul sécurisé (Seccomp), les privilÚges, les événements Perf, etc.
Le Seccomp peut sembler ĂȘtre une technologie de bac Ă sable, mais ce n'est pas le cas. Seccomp est un utilitaire qui permet aux utilisateurs de dĂ©velopper un mĂ©canisme de bac Ă sable. Voyons maintenant comment crĂ©er des programmes d'interaction utilisateur Ă l'aide d'un filtre appelĂ© directement par l'appel systĂšme Seccomp.
Exemple de filtre BPF Seccomp
Ici, nous allons montrer comment combiner les deux actions précédemment examinées, à savoir :
â Ă©crire un programme Seccomp BPF qui sera utilisĂ© comme filtre avec diffĂ©rents codes de retour en fonction des dĂ©cisions prises;
â charger le filtre en utilisant prctl.
Tout d'abord, il faut des en-tĂȘtes de la bibliothĂšque standard et du noyau Linux :
#include <errno.h>
#include <linux/audit.h>
#include <linux/bpf.h>
#include <linux/filter.h>
#include <linux/seccomp.h>
#include <linux/unistd.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <unistd.h>Avant d'essayer d'exĂ©cuter cet exemple, nous devons nous assurer que le noyau a Ă©tĂ© compilĂ© avec CONFIG_SECCOMP et CONFIG_SECCOMP_FILTER dĂ©finis sur y. Sur une machine de travail, cela peut ĂȘtre vĂ©rifiĂ© comme suit :
cat /proc/config.gz | zcat | grep -i CONFIG_SECCOMP
Le reste du code est une fonction install_filter, composée de deux parties. La premiÚre partie contient notre liste d'instructions de filtrage BPF :
static int install_filter(int nr, int arch, int error) {
struct sock_filter filter[] = {
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, arch))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3),
BPF_STMT(BPF_LD + BPF_W + BPF_ABS, (offsetof(struct seccomp_data, nr))),
BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)),
BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW),
}; Les instructions sont définies à l'aide des macro-commandes BPF_STMT et BPF_JUMP, définies dans le fichier linux/filter.h.
Passons en revue les instructions.
â BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, arch))) â le systĂšme charge et empile avec BPF_LD sous la forme d'un mot BPF_W, les donnĂ©es du paquet sont situĂ©es Ă un dĂ©calage fixe BPF_ABS.
â BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, arch, 0, 3) â vĂ©rifie Ă l'aide de BPF_JEQ si la valeur de l'architecture dans le registre constant BPF_K est Ă©gale Ă arch. Si c'est le cas, il passe avec un dĂ©calage de 0 Ă l'instruction suivante, sinon, pour gĂ©nĂ©rer une erreur, il sautera avec un dĂ©calage de 3 (dans ce cas), parce que arch ne correspond pas.
â BPF_STMT(BPF_LD + BPF_W + BPF_ABS (offsetof(struct seccomp_data, nr))) â charge et empile avec BPF_LD sous la forme d'un mot BPF_W, qui est le numĂ©ro de l'appel systĂšme situĂ© Ă un dĂ©calage fixe BPF_ABS.
â BPF_JUMP(BPF_JMP + BPF_JEQ + BPF_K, nr, 0, 1) â compare le numĂ©ro de l'appel systĂšme Ă la valeur de la variable nr. S'ils sont Ă©gaux, il passe Ă l'instruction suivante et interdit l'appel systĂšme, sinon il autorise l'appel systĂšme avec SECCOMP_RET_ALLOW.
â BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ERRNO | (error & SECCOMP_RET_DATA)) â termine le programme avec BPF_RET et gĂ©nĂšre une erreur SECCOMP_RET_ERRNO avec le numĂ©ro de la variable err.
â BPF_STMT(BPF_RET + BPF_K, SECCOMP_RET_ALLOW) â termine le programme avec BPF_RET et autorise l'exĂ©cution de l'appel systĂšme avec SECCOMP_RET_ALLOW.
SECCOMP EST UN CBPF
Peut-ĂȘtre vous demandez-vous pourquoi une liste d'instructions est utilisĂ©e plutĂŽt qu'un objet ELF compilĂ© ou un programme en C compilĂ© avec JIT.Il y a deux raisons Ă cela.
⹠Tout d'abord, Seccomp utilise cBPF (classical BPF) et non eBPF, ce qui signifie : il n'a pas de registres, mais seulement un accumulateur pour stocker le dernier résultat des calculs, comme on peut le voir dans l'exemple.
⹠DeuxiÚmement, Seccomp prend un pointeur vers un tableau d'instructions BPF directement et rien de plus. Les macros que nous avons utilisées aident simplement à spécifier ces instructions dans un format compréhensible pour les développeurs.
Si vous avez besoin d'aide supplĂ©mentaire pour comprendre cette construction, envisagez le pseudocode qui fait la mĂȘme chose :
if (arch != AUDIT_ARCH_X86_64) {
return SECCOMP_RET_ALLOW;
}
if (nr == __NR_write) {
return SECCOMP_RET_ERRNO;
}
return SECCOMP_RET_ALLOW;AprÚs avoir défini le code de filtre dans la structure socket_filter, vous devez définir sock_fprog, qui contient le code et la longueur calculée du filtre. Cette structure de données est nécessaire comme argument pour déclarer le fonctionnement du processus par la suite :
struct sock_fprog prog = {
.len = (unsigned short)(sizeof(filter) / sizeof(filter[0])),
.filter = filter,
};Il ne reste plus qu'une chose Ă faire dans la fonction install_filter : charger le programme lui-mĂȘme ! Pour cela, nous utilisons prctl, avec PR_SET_SECCOMP comme option, pour entrer en mode de calcul sĂ©curisĂ©. Ensuite, nous spĂ©cifions au mode de charger le filtre avec SECCOMP_MODE_FILTER, qui est contenu dans la variable prog de type sock_fprog :
if (prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog)) {
perror("prctl(PR_SET_SECCOMP)");
return 1;
}
return 0;
}Enfin, nous pouvons utiliser notre fonction install_filter, mais avant cela, nous devons utiliser prctl pour définir PR_SET_NO_NEW_PRIVS pour l'exécution actuelle, évitant ainsi que les processus fils obtiennent des privilÚges plus larges que ceux des parents. Cela nous permet d'effectuer les appels prctl suivants dans la fonction install_filter sans avoir les droits root.
Nous pouvons maintenant appeler la fonction install_filter. Bloquons tous les appels systÚme write liés à l'architecture X86-64 et donnons simplement la permission, bloquant ainsi toutes les tentatives. AprÚs la mise en place du filtre, continuons l'exécution en utilisant le premier argument :
int main(int argc, char const *argv[]) {
if (prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0)) {
perror("prctl(NO_NEW_PRIVS)");
return 1;
}
install_filter(__NR_write, AUDIT_ARCH_X86_64, EPERM);
return system(argv[1]);
}Commençons. Pour compiler notre programme, nous pouvons utiliser soit clang, soit gcc, dans tous les cas, il s'agit simplement de la compilation du fichier main.c sans options spéciales :
clang main.c -o filter-writeComme dĂ©jĂ mentionnĂ©, nous avons bloquĂ© toutes les Ă©critures dans le programme. Pour vĂ©rifier cela, nous avons besoin d'un programme qui produit quelque chose, et ls semble ĂȘtre un bon candidat. Voici comment il se comporte gĂ©nĂ©ralement :
ls -la
total 36
drwxr-xr-x 2 fntlnz users 4096 Apr 28 21:09 .
drwxr-xr-x 4 fntlnz users 4096 Apr 26 13:01 ..
-rwxr-xr-x 1 fntlnz users 16800 Apr 28 21:09 filter-write
-rw-r--r-- 1 fntlnz users 19 Apr 28 21:09 .gitignore
-rw-r--r-- 1 fntlnz users 1282 Apr 28 21:08 main.c
C'est formidable ! Voici comment utiliser notre programme shell : il suffit de transmettre le programme que nous voulons tester en tant que premier argument :
./filter-write "ls -la"AprÚs exécution, ce programme produit une sortie complÚtement vide. Cependant, nous pouvons appliquer strace pour voir ce qui se passe :
strace -f ./filter-write "ls -la"Le rĂ©sultat est considĂ©rablement abrĂ©gĂ©, mais la partie pertinente montre que les enregistrements sont bloquĂ©s avec l'erreur EPERM â celle que nous avons configurĂ©e. Cela signifie que le programme ne produit aucune sortie, car il ne peut pas accĂ©der Ă l'appel systĂšme write :
[pid 25099] write(2, "ls: ", 4) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "write error", 11) = -1 EPERM (Operation not permitted)
[pid 25099] write(2, "n", 1) = -1 EPERM (Operation not permitted)Vous comprenez maintenant comment fonctionne Seccomp BPF et vous avez une bonne idĂ©e de ce que l'on peut en faire. Mais ne voudrais-vous pas obtenir le mĂȘme rĂ©sultat avec eBPF au lieu de cBPF, pour exploiter toute sa puissance ?
En rĂ©flĂ©chissant aux programmes eBPF, la plupart des gens pensent qu'ils se contentent de les Ă©crire et de les charger avec des privilĂšges d'administrateur. Bien que cette affirmation soit globalement vraie, le noyau met en Ćuvre un ensemble de mĂ©canismes pour protĂ©ger les objets eBPF Ă diffĂ©rents niveaux. Ces mĂ©canismes sont appelĂ©s points de piĂ©geage BPF LSM.
Points de piégeage BPF LSM
Pour garantir un contrĂŽle des Ă©vĂ©nements systĂšmes indĂ©pendant de l'architecture, LSM met en Ćuvre le concept de points de piĂ©geage. Techniquement, l'appel d'un point de piĂ©geage ressemble Ă un appel systĂšme, mais est indĂ©pendant du systĂšme et intĂ©grĂ© Ă l'infrastructure. LSM fournit un nouveau concept, dans lequel le niveau d'abstraction peut aider Ă Ă©viter les problĂšmes rencontrĂ©s lors de l'utilisation d'appels systĂšmes sur diffĂ©rentes architectures.
Au moment de la rédaction de ce livre, le noyau avait sept points de piégeage associés aux programmes BPF, et SELinux était le seul LSM intégré qui les réalisait.
Le code source des points de piégeage est localisé dans l'arborescence du noyau dans le fichier include/linux/security.h :
extern int security_bpf(int cmd, union bpf_attr *attr, unsigned int size);
extern int security_bpf_map(struct bpf_map *map, fmode_t fmode);
extern int security_bpf_prog(struct bpf_prog *prog);
extern int security_bpf_map_alloc(struct bpf_map *map);
extern void security_bpf_map_free(struct bpf_map *map);
extern int security_bpf_prog_alloc(struct bpf_prog_aux *aux);
extern void security_bpf_prog_free(struct bpf_prog_aux *aux);Chacune d'entre elles sera appelée à différents moments de l'exécution :
â security_bpf â effectue une premiĂšre vĂ©rification des appels systĂšme BPF exĂ©cutĂ©s ;
â security_bpf_map â vĂ©rifie quand le noyau retourne un descripteur de fichier pour une carte ;
â security_bpf_prog â vĂ©rifie quand le noyau retourne un descripteur de fichier pour un programme eBPF ;
â security_bpf_map_alloc â vĂ©rifie si le champ de sĂ©curitĂ© Ă l'intĂ©rieur des cartes BPF est initialisĂ© ;
â security_bpf_map_free â vĂ©rifie si le nettoyage du champ de sĂ©curitĂ© Ă l'intĂ©rieur des cartes BPF est effectuĂ© ;
â security_bpf_prog_alloc â vĂ©rifie si le champ de sĂ©curitĂ© Ă l'intĂ©rieur des programmes BPF est initialisĂ© ;
â security_bpf_prog_free â vĂ©rifie si le champ de sĂ©curitĂ© Ă l'intĂ©rieur des programmes BPF est nettoyĂ©.
Maintenant, en voyant tout cela, nous comprenons : l'idée des interceptors LSM BPF est qu'ils peuvent protéger chaque objet eBPF, en garantissant que seuls ceux qui ont les privilÚges appropriés peuvent effectuer des opérations sur les cartes et les programmes.
Résumé
La sécurité n'est pas quelque chose que vous pouvez implanter de maniÚre universelle pour tout ce que vous souhaitez protéger. Il est important de pouvoir protéger les systÚmes à différents niveaux et de différentes maniÚres. Que vous le croyiez ou non, la meilleure façon de sécuriser un systÚme est d'organiser différents niveaux de protection à partir de différentes positions, de sorte que la réduction de la sécurité d'un niveau n'entraßne pas l'accÚs à l'ensemble du systÚme. Les développeurs du noyau ont fait un grand travail en nous fournissant un ensemble de différentes couches et points d'interaction. Nous espérons vous avoir donné une bonne idée de ce que sont les couches et comment utiliser les programmes BPF pour travailler avec elles.
Ă propos des auteurs
David Calavera est directeur technique chez Netlify. Il a travaillé au support de Docker et a participé au développement d'outils comme Runc, Go et BCC, ainsi que d'autres projets open source. Il est connu pour son travail sur les projets Docker et le développement de l'écosystÚme des plugins Docker. David est passionné par les graphiques de flammes et cherche toujours à optimiser les performances.
Lorenzo Fontana Il travaille au sein de l'Ă©quipe de dĂ©veloppeurs de logiciels open source chez Sysdig, oĂč il se concentre principalement sur Falco â un projet de la Cloud Native Computing Foundation qui assure la sĂ©curitĂ© des environnements d'exĂ©cution des conteneurs et la dĂ©tection d'anomalies via un module de noyau et eBPF. Il est passionnĂ© par les systĂšmes distribuĂ©s, les rĂ©seaux dĂ©finis par logiciel, le noyau Linux et l'analyse des performances.
» Pour plus de détails sur le livre, vous pouvez consulter
»
»
Pour les membres de Habr, une remise de 25 % avec le code promotionnel â Linux
AprÚs le paiement de la version papier du livre, un livre électronique sera envoyé par e-mail.
Source : habr.com
