 Mesdames et messieurs, c'est la toute dernière journée de cette conférence et j'ai l'imense honneur et le plaisir de vous présenter ces deux personnes ici. Jeff Thieleau et Andy Olson qui font vous parler dans cette présentation de tracing dans le carnel avec E-B-P-F. Et pour ceux qui ne comprennent pas ce que c'est, c'est Extended Berkeley Packet Filter. Et ce qu'on va faire là, c'est qu'on va vous présenter ce que ça fait et comment on peut utiliser dans le carnel Linux. Ils vont vous parler de quelques aspects pratiques de cette technologie au-delà de simplement du profiling de code. Et pour moi ce qui m'intéresse le plus, c'est qu'on va un peu jouer au Dieu ici sur cette scène. C'est ça ? On va juste un petit peu avoir le pouvoir qui nous monte à la tête, c'est tout, juste un peu. Et si on joue assez au Dieu, peut-être qu'on verra le Flying Speck des Monsters. Tenez l'interprétation, s'il vous plaît. Bonjour, je suis Jeff et je suis Andy Lee. On est des chercheurs en sécurité, on est des consultants. On fait la recherche, on trouve des bugs, on explique aux gens comment les fixer. Et on a pas mal joué avec E-B-P-F récemment. Donc E-B-P-F, c'est Exonet Package Filter. Pour ceux qui savent pas, c'est Berkeley Package Filter. Donc c'est une machine virtuelle avec du bytecode qui est conçu pour tourner directement dans le carnel et qui est capable de filtrer des paquets de mer très, très, très rapide de faire le package filter très rapidement. E-B-P-F c'est un langage qui n'a rien à voir avec ça. Et c'est conçu pour être compilé just in time. Et ça fonctionne sur à peu près de n'importe quelle architecture de CPU. Donc ça ressemble à un langage très restreint de C, fait vraiment pour être très rapide. Et c'est conçu pour fonctionner mort. N'importe où dans le noyau Linux, à partir du moment, on a besoin de faire certaines choses de manière programmatique, un peu comme avec un script. Donc vous lui envoyez, vous lui dites ce que vous voulez faire et ça vous donne un file descripteur et vous pouvez travailler avec ça. Qu'est-ce que ça ressemble ? Moi si à la conception c'est très horrible, on s'arcie plein de macros, on dirait d'essais. Personne devrait avoir à écrire ça. Mais qu'est-ce que c'est E-B-P-F ? C'est en gros, c'est une application de B-P-F qui met tout direct de côté, qui ajoute plein de registres, plein de fonctionnalités pour rappeler des fonctions dans le noyau de meilleures arbitraires. Et ça s'applique à un tout autre chose. T'as peut vérifier du bytecode. C'est vrai qu'il y a quelque chose pour vérifier le bytecode. C'est pas très bon en fait à vérifier ou à valider, mais sa fonction est principale. C'est les fonctions pour d'assistance qui ont peut-être pas de faire des choses en fonction du contexte dans lequel vous vous trouvez. Par exemple ça permet de lire des zones de mémoire arbitraires dans le noyau. Pourquoi E-B-P-F ? Les performances en premier. Parce que bien sûr, comme vous faites du filtrage de pâquer, vous faites souvent ça au moment ou dans le trafic directement, en ligne directement. Donc vous avez besoin de performance pour pas réduire la vitesse de vos transferts. Et ça s'affordit à un système directement dans le kernel qui permet de faire du filtrage de choses dans le kernel pour réduire le besoin d'avoir des modules kernels custom avec des bugs dedans. Alors attendez, c'est un peu compliqué. Et comme on ajoute des fossilités E-B-P-F, la diffusion n'arrête pas de changer, mais parce que les gens aiment bien ça. Ok, plus simplement, pourquoi on voit ça ? Parce qu'on aime bien mettre des hooks un peu dans tout ce qu'on a, en particulier dans le kernel Linux. Et E-B-P-F nous donne la possibilité de faire ça sans risquer des crashes dans le kernel. Parce que c'est genre de trucs qu'on a tendance à avoir quand on commence à écrire nos propres modules de kernel en C. Et c'est pour ça du coup que cette présentation est sur le tracing dans le kernel. Donc E-B-P-F c'est cool parce que ça a la possibilité de nous donner comme des traces, qui est un système de tracing qu'on trouve dans Solaris et BSD, etc. Et E-B-P-F a un moyen de faire une bonne compétition à des traces. E-B-P-F n'a pas les moyens, comme dans, par exemple, Solaris, d'avoir tout sous forme d'événement. Linux sera pour l'ambassade systématisé, mais on est quand même pas mal. E-B-P-F a beaucoup de fossilités parce que comme on écrit du code qui ressemble presque à du C et qu'ensuite on le compile pour l'exécuter sous forme de fil dans le kernel, on a énormément de flexibilité. Alors, des traces c'est basé sur simplement une ligne de commande et il y a des opérations prédéfinies. E-B-P-F par contre, ça permet de faire plein de trucs complètement fou. Parlons un peu de tracing. Le tracing c'est simplement une forme de logging qui est un petit peu avancée et on s'est donné de logger, par exemple, l'exécution d'un programme. Alors, ça ne nous dit pas grand-chose. Nous, on ne s'intéresse pas spécialement à du logging ici. On veut pouvoir regarder des valeurs et ouquer des trucs et observer des choses et voir ce que ça fait à l'intérieur. Donc, on s'intéresse à ce qu'on appelle du tracing dynamique. Qu'est-ce que c'est le tracing dynamique ? Alors, il y a deux choses. La première, c'est de pouvoir activer et désactiver des fonctionnalités existantes. C'est la première mode. Et le deuxième, c'est de pouvoir ajouter des fonctionnalités arbitraires qui n'étaient pas là avant. Donc, nous, on s'intéresse au deuxième, mais on ne s'intéresse toujours pas vraiment au logging. On s'intéresse à l'instrumentation dynamique. Et il y a deux façons d'instrumenter dynamiquement des choses. La première, c'est ce qu'on avait à faire du hook de fonction. Et le deuxième, c'est d'instrumenter des instructions. Et en fonction de ce que vous voulez faire et de quel est votre objectif quand vous faites du hooking, peut-être que le hooking de fonction est implementé avec de l'instrumentation de l'instruction. Mais uBPF, c'est à peu près ce que ça fait. Donc, le titre réel de cette présentation, c'est d'intéresser à instrumenter l'inux avec uBPF pour du fun et du profit. Mais si on mette ce titre là, ce serait certainement pas accepté, donc on a fait un autre titre à la place. Donc, parlons un petit peu de l'histoire d'UBPF. Donc, quelque chose de plus important, c'est K-Prob, qui est déjà à presque 10 ans d'âge. Et de nos jours, ça vous permet de vous intégrer dans certains endroits du noyau hook. Donc, vous pouvez rajouter de la logique autour de fonctions, par exemple, savoir quand elle se termine. Donc, ça vous permet également de faire des fonctions de single step, breakpoint. On a Ftrace. C'est une API au niveau FastSystem qui vous permet d'ajouter des K-Probs directement dans le carnet. C'est pas très important. Mais c'est pas très important, mais c'est utilisé avec UBPF. Perfevent, c'est plein de choses vraiment folles qui vous permettent de mesurer plein de choses, par exemple, des copies entre le noyau et l'espace userland. TracePoints. Donc, vous pouvez... U-Probs, c'est comme K-Probs, mais pour la mémoire utilisateur. Et UBPF, ça combine toutes ces outils-là ensemble. Toutes ces différentes technologies dans le noyau sont rassembées dans UBPF. Et UBPF vous permet donc de prendre toutes ces données et de les combiner ensemble via une de deux APIs. Donc, typiquement, on va utiliser quelque chose qui permet de charger du code depuis du user space. Et ça, ça permet de faire que on puisse mettre à jour ce code et ensuite envoyer des données dans le kernel pour faire marcher ce code. Alors, la façon que tout ça ça a de marcher, c'est qu'on fait tout un tas de syscalls un peu compliqués et, à la fin, on fait notre appel au syscall.bpf qui lui permet de charger le programme. Et ensuite, on utilise 6FS avec une interface particulière et ça permet de faire K-Probs pour définir une probe. Et ensuite, on peut utiliser PerfEvent et on lui passe l'ID du K-Prob qu'on a créé. À ça, on attache l'ID de notre programme BPF qu'on avait créé et ça permet de l'exécuter. Ça, c'est l'ancienne façon de faire. Il y a une autre façon de le faire maintenant dans laquelle on peut carrément ne pas faire toute la partie avec F-Trace et 13FS. Mais en général, on fait quand même cette méthode-là puisque si on regarde les étapes en bas de cette page, c'est nécessaire, mais parfois, c'est pas nécessaire. Cette méthode, une chose qu'elle fait, c'est qu'elle garde la Perf ouverte pendant toute l'exécution. Donc, une autre façon de ne pas faire de BPF c'est la façon de le faire comme on vient dans SystemD. Et ça, c'est parce qu'écrire du code BPF c'est très difficile. C'est quasiment impossible d'écrire ce code-là. C'est vraiment très difficile. En plus, il n'y a pas de support pour des bibliothèques partagés de haut niveau. Donc, comment on fait ? On utilise BCC. C'est un compilateur de BPF en BPF. Alors, dans cette présentation, je ne vais pas parler beaucoup de BCC. Ce n'est pas le sujet de BCC, mais on va quand même l'utiliser pour apporter tout ce qu'on fait puisque c'est un peu le seul outil qui peut se brancher sur ces API du kernel. La raison pour laquelle c'est le cas, c'est parce que les gens qui écrivent BCC, c'est les mêmes personnes qui écrivent le kernel pour BPF. Donc, ils écrivent un peu leurs propres APIs dont ils ont besoin. Elles ne sont pas spécialement très bien documentées parce qu'il n'y a que quand on a besoin. Même quand il y a de la documentation, ce n'est pas suffisant. Même cette liste de six calls qu'il faut faire, ce n'est pas documenté clairement. J'ai dû aller lire ce qu'il y avait dans BCC pour comprendre comment ça marchait. BCC, c'est un peu la seule option pour vraiment instrumenter ici. Alors, comment on peut écrire un tracer avec BCC ? Généralement, on va utiliser un API que BCC fournit et malheureusement, il va falloir écrire ça comme un seul et unique fichier en Python. Et plus le code devient long, plus c'est compliqué de maintenir. Donc, votre code dans C, généralement, ça va être une chaîne dans votre code Python. Et la structure générale, ça, c'est la structure vue de très haut niveau. C'est globalement, vous avez votre chaîne de caractère, vous dites à BCC de compiler ça en BPF et vous lui dites de l'enregistrer sur certains events, comme par exemple Kprobs ou vos maps d'IO. Et ça vous donne une fonction de callback et tout marche magiquement. Alors, il y a des limites un petit peu compliquées à comprendre qui vont probablement être supprimées dans le futur mais à l'heure actuelle, il y a ces limites qui sont un petit peu embêtantes. Mais bon. Et si on écrit un peu de code, alors voilà un programme BPF qui utilise Kprobs dans lequel on va charger avec BCC. Et voilà quoi ça ressemble. Alors BCC génère énormément de codes de façon cachée. Avec cette syntax avec le double underscore, ça permet de dire à BCC que vous l'utilisez Kprobs sysopen. Et ça c'est une façon de dire quelle syscall vous voulez utiliser. Alors, les noms changent selon la version du kernel que vous avez en particulier récemment, mais la façon globale de marché reste la même. Alors, on écrit ça et ensuite on va juste appeler printk qui est un peu la même chose que un printk comme vous auriez si vous écriviez un module et on l'exécute. Et on n'obtient rien. Alors pourquoi est-ce qu'on n'a rien obtenu ? Ah, mais oui. Lipset. Mince. Alors, la gelipset par le passé a fait plein de choses pour essayer d'éviter d'utiliser le code syscall open. À la place la gelipset essaye d'utiliser open at à la place de façon transparente. Donc à la place on va instrumenter open at et à partir de là on commence à voir ce qui se passe dans votre système. Donc, et si on généralisait ça un peu, pourquoi on s'en va appeler deux fois K-Prob ? Parce qu'en fait, on s'intéresse à une seule fonctionnalité. Parce qu'en fait, open et open at vont au même endroit. Ils appellent sysopen qui est l'implémentation générique dans le kernel pour ces deux choses. Donc on peut instrumenter les deux en même temps. Et dans notre printk, on rajoute un pour son s et là on peut afficher quel passname a été utilisé pour appeler ça. Et ça, c'est... permet de voir plein de trucs qu'ils font dans Pro-KFS on voit SystemD et JournalD qui appellent le truc. SystemD c'est un peu effrayant de voir tout ce que ça fait ici. Mais bon, utiliser this printk ici c'est en fait un peu dangereux. Et la raison pour ça, c'est parce que tout comme ftrass, il y a un seul buffer de log qui est partagé pour tout le système. Donc ça, ça veut dire que ça a plusieurs tracers actifs ils peuvent avoir des payeurs des collisions entre les deux et personne ne sait qu'est-ce qui appartient à qui. Les programmes obbpf ont aussi une risk condition à l'intérieur à cause de ça. Puisqu'à cause de la façon dont obbpf peut être safe, c'est que c'est dans un processus d'exécution qui a créé l'objet, que ça s'exécute et ce processus contrôle le file descriptor. Et globalement quand votre dernier processus qui a ce file descriptor meurt votre programme obbpf est supprimé, déchargé et ça, ça détache toutes les kprobes qui étaient attachées. Et là il y a une risk condition parce que vous, ce programme se termine et si la kprobe est toujours enregistrée ça exécute le code. Ça essaye de logger et votre kprobe va être détachée après mais il n'y avait aucun client pour recevoir le message. Ça reste là, je veux dire que quelqu'un décide de le lire. Et donc la prochaine fois qu'un processus va essayer de tracer quelque chose avec Prinka il va immédiatement voir tous ces messages qui viennent de quelqu'un d'autre. Donc ça c'est un problème. Donc à la place, ce qu'il faut faire c'est qu'on doit tout réécrire pour utiliser votre propre mécanisme de transfert de données. Et donc, une façon de faire ça c'est quand tu dis la fonction le buffer Perf Output de Perfevent. Donc là on a des macros un peu compliqués et on peut aussi mettre en place un espace de stockage temporaire parce que les buffers de BPF, de la stack, c'est seulement 72 octets et nous on utilise des espaces de stockage qui sont plus gros pour ce que nous n'aimons. Et donc ça appart en train. Donc on crée aussi cet espace temporaire donc on utilise ici des systèmes de stockage qui sont parfaitement sûr qui ne changent pas de CPU d'exécution pendant l'exécution. Et on a des notations ici qui permettent de dire comment on accède aux éléments là dedans. Donc ça ça va marcher, ça ne devrait pas échouer mais on doit quand même convaincre le valiateur BPF que c'est le cas. Après ça, on peut copier toutes les données de l'appel à l'espace temporaire et on peut utiliser Perf Submit pour le copier dans le buffer rapide pour que ça soit partagé. Alors il y a beaucoup beaucoup de copies malheureusement si vous êtes familier avec les six types en piton ça c'est une API qui vous permet de vous interfacer entre votre code piton et du code natif. Maintenant on peut définir une structure qui est en gros une réplique de notre structure en C et on enregistre une fonction de Handler qui va être appelée à chaque fois que Perf Submit est exécuté et on peut enregistrer ça sur une table qui s'appelle Output qui est une table qui a été définie en le code en C. Ensuite dans notre Handler on fait un casse, une conversion de notre type de données et à la fin on fait un appel K-PropPole sur cet object qui nous a été fourni par BCC et le nom de cette fonction a changé dans la dernière version de BCC mais c'est toujours la même idée et en gros ça ça va juste poler les Perf event pour nous. Donc comment est-ce que ça marche en pratique ? Ok Regardons un peu la sortie de S-Trace Donc la même chose de regarder ici c'est le BPF ProGlo d'un code en haut donc on récupère le filet de tutor 5 Ensuite tout ces syscoles qu'on voit c'est l'API de F-Trace et la meilleure dont vous vous intéressez avec K-PropPole c'est un c'est vous faites probe A sur l'ID number que vous avez mis suite vous ouvrez Perf Event vous lui passez une config et une séquence d'appel très flexible Ok c'est très, ça ressemble beaucoup à IOCTL Ensuite on prend ça en fait on l'a rappelé vraiment à IOCTL sur ce file descripteur et on attache le programme BPF à ce file descripteur et de cette manière notre programme BPF est associé avec K-Prop qu'est ce que cette BPF a fait vraiment c'est une macro très compliquée mais si vous regardez vraiment elle fait rien ça a créé une structure dynamique et ça en fait une instance mais ça remplit aucun des champs de la structure et c'est bizarre parce que c'est faux et ça fait rien c'est complètement un moque et ça va être remplacé par du code généré c'est juste pour satisfaire le compilateur en fait c'est une manière assez régulière de faire ça ensuite qu'est ce qu'on appelle Perf Submit en fait ça c'est remplacé par une fonction BPF des arguments sont passés parce que c'est pas un flag CPU identifier et du côté qui doit y aller on récupère un objet Perf Event et je dis en disant le CPU sur lequel on est en tant qu'index et dans l'output de S-Trace dans l'output de S-Trace on avait certains de ces éléments qui nous parlaient de BPF et ça se permet d'avoir le mapping avec les files descripteurs donc la première chose qu'on va faire c'est qu'on obtient cet arrêt ici et on obtient un file descripteur qui correspond à cet arrêt qu'on a créé et à la fin on stock les Perf Event donc envers ici on a les 0 et 1, ça c'est les indexes des CPU et donc on obtient 4 files descripteurs pour ça ensuite ce qu'on fait c'est qu'on appelle cette fonction UpdateLM et on utilise le file descripteur qui correspond à l'arrêt Perf Event et ensuite on passe ces numéros chelous en hexadecimal en fait c'est des pointeurs qui correspondent à la clé ça correspond à une structure qui représente le premier CPU et la valeur ça correspond à un pointeur sur une structure dans laquelle la clé c'est 1 et la valeur c'est 9 et ensuite on peut juste appeler Paul sur ces descripteurs des fichiers et tout marche bien ok on va changer un peu de vitesse ici parlons un peu des problèmes avec la validation de BPF le variateur BPF c'est par un endroit sympa pour que BPF soit safe parce que sinon il peut faire plein de trucs complètement fou dans le kernel comme modifier du code au runtime appeler des fonctions, il peut écrire ou lire de la mémoire, il peut faire plein de choses donc il faut le rendre sûr alors le kernel Linux essaye de valider tout le code avant de le charger et donc le kernel fait pas mal de choses il y a des checks qui sont assez simples que tout le monde connaît comme par exemple le code ne peut pas avoir de boucle ou de sauter en arrière ça permet d'éviter les boucles infinies puisque ça s'exécute dans le kernel, c'est pas un processus à part qui exigait donc si cette chose arrive à rentrer à ce bout de l'infini le kernel est complètement bloqué au moins pour ce serait de là et même si votre code ne fait pas de boucle il y a plein de raisons pour les tests ça peut être jeté par le validateur vous pouvez avoir des appels de fonctions qui sont pas statiques inline et si il y a ça vous faites peut-être un jump en avant ou en arrière ou le return vous permettrait de revenir en arrière ou peut-être que la définition de la fonction est avant vous et dans ce cas le call est un jump en arrière en fait donc le kernel n'acceptera pas ça donc on est obligé d'enrôler toutes nos boucles, de les dérouler à l'avance pour que le kernel les accepte mais c'est embêtant parce qu'il y a des optimisations du compilateur qui peuvent être un problème BCC utilise un compilateur qui optimise le code et qui est basé sur LLVM et du coup même si vous par exemple demandez de dérouler la boucle le compilateur généralement va tout dérouler mais en fonction de comment vous modifiez votre code c'est possible que les optimisations changent et que le compilateur soit plus capable de dérouler la boucle et là vous vous retrouvez avec une erreur qui n'a aucun sens parce que la partie du code que vous avez changé n'a rien à voir avec l'erreur que vous recevez maintenant donc le validateur essaye aussi de s'assurer que les fonctions que vous appelez vous leur donnez des arguments qui sont sûrs mais logique pour ça n'est pas forcément très bien il y a tout un tas de choses où vous avez par exemple des limites de tableaux qui ne sont pas forcément les bonnes et le validateur ne s'en rend pas forcément compte de ça la raison pour ça c'est parce que généralement la partie d'optimisation ne voit pas forcément les choses à peut-être enlever des données et le compilateur est plus insigeant que le validateur et ça ça amène à toutes sortes de problèmes une des choses qu'on peut faire c'est qu'il faut rendre le code évidemment sûr avec des bound check mais le compilateur lui a des chances d'enlever ces vérifications inutiles et le validateur ne va pas forcément les voir du coup donc il faut que vous fassiez tout un tas de trucs pour convaincre le compilateur de ne pas enlever votre check pour que le validateur puisse lui les voir et accepter votre code alors ce question maintenant c'est que quand on met à jour bcc ou qu'on met à jour de kernel il y a du code qui est valide qui devient un valide ou du code qui est valide qui devient un valide tout peut se passer ce validateur c'est vraiment bizarre on a du code qui peut être accepté ou rejeté en fonction de si par exemple une fonction retournait un boulet 1 ou un size t et dans les deux cas c'était stocké dans un uint 80 un uint 8 pardon alors il y a d'autres choses par exemple si on commentait des lignes qui n'avait rien à voir avec ça ailleurs dans le code là il était accepté et l'autre non et si on décommentait c'était l'inverse ça n'avait absolument aucun sens donc il y a eu un moment moi ça m'a vraiment énervé et j'ai écrit un module de kernel qui est juste un gros hack qui s'intègre avec le validateur qui passe des parties de la validation pour que je puisse écrire du code moi comme je le voulais parce que un, ce validateur il sait pas ce que c'est avec des limites d'arrêt donc je vais lui dire ce que c'est avec des limites d'arrêt elles sont toutes sûres alors il s'avère que le validateur est très très couplé avec l'interprèteur de WF on ne peut pas juste les désactiver parce que quand l'interprète exécute le code quand le validateur exécute le code il met tout un tas de champs à jour qui vont être utilisés ensuite par l'interprète pendant l'exécution donc il faut s'intégrer assez en profondeur avec pour s'assurer que ces champs soient mis, oui, ensuite vous vous essayez peut-être les réécrire avec d'autres valeurs après qu'ils étaient mis avec d'autres valeurs pour que vous puissiez enfin charger votre code donc on a cette démo de notre module de kernel qu'on a appelé le YOLO-OBPF évidemment donc on a notre propre notation du hooking de fonction parce que c'est utile dans certains cas quand par exemple on a OBPF les API de Ftrass parce que quelqu'un a modifié quelque chose dans leur config de kernel par exemple alors je tiens à dire c'est vraiment spécifique à X8664 on a juste ça on a même du code en binaire qui est dans des strings en C donc c'est même probable que ça ne marche pas dans d'autres versions de kernel typiquement celles qui sont sorties il y a quelques jours c'est quasiment sûr que ça ne marchera pas et si vous avez du code OBPF qui n'est pas sûr, c'est tout à fait possible que ça va cracher votre kernel donc on mettra ce code public mais c'est juste pour faire une démo personne ne devrait utiliser ça à la fin de notre presentation on aura un lien vers notre repos GitHub mais ne l'utilisez pas si vous n'avez pas le luxe de pouvoir utiliser un module aussi hacky comme ça qu'est ce que vous pouvez faire ? des petites choses que vous pouvez faire pour négocier avec le validateur par exemple vous pouvez utiliser la mémoire stack alors ça c'est intéressant il y a des choses qui a beaucoup à voir avec les vulnérabilités par exemple dans les modules de réseau si vous avez par exemple des données qui sont stockées en stack et que ces données il y a du padding entre les champs et qu'il y a juste d'expliquer quelque chose dans un champ et que vous allez lire trop loin vous avez peut-être copié des choses qui étaient dans ces particules de padding OBPF n'aime pas du tout ça et c'est pas vraiment clair pourquoi parce que si vous explique du code OBPF vous êtes déjà plein de privilèges mais il ne vous laisse pas copier des données mais il ne vous laisse pas copier des données qui sont par exemple dans du padding si vous copiez des choses qui viennent de la stack donc il ne vous laisse pas copier ces données de padding il faut aussi éliminer donc non seulement les accès à la stack mais il faut aussi éliminer toutes vos boucles il ne va pas de tout falloir utiliser des APIs spéciales qui vous permettent de forcer et cling à un rôle vos boucles tous vos appels de fonction doivent être des appels inline tous les kernels ne supportent pas forcément le fait de ne pas avoir ça mais par du principe que ça le supportera quand même et enfin même si votre code c'est que des fonctions inline static qui sont dans les header qui vous permettent par exemple d'être des helper functions pour accéder à des morceaux de trucs dans le kernel ça marche pas forcément si bien que ça dans BCC parfois parce que BCC va déréferencer un pointeur vers de la memoire qui est pas dans la région de BPF quand il y a ça, ça va être instrumenté et ça va être de la génération de code qui va appeler une fonction helper qui s'appelle BPF code read et cette fonction est responsable pour lire les données et donc BCC n'est pas forcément très doué pour convertir des chaînes d'appels comme on aurait par exemple quelque chose qui serait très très scopé on sait qu'on a des fonctions qui appellent d'autres fonctions et ce genre de choses ne marche globalement pas en BPF donc on a plein de cas où on a des fonctions qui servent par exemple d'accéder à la structure de tasks par exemple au lieu d'avoir des fonctions qui vont vous aider à lire vous allez devoir appeler BPF property à la main et extraire les données que vous voulez morceaux par morceaux c'est très embêtant ensuite, si vous vous cherchez à avoir beaucoup de performances, vous aurez peut-être besoin d'implementer votre propre buffer en anneau, votre propre ring buffer pour stocker les données que vous voulez transférer de your space ou kernel space et vis-à-vis de ça avec le moins de copies possibles si vous faites ça, attention c'est compliqué parce que vous ne pouvez pas avoir de boucles donc c'est-à-dire qu'il faut avoir un switch qui va être utilisé à chaque fois que vous voulez mettre à jour un compteur synchronisé et à la fin il faut retourner au début de votre ring donc ça c'est du code qui est très très difficile à écrire et qu'il faut écrire vraiment spécifiquement pour plaire au validateur donc par exemple dans ce cas on a 2 possibilités de position 0 et 1 et si on arrive à la fin il faut revenir à 0 si vous aussi rajoutez un deuxième cas, 0 il s'avère que le validateur n'acceptera pas ça pour tout un tas de raisons donc il faut avoir un autre cas pour à la fin pour pouvoir remettre la valeur à 0 et on ne peut pas ajouter d'autres cas aussi facilement que ça pour un tout un tas de raisons pas important ici donc c'est important parce qu'il y a des indirections dans BPF qui sont très très dynamiques pas surement parce que c'est des pointeurs sur pointeurs pointeurs mais c'est parce que c'est des structures qui contiennent nos structures ou qui ont des références à nos structures et si vous voulez explorer ces données vous devez avoir des structures que vous générez vous-même avec des pointeurs que vous allez copier par dire d'autres structures pas BPF n'aime pas les boucles et il n'aime pas des fonctions helper tant qu'elles sont pas statiques inline donc vous devez construire ce système à la main en faisant très attention à comment le construiser une chose qu'il faut garder en tête c'est qu'il faut savoir quand est-ce que vous n'avez pas suffisamment un relais vos boucles pour compter toutes vos données et quand ça c'est le cas par exemple vous avez un relais votre boucle pour y tirer sur tant d'éléments mais vous avez plus d'éléments quand vous avez ça vous devez être capable de gérer ça et peut-être de loguer ça enfin pour survivre quand vous faites du kernel quand vous utilisez le BPF vous le fais ça parce que vous voulez aller vite mais ça peut être utile d'avoir par exemple le kernel qui va faire du filtrage pour vous mais pour faire ce filtrage il faut pouvoir faire des comparaisons et des choses comme ça et si vous n'avez pas déterminé qu'un élément est dans un set comment est-ce que vous allez faire ça sans récursion ou sans boucle et bien ce que vous pouvez faire c'est un BST un arbre de cherche binaire en user space qui va permettre de chercher dans les limites et ça ça va générer le code BPF qui va permettre lui de faire ses calculs cette détermination de façon statique enfin une autre chose que le valiateur déteste vraiment c'est quand vous avez des copies d'une donnée et que la taille que vous avez copie vient de quelque chose de dynamique ça c'est en particulier parce que le valiateur essaye de savoir combien vous copiez de données et que vous n'avez pas le temps de déterminer au moment de la validation donc la façon qu'on a à nous de faire ça c'est qu'on utilise une fonction inline qui permet de casser cette logique en deux morceaux et ça ça permet d'outrepasser le valiateur sur ce problème-là c'est des trucs complètement fou donc une autre chose que vous pouvez faire avec BCC au moins c'est d'activer de l'output de debug qui vous affichera toutes les instructions BPF et qui vous affichera aussi tout le code C qui est la passe de pré-processeur qui est implémentée par ces instructions donc ça ça vous permet de regarder un peu plus ou est-ce que dans l'assembleur quelque chose n'a pas marché bon courage c'est facile hein donc pour ce qu'on est des chercheurs de sécurité on a besoin de se demander est-ce que BPF peut être utilisé pour se défendre pour une mesure de défense pourquoi pas c'est vraiment rapide BPF c'est vraiment rapide peut-être qu'on peut améliorer nos audits avec ça OK essayons qu'est-ce qu'a un ajusté de sécurité qui améliore la sécurité donc faut gérer les questions de programme, les access au fichier et les k-prop des BPF peuvent faire tout ça donc peut-être que c'est un bon feed pour ça donc les problèmes de tracing et BPF peuvent tout voir ils peuvent inspecter n'importe quel appel de fonction du noyau ils peuvent accéder à la faible mémoire, surge de mémoire entre le kernel et le username par exemple pour monitorer des processus des tâches on peut hooker au niveau du appel système XXV oui on a la fonction qui semble assez qu'on avait tout à l'heure sur Open sauf que là on s'est sur XXV et donc on utilise TracePrintK pour afficher quand XXV est appelé faisons quelque chose de plus intéressant on peut comparer le chemin du fichier avec des fichiers qu'on est déjà à l'avance par exemple bin, stagebin userlocalbin et on peut faire ça juste en vérifiant le nom fichier qui est passé à un paramètre on a notre prefix pour le répertoire ce sera compliqué avec Web pour checker tous les autres répertoires donc on utilise un roll loop on vérifie, on fait le check octet par octet quelque chose que l'on a pensé c'est imaginant qu'on a une application web et on voudrait savoir si elle exécute des choses qu'elle n'est pas censée exécuter par exemple la application web ça peut être juste un front-end qui prend une IP adresse en tant qu'entre utilisateur et qui utilise ça pour implémenter un ping donc on devrait jamais exécuter cette application web, on ne devrait jamais exécuter autre chose que l'application ping pareil, on se hook sur execv pour savoir ce que l'application exécute par exemple on vérifie que l'application qu'on est en train de regarder correspond bien à l'application web donc pour ça on peut dire le PID de l'application pour vérifier ça après on veut vérifier que c'est ping pour commencer on va d'abord vérifier la longueur du chemin va l'exécuter et si ça correspond à la longueur du chemin on vérifie que c'est bien ping on continue ensuite on va hook sur open parce qu'on veut vérifier les ouvertures de fichiers pour un répertoire en particulier par exemple le répertoire qu'on ne voudrait pas que l'application accède c'est le répertoire stash-root pareil, on fait la vérification sur le nom du fichier on va vérifier on a un petit secret tous ces exemples qu'on vient de montrer c'est très dangereux ils ne sont pas au tout sécurisés c'est très difficile d'écrire du code ebpf en fait il ne va pas attaquer votre noyau ou faire des choses mauvaises dedans par exemple si vous voyez kprob les données utilisateurs que vous voyez avec kprob peuvent être différentes de ce qui va envoyer au noyau et le deuxième s'ouvre par exemple si il y a votre application multistradée il faut bien penser que les choses peuvent être dans le désordre donc notre recommandation pour ça c'est l'utiliser de hooker les syscalls de plutôt hooker en fait les fonctions qui implementent ces syscalls au sein du noyau par exemple si l'accès n'est pas fait par un chemin absolu mais plutôt par un chemin relatif est-ce qu'on peut déclencher ça ? on peut essayer par exemple de canoniser le fichier mais tout ça va être très compliqué de faire ces choses-là avec ebpf à la place on peut essayer de trouver des fonctions internet au lieu de prendre un chemin relatif prennent forcément un chemin absolu et du coup on passe comment montrer ce problème on a trouvé que par exemple bcc a des codes d'exemple pour monitorer du raison et par exemple on a trouvé que ça prenait pas en compte le fait que les options tcp avaient une longueur variable et c'était par exemple possible de passer outre des vérifications que vous avez bcc en utilisant un peu cette faille et du coup on a écrit un patch mais en général assurez-vous que assurez-vous que les valeurs que vous travaillez vous vouliez leur faire confiance et à vérifier vos pointeurs manuellement est-ce que ce framework peut utiliser pour la défense ? pas vraiment c'est plus utile de regarder comment les données traversent le système on a unix dump c'est comme tcp dump mais pour les sockets unix et on vous montrera à la fin de la présentation mais en gros ça augue juste unix stream send message et unix des grammes send message c'est quelque chose qui est utilisé pour ça se permet de modifier le programme ubpf pendant son exécution et par exemple de mettre des boucles là où il ne devrait pas y en avoir ça c'est le genre de choses qu'on a parlé avant on a aussi notre arrêt par cpu qui lui garde la position dans le ring buffer qu'on se trouve puisqu'on ne peut pas faire de boucles du coup il faut qu'on utilise ça et il faut aussi qu'on s'assure en permanence qu'on vérifie que les valeurs qu'on a si elles ne sont qu'un espace ou urspace parce qu'on peut avoir des rest conditions dans lesquelles le C pourrait réécrire quelque chose que le piton avait par exemple parce que c'est pas si le piton a fini d'utiliser cette valeur donc on s'assure que le coden C c'est qu'il ne faut pas utiliser une valeur qui est toujours utilisée par piton si un BPF n'est pas si bon pour ça pour la défense, qu'est-ce qu'on peut faire avec un BPF ? Parlons un peu d'attaque alors partons du principe que un attaquant a des privilèges sur un système moderne peut-être qu'il a Capsis admin dans un conteneur parce que les conteneurs c'est sûr non ? alors qu'est-ce qu'il peut faire avec un BPF ? il peut faire beaucoup donc déjà tout le tracing peut non seulement tout voir il peut aussi lire de la mémoire user space il peut aussi écrire de la mémoire user space je répète il peut aussi écrire de la mémoire user space donc BPF right user c'est l'appel magique qui nous permet de faire ça ça permet uniquement d'écrire sur de la mémoire user space qui est writable on ne peut pas écrire dans la section texte on peut écrire tout le reste donc qu'est-ce qu'il y a d'utile dans ces régions ? on a des buffers pour lire écrire on a des données qui vont être envoyées au Cisco on a peut-être des trucs un peu importants, un peu secrets qui sont lus dans les file descripteurs peut-être du code qui exécutait partie de ce file descripteur peut-être qu'il y a un processus privilégié qui fait ça je me demande ah mais oui Cron donc j'ai écrit ce truc qui s'appelle con job qui fait des faux Cron job c'est le Cron auto-pwner et c'est... lui ce qu'il fait c'est il regarde tous les Cisco de la famille STAT et quand Cron essaye d'avoir des informations pour savoir s'il doit ou non recharger sa Cron tab il utilise STAT pour regarder la dernière fois que le fichier a été modifié donc nous on va juste intémenter ça pour que Cron charge le fichier à chaque fois ensuite on hook open at et close pour qu'à chaque fois que le fichier est ouvert on s'en garde le file descripteur qui va être utilisé pour ça et on le stock quelque part pour plus tard et s'il est fermé, close on arrête de suivre ce fichier là et ensuite on hook read pour ce fichier en particulier, ce file descripteur particulier et si on voit qu'il y a des lectures sur ce file descripteur c'est vraiment ce qui est retourné on peut modifier ce qui a été ce qui a été retourné avec notre propre commande comme on pourrait faire donc petite démo ça tourne c'est-à-dire un petit peu modifié de Docker parce qu'il s'avère que Docker a changé son profil à part mort il y a peu de temps après qu'on ait publié ça et ça a supprimé un des trucs qu'on disait un Docker à tout petit peu modifié mais on a presque tout mis à jour pour pouvoir faire cet attaque et du coup on va pouvoir ici cette commande route et injecter tout en haut de notre run tab donc c'est plutôt cool mais qu'est-ce qu'il y a d'autre qui est amusant là-dedans alors on a ces maps qui sont par corps et on peut les utiliser pour communiquer les données et on a des hash maps pour échanger des données de l'un à l'autre et on a aussi cette fonction de Sniffer qui permet de garder le temps où on est pour savoir quand est-ce qu'on va retourner en user-pace qu'est-ce qu'on peut faire d'autre dans ABPFXA si vous vous rappelez on peut écrire dans la stack les stacks ont des adresses de retour donc on peut aussi lire toute la stack dans tous les process en user-pace on peut scanner la section texte pour trouver des shares d'abris des bibliothèques partagées donc je pense que vous pouvez voir où est-ce que ça va donc on a aussi écrit ce truc là qui s'appelle l'autoponeur pour système D donc ça ça scanne le PID1 sa mémoire ça cherche la libc ça lit le contenu de la stack et ça cherche une adresse de retour ça s'insère une RobChain qui va exécuter DL Open sur un pass que nous on fournit ça ça permet de charger notre propre bibliothèque dans le PID1 et ensuite on peut juste nettoyer tout ça c'est très propre donc pour parler un peu de ce qui se passe on a ici ce morceau qui s'appelle FindGlipsy pour trouver la libc ça, ce que ça fait c'est ça, nous dump les adresses de retour et où est la base de la libcée et ensuite on passe ça à la fonction PoundLipsy c'est cette partie là qui génère la RobChain qu'on a préférée à la main en se basant sur la libcée donc on exécute ça et au bout d'un moment ça retourne et ça fait 2 trucs ça charge notre bibliothèque partagée donc si vous regardez ce code c'est ce qu'il y avait dedans donc ça va exécuter un syscall et ça va exécuter ID et ça va mettre ça dans TempEval et TempEval est maintenant rempli avec des fichiers qui sont à la route et on peut voir ici dans le syslog qu'on a un message qui dit L35C3 donc on a plus beaucoup de temps mais pour très rapidement parler de tout ça de comment ça ça marche globalement on fait un hook sur le timer fd7 parce que SystemD appelle ça au moins une fois par minute c'est garantie et ça c'est très fiable ensuite on scanne dans des structures qui sont dans la stack qui ça passe à ce syscall pour qu'on puisse trouver une adresse de retour donc ensuite on scanne pour trouver une adresse de retour dans le stub de syscall et ensuite on peut set rax sur le lighting dont on a besoin et on peut appeler du coup la bonne instruction donc on scanne le syscall jusqu'à ce qu'on cherche on trouve où est le syscall stub on cherche l'offset qu'on connaît du départ depuis l'alipsee et ensuite on peut changer l'adresse de retour avec le pointeur qu'on a calculé une fois qu'on a fait ça en tout cas il y avait un hook qu'on pouvait faire qui permettait d'avoir tous les registres associés au processus parce que obbpf et bcc normalement ne vous donnent pas ces processus pour les avoir mais c'est toujours utile pour d'autres choses ensuite on hook timerfd à nouveau et dans la valeur de retour de ça on peut ensuite remonter la stack et la copier quelque part ensuite on écrit notre hopchain et ensuite on retourne en user space donc timerfd retourne directement après l'instruction et ça ça retourne dans notre hopchain notre hopchain met en place la valeur et tous les arguments et tous les pointeurs et ça appelle dl open une fois que c'est fini on ne veut pas que notre moteur notre système est à se nettoyer donc en fait ce qu'on a c'est que la hopchain fait le nettoyage donc la hopchain appelle close avec une valeur magique négative qui normalement ne serait pas valide puisque le kernel part que de ne se faire les scripteurs mais nous dans notre hook sur close on utilise ça comme une valeur de signal donc une fois qu'on est là dedans on peut réécrire l'essentiel de la stack existante sauf le dernier gadget ensuite on écrit une nouvelle route chain qui est après la fin de la stack qu'il y avait avant l'instant pour laquelle on fait ça c'est parce que dl open lui-même peut utiliser beaucoup d'espace en stack et effacer ou réécrire des choses qu'on a réécrites ensuite on retourne en user space dans la dernière partie de notre ancienne hopchain ça s'assette RSP pour pointer sur la nouvelle route chain la nouvelle route chain se contente elle d'effacer ce qui restait de l'ancienne route chain ensuite ça remet rax à sa valeur pour que ça ressemble à la fin d'une execution de timer fd pour que le code parent ait l'impression que timer fd a réussi la jelipsie c'est très stable ces gadgets marchent sur diverses versions de la jelipsie sur différentes distributions c'est cool alors il faudra régénérer les offsets mais les gadgets sont toujours là ils sont toujours en place qu'est ce qu'on peut faire d'autre avec ubpf pour ce que c'est prévu de faire donc une fois qu'on a exécuté cette kprobe il faut pas que quelqu'un vous arrête ça peut empêcher des processus d'interagir avec le kernel par exemple vous pouvez bloquer des processus qui essaieraient de lister des programmes mpf et des kprobes vous pouvez empêcher des programmes de créer des kprobes vous pouvez empêcher des programmes de charger des modules de kernel d'appeler quelqu'un d'autre d'appeler à l'extérieur et donc ça c'est utile parce que ça vous permet de faire que quand vous avez un programme ubpf qui est compilé et chargé vous pouvez avoir une logique qui va envoyer un message sur dmsg pour dire que ce programme s'execute ça c'est un warning intéressant mais il y a une respondition d'intéressante puisque quelqu'un qui lit dms doit pouvoir lire depuis ça et pour ça il faut utiliser un syscall donc ils vont lire et ensuite s'utilise un syscall donc vous pouvez utiliser dpdk par exemple pour empêcher des paquets de sortir même si vous utilise dpdk vous pouvez empêcher les paquets de sortir il y a plein de façons qu'ils vont te faire ça mais vous pouvez tout bloquer on a des exemples ici qu'on a eu un peu de mal de mettre en place mais on peut complètement empêcher un syscall même de se produire donc une chose intéressante à retenir de ça c'est qu'il faut qu'on garde notre process ubpf en vie parce que s'il meurt tout ça arrête et on peut faire en sorte que nos kprobe ubpf soient essentiellement immortels parce qu'on peut prendre le contrôle de PID1 donc pourquoi est-ce qu'on ne pourra pas exécuter d'autres kprobes ubpf depuis PID1 une fois qu'on est à l'intérieur ça veut dire qu'on est en vie jusqu'à ce que le système s'éteigne ou peut-être que PID1 va s'éteindre mais du coup le système s'éteint avec lui donc ça c'est cool puisque en général PID1 c'est systemd pour la peur des gens et comme on le sait tous PID1 ça s'écroule de partout c'est même pas génial ça se fait attaquer par des rootkit ubpf de l'âge de l'espace donc ubpf c'est utile pour tout sauf qu'il y a des gens qui essayent d'écrire des idées avec ça et ça ça a aucun sens il faut que ça devienne il faut que ubpf soit meilleur en matière de support de ce genre de choses alors actuellement ça marche pas du tout donc moi ce que je demande s'il y a des développeurs kernels qui m'écoutent ici il faut vraiment une fonction copy from user où on n'a pas besoin de vérifier les pointeurs à la main nous-mêmes pour s'assurer qu'il n'y a pas quelqu'un qui essaye d'ouvrir un path qui est en fait une adresse dans le kernel et s'il vous plaît est-ce qu'on peut aussi avoir des helpers pour tous ces trucs comme manipuler des fichiers par exemple histoire qu'on puisse par exemple utiliser des paths qui aient du sens peut-être avoir des opérations de comparaison de mémoire comme ça on n'a pas à faire tous ces trucs bizarres et s'il vous plaît memset j'en ai marre de devoir utiliser ma limite d'instruction juste pour pouvoir faire des memset donc c'est une utilisation et de remerciements ici les développeurs BCC ils sont vraiment cool ils ont fait des outils super bien et tous ces trucs ça m'aillure tous les jours merci à Julia Evans parce qu'elle a des posts super sur comment K-Prob c'est tout ça, ça marche même pour Brandon Gregg et Jessie Fazel elle a écrit des choses qui s'appellent ilo à l'écritant qui s'appelle BPFD et ça c'est des choses que par exemple j'aimerais pouvoir injecter dans PID1 comme mon gestionnaire de haute kit et ne pas échapper au futur est-ce qu'il y a des questions est-ce qu'on a des questions si vous écoutez cette traduction on peut poser des questions sur IRC sur 35C3 AllD sur le serveur IRC de Hackint ok question ok micro numéro 2 bonjour super conférence est-ce que vous pouvez juste désactiver les optimisations du compilateur pour éviter un tas de problèmes avec la validation alors si vous voulez modifier le bout de code qui fait ça tout ça c'est du code C++ parce que c'est tout dans LLVM donc il faudrait recompiler BCC et faire pas de changement de BCC pour faire ça alors c'est possible d'acheter des flags qui ferait ça mais je comprends pas vraiment comment se fait l'injection là-dedans et à quel point on peut override les optimisations numéro 3 quelle version vous avez utilisé ce qui a eu plein d'améliorations depuis un certain moment alors l'essentiel de ça on l'a surtout fait sur deux kernels je crois 4.1 sur Ubuntu 18.4 4.18 sur Ubuntu 18.10 et je crois 16 ou 17 sur Kali Linux numéro 2 c'est facile d'injecter des choses est-ce qu'on peut éviter que tout ça soit activé en premier lieu je vois qu'il y a des choses dans le kernel qui utilisent eBPF pour des choses vraiment basiques mais est-ce qu'on peut injecter plus de trucs avec eBPF est-ce qu'on peut rappeler la question s'il vous plaît est-ce qu'on peut faciliter ce qui est très difficile d'injecter du code alors par exemple si on utilise des conteneurs il y a tout un tas d'opérations qui vont être restreintes par les profils par mort et par exemple qui empêchent les interactions avec ccfs le problème c'est que dans les nouveaux kernels Linux il y a une nouvelle façon de faire ça qui est d'utiliser le syscall bpf ou Perf event open et ça se permet d'éviter pour complètement bypasser cette protection est-ce que vous connaissez des providers cloud qui vous offrent des conteneurs alors on n'a pas regardé si il y a quelqu'un qui vous donne Capsis admin vous avez d'autres problèmes si vous êtes Capsis admin arrivé au route c'est pas une vraie escalation de privilège c'est un truc pour montrer qu'il est cool mais si quelqu'un vous donne Capsis admin vous avez d'autres donc si vous avez un professeur de cloud qui vous donne Capsis admin il devrait probablement pas faire ça parce que vous allez avoir le route de toute façon merci beaucoup Jeff Dileo et Andy Olson pour cette présentation on va s'arrêter ici tenir l'applaudissement pour eux s'il vous plaît