2017-04-08:

Eksploitacja JITBF

Na początku lutego w 27 odcinku moich livestreamów stworzyłem prosty kompilator JIT dla interpretera ezoterycznego języka Brainfuck. Standardowo dla zaoszczędzenia czasu nie robiłem żadnego sprawdzania błędów, więc w kodzie pojawił się nawet błąd klasy relative write-what-where, który na pierwszy rzut oka był eksploitowalny. Na końcu odcinka zaproponowałem więc drobny konkurs na eksploitacje wspomnianego błędu. Ostatecznie przyszły trzy eksploity, a dodatkowo dwóch autorów zdecydowało się napisać krótkie artykuły o tym jak ich eksploity działają. O samej eksploitacji, jak i o nadesłanych eksploitach, mówiłem w 33 odcinku livestreamów. Kilka linków na początek:

Artykuły / linki do artykułów / exploitów znajdują się poniżej.

Cudak

Link do exploitu: cudak-exploit.py
Oryginalnie artykuł był opublikowany pod tym adresem: https://github.com/kamillys/miniBlog/blob/master/entries/jitbf/jitbf.md

-- Artykuł Cudaka --
Eksploitacja JitBF
Niniejszy artykuł dotyczy exploitacji programu jitbf który można znaleźć tutaj: https://github.com/gynvael/stream/tree/03774e1f293f318c466a28b03735315c1a6c04b7/019-jitbf Program powstał w trakcie streamu: https://www.youtube.com/watch?v=wJxWBeHWnGQ.

Jak doszedłem do eksploita jako początkujący hakier
Na początku zauważyłem, że obszar JIT jest "rwx", czyli można tam zapisać kod a następnie go wykonać. Założyłem, że jest to najlepsze pole do expoitacji. Pomijam takie rzeczy jak crash w przypadku błędnego kodu BF jak "[" bo to nie jest "wystarczająco fajne". Dosyć łatwo można zauważyć, że można w automacie BF wyjechać poza taśmę i nadpisać sobie wskaźnik ale tylko 1 bajtem. Niestety ale tutaj pomysł jest zły - kod który by przesunął się na początek JITa zajmuje za dużo miejsca: ciąg <<< lub >>> jest jitowany oraz zapisywany 2 bajty per znak oraz limitem 4kB.

Potem odkryłem, że przecież można nadpisać kod który jest kopiowany do JITa: globalne zmienne jit_ptr_inc oraz jit_ptr_dec z których jest kopiowany kod JIT nie są chronione. Oprócz tego te zmienne są bardzo blisko ctx (kilkadziesiąt bajtów oraz "po bliższej stronie"). Zmienne globalne są umieszczone całym blokiem pamięci zatem ich położenie względne jest niezależnie od adresu wirtualnego.

std::string w "starej" implementacji można opisać następująco:

struct {
 char *data;
 uint32_t size;
 union {
   uint32_t capacity;
   char buffer[16];
 }
};

Jeżeli data wskazuje na wewnętrzny bufor to logika kodu implementująca std::string zakłada tzw. krótki string oraz metody .data() oraz .size() zwracają w/w pola. Bardzo łatwo można nadpisać rozmiar stringa do 16 bajtów i nadpisać bufor własnym kodem - trzeba tylko uważać, aby nie używać atakowanego kodu. Więc zaatakowałem jit_ptr_inc poprzez sekwencję <.<. ... <<<. (nadpisuję kolejne bajty kodu z stdin shellcode'm oraz najmniej znaczące bajty rozmiaru do 0x0F). Tylko pytanie co się w takim małym obszarze zmieści - EB FE jako "jmp $" było pomocne w debugowaniu - zapisałem:

add esp,0x50 ; adres kodu BF na stosie: przesuwam się do aby na szczycie stosu był kod BF; znalezione w gdb
mov eax,0x7729b177 ; adres funkcji system wyciągnięty z gdb - zmienia się co reboot
call eax

Kod kompilowałem przy pomocy nasm. Ze swojej strony polecam https://www.onlinedisassembler.com/odaweb/ jako przenośny disasembler.

Następnie exploit uruchomiłem przez (^>^) z czego > jest najważniejszy, a na początku kodu BF umieściłem "calc.exe &:: " (to na końcu to początek komentarza dla cmd.exe).

Zauważyłem potem, że można uzyskać więcej miejsca poprzez nadużycie jit_ptr_inc: przecież następną strukturą w pamięci jest jit_ptr_dec który jest 2-bajtowym stringiem zatem w buforze ma 14 bajtów wolnego. Null na końcu można zignorować - jest używana metoda .size() do wyciągnięcia rozmiaru. Zatem nadpisałem rozmiar tak aby pokryć drugi bufor oraz zapisać kod do obu buforów. Trzeba mieć na uwadze to, że pomiędzy buforami są "święte dane" których nie wolno dotknąć czyli obsługa znaku "<" oraz trzeba w pierszym buforze zrobić skok do drugiego bufora. Miejsca nareszcie starczyło na wykonanie memcpy z kodu bf (a dokładniej kodu asm umieszczonego w pliku) do kodu JIT który miał prawa do zapisu w trakcie wykonania.

Zatem zapisałem kod:
mov edi,DWORD PTR [esp+0x14] ; Adres jit
mov esi,DWORD PTR [esp+0x50] ; Adres kodu BF
add esi,0x79 ; przesuń się z kodem BF na początek kodu asm zapisanego w pliku
nop ; filler
nop
nop
jmp buffer2 ; aby ominąć dane
;[DANE] czyli ptr, size, 2 bajty kodu ważnego JITa i teraz drugi bufor
buffer2:
add edi,0x36 ; przesuń docelowy wskaźnik JIT na koniec "tego co będzie po nopach"
mov cx,0xff ; skopiuj 255 bajtów [można więcej tylko trzeba mieć na uwadze że jest 4kB miejsca minus kod użyty na JIT
rep movs ; memcpy
nop
nop
nop
nop
nop

To skopiowało kod z pliku wejściowego do kodu JIT zatem w tym miejscu mam "arbitrary code execution". No to dlaczego by nie uruchomić calc.exe? To z pomocą googla znalazłem shellcode który uruchamia calc.exe (oczywiście sprawdzić trzeba co to robi, nie wolno wykonywać dziwnego kodu na swojej maszynie).

Pare info o std::string
Użyłem pojęcia "starej implementacji std::string". Rzecz się dotyczy tego, że pewnego dnia zmieniono implementację std::string tak, aby używał refcount'a zamiast wewnętrznego bufora. Ta zmiana wpłynęła na to, że zoptymalizowano "kopiowanie" dużych stringów dzięki wzorcowi copy-on-write lub też leniwej kopii co uznano za bardziej korzystne niż trzymanie małego bufora wewnątrz struktury dla krótkich napisów. Oczywistym jest, że na tej nowej implementacji nie można eksploitować wewnętrznego bufora skoro go nie ma. No i generalnie bug w jitbf polega na możliwości zapisu "poza taśmą" a nie w samym std::string, tak więc sam w sobie std::string jest, był i będzie bezpieczną konstrukcją (przynajmniej z założenia). Z drugiej strony nie ma określonego zachowania poza taśmą w specyfikacji, tak więc można uznać że jitbf zachowuje się zgodnie z założeniem... odpalając calc.exe albo formatując dysk.
-- Koniec artykułu Kamila --

gorski-the-great

Link do exploitu oraz krótkiego write-upu: https://github.com/gynvael/stream/pull/3

-- Fragment komentarza gorski-the-great z PR --
Aby zobaczyć w akcji: jitbf.exe expl.bf

Powinien wyświetlić się calc.exe, marzenie każdego pentestera.
Program całkiem długo mieli, trzeba dać mu trochę czasu.

Program w Brainfucku robi co następuje:
  • Wrzuca krótki, 72-bajtowy shellcode na samym początku swojej pamięci
  • Lokalizuje w pamięci tablicę importowanych funkcji
  • Nadpisuje adres funkcji __filbuf, wywoływanej przez getchar, adresem początku pamięci maszyny BF
  • Wywołuje getchar (komenda ',') aby uruchomić shellcode. Na szczęście aplikacja jest skompilowana z wyłączonym DEP.

Największą przeszkodę w tym zadaniu Gyn zrobił przypadkowo, ustawiając rozmiar tablicy z kodem JIT do zaledwie 0x1000 bajtów. Zatem każdy kod Brainfucka dłuższy niż kilkadziesiąt linii wywalał natychmiast aplikację, co zmuszało do bardzo dokładnego i rozsądnego wykorzystywania każdej pojedynczej instrukcji.

Testowane na Win XP, Win 7 i Win 8.1.
-- Koniec fragmentu komentarza --

Karol Rudnik

Link do exploitu: https://github.com/karol57/jit-bf-exploit

-- Fragment wiadomości od Karola --
Co do problemów napotkanych przeze mnie:
1. Nie wrzuciłeś libgcc_s_dw2-1.dll, libstdc++-6.dll przez co musiałem zgadywać wersję kompilatora i trafić dllki które nie crashowały programu. [Taak, zapomniałem o tym. My bad. // dop. Gynvael]
2. Większość rzeczy które można wykorzystać jest globalna i (chyba napewno) prostego ROPa nie dało się zastosować.
3. Kod do JITa może mieć tylko 4KiB, więc kod bf nie może być zbyt długi (dlatego nie mogłem zrobić bez wykorzystania stdin)

Ale koniec końców udało się. Ogólnie kod działa w ten sposób, że do pamięci VMki wstawia std::string (a raczej to na co std::string pokazuje), którego kopiuje z stdin, a następnie przestawia
jit_ptr_dec, aby wskazywał na nowe dane, który zawierają już payload napisany w assemblerze. Kolejne użycie '>' spowoduje skopiowania payloada zamiast oryginalnej wstawki i uruchomienie jej.
-- Koniec fragmentu wiadomości od Karola --

I tyle :)

Podziękowania dla Karola, Górskiego i Cudaka za podesłanie exploitów/write-upów.

By the way...
On 22nd Nov'24 we're running a webinar called "CVEs of SSH" – it's free, but requires sign up: https://hexarcana.ch/workshops/cves-of-ssh (Dan from HexArcana is the speaker).


P.S. W dyskusjach pojawiło się pytanie ile by takie zadanie było warte na CTFie. Wstępnie stwierdziłem, że na oko byłby to PWN 200-250, ale potem okazało się, że zapomniałem o włączeniu DEP, więc pewnie trochę mniej (PWN 100-200).

Comments:

2017-04-25 15:09:52 = Marcin
{
Świetne streamy Gyn!

Widziałem ostatnio kanał Siraja, który opowiada o Deep Learning i to co rzuciło mi się w oczy to, że na koniec każdego odcinka rzuca jakiś challenge do zrobienia i w skrócie mówił, kto wygrał challenge z poprzedniego odcinka. Cieszy się to sporym zainteresowaniem mimo, że nie ma nagród.

Nie wiem na ile to możliwe, ale też mógłbyś coś takiego wprowadzić, czasem nawet mogą być proste challenge z poruszanego tematu. Może nawet zadanie w stylu "challenge na ten tydzień to zrobienie zadania xyz (z jakiegoś wargame'u) i podesłanie write'upu etc.

Tutaj jeden z jego filmików (challenge jest na samym końcu): https://www.youtube.com/watch?v=vOppzHpvTiQ
}
2017-04-26 08:06:14 = Gynvael Coldwind
{
@Marcin
Świetny pomysł! :)
}
2017-04-27 19:16:22 = DeKrain
{
Porada na szybko:
Wczoraj obejrzałem konferencję o security i zaciekawiło mnie to, że malloc przed blokiem pamięci tworzy strukturę z metadanymi, wspomniane tu: https://youtu.be/hcp9ymfbofs?t=1539
Dzisiaj znalazłem definicję tej struktury, jest ona w malloc.c#L1041. Stworzyłem więc programik, który alokował 5 bajtów na stercie. Następnie tworzy pointer do tej struktury używając "struct malloc_chunk info = ptr-sizeof(struct malloc_chunk)". Potem wypisywałem pola. Problem w tym, że pole mchunk_size nie jest równe 5 tylko to jakaś wysoka liczba. Na live-overflow znalazłem prostszą wersję tej struktury (bez pointerów na poprzedni i następny blok). Jednak to nie pomogło. Więc chciałbym wiedzieć, jak to osiągnąć?
}
2017-04-28 13:47:01 = foxtrot charlie
{
Hej @Gyn!
Taki mały request odnośnie streamów, mógłbyś przedstawić swój toolset do ctfów. Nie chodzi o "asy w rękawie" tylko czego oprócz idy, gdb, hex workshopu i vima używasz :)
}
2017-04-29 14:26:24 = DeKrain
{
Skoro za 2 tygodnie OsDev to przygotowałem listę pomysłów, które mogłyby być zrealizowane:
- NAME [PRIOR] - DESCRIPTION
- Sector Loader [HIGH] - Loader do sektorów na dysku
- ELF Loader [MED/HIGH] - Prosty loader do plików wykonywalnych
- Syscalls [MED/HIGH] - Syscalle do programów
- Shell [MED/LOW] - Prosty Shell do uruchamiania programów
}
2017-05-08 09:04:11 = abc
{
Hej, czy tutaj też można zgłaszać propozycje tematów na live stream? jeżeli tak, byłbym ciekawy czy możesz opowiedzieć coś na temat wielowątkowości w Pythonie. Czy to w ogóle jest możliwe, a jeżeli tak to jak to osiągnąć?

Pozdrawiam,
abc
}

Add a comment:

Nick:
URL (optional):
Math captcha: 10 ∗ 10 + 2 =