Return Oriented Programming – Praktyczna exploitacja binarek

Ten wpis jest częścią cyklu “Od zera do bughuntera” – listę wszystkich postów znajdziesz tutaj.

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][1]“, 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][2] 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][3]. 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:

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][7], a konkretniej binarka o nazwie [lab5b][8].

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()][9] 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()][10] na poziomie procesora.

Chodzi o odpowiednie umieszczenie argumentów w rejestrach CPU:

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][11], 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):

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 😉 )