
Untersuchung des Fehlers hinter CVE-2021-26708
Untersuchung des Fehlers hinter CVE-2021-26708
Dieses Repository enthält eine kleine Zusammenfassung über CVE-2021-26708 und wie dieser Fehler in eine Use-After-Free-Schreibprimitive umgewandelt werden kann. Der PoC hier ist kein vollständiger Exploit, sondern nur meine Testumgebung, die ich bei der Untersuchung dieses Fehlers verwendet habe. Er kann erfolgreich einen Eintrag aus dem kmalloc-64-Cache nach dessen Freigabe nutzen, enthält aber keinen Code zum Grooming des Speichers und zum Platzieren von etwas Interessantem in dem Slot.
Dies ist ein lustiger Fehler, gemeldet von @a13xp0p0v. Er fiel mir auf, weil der Patch so einfach war: Er verhindert lediglich, dass außerhalb der Sperre an 5 verschiedenen Stellen ein Verweis auf vsk->transport erlangt wird.
https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=c518adafa39f37858697ac9309c6cf1805581446
Im Folgenden finden Sie eine kurze Schritt-für-Schritt-Anleitung, wie man vom Patch zu einer Use-After-Free-Primitive gelangt, die für die Ausnutzung verwendet werden könnte. Die Anleitung sollte anderen helfen, die diesen Fehler untersuchen möchten.
Ich habe den Linux-Kernel 5.10.13 heruntergeladen und den oben gezeigten Patch manuell rückgängig gemacht. Weitere Informationen zum Erstellen und Ausführen des Kernels finden Sie in der folgenden Referenz.
https://fedoraproject.org/wiki/Building_a_custom_kernel
Ich habe auch die Boot-Parameter geändert, um das Debuggen des Kernels mit kgdb zu ermöglichen. Mit gdb und der vmlinux-Datei, die ich zuvor erstellt hatte, hatte ich alle Kernel-Symbole für den Hauptkernel, aber nicht für ladbare Kernelmodule. Der mit der Sicherheitslücke verbundene Code wurde standardmäßig nicht geladen, sondern würde in den Kernel geladen, wenn die Familie PF_VSOCK verwendet wird (abhängig davon, wie Sie Ihren Kernel erstellt haben).
Um die Symbole in kgdb für geladene Module zu erhalten, habe ich sichergestellt, dass ich den vsock-Socket mindestens einmal benutzt habe, dann habe ich sudo cat /proc/modules | grep vsock verwendet, um die Basisadressen der zugehörigen Module zu erhalten. In gdb habe ich dann so etwas wie (gdb) add-symbol-file ./net/vmw_vsock/vsock.ko 0xffffffffc0567000 eingegeben, um gdb mitzuteilen, wo im Speicher die Symbole dieser ko-Datei sind. vsock.ko und vmw_vsock_virtio_transport_common.ko waren die beiden relevantesten.
Es macht Spaß, rückwärts von Patches zu arbeiten, denn anders als bei vielen Sicherheitslücken-Suchen weiß man bereits mit Sicherheit, dass man an der richtigen Stelle sucht. In diesem Fall wissen wir aus dem Patch, dass ein Verweis auf den Transport gespeichert wird, bevor die sock_lock erlangt wird. Wir können sicher erwarten, dass die Sicherheitslücke auf den Wechsel des Transports zurückzuführen ist, aber der alte Verweis verwendet wird.
In einem solchen Szenario würden wir hoffen, dass der Transport selbst ein dynamisch alloziertes Objekt ist, das zwischen dem Erhalten des Verweises und dem Halten der Sperre freigegeben und durch ein anderes Objekt ersetzt werden kann. Leider scheinen die von den anderen Modulen implementierten relevanten Transporte alle im globalen Speicher zu liegen. Also werden wir eine Ebene tiefer nach Elementen suchen, die verwendet werden, wenn sie außerhalb des Gültigkeitsbereichs liegen.
In af_vsock.c finden wir zwei Stellen, an denen vsk->transport geändert wird. In vsock_assign_transport und vsock_deassign_transport. In vsock_assign_transport sehen wir, dass, wenn es einen anderen vorhandenen Transport gibt, vsock_deassign_transport aufgerufen wird, bevor der neue Transport platziert wird.
Wenn wir uns die Möglichkeiten für den Aufruf vsk->transport->destruct(vsk) hier ansehen, sehen wir, dass sowohl der Loopback- als auch der Virtio-Transport einfach vsk->trans per kfree freigeben hier. Bingo! Wenn wir (1) einen Pfad zu diesem Aufruf finden können, der mit (2) einer verwundbaren Funktion konkurrieren kann, die einen Transportverweis von vor der Zerstörung verwendet, um auf vsk->trans zuzugreifen, dann haben wir unsere Primitive.
Auf der Suche nach einem Pfad zu vsock_deassign_transport sehen wir, dass es von vsock_sk_destruct oder vsock_assign_transport aufgerufen wird. vsock_sk_destruct ist als die Funktion sock->destruct gesetzt, daher können Aufrufe von __sys_close oder anderen verfügbaren Aufrufen entlang des Zerstörungspfads wie sock_put, sock_close oder vsock_release hier landen.
Der relevanteste Pfad zu vsock_assign_transport führt über vsock_stream_connect, erfordert jedoch, dass sich der Socket in einigen bestimmten Zuständen befindet, und ruft vsock_deassign_transport nur auf, wenn sich der Transport ändert. Und es würde den Parameter vsk->trans ersetzen, wenn wir keinen NULL-neuen Transport erhalten.
Bevor wir zu weit gehen, um den richtigen Pfad zur Freigabe zu finden, wollen wir sicherstellen, dass es einen gültigen Pfad gibt, der das Mitglied vsk->trans mit einem ungültigen Verweis auf einen zerstörten Transport verwendet. Wir können methodisch jede Stelle überprüfen, an der der Transport mit einem möglicherweise ungültigen Verweis verwendet wird. Wenn wir diese Lücken verfolgen, können wir Stellen finden, an denen vsk->trans verwendet wird. Der beste Pfad scheint über vsock_stream_setsockopt hier zu führen, wenn transport->notify_buffer_size in einen Offset innerhalb von vsk->trans für Loopback- und Virtio-Transports schreibt genau hier. Wenn trans bereits freigegeben ist, wenn es dort verwendet wird, erhalten wir einen schönen Schreibvorgang eines u32 auf einen Offset von 0x28 in einer kmalloc-64-Allokation.
Die Verwendung von vsock_stream_setsockopt als Primitive hängt von einem Rennen (Race) ab, bei dem zwischen dem Erhalten des Verweises auf den Transport und dem Erhalten der sock_lock eine kleine Zeitspanne liegt. Das ist ein kleines Fenster, und es gibt viele Anweisungen, um dorthin zu gelangen. Hier können wir eine coole Funktion in Linux namens userfaultfd nutzen, um unsere Chancen zu verbessern. Dieser Mechanismus ermöglicht es uns, Seitenfehler im Benutzermodus nach Belieben zu behandeln.
Siehe https://man7.org/linux/man-pages/man2/ioctl_userfaultfd.2.html und https://man7.org/linux/man-pages/man2/userfaultfd.2.html.
Damit können wir einen Thread (den Gater) haben, der die sock_lock erlangt und dann auf einige Benutzerspeicher zugreift und einen Seitenfehler verursacht. Wir können diesen Thread (mit noch gehaltener Sperre) so lange pausieren, wie wir wollen. Andere Threads, die versuchen, diese Sperre zu erlangen, bleiben dort, bis wir den Gater gehen lassen und die Sperre freigeben. Wir können unseren Thread, der die Zerstörung durchführen wird, und unseren Thread, der den ungültigen Verweis verwenden wird, aufstellen. Beide werden auf die sock_lock warten, und jetzt haben wir eine großartige Chance, das Rennen zu gewinnen. Wenn der Thread, der die Zerstörung durchführt, als Nächstes die Sperre erhält, wird unser setsockopt-Aufruf danach abgeschlossen. Er wird den Zeiger vsk->trans verwenden, nachdem er freigegeben (und ersetzt) wurde.
Wenn stattdessen der setsockopt-Aufruf zuerst kommt, verlieren wir das Rennen, können aber den gesamten Vorgang sicher erneut versuchen.
Beim Versuch, dies zu erstellen, bin ich eine Weile den falschen Weg gegangen. Ich habe versucht, vsock_deassign_transport über close und Timeouts aufzurufen, bin aber auf viele Referenzzählerprüfungen gestoßen, die die tatsächliche Zerstörung verzögerten, bis es zu spät war.
Nebenbei bemerkt, das Debuggen dieser Pfade kann schwierig sein; wie Sie sich vorstellen können, wird ein Haltepunkt auf dem Systemaufruf close oft getroffen. Selbst wenn Sie bedingte Haltepunkte verwenden, um nur im richtigen Thread anzuhalten, wird die Maschine extrem langsam. Ein interessanter Weg, dies zu umgehen, ist die Verwendung von eBPF mit Tracepoints, die bpf_trace_printk nur aufrufen, wenn die Bedingungen korrekt sind. Dann kann ein kgdb-Haltepunkt auf bpf_trace_printk gesetzt werden, der uns in die Nähe der richtigen Stelle bringt. Dies funktioniert nicht mit Kprobes, da Sie sich bereits in einem Haltepunkt-Handler befinden. Ich denke, das Hinzufügen eines bpf_trace_kgdb_break-Aufrufs zu eBPF könnte eine nette Ergänzung zum Kernel sein.
Als ich schließlich dazu überging, den Pfad vsock_assign_transport zu betrachten, fügte sich alles schnell zusammen. Um die Anforderungen zu erfüllen, verbinden wir uns zunächst mit VM_ADDR_CID_LOCAL, wenn kein lauschender Server vorhanden ist. Dies gibt uns den Loopback-Transport, aber wenn unsere Verbindung dann ausläuft oder fehlschlägt, kehrt unser Zustand zu SS_UNCONNECTED zurück. Dies ermöglicht uns eine weitere Verbindung zu einer Adresse, die größer als VM_ADDR_CID_HOST ist, was dazu führt, dass sich unser Transport ändert, unseren vorhandenen Transport zerstört und die Freigabe verursacht. Es ist wichtig, dass wir dies tun, wenn es tatsächlich keinen registrierten transport_g2h oder transport_h2g gibt, sodass unser neuer Transport NULL ist und unser freigegebener Verweis in vsk->trans verbleibt.
Mit all dem, was aufgereiht ist, erhalten wir eine zuverlässige Use-After-Free, die für eine Privilegieneskalation verwendet werden könnte.
Dieses Repository handelt nur davon, wie wir zur anfänglichen Use-After-Free gelangen. Aber jetzt haben wir eine Primitive, um einen Wert an einem Offset im kmalloc-64-Cache zu schreiben, wo zuvor virtio_vsock_sock alloziert war. Ich habe beschlossen, die Anleitung dort zu beenden, denn, was, muss ich hier alles machen?