 Okay, an diesem letzten Kongrestag ist es mir eine Ehre, diese zwei Ehren hervorzustellen, Jeff Lee und Andy Olson, die diesen Vortrag gehalten werden, Colonel Tracing mit EBPF. Also für diejenigen, die das nicht verstehen, Extended Berkeley Packet Filter und was sie machen werden, ist die Funktionalität von EBPF-Vorstellen, die im Linus Colonel vorhanden ist. Sie werden einige praktische Anwendungsferde vorstellen und was für mich am Interessanten ist tatsächlich, dass wir ein bisschen Gott spielen werden. Träuft das zu? Ein bisschen zumindest. Ich weiß nicht genau, welche Art von Gott, aber ja, die Prüne gehört euch. Welcome. Hallo zusammen. Ich bin Jeff, das ist Andy, wir sind Security Sicherheitsberater bei NCC Group und wir suchen hauptberuflich nach Sicherheitslücken, helfen die zu reparieren und so und ja, womit wir in letzter Zeit hier umgespielt haben, ist EBPF, EBPF ist, heißt erweitertes BPF, Extended BPF, ja und nochmal für die, die BPF nicht kennen, das ist der Berkeley Packet Filter, das ist eine Bytecode-VM, also eine Bytecodewirt in der Maschine, die im Kerne läuft, um Netzwerkpakete zu verarbeiten und zu filtern. EBPF ist dann eigentlich komplett anders, hat eigentlich erst mal nichts damit zu tun und die wurde im Linux-Körner entwickelt, als eigenes Ding und hat im Prinzip nur den Namen gemeinsam. Die Idee ist, dass sie Just-in-Time kompiliert wird und direkt auf x86 und andere moderne Security-Körner abbildbar ist und die Idee ist, dass man einen C-Subset transpiliert und dann kompiliert und das wird für alles im Kernel verwendet, um irgendwelche beliebig programmierbarer Logik reinzutun. Es gibt dafür den BPF-System-Coy, mit dem man den BPF-Bycode rein sendet und ein bisschen anderen, ein bisschen andere Metadaten und wenn das alles passt, dann gibt es euch einen EBPF-Programm und worum es eigentlich geht, wie kann man das benutzen, wie sieht das aus? Das hier ist jetzt eine Form, wie das aussieht, das sieht total furchtbar aus, das ist Makro-Hölle, alles in C, das sollte eigentlich nicht jemanden schreiben, aber ja, vielleicht erst mal, was ist das? Es ist im Prinzip eine BPF-Implementierung, die eben paar andere Regeln verwendet, neuere Register hinzufügt und im Prinzip Funktionen breit steht, um arbiträre Kernelfunktionen aufzurufen, die man vorher als Kernel bei EBPF registriert beim Interpreter und ja, das ist jetzt erst mal und es gibt dann noch einen Verifier, der den Code verifiziert, da kann ich so besonders gut hören, aber es tut es und dann gibt es eben noch den spezifischen Anwendungsfall, der und die Isolationen, die einschränkt, was der Bycode genau tun kann. So, warum macht man das alles? Es gibt verschiedene Anwendungsfälle, hochperformantes Paketprozessierung, Netzwerk-Tunnel, spezifische IP-Tables-Regeln, Sys-Golf-Filtern und ja, da kann man dann zum Beispiel, was, okay, ja, irgendwie ist das alles ein bisschen kompliziert, aber ja, EBPF hat im Prinzip einen Riesenhaufen Features bekommen und ja, wird für alles Mögliche verwendet und Leute scheinen das zu mögen. Aber also, wofür verwendet man das tatsächlich, also man will grundsätzlich überall Hochsaben, also einen Eingriffspunkt, wo man Dinge tun kann, ohne den Kernel Crashing zu lassen und das heißt, man muss keine Kernel-Module mehr schreiben, die dann irgendwas alles Crashing lassen können oder so was man dann kann, das Safe in EBPF machen. EBPF ist auch insofern interessant, als dass es eine Alternative zu D-Trace ist, die man aus Solaris und MacOS kennt und es ist tatsächlich sehr viel mächtiger und ja, D-Trace hat im Prinzip immer diesen Vorteil gehabt, dass es ein einheitliches Provider-Framewerk gibt, wo man eben für bestimmte Ereignisse ein einheitliches Interface hatte und EBPF wird wahrscheinlich nie zu den Art von Homogenität kommen, aber EBPF ist deutlich programmatisch und man kann dann im Prinzip einfach beliebigen C-Code in dicken Anführungszeichen in den Kernel ausführen. Und ja, D-Trace ist halt da eben deutlich eingeschränkt dafür, einfach nur die Systemanalyse während EBPF einem im Prinzip ziemlich viele Anwendungsfälle erlaubt. Was ist Tracing? Tracing ist im Prinzip Logging für Programmausführung, also das ist für uns jetzt nicht so interessant, das ist ja für Entwickler interessant, was uns eher interessiert ist ja an Sachen irgendwelche Anhängepunkte, also Hux dranhängen und ja, das ist halt im Prinzip dynamisches Tracing. Dynamisches Tracing kann man unterscheiden in zwei verschiedene Arten, einmal ja im Prinzip dynamisch Logging ein und ausschalten und dann gibt es die Art von Tracing, wo man arbiträre Funktionalität und Logstatements irgendwo reintut, die vorhin auch gar nicht da waren. Und wir interessieren uns speziell für den zweiten Teil, das nennt man auch dynamische Instrumentierung und ja, es gibt dann eben im Prinzip zwei Sachen, Funktion Hooking und Instruction Instrumentation, was ja, es hängt ein bisschen davon ab, wie man das Hooking implementiert und ja, schauen wir mal weiter. Der Titel wäre einfach nicht akzeptiert worden, damit ihn so eingereicht hätten. Also jetzt wollen wir uns mal ein bisschen die Historie des Ganzen angucken, was es da so für Technologien gab. Das Wichtigste am Anfang war K-Propes, das ist einfach schneller geworden über die Zeit. Was es erlaubt hat, war, dass man beliebige Instruktionen im Kern erhucken konnte. Man konnte eine Funktion da reinhängen, die dann ausgeführt wurde und die wurde dann ausgeführt, wenn das Ding benutzt wurde. Es hat schon immer versucht einen FastPath zu machen und wenn es keinen FastPath machen kann, dann macht das ein Single Stepping durch die Instruktionen. Dann gab es Ftrace, das war eine Filesystem-API, da konnte man Profiling und Tracing mitmachen. Nicht so richtig interessant aus unserer Sicht, aber kommt vielleicht auch ein eBPF ran. Dann gab es Perfevents, das war auch für Profiling und am Ende ist es einfach nur ein sehr schneller Ringbuffer, den man benutzen kann, um Informationen rauszukriegen. Dann gab es TracePoints, das ist eine Logging Funktionalität, die eingefügt wurde. Dann wurde U-Props eingeführt, das war Kernel-Props für User Space und jetzt gibt es das eBPF und das soll das alles zusammenführen. EBPF integriert sich mit all diesen verschiedenen Kernel-Technologien und ganz besonders die, die für Tracing verwendet werden. Die Idee ist, dass man ein eBPF-Programm schreiben kann, das sich dann an gewisse Datensourcen ranhängen kann mit der Perfevents-API. Es benutzt Memory-Mapping, um die Daten aus dem Kernel rauszukriegen. Das ist ein bidirektionales Mapping zwischen Kernel Space und User Space. Der User Space kann auch den Input darüber reingeben in den Kernel und den Updaten und dann hat es verschiedene Quellen. Also es gibt verschiedene TracePoints, das sind diese ganzen Probes. Was die Idee ist bei Carat-Probes, ist, dass man ein Haufen TracePoint hat, die man verwendet und dann nimmt man ein eBPF-API, um eine eBPF-Probe zu registrieren und eine Idee dafür zu kriegen. Dann verwendet man Perf-Event Open und passt da die Idee rein, die man gerade bekommen hat. Danach kann man das eBPF-Programm da dranhängen über einen IOControl. In neueren Kernels ist die Form ein bisschen anders. Da kann man das F-Trace direkt auslassen. Im Endeffekt braucht man die Zeile 6 gar nicht mehr heutzutage. Die U-Probes sind im Prinzip das same wie vorher, nur mit kleinen Änderungen. Jetzt ist wichtig, wie man das nicht verwenden sollte. Und zwar, das ist ungefähr so wie System D ist benutzt. Das sollte man nicht machen, nämlich im Bytecode, das haben die von Hansch schreiben. Das ist einfach schwierig. Man kann nichts wirklich kompliziertes damit machen, weil es keiner mehr versteht. Und das sollte man einfach nicht tun. Was man dann machen sollte, ist BCC verwenden. Das ist im Grunde genommen ein Compiler. Es ist im Prinzip wie ein GCC für eBPF. Da kann man alle die ganzen Quellen mit einbinden. Dieser Talk hier geht eigentlich gar nicht um BCC, aber wir verwenden es trotzdem für alles, weil es am Ende des Tages der einzige wirkliche Nutzer der Kölner APIs ist. Weil die Leute, die BCC schreiben, diejenigen sind die auch die Quellen verwenden. Deswegen ist die APIs gar nicht so richtig dokumentiert. Und wenn das der einzige Nutzer ist, das ist relativ stark verzernt. Und dieser CIS Call Kram ist eigentlich gar nicht wirklich dokumentiert. BCC ist eigentlich die einzig einstehende Option an der Stelle. So jetzt ist die Frage, wie schreibt man so ein Tracer mit BCC? Normalerweise nimmt man einfach die Pfeifen API. Und blöderweise muss man das alles als ein einziger Pfeifen Datei schreiben. Und es wird immer schwieriger, das zu lesen. Und der C-Code wird dann als Pfeifen String abgelegt. Das ist jetzt so die grobe Struktur, die man auf der Folie sieht. Dann muss man BCC sagen, dass er es zur eBPF kompilieren soll. Und dann hängt es sich an Event K-Probes dran. Und dann hat man Callback Funktionen und am Ende funktioniert einfach alles. Es hat diese merkwürdige Einschränkung, dass es eben nur eine einzige Pfeifendatei sein soll. Wir gehen davon aus, dass es sich irgendwann erledigt hat, dass dies ungeschrieben wird. Aber im Moment ist es halt leider so. Das ist jetzt ein Beispiel hier, das ist ein sehr, sehr simples Programme, das wir geschrieben haben. Das hat relativ viel Kurzgenerierung im Hintergrund, die man jetzt hier nicht sieht. Das sagt im Prinzip dass er einen K-Probes offen verwenden will. Der Name dieser Funktion ändert sich regelmäßig mit neueren Kernels. Zumindest in der letzten Zeit. Wir hocken jetzt dieses Ding und dann machen wir einfach einen Print K, also einfach einen Kernel Ausgabe. Und dann lassen wir es laufen, nachdem wir es kompiliert haben. Und wir kriegen gar nichts. Warum kriegen wir nichts? Ah, right, wir haben G-Lib C verwendet. In G-Lib C hat in der letzten Zeit die, wenn jede Verwendung von Open ländet, wird es umwandeln in eine andere Funktion. Und stattdessen muss man eigentlich Open-Ed verwenden. Aber jetzt wollen wir das gleich noch ein bisschen generalisieren. Wir wollen eigentlich nur eine einzige Funktionalität haben und wir haben aber K-Probes dafür verwenden müssen. Open-Ed landet dann auch am Ende bei SysOpen. Das ist die zugrunde liegende Kernelfunktion. Und dann können wir auch gleich in Print K das reinschreiben und den Part dazu schreiben. Und jetzt sehen wir diese merkwürdigen Probs, das FS-Outgram. Print K ist, das Trace-Print K ist F-Trace-Print K, das ist F-Trace im Prinzip. Und es ist relativ schädlich deswegen. Es benutzt einen einzigen Lockbuffer über das ganze System, so dass die sich gegenseitig überschreiben können, weil man mehr als einen Tracer hat. Das eBPF-Programm wird entladen, wenn der Prozess aussteigt. Das hängt an der Exit-Funktion des Programmes, dass diese Probe gezeugt hat. Das heißt, wenn es dann stirbt, dann wird es auch wieder entladen. Und dann gibt es auch noch eine merkwürdige Race-Condition zwischen dem Ende des Programms, dem K-Probe-Hit, also wenn der K-Probe anschlägt und dem Detach. Und was dann passiert ist, dass die Nachrichten einfach noch zurückbleiben im Buffer, bis sie jemand liest. Und der nächste Prozess kriegt dann dummerweise die Nachrichten im Lock, die eigentlich für das andere Programm bestimmt waren. So, jetzt muss man das, kann man lösen, indem man das ganze umschreibt, indem man seinen eigenen Kassen-Daten-Fahrt verwendet. Also hier verwendet wir diesen Perfeeband Ringbuffer. Das ist eine Hilfstellung, die uns der BST für den unseren Code gibt. Und was wir jetzt noch brauchen, ist so ein Ablageort, wo wir da irgendwas einschreiben können, wenn wir operieren auf Fahrten, die geöffnet werden. Und die haben Parf-Marks viele Zeichen, die die lang sein dürfen. Und deswegen brauchen wir genug Platz, wo die reinpassen. Und wir benutzen jetzt diesen C-Ray. Das ist relativ safe, sondern wenn nur einen Ausführungswert hat, wenn wir das jetzt machen, dann haben wir hier diese Notation. Wir haben nur eine Zeit, die wieder abgreifen und es kann noch nicht schiefgehen. Was wir jetzt machen, ist, wir nehmen die Daten aus dem Call und packen sie in diesen Ablageort und mapen sie dann in den Buffer für das schnelle Sharing. Und das ist halt ein Haufen, rumkopieren, was unnötig ist. Aber so ist es jetzt erst mal. Und wenn wir jetzt in Pfeifen sind, da gibt es diese C-Tides. Das ist ein Interface, mit dem man CD-Strukturen benutzen kann in Pfeifen. Das müssen wir nehmen, weil wir uns ja auf C-Code runtergenerieren wollen. Und hier schreiben wir diesen Event-Händler, das ist der, wenn Data ist die Tabelle, die wir oben definiert haben im C-Code und die können wir hier verwenden. Und wir machen ein bisschen Casting, damit das funktioniert. Und am Schluss machen wir den Poll auf dem K-Probe. Das heißt, wir fragen die Events ab. Der Name hat sich auch mal wieder geändert in den CC in letzter Zeit. Aber momentan, das funktioniert. So wie funktioniert das alles? Ja, schauen wir uns mal ein bisschen etwas aufgeräumten S-Trace-Ausgabe an. Der Hauptfokus hier ist der dieser Aufruf BPF Proc Lord mit den Typen da oben. Das sind rot. Und wir bekommen da den Pfeildescriptor Nr. 5. Dann gibt es diesen Sys-Colonial-Debug-Tracing-Aufrufe im Sys-FS-Weisystem. Und die Art und Weise, mit dem man mit den interagiert ist, dass man da was reinschreibt und dann probt man die jeweilige ID-Nummer, die man jetzt gerade da reingeschrieben hat. Und dann öffnet man eben genau mit dieser ID wieder so ein Pseudo-File. Und das ist alles relativ flexibel. Ist im Prinzip so wie ein IO-CTL. Wir nehmen das dann und rufen IO-CTL tatsächlich darauf auf und dann benutzen wir eben genau das, um das BPF-Programm da dran zu hängen. Und so wird dann tatsächlich unser BPF-Programm mit dieser Cape-Probe, die wir eben erstellt, haben assoziiert. So, was macht denn dieses BPF-Perf-Output-Ding tatsächlich? Es ist eigentlich einfach nur ein Makro. Es macht eigentlich gar nichts. Es erstellt eigentlich einfach nur einen dynamischen Struct-Typen und erstellt eine Instanz davon. Aber es führt gar keinen Code aus oder so. Sondern ja, es ist eigentlich einfach nur Fake. Es ist einfach nur Code, der bei der Code-Generierung dann eh ersetzt wird. Und das ist einfach nur da, damit der Compiler-Quickcheck am Anfang zufrieden gestellt wird. Und das ist relativ normal in Code-Generierungsbasierten APIs. Also, was passiert, wenn wir dieses Perf-Sub mit aufrufen? Das wird tatsächlich ersetzt mit einem BPF-Perf-Event-Output-Helperfunktion auf Ruf. Und was es da macht, ist die ganzen Argumente reinwerfen und unter anderem ein Flag-Curr-CPU-Identifier. Und was das heißt, ist, dass es ein Perf-Event-Objekt von der Kerneseite und ein Perf-Event-Array nimmt mit dem aktuellen CPU als Index. Und erinnert euch an diese Perf-CPU-Arrays. Und dann gab es ja eben diese ganzen Syscoids mit Perf-Event 001, mit dem BWI-File-Deskriptor. Und das ist eben genau das erste Ding, was wir machen. Wir erstellen dieses Perf-Array und haben dann ein File-Deskriptor da drauf. Dann öffnen wir die ganzen Perf-Events. Und dann seht ihr hier in grün die jeweiligen CPU-Indices. Und da bekommen wir auch wieder File-Deskriptoren zurück. Dann rufen wir Update-LM auf und benutzen diesen File-Deskriptor aus dem Perf-Event-Array. Und dann wieder diesen Key, den geben wir dann wieder da rein. Und dieser Key ist tatsächlich einfach nur ein Pointer auf die CPU Nummer 0. Und Value ist der Pointer auf die Datei hinter der File-Deskriptor Nummer 8. Und dann fangen wir da an zu pollen und alles ist super. Aber jetzt schalten wir mal gerade ein bisschen um. Schauen wir mal auf den EBPF-Validator. Und das macht alles gar keinen Spaß. Denn EBPF soll ja safe sein, denn wir führen da ja irgendwelchen Code im Kernel aus und die Funktionen können Speicher lesen und schreiben und so weiter und das muss safe sein. Also das ist ein Crash alles. Und ja, deswegen validiert EBPF den ganzen Code, bevor es den tatsächlich ausführt. Und ja, es gibt dann so ein paar simple checks, die sich alle ausdenken könnten. Also es darf nicht lupen oder zurückspringen, weil das eben direkt im Kernel läuft. Also da, ja, da geht man halt die ganzen üblichen Probleme im Kernel. Wenn das hängt, dann hängt der ganze Kernel und so weiter. Und ja, selbst wenn euer Code jetzt keinen Schleifen hat oder so, kann es trotzdem sein, dass der validator den ablehnt. Denn es kann zum Beispiel Funktionsaufrufe geben, die nicht static inline sind. Und das heißt, es kann sein, dass er im Prinzip zurückspringt. Also es könnte effektiv dasselbe sein und das mag der validator gerade nicht. Und dann gibt es so Geschichten wie Loop Unrolling und so weiter. Also das macht man dann manuell. Aber dann gibt es wieder Compileroptimierung. Und ja, also BCC nutzt halt einen optimierten, also nutzt Klang LVM und da einen optimierten Compilerpass. Und das heißt es kann sein, dass es bestimmte Sachen selber inline und ausrollt. Und wenn man jetzt anfängt, den Code langsam zu erweitern und dann funktioniert die Optimierung irgendwann nicht mehr. Und dann gibt es auf einmal ein Fehler, den vorher gar nicht gab, weil es quasi keinen, also für den Nutzer keinen Kausal im Zusammenhang gibt zwischen seinen Änderungen und den Fehlern, den jetzt der Compiler ausgibt. Dann gibt es auch noch den, ja, dann gibt es auch noch Validator Helpercoils, die den Validator quasi davon überzeugen können oder der Validierung helfen können, zu sagen, dass etwas tatsächlich safe ist. Und ja, die Logik dafür ist aber nicht so super ausgefeilt, weil der Compiler oft sehr viel schlauer ist als der Validator. Und grundsätzlich gilt, je mehr Code man da, je mehr Code man reinschreibt, um den Code offensichtlich safe zu machen. Also das denkt man als Entwickler, da kann es halt trotzdem passieren, dass der Compiler den dann wieder rausoptimiert. Und das heißt, man spielt da häufig dann ein Spiel gegen den Optimizer im Compiler gegen den Optimierer, um ja diese Optimierung zu verhindern. Und dann hat man noch die Geschichte, dass das ja das Interne-Regierungsabhitz sind und das heißt Code, der bei einer früheren Lehrungsversion funktioniert hat, funktioniert dann später vielleicht nicht mehr. Und das ist uns auch alles passiert. Also wir hatten zum Beispiel Situationen, wo eine Funktion akzeptiert oder abgelehnt wurde, wo ein Bool entweder eins heißt T0 oder 1 war. Und das wurde in einem UND 8T gespeichert. Und ja, als wir dann irgendwo eine andere Code Zeile rausgeschmissen haben, dann hat das eben zu einer Ablehnung oder Annahme geführt durch den Validator. Und ja, das hat uns alle sehr verrückt gemacht. Und ich habe dann einen Kernemodul geschrieben, das sich ziemlich hacky in den Validator reinhängt und dann einfach viele von den Checks ausschaltet. Ja, dummer Validator hat keine Ahnung, was Bauern sind und ja, dann sage ich dem einfach, die sind alles safe. Und ja, stellt sich raus, der Validator eng tatsächlich ziemlich nah eng am Interpreter für diesen Bytecode an und man kann es nicht komplett ausschalten, denn er läuft quasi während der Interpretierung. Und das heißt, man muss eben genau an den richtigen Stellen besondere Hooks einbauen, um besondere Checks zu überspringen. Und dann, ja, und dann kommt da daraus aber noch mal Folgenprobleme an. Also, ja, also, wir haben dann was geschrieben, das heißt YOLO-EBPF, das ist im Prinzip so ein Proof-of-Konzept, der das mit dem Functionhooking und so etwas einfacher macht in Validator. Und ja, also es funktioniert mit EBPF, aber wir haben jetzt Ftrace, es funktioniert gerade nicht, weil jemand irgendwas im Kernel geändert hat. Und es funktioniert nur auf x8664 und ich habe da einen besonderen Checkcode drin, der in Strings und Arrays drin liegt. Also, das funktioniert, außen funktioniert es bestimmt nicht mit aktuellen Kernel-Versionen, sondern nur mit der, wo wir es entwickelt haben. Und wenn, ja, wenn ihr da irgendwelches Unsafe eBPF reinschreibt, dann kann es tatsächlich sein, dass euer Kernel crashed. Also, ihr solltet es eigentlich wirklich nicht benutzen und ja, wir verlinken das Repo am Ende von den Folien und wir werden es dann publik machen nach dem Vortrag. Ja, also, wenn ihr nicht den Luxus von so einem hacky Kernel-Modul habt, was könnt ihr denn tatsächlich machen? Also, es gibt, es gibt ein paar Schritte, wie man den Validator glücklich machen kann. Man kann zum Beispiel den Stack Speicher immer schön initialisieren. Das ist tatsächlich eine generische, eine generische Verwundbarkeit in vieler Software, die irgendwelche Trust Boundaries überschreitet. Wenn man irgendwelche Stack-Daten nicht auf einen wohldefinierten Wert stellt, dann liegt man tatsächlich Informationen aus diesen Feldern, die man nicht initialisiert hat an den Aufgerufenen. Und ja, in dem Fall hier ist das alles nicht so wichtig, weil wir liegen ja quasi per Definition Kernel-Speicher beim WBPF benutzen. Aber ihr müsst euch, um den Validator zufrieden zu stellen, zum Beispiel Paddingfelder und so weiter abschalten. Paddingfelder gehören auch zu diesem Informationsleck. Dann ja, geht es darum, Schleifen zu eliminieren. Also, statisches Ausrollen von Schleifen und Inline Function Colts sind beides wichtige Maßnahmen dafür. Neuere Kernel unterstützen tatsächlich, dass man das nicht machen muss. Aber ja, ihr habt vielleicht diese Kernel-Version einfach auch gerade gar nicht. Also macht es. Dann gibt es noch so statische Funktionshäder, die von Kernel-Version gestellt werden, die dafür da sind, dass ihr einige Kernel-Datenstrukturen besser pausen könnt. Das Problem ist, die Funktion ist nicht so gut im WBPF, denn wenn die WC-Code-Generierung irgendwelchen Kernel-Speicher zugreift, der nicht in der WBPF-Speicherregion ist, dann gibt es häufig sehr viele Probleme. Und es gibt dann da häufig so komische Probleme in irgendwelchen komischen Scoping-Kontexten und so weiter. Und manche Sachen funktionieren dann einfach nicht. Und es gibt halt besonders viele, also ein besonders häufiger Anwendungsfall ist, wenn man mit dem Taskstruck interagiert. Und das heißt, man muss dann einfach einzelne Informationen aus dem Taskstruck per Hand rauskratzen. Dann geht es nochmal um Performance. Also, es kann sein, dass ihr euren eigenen Ringbuffer implementieren musst, um Daten in den Userspace zu senden und dabei möglichst wenig Kopien zu machen. Also, das heißt, ihr müsst irgendeine Art von Switch-Satment haben, die jedes Mal aufgerufen wird, wenn irgendeine synchronisierte Zellervariable aufgerufen wird. Und das ist tatsächlich jetzt sehr vorsichtig, schriggender Code, der den Validator zufrieden stellt. Und wenn 2 jetzt wieder zu 0 wird oder so, dann müssen wir damit umgehen. Wenn man hier ein Case 2 reinschreibt, dann würde der eBPF Validator das tatsächlich nicht annehmen. Und das heißt, wir mussten das hier in Default-Fall reinschreiben, um das um zu flippen. Und ja, das war halt einfach so, weil der Validator so funktioniert. Und dann gibt es noch das Problem mit dynamischen Kernel-Daten-Strukturen. Also es gibt da tief verkehrtete Pointer-Strukturen und es gibt teilweise Strukturen, die einfach mem-copied werden. Und was man da im Prinzip machen muss, ist dann diese Pointer dynamisch langlaufen und ihr müsst das alles statisch machen, im Prinzip die ganzen Schleifen pernt ausrollen. Und ihr müsst da sehr vorsichtig sein, wenn ihr das macht. Aber normalerweise funktioniert es dann auch, wenn ihr das richtig macht. Und das tricky Ding, was ihr dabei hinbekommen müsst, ist, dass die ausgerollten Schleifen vielleicht nicht, vielleicht Daten irgendwie nicht ganz auslesen können und ihr müsst das irgendwie anders machen. Der Grund, warum man das mit eBPF macht, ist, weil es schneller ist und weil man hoffen Tracing machen will. Also was man machen kann, ist, man macht Pre-Processing im Kernel um Filtering damit zu implementieren. Wenn man z.B. verauschnitten muss, ob ein Pid in einer Menge ist, wie macht man das ohne Rekursion und ohne Loops oder ohne Schleifen? Was man machen kann, ist, ein Balance-Binary-Search-Tree im User-Space kann dann rausfinden, was der Vergleich wäre, das Statisch ausrollen und das dann im Kernel zurückholen. Was wir jetzt noch machen, ist, dynamische Längen bytekoppieren. Der Compiler wird größtenteils Sachen wegoptimieren und da lasst euch nicht genug Informationen überum zu wissen, was eigentlich wirklich passiert. Und wie wir das gelöst haben, ist, dass wir eine merkwürdiges Static-Inline gemacht haben, dass das ganze Umgang hat. Das Zeug ist ziemlich merkwürdig. Eines der hilfreichsten Sachen, die ihr machen könnt für WCC ist Debug-Output, weil es euch alle eBPF-Instruktionen ausgibt und nebenbei auch noch den C-Code, der Pre-Prozessiert ist, der mit diesen Instruktionen erzeugt wird. Und dann kann man zurückgucken, wo in der Assembli irgendwas viel gelaufen ist. Ja, viel Erfolg damit. Das klingt einfach, oder? Jetzt übergebe ich an meinen Kollegen. Also, nachdem wir ja Sicherheits-Consorting machen, fragen uns natürlich, was kann wir mit eBPF machen, beziehungsweise können wir damit auch uns verteidigen gegen andere Sachen. Und der Clue ist natürlich, eBPF ist extrem schnell, so dass wir überlegen, ob wir damit auditiv beschleunigen können. Lass uns das einfach mal ausprobieren. Also, was macht Security Monitoring? Es schaut einfach im Prinzip alles an und schaut dabei Programmausführungen, Fallzugriffe, Netzwerkverkehr und im Prinzip Cake-Probes können alles auch. Warum würde das dann eigentlich gut zusammenpassen? Tracen der eBPF-Programme können im Prinzip alles sehen, die können sich in alle Kernelfunktionen reinhängen, die können sowohl im User Space als auch im Kernelspace-Speicher anschauen und noch vieles weitere. Zwar mal ein paar relativ simple Monitoring-Aufgaben implementieren mit eBPF, um zu sehen, wie es funktioniert. Wir schauen uns einfach mal den Exec-V-E-Call an und versuchen den reinzuhucken. Hier wir nehmen wieder K-Probes Exec-V, also das ist das, was wir schreiben. Und das macht nur ein Trace-Print-K, das heißt, wir wollen einfach nur sehen, dass der ausgeführt wird. Jetzt machen wir noch was Spannendes. Wir schauen uns den übergebenen Dateipfad gegen bekannte Standard-Standard-Verzeichnisse an. So was wie Local Bin, User Bin. Wir können das machen, indem wir einfach den Dateinamen überprüfen gegen unsere Liste. Wir haben hier unser Directory Prefix gesetzt. Das ist alles schon wieder in eBPF zu kompliziert, um das zu prüfen gegen alle anderen Direkturys. Deswegen haben wir den Geschleife hier ausgerollt. Das schauen wir halt, ist es in bin oder nicht. Dann haben wir noch einen weiteren Gedanken gehabt, wenn wir jetzt eine Web-Applikation haben und es führt jetzt Zeug aus, dass es nicht ausführen sollte. Also wir können uns zum Beispiel vorstellen, dass es nur ein Frontend für Ping ist. Es nimmt eine IP-Adresse vom User und lässt dann halt Ping dagegen laufen, was man halt so macht. Wir wollen wissen, ob es auch noch was anderes ausführt als Ping. Das ist hier das C-Programm, das wir hier machen. Wir hocken uns wieder rein ins Sys-Execv-E. Wenn die PID mit Pfeifen zugewiesen wird, dann machen wir einen Pass. Wenn das nicht für unsere Web-Anwendung passend ist, dann macht einfach gar nichts. Und wir passen hier auch die Länge von dem Buffer an. Wir haben die derselbe Länge. Dann passt es sowieso nicht. Und wenn es die Länge acht hat, dann lassen wir den User wissen, wenn es Ping ist oder nicht. Und jetzt wollen wir File aus Zugriffe monitoren. Dabei schauen wir uns an, wenn ein Programm einen Teil einem bestimmten Verzeihungsöffnete. Und wir hocken dazu open und das jetzt ein bisschen nützlich zu machen. Wollen wir wissen, wenn irgendetwas unterhalb des Route-Directories aufgemacht wird? Das ist ein Service-Problem. Wir setzen oben das Headset und dann wieder die Schleife ausrollen. Jetzt müssen wir euch aber ein Geständnis machen. All die Beispiele, die wir gerade gemacht haben, sind alle unsicher. Wirklich, wirklich unsicher. Nur weil eBPF den Kernel nicht crashen kann, heißt noch lange nicht, dass es irgendwie safe ist. Die Beschränkungen machen es wahnsinnig schwierig, eBPF sicher zu schreiben. Was uns aufgefallen ist, dass ein Time of Check, Time of Use Schwachstelle da drin war. Also wenn dein K-Pro Syscall ist, also die Daten, die ein Nutzer reinsteckt in so einen Syscall, können sich ändern zwischen der Zeit, wo der Kernel das kopiert und den dann ausführt. Was wir jetzt machen ist, wir nehmen mal ein 2-Freaded-Programme. Der erste Fread kopiert einfach nur verschiedene Pfeilpfade in einen String und der zweite beruft open und drauf auf. Und dann k-proben wir den Open Call. Man sieht hier, ist das alles durcheinander und die Frage ist, wie gehen wir damit um? Ja, wir verwenden interne Kernelfunktionen statt den Syscalls. Es gibt noch ein paar andere Sachen, die wir im Gerechtnis behalten müssen, wenn wir das machen. Zum Beispiel wie die Dateinamen unter Unix funktionieren. Was ist, wenn wir es zum Beispiel nicht durch einen absoluten Pfad zugegriffen wird, als zum Beispiel ein relativer Pfad? Kann man das irgendwie lösen? Ja, wir können versuchen das selber zu kanalisieren, also kanalisch machen. Es ist wahnsinnig kompliziert das zu tun in eWPF. Oder wir können halt versuchen, eine interne Funktion zu finden, die den Zugriff auf den absoluten Pfad hat. In diesem Beispiel hat es am Ende leider den gleichen Wert bekommen wie der Syscall. Also funktioniert es sich wirklich. Wir wissen, dass BCC einen Beispielcode hat, um eWPF für Netzwerk Monitoring zu verwenden. Etat hat zum Beispiel TCP-Header nicht richtig gepasst und es war dann möglich, den TCP-Header zu spufen. Und wir haben den sowohl ein Beispiel für den Herg gezeugt und dann auch ein Patch geschrieben. Im Allgemeinen gilt, dass man jeden Wert Sanitize, den man irgendwie vom User bekommt. Wie eWPF hat keine Copy from User, keine Operfunktion. Also das heißt, die Sanitizing machen würde auf UserInput. Wenn man jetzt einfach irgendetwas ausführt auf einem Pointer, den der User übergeben hat, dann kann man auch blind irgendwo im Kernel rumstochen im Speicher. Kann man eWPF daher für Verteidigung verwenden? Wir sagen jetzt nicht so direkt. Also die Beschränkungen, die es hat, machen es relativ kompliziert, es sich jetzt zu verwenden. Es ist sehr viel nützlich, um irgendwie die Daten im Kernel zu verwenden, zu beobachten. Unix Dumps ist ein Programm, das wir uns anschauen wollen. Das ist das TCP-Dump für Unix Domain Sockets. Es sollte irgendwie für Pre-Tracing verwendet werden früher. Es hängt sich in den Stream Send Manage Funktions Call rein und einen weiteren. Und es benutzt Pfeifen, um dynamisch C-Code zu generieren wie vorhin von Jeff erklärt. Und das versuchen wir zu nutzen, um den EWPF-Programm während der Laufzeit läuft zu verändern. Und das ist hier der binäre Suchbaum, den Jeff vorhin erwähnt hat. Wir haben einen weiteren Perse-CPU Array, der den jeweils benutzen Ringbuffer festhält. Wir müssen sicherstellen, dass die Werte, die im Kernelspace und im User Space überprüft werden, weil da ist eine Race-Condition, weil C weiß nicht, dass Pfeifen jetzt fertig ist mit dem Verwenden der Werte. Und wir müssen sicherstellen, dass C weiß, dass er die Werte noch nicht verwendet, bevor sie fertig sind. Wenn EWPF nicht so gut ist bei der Verteiligung wofür können wir es noch verwenden? Also lass uns mal über Angriff reden. Also wir haben jetzt einen bösen Jungen, der irgendwie Zugriff auf ein modernes Linux kriegt. Und er benutzt zum Beispiel Capsus-Admin in einem Container. Container sollten sicher sein, insofern ist es kein Thema, oder? Also was könnte er jetzt mit EWPF machen? Ziemlich viel, wie sich rausstellt. Man kann damit so ziemlich alles sehen, nicht nur K-Prob, U-Probs und so. Es kann dummerweise auch schreiben, und das sogar in den User Space. Das ist jetzt ein magischer Syscall, BWPF Pro-Bright-User, der mir erlaubt, dass ich auch in den Speicherschreibe. Ist da irgendwas in diesen Sektionen, die er schreiben kann? Die einzige, die ich schreiben kann, ist die Text, also der Code, Section. Ist aber was Interessantes in den anderen Dingen drin? Ja klar, das sind Buffer für Daten, die gelesen und geschrieben werden durch Syscalls. Und die Frage ist jetzt, was können wir damit machen? Hier hat ein Kron-Job geschrieben, das spooft Kron-Jobs. Was er macht, ist er autohuckt alle Stats, Syscalls. Und jedes Mal, wenn ein Kron versucht, etwas aus Kron-Tab, also wenn er rausführen will, aber ein Job ausführen muss, dann schaut er auf das Datum der letzten Änderung, und wir ändern das, um das jedes Mal auszuführen. Und dann hucken wir da rein, schreiben, erinnern dann den FD und schweinern das dann woanders ab, wo wir das dann machen. Und wenn es closed macht, dann hören wir damit auf. Und dann lesen wir das durch den FD. Und wenn wir ein Read sehen, dann überschreiben wir den Inhalt der Datei mit, was auch immer wir wollen, also einem Loot Command zum Beispiel. So, und das wollen wir euch jetzt mal zeigen. Das ist ein Container, es ist tatsächlich ein bisschen modifiziert, denn Docker hat ja Profiler ein bisschen aktualisiert, und zwar nachdem wir das gemacht haben, und das heißt, hat einen von den Angehobensvektoren eliminiert. Aber, ja, wir haben es jetzt ein bisschen angepasst, dass es funktioniert, um die beschriebene Attacke zu machen. Also, was ihr eben gesehen habt, ist, dass das Loot Command, das ist Loot Command nur genau am Anfang von der Grundhab eingefügt wurde. Aber, ja, wir nutzen auch noch Pro-CPU-Maps, um, ja, das ist ganz klar, sich miteinander kommunizieren zu lassen und dann, dann haben wir auch noch eine Helperfunktion, die uns erlaubt, die Zeitfotore einzulassen, ohne wieder zurück in Userspace zu gehen. Also, was kann man in Userspace machen? Man kann zum Beispiel Return-oriented Programming machen. Das heißt, man kann den Stack modifizieren, der Stack enthält Return-Adressen. Wir können den ganzen Stack im Userspace-Speicher lesen, und wir können den Programm- und Datenabschnitt lesen. Und das heißt, wir haben G-Lib-C-Porn geschrieben, was, ja, der System D-Autoporner ist. Und das heißt, es scannt durch den PID 1, also den Speicher vom Prozess mit PID 1, und speichert den Stack-Inhalt und speichert den Stack-Inhalt an der Rücksprung-Adresse vom Lütscher Cisco Stubb und, ja, wir laden dann unsere bösartige Library in PID 1 rein und räumen danach wieder hinter uns auf. Also, wieder Demo. Ups, nicht das. Ah, genau. Also, wir haben hier dieses Stückchen Finding-Lib-C. Was das macht, ist, es dampnt die ganzen Rücksprung-Adressen und die Basis-Adresse von Lib-C. Und dann rufen wir die Porn-Lib-C-Funktion auf, die dann eben diese Return-oriented Programming-Chain-Aufrufe, die ich eben erwähnt habe, hat ganz viele lustige Gadgets drin, die jetzt ziemlich nützlich sind. Und wenn wir das laufen lassen und ein bisschen warten, dann returned es irgendwann und, ja, lässt dann eben unsere Shared-Library laufen und wird Sys-Log aufrufen und dann noch Sachen in Slash-Temp, Slash-Evil schreiben. Und wir bekommen auf einmal von SystemD geloggt Hallo35C3. Das heißt, unser Angriff hat funktioniert. Wir haben jetzt nicht mehr so viel Zeit, aber nochmal wie das Ganze funktioniert. Also, wir hängen uns an Timer-FDZ-Timer dran, denn in SystemD ruft das im Prinzip einmal pro Minute auf. Das ist ziemlich zuverlässig. Dann scannen wir durch den ganzen, also durch dieses stackbasierte Datenstruktur eTimerspec und suchen nach Rücksprung-Adressen und patchen die, dass sie zurück in den Cisco-Stab zeigen und, ja, im Prinzip setzt das einfach die Cisco-Nummer und ruft dann die Cisco-Instruktion auf. Und das heißt, wir suchen nach dem Cisco-Stab. Wir kennen ja den Offset vom Start von libc und so weiter. Und das heißt, nachdem wir das gemacht haben, ist uns dann noch aufgefallen, dass wir auch noch ein Weg haben, alle Register von dem Prozess zu bekommen und eBPF gibt uns das normalerweise nicht. Also, wir müssen eigentlich den Server-Scan, aber das ist ziemlich nützlich für andere Dinge. Dann, ja, hängen wir uns nochmal an Timer-FDZ dran, um den Rückgabewert zu bekommen. Wir speichern uns zuerst den Stack, dann hängen wir die returner-in-programming-Chain rein, dann gehen wir wieder zurück in User-Space, dann springt unser Code wieder in die Rob-Chain rein und die Rob-Chain ruft dann DL Open auf und so weiter. Und dann wollten wir noch vermeiden, dass wir aufräumen. Und das bedeutet, dass wir Clause mit so einer magischen Argument aufrufen, dass wir in unserem eBPF hook dann wieder verwenden, also wir verwenden das diese speziellen Wert als Signalwert und das heißt, wir können dann aus unserer Pro-Peraus aufräumen. Das heißt, wir schreiben da auch wieder eine Rob-Chain rein und wir machen das, weil die originale Rob-Chain eventuell relativ viel Stackplätze braucht und so ist es deutlich einfacher. Dann gehen wir aus dem Kernel zurück in den User-Space und im Prinzip führt dann dazu, dass die neue Rob-Chain ausgeführt wird. Und das bedeutet, dass dann die neue Rob-Chain ausgeführt wird, die restauriert den originalen Stack, setzt die Register zurück und es sieht dann nachher für System dieser Aussätze, ob der Timer-FD-Aufruf tatsächlich funktioniert hätte. Der Vorteil von GLEBC ist auch, dass es relativ stabil ist, selbst zwischen verschiedenen Versionen und Lenungstistros und das heißt, man braucht zwar die verschiedenen Offsets, aber im Prinzip ist es alles immer das Gleiche, es sind immer die gleichen Gadgets und man kann das dann eben, das ist quasi eine Sache, die man mit eBPF machen kann. Da gibt es noch die Möglichkeit, das so zu verwenden, wie es eigentlich mal gedacht war. Also man kann Prozesse zum Beispiel davon abhalten, mit dem Kernel zu interrogieren. Also man kann zum Beispiel Prozesse davon abhalten, eBPF K-Props und so weiter zu listen. Man kann die Blockier neue eBPF Programme zu erstellen, Körne Module zu laden und so weiter. Und das ist super wichtig, denn während das eBPF Programme kompiliert und geladen wird, dann gibt es eine D-Message Warning, die sagt, dass das jetzt der Fall ist und dass das möglicherweise zur Ruktion führt. Und der interessante Teil ist aber, dass alle, die sich D-Message Output ausgeben lassen, wollen ja da von etwas lesen müssen. Und das heißt, ja, man kann dann da interessante Sachen tun. Dann gibt es noch so Geschichten wie DPDK, mit denen man Memory Map.io direkt mit einem Netzwerkgerät und so machen kann. Das heißt, die kann man jetzt vielleicht nicht stoppen, aber ja, im Prinzip ist es möglich, einen Zyscall davon abzuhalten, das immer auch passiert. Und ja, das eine Problem ist aber, dass wir unseren eBPF am Prozess am Leben halten müssen, denn sobald der eBPF-Prozess, also der Prozess, der das eBPF mal in den Kanal geladen hat, stirbt, dann geht alles mit dem weg. Und ja, welcher Prozess läuft dann immer? Ach ja, das ist ja Bitwon. Und das heißt, wir können einfach, wenn wir Bitwon gekapert haben, alle eBPF K-Probes in das System initiieren und das, der Bitwon lebt halt so lange, bis das System herunterfährt. Und ja, das ist für uns super, denn Bitwon ist System D und ja, alle wissen ja, dass das aus einer Feld. Und ja, das heißt, das ist auf jeden Fall ein guter Angriffszweck. Zusammenfassend, eBPF ist nützlich für alle. Man kann allerdings keine Intrusion Detection Systeme da drauf bauen. Und ja, es ist einfach gerade noch nicht so ganz da, wo es sein will, um da jetzt Sachen zu bauen, anders als das Monitor. Deswegen eine Bitte an Kernel-Entwickler. Wir brauchen mehr Helperfunktionen. Also, wir brauchen sowas wie Copy from User, so dass Leute nicht ausversehen, irgendwelche beseitigen Pointer da laden. Und man möchte vielleicht auch noch irgendwelche Helper für irgendwelche Datenstrukturen, also komplizitere Datenstrukturen anbieten, zum Beispiel für Dateien und Fahre und so weiter, dass man halt nicht so komische Sachen machen muss, die wir hier vorgestellt haben. Dann irgendwelche Operationen auf Strings und so weiter. Naja, dann ein paar Shoutouts und Grüße an Leute. Also, die PCC-Leute sind super und machen total cooles Tooling. Julia Evans hat ziemlich viele coole Blockbos, wie das alles funktioniert. Brandon Greg auch. Und Jesse Frazel schreibt, was das BPFD heißt. Und das klingt genau nach dem, was sie eigentlich in Pit 1 als mein Rootkit-Manager reinbauen möchte. Aber das managt dann, ja, all die Sachen. Abschließend, ihr könnt euch vor der Zukunft nicht verstecken. Gibt es irgendwelche Fragen? Yeah. Are there questions? Gibt es Fragen? So, instead of going into the god motors, I went into the flying spaghetti monster motors, isn't it? Questions? Ladies, gentlemen, number two here, two. Hi there, great talk. Can you just turn off compiler optimizations in BPF and save yourself a lot of trouble in the validator? Kann man einfach die compiler Automatisierung abschalten an der Stelle? Also, der ganze Code ist in C++, der basiert auf LVM und das heißt, wenn man da jetzt irgendwas anpassen will am validator oder so, dann muss man das alles in LVM und LVM neu kompellieren. Und ja, ich weiß jetzt nicht genau wann das genau passiert. Welche Versionen habt ihr getestet? Welche kernel Versionen habt ihr ausprobiert? Es gab einen Haufen Verbesserungen bei den Sachen letztens. Also, die Versionen, auf der wir gearbeitet haben, waren zwei verschiedene Kernel, 4.15 von Ubuntu 18.04 und dann 16 oder 17 von Kali Linux. Wir haben gesehen, das ist einfach Sachen damit zu injecten. Wie können wir das von vornherein verhindern? Ich sehe, dass ich eBPF im Kernel brauche, um zum Beispiel für Firewalls und so. Aber wie schaffe ich es als User? Dass es weiteren eBPF-Code in meinen Kernel rein und jagt. Kannst du das nochmal wiederholen? Wie kann man es schwerer machen für irgendwelche Skripten, dass sie damit Sachen ins checken? Also, du brauchst dafür Capsus, Admin und ja, also bei Containern und App-Armor-Profilen und so was gibt es eigentlich relativ viele Gegenmaßnahmen, die Interaktion mit SysFS maskieren. Das heißt, das funktioniert nicht. Es gibt dann aber einige Workarounds dafür, die zum Beispiel direkt den eBPF-Syscall verwenden und dann nicht durch SysFS gehen. Und das geht dann halt eben um diese Schutzmaßnahmen, die bereits existieren drum herum. Ist dir, ob es irgendwelche Cloud-Providers gibt, wo das funktioniert, dass man das auswirkt? Also, wenn die euch Capsus, Admin gibt, dann haben sie echte andere Probleme. Also von Capsus, Admin zu vollem Rotzugraff gehen, das ist echt einfach, quasi so ein lustiger Party-Trick. Das ist keine echte Privilege-Escalation-Attack oder so was. Aber wenn ihr rausfindet, also das sollten die, das sollte der Provider eigentlich einfach nicht machen. Das ist sein großes Problem für die. Fragen Sie noch im Internet? Oder hier noch im Saal? Gut. Danke Jeff und Andy. Ein guter Applaus nochmal und damit verabschieden und kassend sich von der Besetzung.