Dziura w firewallu Linuxa

W artykule Yet another bug into Netfilter omówiono lukę znalezioną w podsystemie netfilter jądra Linux. Podczas kolejnego śledztwa autor tamtego tekstu natrafił na dziwne porównanie, które nie chroni w pełni kopii wewnątrz bufora. Prowadziło to do przepełnienia stosu bufora, które zostało wykorzystane do uzyskania uprawnień roota na Ubuntu 22.04.

 

Mały skok w przeszłość

 

W poprzednim artykule autor dotarł do błędu w strukturze nft_set (/include/net/netfilter/nf_tables.h).

 

Ponieważ nft_set zawiera dużo danych, niektóre inne pola tej struktury mogłyby zostać wykorzystane do uzyskania lepszego prymitywu zapisu. Przeszukano więc pola długości (udlenklen i dlen), ponieważ mogły się one okazać pomocne do wykonania kilku przepełnień.

 

Badanie kodu i anomalii

 

Badając różne dostępy do pola dlen, uwagę autora przykuło wywołanie funkcji memcpy (1) w nft_set_elem_init (/net/netfilter/nf_tables_api.c).

To wywołanie jest podejrzane, ponieważ używano tutaj dwóch różnych obiektów. Bufor docelowy jest przechowywany w obiekcie nft_set_ext, ext, natomiast rozmiar kopii jest pobierany z obiektu nft_set. Obiekt ext jest dynamicznie przydzielany w (0) z elem, a zarezerwowany dla niego rozmiar to tmpl->len. Chciano sprawdzić, czy wartość przechowywana w set->dlen jest używana do obliczania wartości przechowywanej w tmpl->len.

 

 

Nieprawidłowe miejsce

 

nft_set_elem_init jest wywoływany (5) wewnątrz funkcji nft_add_set_elem (/net/netfilter/nf_tables_api.c), która odpowiada za dodanie elementu do zbioru netfilter.

 

Jak można zauważyć, set->dlen nie służy do zarezerwowania miejsca na dane związane z id NFT_SET_EXT_DATA, zamiast tego jest to desc.len (5). desc jest inicjalizowany w ramach funkcji nft_setelem_parse_data (/net/netfilter/nf_tables_api.c) wywołanej w punkcie (3) Przede wszystkim data i desc są wypełniane w nft_data_init (/net/netfilter/nf_tables_api.c) zgodnie z danymi dostarczonymi przez użytkownika (6). Krytyczną częścią jest sprawdzenie pomiędzy desc->len i set->dlen w (7), występuje ono tylko wtedy, gdy dane związane z dodawanym elementem mają typ inny niż NFT_DATA_VERDICT.

 

Jednak set->dlen jest kontrolowany przez użytkownika, gdy tworzony jest nowy zestaw. Jedynym ograniczeniem jest to, że set->dlen powinien być mniejszy niż 64 bajty, a typ danych powinien być różny od NFT_DATA_VERDICT. Ponadto, gdy desc->type jest równy NFT_DATA_VERDICTdesc->len jest równy 16 bajtom.

 

Dodanie elementu typu NFT_DATA_VERDICT do zbioru z typem danych NFT_DATA_VALUE zwykle prowadzi do tego, że desc->len jest różne od set->dlen. Dlatego możliwe jest wykonanie przepełnienia stosu bufora w nft_set_elem_init przy (1). To przepełnienie bufora może być rozszerzone do 48 bajtów długości.

 

Zostań tutaj! Niedługo wrócę!

 

Niemniej jednak, nie jest to standardowe przepełnienie bufora, kiedy użytkownik może bezpośrednio kontrolować przepełnione dane. W tym przypadku losowe dane zostaną skopiowane z zaalokowanego bufora.

 

Jeśli sprawdzimy wywołanie nft_set_elem_init (5), można zauważyć, że kopiowane dane są pobierane ze zmiennej lokalnej elem, która jest obiektem nft_set_elem.

 

Obiekty nft_set_elem (/net/netfilter/nf_tables.h) służą do przechowywania informacji o nowych elementach podczas ich tworzenia. Jak można zauważyć, 64 bajty są zarezerwowane na tymczasowe przechowywanie danych związanych z nowym elementem. Jednak w momencie wywołania przepełnienia bufora do elem.data zapisywanych jest najwyżej 16 bajtów. Dlatego w przepełnieniu wykorzystywane są losowe bajty.

 

Wreszcie, nie tak losowo

 

Znaleziono przepełnienie bufora bez kontroli na danych używanych do korupcji. Zbudowanie exploita z niekontrolowanym przepełnieniem bufora to prawdziwe wyzwanie.

elem.data użyta w przepełnieniu nie jest zainicjalizowana (2). Mogłoby to zostać wykorzystane do kontroli przepełnienia.

 

Spójrzmy do callera, może wcześniej wywołana funkcja może pomóc w kontroli danych użytych do przepełnienia. nft_add_set_elem jest wywoływany w nf_tables_newsetelem (/net/netfilter/nf_tables_api.c) dla każdego elementu, który użytkownik chce dodać do zbioru.

 

nla_for_each_nested jest używane do iteracji po atrybutach przesłanych przez użytkownika, więc użytkownik jest w stanie kontrolować liczbę iteracji, które zostaną wykonane. nla_for_each_nested używa tylko makr i funkcji inline, więc wywołanie nft_add_set_elem może być bezpośrednio poprzedzone innym wywołaniem nft_add_set_elem. Jest to bardzo przydatne, ponieważ pozwala na użycie danych poprzedniego elementu w przepełnieniu, gdyż elem.data nie jest inicjalizowany. Ponadto można zignorować losowość układu stosu. Dlatego sposób kontroli przepełnienia będzie niezależny od kompilacji jądra.

 

Poniższy schemat podsumowuje różne etapy elem.data w obrębie stosu w celu wytworzenia kontrolowanego przepełnienia.

 

Losowe dane są przechowywane w obrębie stosu, dodanie nowego elementu z danymi NFT_DATA_VALUE prowadzi do danych kontrolowanych przez użytkownika w obrębie stosu. Wreszcie dodanie drugiego elementu z danymi NFT_DATA_VERDICT wywoła przepełnienie bufora, a pozostałości danych ostatniego elementu zostaną skopiowane podczas przepełnienia.

 

Wybór pamięci podręcznej

 

Ostatnią rzeczą, która nie została omówiona przed opracowaniem strategii eksploatacji, jest pamięć podręczna, w której dochodzi do przepełnienia. elem, zaalokowany na (0), jest zależny od różnych opcji wybranych przez użytkownika, jak pokazano w poprzednim fragmencie funkcji nft_add_set_elem, jego rozmiar może być różny. Istnieje kilka opcji, które można wykorzystać do jego zwiększenia, takich jak NFT_SET_ELEM_KEY i NFT_SET_ELEM_KEY_END.

 

Pozwalają one zarezerwować w elem dwa bufory o długości do 64 bajtów. Tak więc to przepełnienie może wyraźnie wystąpić w kilku cache. elem jest przydzielany na Ubuntu 22.04 z flagą GFP_KERNEL. Dlatego interesujące pamięci podręczne to kmalloc-{64,96,128,192}.

 

Teraz pozostaje tylko wyrównać elem na rozmiar obiektu cache, aby wykonać najlepsze przepełnienie. Następny schemat reprezentuje budowę elem w celu wyrównania go na 64 bajty.

 

Użyto następującej konstrukcji, aby celować w pamięć podręczną kmalloc-64:

 

  • 20 bajtów na nagłówek obiektu
  • 28 bajtów wypełnienia przez NFT_SET_ELEM_KEY
  • 16 bajtów do przechowywania danych elementu typu NFT_DATA_VERDICT.

Daj mi przeciek

 

Teraz, gdy możliwe jest kontrolowanie przepełnienia danych, następnym krokiem jest znalezienie sposobu na odzyskanie bazy KASLR. Ponieważ przepełnienie wystąpiło tylko w cache kmalloc-x, klasyczne obiekty msg_msg nie mogą być użyte do wykonania wycieku informacji, ponieważ są one alokowane w ramach cache kmalloc-cg-x.

 

Obiekty user_key_payload (/include/keys/user-type.h), normalnie używane do przechowywania wrażliwych informacji o użytkowniku w Kernel Land, stanowią dobrą alternatywę. Są one podobne w swojej strukturze do obiektów msg_msg: nagłówek z rozmiarem obiektu, następnie bufor z danymi użytkownika.

 

Obiekty te są alokowane w funkcji user_preparse (/security/keys/user_defined.c) Alokacja dokonywana w punkcie (6) uwzględnia długość danych podawanych przez użytkownika. Następnie dane są zapisywane zaraz po nagłówku za pomocą wywołania memcpy w (7). Nagłówek obiektów user_key_payload ma długość 24 bajtów, w związku z tym mogą one być użyte do rozproszenia kilku pamięci podręcznych, od kmalloc-32 do kmalloc-8k.

 

Podobnie jak w przypadku obiektów msg_msg, celem jest nadpisanie pola datalen większą wartością niż początkowa. Podczas pobierania przechowywanych informacji, uszkodzony obiekt zwróci więcej danych niż początkowo podał użytkownik.

 

Istnieje jednak istotna wada tego sprayu. Liczba przydzielonych obiektów jest ograniczona. Zmienna sysctl kernel.keys.maxkeys określa limit liczby dozwolonych kluczy w ramach breloka użytkownika. Ponadto kernel.keys.maxbytes ogranicza liczbę przechowywanych bajtów w ramach breloka. Domyślne wartości tych zmiennych są bardzo niskie. Są one przedstawione poniżej dla Ubuntu 22.04

 

Wyciek jest dobry, wyciek użyteczny jest lepszy

 

Pojawił się sposób na wyciek informacji, następnym krokiem było więc znalezienie interesujących informacji. Praca w obrębie cache kmalloc-64 wydawała się najlepsza, jest to cache z najmniejszymi obiektami, w którym może dojść do przepełnienia. Co za tym idzie, większa liczba obiektów może wyciekać.

 

Obiekty percpu_ref_data (/include/linux/percpu-refcount.h) są również alokowane w tym cache. Są one interesującymi obiektami, ponieważ zawierają dwa użyteczne rodzaje wskaźników.

Obiekty te przechowują wskaźniki do funkcji (pola release i confirm_switch), które mogą być użyte do obliczenia bazy KASLR lub baz modułów, gdy te wyciekają, a także wskaźnik do dynamicznie alokowanego obiektu (pole ref) przydatnego do obliczenia bazy physmap.

 

Obiekty takie są alokowane podczas wywołania percpu_ref_init (/lib/percpu-refcount.c).

Najprostszym sposobem alokacji obiektów percpu_ref_data jest użycie syscall’a io_uring_setup (/fs/io_uring.c). A do zaprogramowania zwolnienia takiego obiektu wystarczy proste wywołanie syscallu close.

 

Alokacja obiektu percpu_ref_data odbywa się podczas inicjalizacji obiektu io_ring_ctx (/fs/io_uring.c) w ramach funkcji io_ring_ctx_alloc (/fs/io_uring.c).

Ponieważ io_uring jest zintegrowany z rdzeniem Linuksa, wyciek z funkcji io_ring_ctx_ref_free (/fs/io_uring.c) pozwala na obliczenie bazy KASLR.

 

Podczas dochodzenia, niektóre nieoczekiwane obiekty percpu_ref_data znajdowały się w przecieku, ale z adresem funkcji io_rsrc_node_ref_zero (/fs/io_uring.c) w obrębie wydania polowego. Po przeanalizowaniu pochodzenia tych obiektów okazało się, że pochodzą one również z syscall’a io_uring_setup. Ten dobry efekt uboczny wywołania syscall io_uring_setup pozwolił na poprawienie przecieku w exploicie.

 

Jestem (G)root

 

Teraz, gdy możliwe jest uzyskanie użytecznego wycieku informacji, potrzebny jest dobry prymityw do zapisu, aby wykonać eskalację uprawnień.

 

Kilka tygodni temu Lam Jun Rong ze Starlabs opublikował artykuł, w którym opisuje nowy sposób na wykorzystanie CVE-2021-41073. Przedstawia on nowy prymityw zapisu, atak unlinking. Opiera się on na operacji list_del. Po uszkodzeniu obiektu list_head z dwoma adresami, jeden adres zostaje zapisany pod drugim.

 

Podobnie jak w artykule L.J. Ronga, celem dla uszkodzenia list_head w exploicie jest obiekt simple_xattr.

 

Aby zadziałać, ta technika wymaga wiedzy, który obiekt został uszkodzony. W drugim przypadku usunięcie losowych elementów z listy prowadzi surowo do błędu list traversal. Elementy na liście są identyfikowane za pomocą swoich nazw.

 

Aby zidentyfikować uszkodzony obiekt, wykonano sztuczkę na polu name alokując nazwę o długości wystarczająco dużej, aby mieć zarezerwowane 256 bajtów, najmniej znaczący bajt zwróconego adresu jest zerowy. Architektury little endian, takie jak x86_64, pozwalają nam po prostu wymazać najmniej znaczący bajt nazwy po dwóch wskaźnikach w list_head. Dzięki temu można przygotować pole listy do zapisu i jednocześnie zidentyfikować uszkodzony obiekt obcinając jego nazwę. Jedynym wymaganiem jest, aby wszystkie nazwy miały ten sam koniec.

 

Poniższy schemat podsumowuje budowę nazwy dla sprayu z obiektami simple_xattr.

 

Za pomocą tego write primitive można edytować modprobe_path ze ścieżką w folderze /tmp/. Pozwala to wykonać dowolny program z uprawnieniami roota i cieszyć się powłoką roota!

 

Ta metoda exploita opiera się na hipotezie, że określony adres jest zmapowany w kernelu, co nie zawsze ma miejsce. Tak więc exploit nie jest w pełni niezawodny, ale nadal ma dobry współczynnik sukcesu. Drugą wadą ataku unlinking jest kernel panic, który pojawia się po zakończeniu exploita. Można tego uniknąć poprzez znalezienie obiektów, które mogą pozostać w pamięci jądra na końcu procesu exploita.

 

Koniec historii

 

Ta luka została zgłoszona do zespołu bezpieczeństwa Linuksa i przypisano jej CVE-2022-34918. Zaproponowali oni poprawkę, którą przetestowano i przejrzano, a która została opublikowana w upstream tree w ramach commita 7e6bc1f6cabcd30aba0b11219d8e01b952eacbb6.

 

Wnioski

 

Podsumowując, znaleziono przepełnienie stosu bufora w podsystemie Netfilter w jądrze Linuxa. Luka ta może zostać wykorzystana do uzyskania eskalacji uprawnień na Ubuntu 22.04. Kod źródłowy exploita jest dostępny na GitHubie.

 

Potrzebujesz profesjonalnej ochrony? Chcesz dowiedzieć się, jak działać z takimi problemami, jak ten opisany powyżej? Skontaktuj się z naszymi ekspertami już dziś. 

 

 

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *