1 Punkte von GN⁺ 2024-08-10 | 1 Kommentare | Auf WhatsApp teilen
  • Google hat CVE-2023-2163 im eBPF-Verifier mit Buzzer entdeckt und bestätigt, dass die Schwachstelle für lokale Privilegienausweitung und Container-Escape ausgenutzt werden kann
  • eBPF erweitert Kernel-Funktionen zur Laufzeit, führt aber beliebigen Bytecode mit hohen Rechten aus; daher ist die Verifier-Prüfung vor dem Laden eine zentrale Säule der Sicherheit
  • Das Problem entsteht dadurch, dass das Path Pruning des Verifiers tatsächlich unterschiedliche Ausführungspfade fälschlich als gleichwertig mit bereits sicheren Pfaden einstuft
  • Der Exploit nutzt den Unterschied aus, dass ein Register, das der Verifier als 0 betrachtet, zur Laufzeit einen anderen Wert hat, korrumpiert damit den eBPF-Stack-Pointer und ermöglicht beliebiges Lesen/Schreiben sowie eine KASLR-Umgehung
  • Der Patch markiert auch imprecise Register, die precise Register beeinflussen, als precise; mit derselben Pointer-Arithmetic-Fuzzing-Strategie wurden danach keine weiteren Probleme gefunden

Die durch den eBPF-Verifier vergrößerte Kernel-Angriffsfläche

  • eBPF ist eine Technologie, mit der sich Funktionen des Linux-Kernels zur Laufzeit erweitern lassen, ohne komplexe Kernel-Module zu benötigen
  • eBPF-Programme werden als eigener Bytecode geschrieben und vor ihrer Ausführung bei bestimmten Ereignissen auf Sicherheit geprüft
    • Ein typisches Beispiel ist ein eBPF-Programm, das bei einem bestimmten syscall ausgeführt wird
  • Da die Struktur die Ausführung beliebigen Codes mit hohen Privilegien erlaubt, wächst die Angriffsfläche des Kernels deutlich
  • Programme müssen vor dem Laden den Verifier durchlaufen; dieser prüft, ob die Sicherheitsannahmen von eBPF erfüllt sind
  • Wird eine Verifier-Schwachstelle ausgenutzt, führt das in der Regel zu lokaler Privilegienausweitung oder in Container-Umgebungen zu einem Container-Escape

Buzzer und Pointer-Arithmetic-Fuzzing

  • Google hat Buzzer entwickelt, um den eBPF-Verifier-Code automatisch zu auditieren
  • Buzzer ist ein Fuzzer, der große Mengen syntaktisch gültiger eBPF-Programme erzeugt und Strategien zum Auslösen logischer Bugs konfigurieren kann
  • Für die Entdeckung von CVE-2023-2163 wurde eine Pointer-Arithmetic-Strategie verwendet
    • Er erzeugt einen Header, der Register mit Zufallswerten initialisiert
    • Er erzeugt eine Sequenz zufälliger arithmetischer und Jump-Instruktionen
    • Er wählt ein zufälliges Register aus und führt eine Addition mit einem Pointer auf ein eBPF-Map-Element aus
    • Er schreibt einen Magic Value in dieses Element
    • Wird der geschriebene Wert im Userspace nicht beobachtet, ist möglicherweise ein Out-of-Bounds-Write aufgetreten
  • Die von Buzzer gefundene Schwachstelle CVE-2023-2163 lag in der eBPF-Path-Pruning-Logik und führte zu einem Exploit, der sowohl für Container-Escape als auch für lokale Privilegienausweitung genutzt werden kann

Path Pruning und Bug im Precise Tracking

  • Der eBPF-Verifier simuliert mögliche Ausführungspfade, um sicherzustellen, dass ein Programm sicher ausgeführt werden kann
  • Wenn Registerwerte bei einer bedingten Verzweigung nicht sicher bekannt sind, versucht der Verifier, die Ausführung aller möglichen Zustände nachzuverfolgen
  • Mit zunehmender Zahl bedingter Sprünge wächst die Anzahl der Ausführungspfade exponentiell, was sich auch negativ auf die Performance beim Laden eines Programms in den Kernel auswirkt
  • Um das zu reduzieren, haben eBPF-Entwickler Path Pruning eingeführt
    • Wenn der Verifier garantieren kann, dass ein zu einem bestimmten Zustand gleichwertiger Zustand bereits sicher eine exit-Instruktion erreicht, untersucht er diesen Pfad nicht weiter
  • Für effizienteres Pruning wird zusätzlich das Konzept des Precise Tracking verwendet
    • Ein Register wird als precise markiert, wenn es an Pointer-Arithmetic-Operationen beteiligt ist oder als Konstante an eine Helper-Funktion übergeben wird
    • Der Verifier muss alle Zustände untersuchen, die dieses Register betreffen
  • Bei CVE-2023-2163 wurde r9, das die Präzision von r6 beeinflusste, nicht korrekt markiert
    • Der Verifier nahm an, dass r9 nicht zur Preciseness von r6 beiträgt
    • Nachdem er in einem früheren Pfad entschieden hatte, dass r6 sicher einen exit erreichen kann, betrachtete er die übrigen Zustände als gleichwertig und prunte sie
    • Zur Laufzeit wurde der Pfad 1:2:4:6 ausgeführt; an Punkt 6 konnte r6, das der Verifier als 0 gesehen hatte, tatsächlich einen anderen Wert haben und in Pointer-Arithmetic-Operationen verwendet werden

Beliebiges Lesen/Schreiben und KASLR-Umgehung

  • Der Exploit-Code ist im Google Security Research Repository veröffentlicht
  • Für die Exploit-Entwicklung waren Arbeiten von @chompie und @_manfp wichtig
  • Der Ablauf besteht grob darin, zunächst beliebiges Lesen/Schreiben zu erreichen und danach die Process Credentials zu finden sowie uid und den fs_struct-Pointer zu patchen, um die Privilegien auszuweiten
  • Im ersten Schritt wird der korrumpierte Registerwert auf 1 gebracht
    • In der manuellen Analyse lag der r6-Wert zur Laufzeit bei 0x400, während der Verifier ihn als 0 sah
    • Mit der Instruktion r6 >>= 10 wurde der gewünschte Wert 1 erzeugt
  • Anschließend wird mit der Helper-Funktion bpf_skb_load_bytes_relative ein Pointer auf dem eBPF-Stack korrumpiert
    • Der Verifier entscheidet, dass len 8 ist, zur tatsächlichen Laufzeit wird len wegen des r6-Werts jedoch 9
    • Dadurch werden 9 statt 8 Byte geschrieben und das erste Byte des Werts am Stack-Offset -32 korrumpiert
  • Durch Manipulation des korrumpierten Stack-Pointers lässt sich ein eBPF-Map-Pointer leaken
    • Im Userspace werden die Werte R2 und R3 gelesen und geprüft, ob R2 0xBACA wird
    • Ist diese Bedingung erfüllt, ist R3 der geleakte Map-Pointer
    • Der eBPF-Map-Pointer-Leak führt zu einem Zustand, in dem KASLR umgangen ist
  • Wird mit derselben Strategie nicht nur ein einzelnes Byte, sondern ein zusammenhängender Stack-Bereich vollständig mit einem gewünschten Pointer überschrieben, wird beliebiges Lesen/Schreiben möglich
    • Aus Sicht des Verifiers ist es eine Manipulation des BPF-Stacks, tatsächlich kann aber Kernel-Speicher gelesen und geschrieben werden

Vom Map-Leak zur Root-Shell

  • Nach dem Map-Pointer-Leak unterscheidet sich der Exploit nicht wesentlich von Chompies Exploit und übernimmt Teile des Codes
  • Das grobe Verfahren ist wie folgt
    • In kstrtab wird wiederholt nach dem String init_pid_ns gesucht
    • Das ksymtab-Symbol, das diesen String referenziert, wird gesucht, um die Adresse der Struktur init_pid_ns zu erhalten
    • Der radix tree wird durchlaufen, um einen entry zu finden, dessen comm-Feld mit dem Namen der laufenden Exploit-Datei übereinstimmt
      • Wenn der Exploit in einem Container läuft, ist die PID keine verlässliche Heuristik, daher wird nicht nur die PID verwendet
    • Die uid wird auf 0 gepatcht und der fs_struct-Pointer wird gepatcht
    • Wird fs_struct auf denselben Wert gepatcht wie der Pointer, den PID 1 referenziert, kann bei Ausführung im Container das Host-Dateisystem beobachtet werden
    • system("/bin/bash") wird ausgeführt, um eine Root-Shell zu erhalten
  • Der auf GitHub veröffentlichte Code funktioniert nur auf bestimmten Linux-Versionen als Container-Escape
  • Auf Ubuntu und einigen Distributionen unterscheiden sich die Offsets der überschriebenen Datenstrukturen, sodass er nur als lokale Privilegienausweitung funktioniert
  • Damit er auf jeder Linux-Distribution funktioniert, muss der Code angepasst werden

Patch und anschließende Verifikation

  • Ursachenanalyse und Patch für CVE-2023-2163 sind in der Kernel-Mailingliste einsehbar
  • Die Korrektur markiert imprecise Register von Operationen, die ein precise Register beeinflussen, ebenfalls als precise
  • Ob diese Änderung die Performance des eBPF-Verifiers beeinflusst, ist nicht klar
  • Dieselbe Pointer-Arithmetic-Fuzzing-Strategie wurde weiter ausgeführt, es wurden jedoch keine weiteren Probleme gefunden
  • Buzzer wird weiterentwickelt und nimmt Beiträge der Open-Source-Community über das GitHub-Repo entgegen

1 Kommentare

 
GN⁺ 2024-08-10
Meinungen auf Hacker News
  • Auf den Plattformen, auf denen eBPF am häufigsten eingesetzt wird, ist die Auswirkung von Verifier-Bugs oft nicht besonders groß, weil unprivilegierter Code von vornherein keine eBPF-Programme laden kann.
    Solche Bugs sind letztlich Root-→-Ring-0-Schwachstellen; das heißt nicht, dass sie belanglos wären, aber bei serverseitigen Workloads ist das meist ein vertretbarer Kompromiss.
    Insbesondere ist die Historie lokaler Kernel-Privilege-Escalations bei eBPF im Vergleich zum Kernel insgesamt ziemlich ordentlich, und der größte Wert des Verifiers in heutigen eBPF-Umgebungen besteht darin, dass es schwer wird, den Kernel versehentlich mit fehlerhaften eBPF-Programmen zum Absturz zu bringen.
    Bei gewöhnlichen ladbaren Kernelmodulen gilt das in fast schon lächerlichem Maße nicht.

    • Der PoC schreibt mit einem Out-of-bounds-Pointer in eine eBPF-Map, aber bei einem einfachen Fehler in der Bereichsverfolgung skalarer Werte scheint es auch über ein nicht erweitertes, per seccomp ladbares BPF-Programm ausnutzbar zu sein.
      In diesem Fall sind auf den meisten Plattformen keine Privilegien erforderlich.
      Und wenn unprivilegierte User-Namespaces verfügbar sind, kann man sich selbst zu „root“ machen, wodurch auch Root → Ring 0 zu einem weniger stark eingeschränkten Problem wird.
      Dieses Muster sieht man immer wieder in eBPF-Bug-PoCs, seit Distributionen diese Funktion erst aktiviert und dann größtenteils wieder deaktiviert haben.
    • Man sollte auch nicht vergessen, dass man Containern CAP_BPF geben kann.
      Je größer Tools wie Cilium werden, desto realistischer werden Angriffspfade, die in eine Container-Umgebung mit cap_bpf führen.
    • Verifier-Bugs zu beheben ist wichtig, weil es eine Voraussetzung dafür ist, unprivilegierte eBPF-Nutzung sicher zu machen.
  • „Uno no es ninguno“ heißt wörtlich etwa „eins ist nicht keines“, also eher „One is not none“.

    • Im Spanischen ist eine doppelte Verneinung häufig tatsächlich keine doppelte Verneinung.
      Zum Beispiel sagt man für „hier ist nichts“ „no hay nada aquí“, was wortweise übertragen wie „hier ist nicht nichts“ aussehen kann.
      Auch die Royal Spanish Academy erklärt das so:

      https://www.rae.es/espanol-al-dia/doble-negacion-no-vino-nad...

      Die sogenannte „doppelte Verneinung“ entsteht im Spanischen und anderen romanischen Sprachen durch negative Kongruenz, die in bestimmten Situationen zwingend gilt; dadurch erscheinen das Adverb no und andere Elemente mit negativer Bedeutung gemeinsam in einem Satz.
      Dass diese beiden „Verneinungen“ gleichzeitig vorhanden sind, hebt die negative Bedeutung des Satzes nicht auf.

  • Als ich früher versucht habe, eBPF zu verwenden, war es für die benötigte Aufgabe nicht ausdrucksstark genug.
    Ich frage mich, ob es wirklich gerechtfertigt ist, die Komplexität im Kernel-Space zu erhöhen, nur um begrenzte Flexibilität zu gewinnen.
    Für Packet Filtering verstehe ich es, aber bei anderen Einsatzzwecken wie Sandboxing wirkt es weniger überzeugend.

    • Für solche Zwecke gibt es auch andere Technologien wie DTrace.
      Die Wahl für den Kernel ist nicht eBPF oder gar nichts, sondern eBPF oder etwas anderes, das ihm ähnelt.
      Man nutzt es vielleicht selbst nicht viel, aber es gibt Leute, die den ganzen Tag damit arbeiten.
      Ich meine, FAANG-Ingenieure hätten gesagt, dass auf all ihren Servern ständig Dutzende, vielleicht sogar Hunderte solcher Programme laufen, und das ohne einmalige Einsätze mitzuzählen.
      FAANG beschäftigt auch eigene Kernel-Entwickler, finanziert also gewissermaßen auch die Komplexität, die sie selbst nutzen.
      Auch ich habe schon Probleme mit eBPF gelöst.
      Ohne eBPF wären das Probleme gewesen, die für jemanden, der kein Kernel-Experte ist, praktisch unlösbar sind; man braucht es nicht oft, aber wenn man es braucht, gibt es keinen Ersatz.
      In manchen Fällen haben selbst Kernel-Experten die Wahl zwischen eBPF und der dauerhaften Pflege eines Custom-Kernel-Patches.
    • Wären traditionelle ladbare Kernel-Mode-Treiber nicht besser als Patches oder eBPF?
      Ich weiß, dass sie unsicher sind, aber die Leute, die damit arbeiten, wissen, dass mit großer Macht Verantwortung einhergeht.
  • „Uno no es ninguno“ scheint korrekt mit „One is not none“ übersetzt zu werden.

    https://bughunters.google.com/blog/6303226026131456/a-deep-d...

    • Stattdessen wird es als „One is none“ übersetzt.
      Das ist die berüchtigte doppelte Verneinung, mit der Fremdsprachler, mich eingeschlossen, zu kämpfen haben.

      https://spanish.stackexchange.com/questions/26777/how-does-d...

    • Wörtlich ist das so, aber im Spanischen wird die doppelte Verneinung merkwürdigerweise normalerweise einfach als Verneinung verwendet.

    • Vielleicht wäre etwas wie „one ain't nothin'“ treffender.

  • Bei uns gibt es den Ausdruck „ein Igel in der Hose“.
    So nützlich es auch sein mag: Das wirkt nicht so, als wäre es sicher und umsichtig geschrieben worden.

    • Mit wachsender Erfahrung lernt man, dass Fehler passieren können, selbst wenn man sicher und umsichtig vorgegangen ist.