Analiza zrzutów pamięci na przykładzie nginx

01 lipca 2016, 08:55 | Teksty | komentarzy 5
: oglądaj sekurakowe live-streamy o bezpieczeństwie IT.

Zrzut pamięci (ang. core dump, memory dump) to zrzut zawartości pamięci procesu wykonywany w momencie jego wyjątkowego zakończenia (np. naruszenia ochrony pamięci – ang. segmentation fault, segfault).

Analiza zrzutów pamięci to technika przydatna gdy proces niespodziewanie kończy swoje działanie, a w logach, czy systemach monitoringu nie mamy wystarczających informacji aby tę sytuację wyjaśnić. W złożonych systemach zdarza się także, że trudno jest problem reprodukować, co z kolei znacznie utrudnia debugowanie, a w konsekwencji przygotowanie poprawki.

Z artykułu dowiesz się:

  • jak przygotować środowisko do zbierania zrzutów pamięci
  • jak analizować zrzuty pamięci

W niniejszym artykule posłużę się przykładem serwera nginx obsługującego setki tysięcy zapytań na minutę, z których kilka na dobę powodowało segfaulty widoczne w /var/log/messages. Logi nginx nie zawierały nic nt. tych zapytań, a przy tak dużym ruchu trudno było określić w inny sposób które żądania sprawiają problem. Analiza zrzutu pamięci pozwoliła znaleźć ten błąd bez konieczności reprodukowania go – wystarczyło przygotować środowisko i poczekać, aż wystąpi on po raz kolejny.

Przygotowanie środowiska

Na przygotowanie środowiska składają się dwa elementy:

  • binaria skompilowane z włączonymi symbolami debugowania,
  • konfiguracja zawierająca dyrektywę włączająca robienie zrzutów pamięci.

Niektóre dystrybucje Linuksa dostarczają binaria skompilowane z włączonymi symbolami debugowania. W systemach CentOS takie paczki dostępne są w repozytorium debuginfo. Jeśli nasza dystrybucja nie dostarcza takich paczek, lub używamy paczki, którą sami budujemy, pozostaje nam własnoręczna kompilacja kodu z włączonymi symbolami. Przełącznik do takiej kompilacji zależy od kompilatora. Dla GCC (ang. GNU Compiler Collection) jest to -g . Warto także wyłączyć optymalizacje, co w przypadku GCC możemy zrobić korzystając z -O0.

Opcje konfiguracji ustalające rozmiar i miejsce zapisu zrzutu pamięci oczywiście także zależą od programu, który debugujemy. Dla nginx są to:

Mając binaria skompilowane z włączonymi symbolami debugowania oraz konfigurację włączającą wykonywanie zrzutów pamięci, jedyne co nam pozostaje to czekać na wyjątkowe zakończenie programu, lub jeśli potrafimy, to je wywołać.

Analiza zrzutu pamięci

Po wystąpieniu błędu, w naszym przypadku segfaulta, zrzut pamięci znajdziemy w katalogu podanym w konfiguracji, czyli /var/cores:

Analizę takiego pliku możemy przeprowadzić z użyciem GDB (ang. GNU Debugger), podając plik wykonywalny programu i plik zrzutu pamięci jako argumenty uruchomienia:

W powyższym przykładzie widzimy ostrzeżenia o braku symboli ( no debugging symbols found), ale dotyczą one bibliotek, które w naszym dochodzeniu nie będą potrzebne. Oczywiście jeśli potrzebowalibyśmy symboli także dla tych bibliotek, musielibyśmy je także przekompilować z opcją  -g.

Pierwszym krokiem do określenia jak doszło do unicestwienia procesu jest wyświetlenie backtrace:

Powyższy backtrace pokazuje nam kolejno wywoływane funkcje. Ostatnią z nich jest ngx_list_push, a problem dotyczy linii 38 w pliku src/core/ngx_list.c. Polecenie frame pokaże nam ten kawałek kodu źródłowego:

Widać tutaj odwołania do pamięci last->nelts i l->nalloc. Wartości zmiennych w GDB możemy podglądać przy użyciu polecenia print:

Okazuje się, że last wskazuje na adres 0x0. Próba odczytu danych spod tego adresu przez proces nginx oczywiście kończy się naruszeniem ochrony pamięci i unicestwieniem procesu. Mamy więc już bezpośredni powód segfaulta, ale dalej nie wiemy skąd w funkcji ngx_list_push wziął się wskaźnik równy 0x0 i jakie żądanie HTTP spowodowało ten błąd. Przy pomocy polecenia list możemy wyświetlić szerszy kontekst kodu źródłowego:

Widzimy teraz, że last = l-> last (linia 36). Z kolei wyświetlając jeszcze wcześniejsze linie kodu dowiadujemy się, że zmienna l jest parametrem funkcji ngx_list_push:

Aby prześledzić, skąd wziął się parametr l->last równy 0x0, musimy zmienić ramkę na wcześniejszą, tj. fukcji która wywołała ngx_list_push. W GDB możemy to zrobić używając frame i podając nr ramki widoczny na zrobionym wcześniej backtrace. W naszym przykładzie będzie to frame 1:

Udało się nam więc ustalić, że problematycznym parametrem jest &r->headers_in.headers. Podobnie jak poprzednio dla funkcji ngx_list_push, tym razem w kodzie źródłowym funkcji ngx_http_headers_more_headers_in odszukujemy sprawiającą kłopoty zmienną r. Okazuje się, że jest to zmienna zawierająca dane żądania. Jej zawartość podobnie jak poprzednio możemy wyświetlić przy użyciu polecenia print:

Ostatecznie ustalamy, iż błąd naruszenia ochrony pamięci wystąpił dla żądania z niepoprawną metodą HTTP ( GeT / HTTP/1.1), wykonanego przez skaner podatności openvas.

Opisany błąd występuje w nginx co najmniej w wersji najnowszej na dzień 1 czerwca 2016, a ujawnia się w przypadku określonej konfiguracji z użyciem modułu headers-more-nginx-module. Poprawka dla nginx nie została przyjęta przez autorów, natomiast poprawkę dla modułu autor przygotował i opublikował w ciągu kilku godzin.

Marcin Teodorczyk
Gigaset Communications Polska 

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. LL

    Brawo Marcin, świetny artykuł!

    Odpowiedz
  2. Hary

    Dobry jestes.

    Odpowiedz
  3. Tytus Bomba

    Fantastyczny art, gratulacje! Wincyj! :)

    Odpowiedz
  4. adrb

    „Poprawka dla nginx nie została przyjęta przez autorów”

    Możesz podlinkować jakiś wątek, albo napisać uzasadnienie tej decyzji?

    Odpowiedz
  5. Marcin

    @adrb
    Dyskusja była przez e-mail. Uzasadnieniem ze strony nginx było, że to jest bug w module headers-more. Autor headers-more się z tym nie zgodził, ale dość szybko przygotował poprawkę dla modułu.

    Odpowiedz

Odpowiedz