CVE und Quadratwahrscheinlichkeit

Vor ungefähr einem Jahr, seit Juli 2019, erhielten wir seltsame Fehlerberichte über den RHEL7-basierten Kernel in OpenVz. Auf den ersten Blick waren die Fehler unterschiedlich: Die Knoten stürzten an verschiedenen Orten und sogar in verschiedenen Subsystemen ab, aber jedes Mal, wenn die Untersuchung das eine oder andere "krumme" Objekt fand. Die Objekte waren unterschiedlich, manchmal wurde dort eine Art Müll gefunden, manchmal ein Verweis auf den freigegebenen Speicher, manchmal wurde das Objekt selbst freigegeben, aber in allen Fällen wurde der Speicher für dieses Objekt aus dem kmalloc-192-Cache zugewiesen. Unter dem Schnitt - eine detaillierte Geschichte über diese Geschichte.



Bild



Die übliche Fehlerbehebung in solchen Fällen besteht darin, den Lebenszyklus des betroffenen Objekts sorgfältig zu untersuchen: Sehen Sie, wie Speicher dafür zugewiesen wird, wie er freigegeben wird, wie korrekt Referenzzähler genommen und freigegeben werden, wobei Fehlerpfade besonders zu beachten sind. In unserem Fall wurden jedoch verschiedene Objekte zerstört, und bei der Überprüfung ihres Lebenszyklus wurden keine Fehler gefunden.



Der kmalloc-192-Cache ist im Kernel sehr beliebt und kombiniert mehrere Dutzend verschiedene Objekte. Ein Fehler im Lebenszyklus eines von ihnen ist der wahrscheinlichste Grund für diese Art von Fehlern. Selbst das Auflisten all dieser Objekte ist ziemlich problematisch, und es steht außer Frage, alle zu überprüfen. Fehlerberichte kamen weiterhin an, aber wir konnten ihre Ursache nicht durch direkte Untersuchung finden. Ein Hinweis war erforderlich.



Von unserer Seite aus wurden diese Fehler von Andrey Ryabinin untersucht, einem Spezialisten für Speicherverwaltung, der in engen Kreisen von Kernel-Entwicklern als Entwickler von KASAN bekannt ist, einer großartigen Technologie zum Abfangen von Speicherzugriffsfehlern. Tatsächlich war KASAN am besten geeignet, um die Ursachen unseres Fehlers zu entdecken. KASAN war nicht im ursprünglichen RHEL7-Kernel enthalten, aber Andrey hat die erforderlichen Patches in OpenVz auf uns portiert. Wir haben KASAN nicht in die Produktionsversion unseres Kernels aufgenommen, aber es ist in der Debug-Version des Kernels enthalten und hilft unserer Qualitätssicherung aktiv bei der Suche nach Fehlern.















Neben KASAN enthält der Debug-Kernel viele andere Debug-Funktionen, die wir von Red Hat geerbt haben. Infolge des Debuggens erwies sich der Kernel als ziemlich langsam. Laut QA dauern dieselben Tests auf einem Debug-Kernel viermal länger. Für uns ist dies nicht grundlegend, wir messen dort nicht die Leistung, sondern suchen nach Fehlern. Eine solche Verlangsamung war jedoch für die Kunden nicht akzeptabel, und unsere Anfragen, einen Debug-Kernel in Produktion zu bringen, wurden ausnahmslos abgelehnt.



Als Alternative zu KASAN wurden Clients gebeten, slub_debug auf betroffenen Knoten zu aktivieren... Diese Technologie ermöglicht auch die Erkennung von Speicherbeschädigungen. Mithilfe einer roten Zone und einer Speichervergiftung für jedes Objekt prüft der Speicherzuweiser bei jeder Zuweisung und Freigabe von Speicher, ob alles in Ordnung ist. Wenn etwas schief geht, wird nach Möglichkeit eine Fehlermeldung ausgegeben, der erkannte Schaden wird behoben, und der Kernel kann weiterarbeiten. Außerdem werden Informationen darüber gespeichert, wer ein Objekt zuletzt zugewiesen und freigegeben hat, so dass im Fall der post-factum-Erkennung von Speicherbeschädigungen nachvollzogen werden kann, "wer" dieses Objekt in einem "früheren Leben" war. Slub_debug kann in der Kernel-Befehlszeile eines Produktionskerns aktiviert werden, diese Überprüfungen verbrauchen jedoch auch Speicher- und CPU-Ressourcen. Für die Entwicklung und das QA-Debugging ist dies in Ordnung, aber Produktionskunden verwenden es ohne große Begeisterung.



Sechs Monate sind vergangen, das neue Jahr rückte näher. Lokale Tests auf dem Debug-Kernel mit KASAN haben das Problem nicht erkannt. Wir haben keine Fehlerberichte von den Knoten mit aktiviertem slub_debug erhalten. Wir konnten nichts in den Rohstoffen finden und haben das Problem nicht gefunden. Andrey war mit anderen Aufgaben beladen, im Gegenteil, ich bekam eine Lücke und wurde angewiesen, den nächsten Fehlerbericht zu analysieren.



Nachdem ich den Absturzspeicherauszug analysiert hatte, entdeckte ich bald das problematische kmalloc-192-Objekt: Sein Speicher war mit einer Art Müll gefüllt, Informationen, die zu einem anderen Objekttyp gehörten. Es war den Konsequenzen einer nachträglichen Nutzung sehr ähnlich, aber nachdem ich den Lebenszyklus des beschädigten Objekts in den Rohstoffen sorgfältig untersucht hatte, fand ich auch nichts Verdächtiges.



Ich habe die alten Fehlerberichte durchgesehen und versucht, dort einen Hinweis zu finden, aber auch ohne Erfolg.



Schließlich kehrte ich zu meinem Fehler zurück und begann, das vorherige Objekt zu betrachten. Es stellte sich auch als in Gebrauch heraus, aber von seinem Inhalt her war es völlig unverständlich, was es war - es gab keine Konstanten, Verweise auf Funktionen oder andere Objekte. Nachdem ich mehrere Generationen von Verweisen auf dieses Objekt aufgespürt hatte, stellte ich schließlich fest, dass es sich um eine Shrinker-Bitmap handelte. Dieses Objekt war Teil der Optimierungstechnik zum Freigeben von Containerspeicher. Die Technologie wurde ursprünglich für unsere Kernel entwickelt, später wurde sie von ihrem Autor Kirill Tkhai an die Linux-Hauptlinie übertragen.



"Die Ergebnisse zeigen, dass die Leistung mindestens 548-mal steigt."



Mehrere tausend solcher Patches ergänzen den ursprünglichen rockstabilen RHEL7-Kernel und machen den Virtuozzo-Kernel für Hoster so bequem wie möglich. Wann immer möglich, versuchen wir, unsere Entwicklungen an die Hauptleitung zu senden, da dies die Aufrechterhaltung eines guten Zustands des Codes erleichtert.



Nach den Links fand ich eine Struktur, die meine Bitmap beschreibt. Der Deskriptor war der Ansicht, dass die Bitmap-Größe 240 Byte betragen sollte, und dies konnte in keiner Weise zutreffen, da das Objekt tatsächlich aus dem kmalloc-192-Cache zugewiesen wurde.



Bingo!



Es stellte sich heraus, dass Funktionen, die mit Bitmap arbeiten, über die Obergrenze hinaus auf den Speicher zugreifen und den Inhalt des nächsten Objekts ändern können. In meinem Fall gab es am Anfang des Objekts eine Nachzählung, und als die Bitmap sie aufhob, führte der nachfolgende Put zur plötzlichen Freigabe des Objekts. Später wurde Speicher für ein neues Objekt neu zugewiesen, dessen Initialisierung vom Code des alten Objekts als Müll wahrgenommen wurde, was früher oder später unweigerlich zum Absturz des Knotens führte.



Bild


Es ist gut, wenn Sie sich mit dem Autor des Codes beraten können!



Als wir seinen Code mit Kirill betrachteten, fanden wir bald die Grundursache für die festgestellte Diskrepanz. Mit zunehmender Anzahl von Containern hätte die Bitmap zunehmen sollen, aber wir haben einen der Fälle ausgelassen und daher manchmal die Bitmap zur Größenänderung übersprungen. In unseren lokalen Tests wurde diese Situation nicht gefunden, und in der Version des Patches, den Kirill an die Hauptleitung gesendet hat, wurde der Code überarbeitet, und es gab dort keinen Fehler.



Mit 4 Versuchen haben Kirill und ich zusammengearbeitet, um einen solchen Patch zu erstellen. Einen Monat lang haben wir ihn in lokalen Tests ausgeführt und Ende Februar ein Update mit einem festen Kernel veröffentlicht. Wir haben selektiv andere Crash-Dumps überprüft, auch die falsche Bitmap in der Nachbarschaft gefunden, den Sieg gefeiert und alte Bugs in der Stille abgeschrieben.



Die alten Frauen fielen jedoch immer weiter. Das Rinnsal dieser Art von Fehlerberichten ist geschrumpft, aber nicht vollständig ausgetrocknet.



Im Allgemeinen wurde dies erwartet. Unsere Kunden sind Hoster. Sie mögen es nicht, ihre Knoten neu zu starten, weil Neustart == Ausfallzeit == Geld verloren hat. Wir veröffentlichen auch nicht häufig Kernel. Die offizielle Veröffentlichung des Updates ist ein ziemlich mühsames Verfahren, bei dem eine Reihe verschiedener Tests durchgeführt werden müssen. Daher werden ungefähr vierteljährlich neue stabile Kernel veröffentlicht.



Um eine schnelle Übermittlung von Fehlerkorrekturen an Client-Produktionsknoten sicherzustellen, verwenden wir ReadyKernel-Live-Patches. Meiner Meinung nach tut dies niemand außer uns. Virtuozzo 7 verwendet eine ungewöhnliche Strategie für die Verwendung von Live-Patches.



Normalerweise ist Lifepatch nur Sicherheit. In unserem Land sind 3/4 der Korrekturen Fehlerkorrekturen. Korrekturen für Fehler, auf die unsere Kunden bereits gestoßen sind oder auf die sie in Zukunft leicht stoßen könnten. Tatsächlich können solche Dinge nur für Ihr Distributionskit ausgeführt werden: Ohne Feedback von Benutzern ist es unmöglich zu verstehen, was für sie wichtig ist und was nicht.



Live-Patches sind sicherlich kein Allheilmittel. Es ist im Allgemeinen unmöglich, alles hintereinander zu patchen - die Technologie erlaubt es nicht. Neue Funktionen werden auf diese Weise ebenfalls nicht hinzugefügt. Ein erheblicher Teil der Fehler wird jedoch mit den einfachsten einzeiligen Patches behoben, die sich hervorragend für lebenslange Patches eignen. In komplexeren Fällen muss der ursprüngliche Patch „kreativ mit einer Datei geändert“ werden, manchmal ist die Live-Patch-Maschinerie fehlerhaft, aber unser Zauberer des Lebens, der Zhenya Shatokhin flickt, kennt seinen Job perfekt. Kürzlich hat er zum Beispiel ausgegrabenbezaubernder Fehler in kpatch , über den es sich aus guten Gründen im Allgemeinen lohnt, eine separate Oper zu schreiben.



Wenn sich entsprechende Fehlerbehebungen ansammeln, normalerweise alle ein bis zwei Wochen, startet Zhenya eine weitere Reihe von ReadyKernel-Live-Patches. Nach der Veröffentlichung verteilen sie sich sofort auf die Clientknoten und verhindern den Angriff auf den bereits bekannten Rake. Und das alles ohne Neustart der Clientknoten. Und unnötig häufig Kernel freigeben. Kontinuierliche Vorteile.



Oft kommt der Live-Patch jedoch zu spät bei den Clients an: Das Problem, das er schließt, ist bereits aufgetreten, aber der Knoten ist dennoch noch nicht abgestürzt.



Aus diesem Grund war das Auftreten neuer Fehlerberichte mit dem Problem, das wir bereits behoben haben, für uns nicht unerwartet. Das wiederholte Parsen zeigte bekannte Symptome: alter Kernel, Müll in kmalloc-192, "falsche" Bitmap davor und ein entladener oder spät geladener Live-Patch mit einem Fix.



Einer dieser Fälle war der OVZ-7188 von FastVPS , der Ende Februar zu uns kam. „Vielen Dank für den Fehlerbericht. Unser Beileid. Sofort sehr ähnlich zu dem bekannten Problem. Schade, dass es in OpenVZ keine Live-Patches gibt. Warten Sie auf eine stabile Kernel-Version, wechseln Sie zu Virtuozzo oder verwenden Sie instabile Kernel mit einem Bugfix. "



Fehlerberichte sind eines der wertvollsten Dinge, die OpenVZ uns bietet. Wenn wir sie untersuchen, haben wir die Möglichkeit, ernsthafte Probleme zu erkennen, bevor einer der fetten Kunden auf sie tritt. Deshalb habe ich trotz des bekannten Problems trotzdem darum gebeten, Crash-Dumps für uns auszufüllen.



Das Parsen des ersten von ihnen hat mich etwas entmutigt: Die "falsche" Bitmap vor dem "krummen" kmalloc-192-Objekt wurde nicht gefunden.



Wenig später wurde das Problem auf dem neuen Kernel reproduziert. Und dann noch einer, noch einer und noch einer.



Hoppla!



Wieso das? Nicht behoben? Ich habe die Rohstoffe noch einmal überprüft - alles ist in Ordnung, der Patch ist vorhanden, nichts geht verloren.



Wieder Korruption? Am gleichen Ort?



Ich musste es wieder herausfinden.



Bild

(Was ist das? Siehe hier )



In jedem der neuen Crash-Dumps stieß die Untersuchung erneut auf das kmalloc-192-Objekt. Im Allgemeinen sah ein solches Objekt ganz normal aus, aber ganz am Anfang des Objekts wurde jedes Mal die falsche Adresse gefunden. Als ich die Beziehung des Objekts verfolgte, stellte ich fest, dass zwei interne Bytes in der Adresse ungültig waren.



in all cases corrupted pointer contains nulls in 2 middle bytes: (mask 0xffffffff0000ffff)
0xffff9e2400003d80
0xffff969b00005b40
0xffff919100007000
0xffff90f30000ccc0


Im ersten der aufgeführten Fälle sollte anstelle der "falschen" Adresse 0xffff9e2400003d80 die "richtige" Adresse 0xffff9e24740a3d80 lauten. Eine ähnliche Situation wurde in anderen Fällen festgestellt.



Es stellte sich heraus, dass ein fremder Code unser Objekt mit 2 Bytes aufhob. Das wahrscheinlichste Szenario ist "Use-After-Free", wenn ein Objekt nach seiner Freigabe ein Feld in seinen ersten Bytes löscht. Ich habe die am häufigsten verwendeten Objekte überprüft, aber es wurde nichts Verdächtiges gefunden. Wieder eine Sackgasse.



FastVPSAuf unsere Anfrage habe ich den Debug-Kernel eine Woche lang mit KASAN ausgeführt, aber es hat nicht geholfen, das Problem wurde nie reproduziert. Wir haben darum gebeten, slub_debug zu registrieren, aber es war ein Neustart erforderlich, und der Vorgang dauerte lange. Von März bis April stürzten die Knoten mehrmals ab, aber slub_debug wurde deaktiviert, und dies gab uns keine neuen Informationen.



Und dann gab es eine Pause, das Problem hörte auf zu reproduzieren. April endete, Mai verging - es gab keine neuen Stürze.



Das Warten endete am 7. Juni - schließlich traf ein Problem den Kern mit aktiviertem slub_debug. Beim Überprüfen der roten Zone beim Freigeben des Objekts slub_debug habe ich zwei Null-Bytes hinter der Obergrenze gefunden. Mit anderen Worten, es stellte sich heraus, dass es nicht nachträglich verwendet werden konnte, sondern dass das vorherige Objekt erneut der Schuldige war. Es gab eine normal aussehende Struktur nf_ct_ext. Diese Struktur bezieht sich auf die Verbindungsverfolgung, eine Beschreibung der von der Firewall verwendeten Netzwerkverbindung.



Es war jedoch immer noch nicht klar, warum dies geschah.



Ich fing an, auf conntrack zu schauen: Jemand klopfte mit ipv6 am offenen Port 1720 an einen der Container. Nach Port und Protokoll habe ich den entsprechenden nf_conntrack_helper gefunden.



static struct nf_conntrack_helper nf_conntrack_helper_q931[] __read_mostly = {
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .data_len               = sizeof(struct nf_ct_h323_master),
                .tuple.src.l3num        = AF_INET, <<<<<<<< IPv4
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
        {
                .name                   = "Q.931",
                .me                     = THIS_MODULE,
                .tuple.src.l3num        = AF_INET6, <<<<<<<< IPv6
                .tuple.src.u.tcp.port   = cpu_to_be16(Q931_PORT),
                .tuple.dst.protonum     = IPPROTO_TCP,
                .help                   = q931_help,
                .expect_policy          = &q931_exp_policy,
        },
};


Beim Vergleich der Strukturen stellte ich fest, dass der ipv6-Helfer .data_len nicht definiert hat. Um herauszufinden, woher es kommt, habe ich einen 2012er Patch entdeckt.



1afc56794e03229fa53cfa3c5012704d226e1dec verpflichten

Autor: Pablo Neira Ayuso <pablo@netfilter.org>

Datum: Do 7. Juni 2012 12.11.50 0200



netfilter: nf_ct_helper: Private Daten variabler Länge Helfer implementieren



Dieser Patch verwendet die neuen conntrack Erweiterungen variabler Länge.



Anstatt union nf_conntrack_help zu verwenden, die alle

Informationen zu privaten Helferdaten enthält , weisen wir einen Bereich variabler Länge

zum Speichern der privaten Helferdaten zu.



Dieser Patch enthält die Änderung aller vorhandenen Helfer.

Es enthält auch einige Include-Header, um eine Kompilierung zu vermeiden

Warnungen.



Der Patch fügte dem Helfer ein neues Feld .data_len hinzu, das angibt, wie viel Speicher der entsprechende Netzwerkverbindungshandler benötigt. Der Patch sollte .data_len für alle zu diesem Zeitpunkt verfügbaren nf_conntrack_helpers definieren, aber es fehlte die Struktur, die ich gefunden hatte.



Als Ergebnis stellte sich heraus, dass die Verbindung über ipv6 zum offenen Port 1720 die Funktion q931_help () startete und in eine Struktur schrieb, für die niemand Speicher zuordnete. Ein einfacher Port-Scan stellte ein paar Bytes auf Null, die Übertragung einer normalen Protokollnachricht füllte die Struktur mit aussagekräftigeren Informationen, aber auf jeden Fall war der Speicher eines anderen ausgefranst und dies führte früher oder später zum Absturz des Knotens.



Florian Westphal hat den Code 2017 erneut überarbeitetund entfernte .data_len, und das Problem, das ich entdeckte, blieb unbemerkt.



Trotz der Tatsache, dass der Fehler nicht mehr in der aktuellen Linux-Kernel-Hauptzeile gefunden wird, wurde das Problem von den Kerneln einer Reihe von Linux-Distributionen geerbt, darunter RHEL7 / CentOS7, SLES 11 & 12, Oracle Unbreakable Enterprise Kernel 3 & 4, Debian 8 & 9 und Ubuntu 14.04 & 16.04 LTS.



Der Fehler wurde trivial auf dem Testknoten reproduziert, sowohl auf unserem Kern als auch auf dem ursprünglichen RHEL7. Explizite Sicherheit: Remoteverwaltete Speicherbeschädigung. Wo 1720 IPv6-Port geöffnet ist - praktisch Ping des Todes.



Am 9. Juni machte ich einen einzeiligen Patch mit einer vagen Beschreibung und schickte ihn an die Hauptlinie. Ich habe die detaillierte Beschreibung an Red Hat Bugzilla gesendet und separat an Red Hat Security geschrieben.



Weitere Veranstaltungen entwickelten sich ohne meine Teilnahme.

Am 15. Juni veröffentlichte Zhenya Shatokhin den ReadyKernel-Live-Patch für unsere alten Kernel.

https://readykernel.com/patch/Virtuozzo-7/readykernel-patch-131.10-108.0-1.vl7/



Am 18. Juni haben wir einen neuen stabilen Kernel in Virtuozzo und OpenVz veröffentlicht.

https://virtuozzosupport.force.com/s/article/VZA-2020-043



Am 24. Juni hat Red Hat Security dem Fehler eine CVE-ID zugewiesen.

https://access.redhat.com/security/cve/CVE-2020-14305



Problem erhielt eine moderate Wirkung mit einem ungewöhnlich hohen CVSS v3 Score 8.1 und in den nächsten Tagen reagierten andere

SUSE- Distributionen auf den öffentlichen Hat-Fehler https://bugzilla.suse.com/show_bug.cgi?id=CVE-2020-14305

Debian https: / /security-tracker.debian.org/tracker/CVE-2020-14305

Ubuntuhttps://people.canonical.com/~ubuntu-security/cve/2020/CVE-2020-14305.html



Am 6. Juli veröffentlichte KernelCare ein Livepatch für betroffene Distributoren.

https://blog.kernelcare.com/new-kernel-vulnerability-found-by-virtuozzo-live-patched-by-kernelcare



Am 9. Juli wurde das Problem in stabilen Linux-Kerneln 4.9.230 und 4.4.230 behoben.

https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/commit/?h=linux-4.9.y&id=396ba2fc4f27ef6c44bbc0098bfddf4da76dc4c9



Distributionen haben das Loch jedoch noch nicht geschlossen ...



„Schau, Kostya“, sage ich zu meinem Partner Kostya Khorenko, „unsere Granate hat zweimal denselben Krater getroffen! Ich und ein Zugang jenseits des Objektendes, als ich nepoymi das letzte Mal traf, als, und hier besuchte es uns zweimal hintereinander. Sag mir, ist es wie eine quadratische Wahrscheinlichkeit? Oder nicht quadratisch?

- Die Wahrscheinlichkeit ist quadratisch, ja. Aber hier muss man schauen - welches Ereignis ist die Wahrscheinlichkeit? Die quadratische Wahrscheinlichkeit des Ereignisses, dass ungewöhnliche Fehler genau zweimal hintereinander aufgetreten sind. Es ist in einer Reihe.



Nun, Kostya ist schlau, er weiß es besser.



All Articles