CVE-2022-0435: Zdalne przepełnienie stosu w jądrze Linux

Usługa Appgate Threat Advisory Services wykryła lukę, której lokalne lub zdalne wykorzystanie może doprowadzić do odmowy usługi i wykonania kodu. Przeczytaj więcej o odkryciu i o tym, jak zaradzić

Streszczenie

  • Usługa Appgate Threat Advisory Services wykryła lukę przepełnienia stosu w module TIPC jądra Linux
  • Lokalna lub zdalna wykorzystanie może prowadzić do odmowy usługi i wykonania kodu
  • Wykorzystanie wymaga załadowania modułu TIPC na cel
  • Wersje, których dotyczy problem, to między innymi od 4.8 do 5.17-rc3
  • Łatka wydana 10 lutego 2022

Wstęp

W listopadzie 2021 r. SentinelLabs publicznie ujawniło zdalne przepełnienie sterty, które znaleziono w module sieciowym jądra systemu Linux dla protokołu Transparent Inter-Process Communication (TIPC) (CVE-2021-43267).

Nie minęło wiele czasu od jego publicznego ujawnienia, a badacze opublikowali przykłady lokalnych eskalacji uprawnień (np. ten artykuł autorstwa @bl4sty).

Jednak nie było nic na temat wykorzystania luki w celu zdalnego wykonania kodu. Oczywiście to gwarantowało sprawdzenia. Możesz sobie wyobrazić moje zdziwienie, gdy odkryłem przepełnienie stosu zdalnego.

W tym poście omówię TIPC, aby przedstawić niezbędny kontekst, zanim zagłębię się w samą lukę, środki zaradcze, łatanie i naszą oś czasu ujawnienia.

TL;DR na TIPC

Transparent Inter-Process Communication (TIPC) to mechanizm IPC przeznaczony do komunikacji wewnątrz klastra. Topologia klastra jest zarządzana wokół koncepcji węzłów i połączeń między tymi węzłami.

Komunikacja TIPC odbywa się za pośrednictwem „nośnika”, który jest abstrakcją TIPC interfejsu sieciowego. „Media” to typ nośnika, z których obecnie są obsługiwane cztery: Ethernet, Infiniband, UDP/IPv4 i UDP/IPv6.

Weź ten przykład z przewodnika TIPC:

$ tipc bearer enable media eth dev eth0

Tutaj konfigurujemy nasz węzeł (czyli nasz komputer), aby używał nośnika z typem Ethernet na naszym interfejsie eth0. Teraz TIPC wie, że może używać eth0 do komunikacji przez Ethernet.

Warto tutaj zauważyć, że zdalny atakujący jest ograniczony przez typy mediów TIPC, które cel już skonfigurował. Lokalnie, jeśli moduł jest załadowany, atakujący może użyć podstawowej komunikacji netlink do skonfigurowania nośnika (podziękowanie dla bl@sty za jego pracę nad CVE-2021-43267). Nie będą jednak mieli uprawnień do wysyłania surowych ramek Ethernet, pozostawiając prawdopodobną opcję nośnika UDP.

Mamy określone węzły, nośniki i typy mediów. Ostatnią integralną częścią naszej topologii jest „łącze”. Po ustaleniu nośnika dla naszej komunikacji TIPC, nasz węzeł zacznie rozgłaszać pakiety wykrywania i szukać innych węzłów.

Intencją jest tutaj nawiązanie połączenia z innymi węzłami. Łącze definiuje kanał komunikacyjny między parą węzłów. To łącze ma różne właściwości dotyczące tolerancji i gwarancji dostawy, a także nadzoru.

Ok, nie obawiaj się, na razie wystarczy ten ogólny kontekst TIPC! Czas wskoczyć do samej luki.

Podatność

Jedną z wielu cech modułu TIPC jest jego struktura monitorowania. Wprowadzona do jądra w czerwcu 2016 r. platforma, wykorzystuje rozproszony „Overlapping Ring Supervision Algorithm”, do monitorowania sąsiednich węzłów w tej samej domenie.

Węzły tl;dr komunikują się ze sobą, a każdy węzeł śledzi widok topologii domeny przez swojego partnera — np. czy moi partnerzy widzą taką samą liczbę węzłów w domenie jak ja i czy ta sama liczba partnerów żyje?

Jak widać, między innymi zachowujemy odniesienie do struct tipc_mon_domain. Ta struktura reprezentuje rekord domeny używany do definiowania widoku topologii TIPC, na przykład liczbę znanych członków. Zobacz definicję poniżej:

Kopie tych rekordów domeny są przesyłane między urządzeniami równorzędnymi, aby umożliwić sobie wzajemne poznanie ich widoków topologii. Następnie każdy węzeł przechowuje kopię najbardziej aktualnego rekordu domeny otrzymanego od każdego z jego partnerów za pośrednictwem tipc_peer->domain field.

W TIPC wiadomości między węzłami są kategoryzowane za pomocą pól nagłówka do nadrzędnych „użytkowników wiadomości” (tj. jaka część stosu TIPC tego używa), a następnie dalej dzielona na „typy wiadomości”.

Te rekordy domeny są przekazywane między łączami przy użyciu wiadomości użytkownika LINK_PROTOCOL TIPC i typu wiadomości STATE_MSG. Ta wiadomość zawiera nagłówek TIPC (zawierający pola ogólne i pola specyficzne dla typu wiadomości) oraz opcjonalną treść zawierającą kopię nadawcy struct tipc_mon_domain.

Po otrzymaniu STATE_MSG, jeśli przejdzie on weryfikację nagłówka i kilka innych sprawdzeń, treść komunikatu jest przekazywana do funkcji tipc_mon_rcv. Rolą tej funkcji jest aktualizowanie rekordów domeny dowolnych elementów równorzędnych, w przypadku, gdy zawierają one rekord domeny w STATE_MSG.

To wszystko brzmi dość prosto, prawda? Przyjrzyjmy się bliżej kodowi, aby zobaczyć, gdzie pojawia się problem:

Po pierwsze, funkcja wykonuje kilka podstawowych testów poprawności [1], aby upewnić się, że a) treść wiadomości zawiera rekord domeny oraz b) zawiera poprawną struct tipc_mon_domain.

To oczyszczanie obejmuje sprawdzenie, czy dlen (długość STATE_MSG zdefiniowana w nagłówku) jest wystarczająco duża, aby zawierać pusty rekord domeny z zerowymi elementami członkowskimi [2]. Następnie sprawdza, czy dlen pasuje do oczekiwanej długości rekordu domeny z członkami member_cnt, gdzie member_cnt pochodzi z przychodzącego rekordu domeny [3]. Na koniec sprawdza, czy długość podana w nagłówku dlen jest zgodna z polem len podanym w nowym rekordzie domeny [4].

Po kilku dalszych sprawdzeniach pobieramy struct peer nadawców, aby sprawdzić, czy otrzymaliśmy już od nich rekord domeny [6]. Jeśli tak, chcemy tymczasowo buforować kopię starego rekordu, aby później dokonać porównania [7].

Następnie, upewniwszy się, że jest to nowy i prawidłowy rekord, zaktualizujemy pole struct peer->domain o nowe informacje [9]. Jeśli jest to pierwszy rekord domeny, dokonamy dla niego nowego przydziału km lub jeśli jest większy niż ostatni, zwolnimy stary rekord i ponownie przydzielimy więcej miejsca [8].

Składanie elementów razem

Ok, więc dokąd to wszystko zmierza? Cóż, niektórzy z was mogli zauważyć, że wcześniej wyraźnie umieściłem #define dla MAX_MON_DOMAIN, który definiuje członka u32 members[] jako tablicę 64-elementową, tj. będziemy śledzić do 64 członków domeny.

Jeśli jednak spojrzymy wstecz na oczyszczanie rekordów [1], funkcja nie sprawdzi, czy new_member_cnt jest mniejszy lub równy MAX_MON_DOMAIN; sprawdzamy minimalne wymagania dotyczące rozmiaru [2], ale nie maksymalne.

Wiedząc o tym, możemy utworzyć łącze z węzłem docelowym i przesłać struct tipc_mon_domain z dowolnym polem member_cnt i members[] — o ile wartości dlenlen i member_cnt sumują się poprawnie.

Rozmiar jest w rzeczywistości ograniczony tylko przez MTU mediów używanych do komunikacji, np. typowa maksymalna ramka Ethernet (ignorując ramki jumbo) to 1518 bajtów.

Na przykład jako atakujący moglibyśmy odpowiedzieć na jeden z pakietów rozgłoszeniowych i ustanowić łącze, udając węzeł równorzędny. Następnie będziemy mogli wysłać rekord domeny o wielkości 1072 bajtów (z 264 członkami) i przejść weryfikację.

Nie mamy nic do buforowania [6], ponieważ jest to nasz pierwszy rekord domeny przesłany z naszego złośliwego węzła, ale teraz węzeł przydzielił miejsce dla naszego rekordu 1072 bajtów bez problemów [8], a nasza struktura równorzędna odwołuje się do niego [9].

Widzisz, dokąd to zmierza? Przy drugim rzucie wysyłamy „nowszy” rekord domeny [5] do węzła docelowego. Dopóki przejdziemy te same testy, trafimy w [6], gdzie musimy buforować rekord domeny, który wysłaliśmy wcześniej, z wywołaniem memcpy [7]:

memcpy(&dom_bef, dom, dom->len)

Przypomnijmy sobie, jak wygląda nagranie, które właśnie wysłaliśmy:

Co to jest znowu &dom_bef? Jest to struktura lokalna [0], a ponieważ oczekuje, że members będą tablicą 64-elementową, jest alokowana jako bufor 272-bajtowy na stosie. I zaraz skopiujemy do niego 1072 bajty!

Jedynymi ograniczeniami naszego ładunku struct tipc_mon_domain są:

– len = dlen = sizeof(len,gen,ack_gen,member_cnt,up_map) + member_cnt * sizeof(u32)

– gen musi być wyższy niż ostatni — musi mieścić się w MTU nośnika.

Ponieważ członkowie znajdują się na końcu struktury, możemy teraz nadpisać wszystko, co następuje na stosie, dowolnym ładunkiem o dość dużych wymaganiach dotyczących rozmiaru. O tym, jak można to wykorzystać, poruszymy w późniejszym poście.

Przegląd luk w zabezpieczeniach

Podsumowując wszystko, co omówiliśmy:

  • CVE-2022-0435 umożliwia atakującemu lokalnemu lub zdalnemu wywołanie przepełnienia stosu w podsystemie sieciowym TIPC
  • Rozmiar przepełnienia jest ograniczony przez MTU włączonego nośnika (Ethernet/Infiniband/UDP)
  • Bardzo lekkie ograniczenia dotyczące rzeczywistej zawartości ładunku, przy czym większość jest arbitralna

Kopiując więcej niż 272 bajty do bufora stosu dom_bef, jesteśmy w stanie nadpisać wszystko, co znajduje się po nim na stosie, co prawdopodobnie obejmuje stack canary, a także wskaźnik bazowy i adres powrotu, co potencjalnie prowadzi do przejęcia kontroli nad przepływem.

Warto jednak zauważyć, że przy nowoczesnych rozwiązaniach łagodzących, nie jest łatwo wykorzystać możliwości wykraczające poza odmowę serwera. Te ograniczenia obejmują CONFIG_FORTIFY_SOURCE=y(twarde ograniczenie w celu kontrolowania przechwytywania przepływu), CONFIG_STACK_PROTECTOR=y(stack canary zwiększają ilość informacji wymaganych do przejęcia kontroli nad przepływem) i oczywiście KASLR.

Te ostatnie szczególnie wpływają na łatwość zdalnej wykorzystania. Jednak żadne z nich nie zmniejsza możliwości wywoływania zdalnej paniki jądra. Co więcej, przy odpowiednim wycieku informacji wykonanie dowolnego kodu staje się trywialne.

Łagodzenie

Ta luka występuje od czasu wprowadzenia struktury monitorowania w czerwcu 2016 r. i ma wpływ na wersje od 4.8 do 5.17-rc3.

Poniższa łatka została wprowadzona w commit 9aa422ad3266 i została połączona w stabilne gałęzie; aktualizacja systemu w celu uwzględnienia tej poprawki jest najlepszym sposobem na złagodzenie CVE-2022-0345.

Aby system był podatny na ataki, moduł TIPC musi być załadowany. Ponadto, aby system mógł być namierzany zdalnie, musi mieć TIPC włączony. Jeśli nie musisz korzystać z TIPC lub nie masz pewności, czy tak jest, możesz wykonać następujące czynności:

  • $ lsmod | grep tipc poinformuje Cię, czy moduł jest aktualnie załadowany
  • modprobe -r tipc może pozwolić na usunięcie modułu, jeśli jest załadowany, jednak może być konieczne ponowne uruchomienie systemu
  • $ echo „install tipc /bin/true” >> /etc/modprobe.d/disable-tipc.conf uniemożliwi załadowanie modułu

Jeśli potrzebujesz użyć TIPC i nie możesz natychmiast załatać swojego systemu, spróbuj wymusić wszelkie konfiguracje, które uniemożliwiają lub ograniczają możliwość imitowania węzłów w klastrze przez atakujących. Opcje obejmują szyfrowanie na poziomie protokołu TIPC, IPSec/MACSec i separację sieci.

Łatka

W ramach wstępnego odkrycia, zamieściłem sugerowaną poprawkę luki. W poniższej dyskusji, inny problem dotyczący przepełnienia u16, został zauważony przez Erica Dumazeta, którego poprawka jest również zawarta w ostatniej łatce autorstwa Jona Maloya:

Podziękowanie

Chciałbym podziękować opiekunom modułu TIPC, a także członkom security@kernel.org i linux-distros@vs.openwall.org za ich pomoc i pracę podczas procesu ujawniania.

Chciałbym również wspomnieć o badaniach podatności przeprowadzonych wcześniej na TIPC przez SentinelLabs z ich pracą nad CVE-2021-43267 i innych badaczy bezpieczeństwa, takich jak @bl4sty, za opublikowanie swoich odkryć na temat wykorzystywania TIPC.

Oś czasu ujawnienia

27 stycznia 2022: Appgate Threat Advisory Services wysłał wstępny raport do dystrybucji linux i zespołu ds. bezpieczeństwa jądra

5 lutego 2022 r.: sfinalizowanie łatki

10 lutego 2022: Skoordynowana data wydania (14:00 GMT)

Źródło

https://www.appgate.com/blog/a-remote-stack-overflow-in-the-linux-kernel

Skill