Xz/liblzma: Erklärung der Bash-Stufen-Verschleierung
(gynvael.coldwind.pl)- Die xz/liblzma-Backdoor ist nicht nur ein Problem des finalen binären Payloads; die während des Builds ausgeführte Bash-Stufe ist durch mehrere Schichten aus Extraktion und Entschlüsselung verborgen, was die Analyse erschwert.
- Betroffen sind xz/liblzma 5.6.0 und 5.6.1; die verschleierten Skripte und der binäre Payload stecken in zwei Dateien, die wie Testdateien aussehen.
- Stage 0 beginnt in
m4/build-to-host.m4, repariert einen scheinbar beschädigten xz-Stream und extrahiert mitxz -ddas Stage-1-Skript, das anschließend ausgeführt wird. - Stage 1 schneidet Daten aus
good-large_compressed.lzmaheraus, ersetzt sie und führt dann ein Stage-2-Bash-Skript aus; in 5.6.1 wurde Code hinzugefügt, der fünfmal prüft, ob Linux ausgeführt wird. - Stage 2 nutzt File Carving, eine Substitutionschiffre und eine AWK-basierte RC4-Variante zur Entschlüsselung, um eine
.o-Datei in den Build- und Link-Prozess einzuschleusen; 5.6.1 enthält außerdem eine Erweiterungsstruktur, mit der künftig zusätzliche Skripte ausgeführt werden könnten.
Umfang der Bash-Stufen der xz/liblzma-Backdoor
- Andres Freund meldete auf der oss-security-Mailingliste die Entdeckung der xz/liblzma-Backdoor; diese Backdoor betrifft den OpenSSH server.
- Gegenstand der Analyse ist nicht die binäre Backdoor selbst, sondern die zuvor ausgeführten Bash-basierten Anfangsstufen und ihre Verschleierung.
- Der Bash-Ablauf reicht von Stage 0 bis Stage 2; auch eine potenzielle Stage 3, die offenbar nicht vollständig implementiert ist, wird teilweise sichtbar.
- Die verschleierten und verschlüsselten Stufen sowie die spätere binäre Backdoor sind in zwei Testdateien versteckt:
tests/files/bad-3-corrupt_lzma2.xztests/files/good-large_compressed.lzma
- Betroffen sind die Versionen 5.6.0 und 5.6.1; zwischen beiden Versionen gibt es kleinere Unterschiede.
Stage 0: Reparatur eines xz-Streams, der wie eine beschädigte Testdatei aussieht
- Ausgangspunkt ist Code in
m4/build-to-host.m4, der offenbar an irgendeiner Stelle während des Build-Prozesses ausgeführt wird. - Die zentrale Pipeline verbindet
sed,trundxz -d, um das Stage-1-Skript zu extrahieren. - Nachdem die Bytes aus
tests/files/bad-3-corrupt_lzma2.xzgelesen wurden, werden einige Bytes mittr "\t \-_" " \t_\-"ersetzt:- Der Tabulator
0x09wird zu einem Leerzeichen0x20. - Das Leerzeichen
0x20wird zu einem Tabulator0x09. - Der Bindestrich
0x2dwird zu einem Unterstrich0x5f. - Der Unterstrich
0x5fwird zu einem Bindestrich0x2d.
- Der Tabulator
- Diese Ersetzung dient dazu,
bad-3-corrupt_lzma2.xzwieder in einen gültigen xz-Stream zu verwandeln. - Am Ende wird der reparierte Stream mit
xzentpackt; Fehler werden ignoriert, das Stage-1-Skript wird gewonnen und sofort ausgeführt.- Der Stream sieht abgeschnitten aus, aber die relevanten Ausgaben wurden bereits geschrieben, sodass das kein Problem ist.
Stage 1: Stage 2 aus good-large_compressed.lzma herausholen
- Stage 1 ist die kurze Bash-Datei aus Andres Freunds Mail, die mit
####Hello####beginnt. - Der erste Unterschied zwischen den beiden Versionen sind zufällige Bytes im Kommentar der zweiten Zeile:
- 5.6.0:
86 F9 5A F7 2E 68 6A BC - 5.6.1:
E5 55 89 B7 24 04 D8 17
- 5.6.0:
- In 5.6.1 wurde eine Linux-Prüfung der Form
[ ! $(uname) = "Linux" ] && exit 0hinzugefügt; dieselbe Prüfung wird fünfmal wiederholt. - Die Extraktion von Stage 2 beginnt mit einer langen Zeile
export i=...; dieser Wert wird später wie eine Art Funktion verwendet, die übereval $iaufgerufen wird. - Der Extraktionsablauf ist als Pipeline aufgebaut, in der Standardausgabe und Standardeingabe fortlaufend verbunden werden:
- Zunächst wird
good-large_compressed.lzmamitxz -dcentpackt. eval $iwiederholthead-Aufrufe in einem Muster, bei dem 1024 Bytes verworfen und 2048 Bytes ausgegeben werden.- Am Dateiende gibt 5.6.0 724 Bytes aus, 5.6.1 939 Bytes.
- Zunächst wird
- Diese
head-Kette entfernt leere Bereiche mit niedriger Entropie und fügt nur die Bereiche zusammen, die tatsächliche Daten enthalten. - Anschließend wird mit
tail -c +31233der vordere Teil verworfen.- In diesem vorderen Teil ist die im nächsten Schritt extrahierte binäre Backdoor versteckt.
- In 5.6.0 werden dadurch effektiv die ersten 31264 Bytes verworfen, in 5.6.1 die ersten 31232 Bytes.
- Danach wird
trwie eine einfache Substitutionschiffre verwendet; die Mapping-Keys unterscheiden sich zwischen 5.6.0 und 5.6.1:- 5.6.0:
tr "\5-\51\204-\377\52-\115\132-\203\0-\4\116-\131" "\0-\377" - 5.6.1:
tr "\114-\321\322-\377\35-\47\14-\34\0-\13\50-\113" "\0-\377"
- 5.6.0:
- Die ersetzten Daten werden mit
xz -F raw --lzma1 -dcerneut entpackt; die daraus resultierende Stage 2 wird sofort ausgeführt.
Stage 2: Änderung des Build-Prozesses und Erweiterungsmechanismus
- Stage 2 ist die Datei
infected.txt, die Andres Freund der ursprünglichen Mail angehängt hatte; es handelt sich um ein Bash-Skript, in dem die eigentliche Änderung des Kompilierungsprozesses stattfindet. - Aus Sicht der Verschleierung gibt es in Stage 2 drei interessante Fragmente, von denen zwei nur in 5.6.1 vorkommen.
- Die beiden Fragmente in 5.6.1 sind nahezu identisch und suchen in
tests/files/nach Dateien mit bestimmten Signaturen:- Fragment 1 sucht nach
~!:_ Wund|_!{ -. - Fragment 3 sucht nach
jV!.^%und%.R.1Z.
- Fragment 1 sucht nach
- Wenn die Signaturen gefunden werden, werden Dateiname und Offset extrahiert, und der Bytebereich ab der ersten Signatur bis vor die zweite Signatur wird herausgeschnitten.
- Die herausgeschnittenen Daten werden mit dem in 5.6.0 verwendeten
tr-Key per Substitution entschlüsselt, dann mitxz -F raw --lzma2 -dcentpackt und perevalausgeführt. - In den untersuchten TAR-Archiven von 5.6.0 und 5.6.1 gab es keine Datei mit diesen Signaturen.
- Diese Struktur wirkt wie ein Erweiterungs- oder Patch-System, mit dem künftig Skripte ausgeführt werden könnten, indem lediglich neue Testdateien hinzugefügt werden, ohne die vorhandene Testdatei zum Transport des Payloads erneut zu verändern.
Stage 2: Extraktion der binären Backdoor-.o-Datei
- Ein weiterer Kernbestandteil von Stage 2 ist der Code, der eine
.o-Datei extrahiert und in den Kompilier- und Link-Prozess einschleust. - Das extrahierte Ergebnis heißt
liblzma_la-crc64-fast.o; dabei handelt es sich um die finale binäre Backdoor. - Der Extraktionsprozess verbindet wie in den vorherigen Stufen mehrere Standardwerkzeuge zu einer Pipeline:
good-large_compressed.lzmawird mitxz -dcentpackt.- Die
head-Kette auseval $i, die in Stage 1 verwendet wurde, wird erneut genutzt. - Mit
sed "s/\(.\)/\1\n/g"wird nach jedem Byte ein Zeilenumbruch eingefügt, sodass eine Form mit einer Zeile pro Byte entsteht.
- Das folgende AWK-Skript führt auf dem Eingabestream eine RC4-ähnliche Entschlüsselung aus:
- Das Key-Array wird mit
((i * 7) + 5) % 256erzeugt. - 5.6.0 überspringt die ersten 4096 Bytes des ausgegebenen PRNG-Streams.
- 5.6.1 überspringt 8192 Bytes.
- Das ursprüngliche RC4 verwendet XOR, dieser Code nutzt jedoch Addition in der Form
(v + k) % 256.
- Das Key-Array wird mit
- Die entschlüsselten Daten werden mit
xz -dc --single-streamentpackt. - Anschließend wird mit
headnur der benötigte Bytebereich herausgeschnitten und alsliblzma_la-crc64-fast.ogespeichert.- Die komprimierte, aber teilweise modifizierte Payload-Größe beträgt 88792 Bytes in 5.6.0 und 88664 Bytes in 5.6.1.
- Der endgültig extrahierte Bereich hat eine Größe von etwa 86 KB.
Charakter der mit Standardwerkzeugen gebauten Verschleierung
- Die gesamte Bash-Stufe besteht ausschließlich aus Standard-Kommandozeilenwerkzeugen:
sedtrxzheadtailawkgrepcutexpr
- Der Payload ist in Binärdateien versteckt, die wie normale Testdateien aussehen, und nur die benötigten Bereiche werden per File Carving herausgezogen.
- Für die Verschleierung werden eine einfache Substitutionschiffre und eine AWK-basierte RC4-Variante zur Entschlüsselung kombiniert.
- Die Ausführung verläuft über insgesamt drei Stufen; in 5.6.1 ist eine Struktur enthalten, mit der später durch Hinzufügen separater Testdateien Skripte ausgeführt werden könnten.
- Die gesamte Struktur ist so gestaltet, dass sie harmlos wirkt und gut verborgen bleibt; wenn dieser Fall zufällig entdeckt wurde, stellt sich die Frage, wie viele ähnliche, bislang unentdeckte Fälle es noch gibt.
1 Kommentare
Meinungen auf Hacker News
Dank der vereinfachten Erklärung und des Vergleichs mit dem Rauschbild bekommt man ein Gefühl dafür, was die Leute mit Raffinesse meinen.
Auf reddit habe ich auch gesehen, dass die „Sandboxing“-Methode durch einen einzigen Punkt kaputtgemacht wurde; ganz links in der Zeile direkt nach
#includeist ein Punkt zu sehen.https://git.tukaani.org/?p=xz.git;a=commitdiff;h=328c52da8a2...
https://old.reddit.com/r/linux/comments/1brhlur/xz_utils_bac...
+,+.,+aus, und dieser eine Punkt fällt nicht auf.Schon einfache Fehler, bei denen Tests immer fehlschlagen oder immer erfolgreich sind, sind lästig; man sieht gut, warum das ein attraktives Ziel für böswilliges Verhalten ist.
a) Praktisch niemand baut dieses Paket tatsächlich mit cmake.
b) Wenn man es mit cmake und
-DENABLE_SANDBOX=landlockbaut, schlägt der Build einfach fehl: https://i.imgur.com/7xbeWFx.pngDer Punkt deaktiviert das Sandboxing nicht, sondern sorgt nur dafür, dass es sich mit cmake nicht bauen lässt. Wenn tatsächlich jemand mit cmake gebaut hätte, hätte er den Fehler gesehen und gemerkt, dass etwas nicht stimmt. Deshalb ergibt es wenig Sinn, das als böswilligen Versuch zur Schwächung der Sicherheit zu deuten.
„Wenn das zufällig entdeckt wurde, wie viel gibt es dann noch, das nicht entdeckt wurde?“ ist die wichtigste Frage.
Es ist kaum vorstellbar, dass Andres Freund zufällig genau das einzige populäre Open-Source-Projekt mit eingeschleuster Backdoor gefunden hat. Es wäre nicht überraschend, wenn schon ein Dutzend solcher Dinge in freier Wildbahn existierten.
Der nächste Angreifer wird weit weniger unvorsichtig damit sein, Spuren wie längere Laufzeiten zu hinterlassen.
Der unangenehme Gedanke hier ist, dass Unit-Tests den Angriffsweg eröffnet haben. Ohne Tests wäre es viel schwieriger gewesen, das so zu verstecken.
bad-3-corrupt_lzma2.xzenthält drei Streams; der erste und der dritte sind gültige xz-Streams. Beim mittleren Stream sind Stream-Header, Block-Header, Index und Stream-Footer korrekt, nur die LZMA2-Daten sind beschädigt, und es steht dort, dass er mit--single-streamdekomprimiert werden sollte.Die Strings
####Hello####und####World####sorgen dafür, dass ein plausibel normal wirkendes Ergebnis herauskommt, wenn man die README-Anweisungen tatsächlich befolgt.$ cat tests/files/bad-3-corrupt_lzma2.xz | xz -d --single-stream####Hello####Diese Strings sind Shell-Kommentare und behindern daher die Ausführung der Payload nicht.
Schließlich dienen sie später als Marker, mit denen die Regex die Datei finden kann, ohne den Dateinamen direkt zu referenzieren oder die tatsächlichen Hello-/World-Strings zu verwenden.
$ gl_am_configmake=\grep -aErls "#{4}[[:alnum:]]{5}#{4}$" $srcdir/ 2>/dev/null``$ echo $gl_am_configmake./tests/files/bad-3-corrupt_lzma2.xzDiese Prüfung müsste transitiv gelten, sodass auch beim Versuch einer Linux-Distribution, auf eine neue liblzma-Version zu aktualisieren, der Build wegen neuer binärer Abhängigkeiten fehlschlägt oder warnt.
Ich weiß nicht, wie verbreitet diese Praxis bei Builds von Linux-Distributionen ist. Wenn sie verbreitet ist, wäre das Aufräumen enorm viel Arbeit, und es ist unklar, ob es überhaupt machbar ist. Mit bazel scheint es möglich, bei anderen Build-Systemen weiß ich es nicht.
Ich frage mich, ob schon jemand auf GitHub nach ähnlichen head-|tail-Tricks gesucht hat. Schwer zu glauben, dass das eigens hierfür erfunden wurde.
Eine riesige
.sh-Datei zeigt die Lizenz an und holt die Zustimmung ein, danachcattet sie sich selbst und extrahiert über einehead/tail-Pipe mitcpiodie eigentlichen Assets.Eine bessere Antwort habe ich nicht, aber ist dieser undurchsichtige Bash-Klumpen an sich nicht schon ein Code Smell?
Wie in anderen Bereichen der Softwareentwicklung auch: Hätte man das nicht weniger opak schreiben können, damit klarer sichtbar ist, was passiert?
Mir ist klar, dass ein Maintainer bösartigen Code ohne so strenge Prüfung einschleusen kann wie ein externer Contributor, aber es muss doch einen besseren Weg geben als einen „knappen“ Code-Klumpen, der praktisch wie unbeabsichtigte Obfuscation wirkt.
Das Kernproblem ist, dass es in den 80er- und 90er-Jahren unzählige Unix-artige Systeme gab, jeweils mit eigenen Fehlern und fehlenden Features, und Softwareautoren ihre Build-Abhängigkeiten minimieren wollten.
Deshalb haben viele Communities Builds auf Shell-Skripte standardisiert, die überall laufen. Shell-Skripte zu schreiben war aber mühsam, und die Leute begannen, Tools wie den M4-Makro-Präprozessor zu verwenden, um Shell-Skripte zu erzeugen.
Das Ergebnis ist, dass viele Projekte riesige, undurchsichtige Shell-Skript-Brocken haben, für den Fall, dass jemand den Code auf AIX oder einem kaputten uralten Unix ausführen möchte.
Um dieses undurchdringliche Shell-Gestrüpp loszuwerden, müsste man die Zahl der unterstützten Plattformen stark reduzieren, sich auf sauberere Build-Tools standardisieren und mehr Kerninfrastruktur in Sprachen bauen, die für portable Builds keine Shell benötigen.
Das ist aber eine riesige Aufgabe, und ein großer Teil der zentralen C-Bibliotheken wird von ein oder zwei unbezahlten Freiwilligen gepflegt. Die Unterstützung für „Obscurnix-1997“ einzustellen, ist meist auch eine ziemlich kontroverse Entscheidung.
Deshalb ist ein großer Teil der Kerninfrastruktur immer noch von einem Sumpf aus undurchschaubaren, maschinell erzeugten Shell-Skripten umgeben.
Eine nicht geringe Zahl von Tools wird auf diese Weise gebaut.
Verdächtig wirken die wiederholten tr-Aufrufe. Wenn ich so etwas sehe, gehe ich davon aus, dass jemand besonders clever sein will, und „clever“ ist hier negativ gemeint. Wäre ich Maintainer, hätte ich bei solchem Code um eine Erklärung gebeten, was er tut. Denn fast immer gibt es bessere Lösungen, die solches Chaining vermeiden.
Das eigentliche Problem ist, dass es keinen anderen Maintainer gab, der sich diesen Code beim Eingang hätte ansehen können. Eine wichtige Komponente des Stacks hing von einer einzelnen Person ab, und in diesem Fall war diese Person böswillig.
execen kann? Ich frage mich, ob Bash so etwas wie einen „Sicherheitsmodus“ haben könnte.Allerdings kann ich mir beim configure-Skript von xv nicht vorstellen, wie man Bash in einem solchen hypothetischen „Sicherheitsmodus“ starten könnte, also nehme ich das zurück.
https://github.com/tukaani-project/.github/issues/2
Das gesamte C-Ökosystem, einschließlich Build-Tools und alter Unix-Utilities, ist ein Sicherheitschaos, das nur darauf wartet, ausgenutzt zu werden, und irgendwann wird es ausgenutzt.
Man muss sich nur ansehen, wie leicht ein einzelner Punkt alles kaputtmachen kann. Die Leute müssen endlich begreifen, dass man die Sicherheit der Welt nicht mehr auf C setzen kann.
Ich hoffe, man nutzt Ada oder Rust mit modernen Toolchains.
Ich verstehe überhaupt nicht, wie das durch ein Code Review gekommen und gemergt werden konnte. Wenn ich nichts übersehe, wirkt das absurd nachlässig.
Außerdem war es stark obfuskiert in einer als Testdatei ausgewiesenen Binärdatei, also in „good“/„bad“-xz-komprimierten Testdateien. Wenn man nicht weiß, wonach man suchen soll, kann man das nicht erkennen.
Mit LTS-Distributionen kann man bis zu einem gewissen Grad geschützt sein. Slackware scheint für Pakete lzma, also
tar.xz, zu verwenden, aber die letzte stabile Version außer-currenthatte dieses Problem nicht.Wenn man noch einen Schritt weiter in Richtung freie Software gehen möchte: Auch Hyperbola GNU hatte dieses Problem nicht.
Zusätzlich linkt auch Slackware
-currentsshdnicht gegen xz und verwendet auch kein systemd.