2009-03-11:

OS X vs Write-What-Where Condition

security:macosx:easy
Jak moi stali czytelnicy wiedzą, od jakiegoś czasu dysponuje MacBookiem z OS X'em. W końcu stwierdziłem że fajnie byłoby sprawdzić jak wygląda exploiting standardowych rodzajów błędów na OS X'ie. Muszę przyznać że raz czy dwa OS X mnie pozytywnie zaskoczył. Natomiast ten post będzie o innym "razie", gdy niespodzianka nie była pozytywna (pod względem bezpieczeństwa), ale niewątpliwie była (hermetycznie) komiczna ;>

Cała sprawa dotyczyła prostej aplikacji z błędem typu write-what-where condition - mając na uwadze fakt iż termin ten jest mało znany (w porównaniu do niesławnego buffer overflow) już tłumacze o co chodzi.

A chodzi o rzecz prostą - w wyniku pewnych operacji atakujący uzyskuje możliwość WSKAZANIA pewnego obszaru pamięci, oraz ZAPISANIA w ów wskazany obszar dowolnego, kontrolowanego przez atakującego, ciągu bajtów od długości zależnej od specyfiki przypadku. Czy, mówiąc w kodzie, mamy następującą sytuacje:

memcpy(DST, SRC, X);

Gdzie DST to pointer kontrolowany przez atakującego, SRC to adres bufora którego zawartość kontroluje atakujący, a X to ilość kopiowanych danych, zależna od przypadku.

Należy zauważyć iż write-what-where condition nie jest terminem określającym rodzaj błędu (jak np. format bug czy wspomniany wyżej buffer overflow), a skutek błędu. Przykładowo, zazwyczaj format bug powoduje powstanie write-what-where condition.

W przypadku tego właśnie "skutku błędu" powstaje pytanie: co i czym nadpisać aby uzyskać wykonanie kodu?

Zazwyczaj sprawa jest prosta - nadpisujemy adres jakiejś funkcji, z którego to adresu korzysta potem jakiś kod typu call [X] czy tam jmp [X] (lub analogiczny). Dzięki temu jak tylko program stwierdzi że fajnie byłoby wykonać tą funkcje, EIP zostanie przekierowane we wskazane przez nas miejsce (np. do czekającego shellcode'u). Powstaje jednak pewien problem - ów adres musi być adresem bezwzględnym, stałym względem różnych kopii danej wersji OS'u/aplikacji (pomijam local priv escal w których wystarczy stały względem danej kopii OS'u/aplikacji) LUB relatywnym ale dającym się obliczyć run-time (co zdarza się nieczęsto). A znalezienie dobrego adresu bezwzględnego ostatnio staje się trudne - programiści powprowadzali jakieś ASLR'y i inne randomizujące adresy bibliotek/stosów/heapów/etc ciekawostki.

W przypadku systemu Windows zazwyczaj szpera się po EXEku lub jakiejś DLLce w poszukiwaniu jakiegoś pointera w sekcji .data (tablica importów często odpada z uwagi na read-only), pod Linuxem dobrym celem jest .got (tablica importów, często read-write), a na OS X?

I wracamy do mojego przypadku. Na początku myślałem że będzie prosto. Odpaliłem kilka razy aplikację, rzuciłem okiem na rozkład pamięci (vmmap - świetne polecenie), i wydał się stały (pomijam jakieś małoistotne detale) - cool, nie ma ASLR. Na wszelki wypadek zrobiłem reboot, i ponownie sprawdziłem rozkład pamięci. Nic znaczącego się nie zmieniło. Coś mnie jednak tknęło i odpaliłem Google, i po chwili już wiedziałem że wcale tak różowo nie jest - otóż ASLR jest, ale randomizacja następuje tylko przy ważniejszych update'ach. Ale jednak następuje, więc jeżeli bym chciał by mój testowy app + exploit działał też na innym OS X'ie, to ASLR to skutecznie utrudni.

Na szczęście (lub nieszczęście, kwestia perspektywy) okazało się również że położenie głównego EXEka (EXEka w sensie pliku wykonywalnego, a nie PE; na OS X obowiązują pliki MACH-O) jest stałe, i nie podlega randomizacji. Rzuciłem się więc do disasma EXEka, w nadziei na znalezienie jakiegoś pointera - niestety, jak się okazało pointery leżą jedynie w sekcji importów leżącej na adresach 0x5210 - 0x5220, która była read-only:

==== Non-writable regions for process 595
__PAGEZERO             00000000-00001000 [    4K] ---/--- SM=NUL  .../testproj3
__TEXT                 00001000-00002000 [    4K] r-x/rwx SM=COW  .../testproj3
__LINKEDIT             00005000-00006000 [    4K] r--/rwx SM=COW  .../testproj3

Kapkę smutny rzuciłem okiem na listę adresów zapisywalnych, w nadziei że jednak uda mi się jakoś wyexploitować mój testowy projekcik. I wtedy moim oczom ukazało się:

==== Writable regions for process 595
__DATA                 00002000-00003000 [    4K] rw-/rwx SM=COW  .../testproj3
__OBJC                 00003000-00004000 [    4K] rw-/rwx SM=COW  .../testproj3
__IMPORT               00004000-00005000 [    4K] rwx/rwx SM=COW  .../testproj3

Heh. Huh. Albo developer kompilatora, albo developerzy OSu się kapkę zapomnieli, i stworzyli sekcję w której nie dość że można pisać i czytać, to jeszcze można umieścić kod. A cóż takiego jest w tej sekcji? Na to pytanie odpowiedział disassembler:

jump_table:4000                 jmp     [NSApplicationMain]
jump_table:4004                 jmp     [puts]
jump_table:4008                 jmp     [memcpy]
jump_table:400C                 jmp     [printf]
...

Jeżeli ktoś jeszcze nie załapał dowcipu, to spiesze z wytłumaczeniem: importowane funkcje rzadko kiedy są wywoływane bezpośrednio, zamiast tego tworzona jest oddzielna sekcja ze "stubami" typu jmp [wpis_w_tablicy_importów], a same wywołania funkcji w kodzie zapisywane są jako call adres_ów_stubu - jest tak za równo pod Windowsem, jak i pod Mac OS X'em (to ofc od kompilatora zależy, nie od systemu, tak że ta uwaga dotyczy tendencji na ów systemach, a nie samych systemów). No i puenta brzmi - ten kod można spokojnie zmodyfikować (jest to nie do pomyślenia na windzie, a co dopiero na *nixach na których od kilku ładnych lat krzyczą o polityce W^X).

Dla pewności sprawdziłem kilka innych systemowych aplikacji, i było w nich tak samo - sekcja ze stubami była zapisywalna.

Podsumowując, mając write-what-where condition na OS X, znajdźmy sobie sekcje jump_table w głównej binarce, wrzućmy tam dowolny kod. Taka drobna uwaga co do tego "jak to dobrze wykorzystać" - sposoby są dwa:
1) upatrujemy jakąś funkcje, i podmieniamy adres z którego jmp odczytuje adres funkcji - docelowy adres w jmp powinien być adresem adresu shellcode'u - ów adres można umieścić na końcu lub na początku shellcode'u
2) jeżeli możemy pisać dużo, to NOPujemy całą sekcje, i na końcu (który powinien się znaleźć PO ostatnim stubie) dajemy jakieś PUSH adres_shellcode / RET, lub inny skok, lub po prostu shellcode (to trochę armata na muchę, ale czemu by nie)

Taki hint dla devów na Mac'ach - zróbcie jump_table read-only, zawsze będzie to dobry krok w stronę poprawy bezpieczeństwa tego systemu (jak wiadomo Mac OS X jest bardzo bezpiecznym systemem... no OK... był bardzo bezpieczny na PPC... bo się nikt nim nie interesował... welcome to x86 ;D)

P.S. Przedwczoraj j00ru stworzył sobie bloga, imo warto wrzucić go do RSSów, z tego co mi wiadomo pojawiać się tam będą niezłe kąski dot. security bądź RE.

Comments:

2009-03-11 02:59:34 = mik01aj
{
pisze się "rzadko", a nie "żadko" ;p
I coś mi się wydaje, że niezbyt porządnie oznaczasz te wpisy, bo chyba wszystko z przyzwyczajenia dajesz jako "easy" ;)
}
2009-03-11 03:06:58 = Gynvael Coldwind
{
@mik01aj
Ah, zastanawiałem się nad tym czy rz czy ż ;>
Co do oznaczenia "easy" to raczej uważam że jest OK, tj głównie opisuje proste rzeczy, do których zrozumienia wystarcza podstawowa wiedza z tematyki którą poruszam. Zresztą, przedwczorajszy wpis był "medium", i był to 3ci wpis typu "medium" na moim blogu ;>
Oczywiście jeżeli kiedyś postanowię napisać o polskiej gramatyce i ortografii to dam tag "hard" ;D

}

Add a comment:

Nick:
URL (optional):
Math captcha: 4 ∗ 8 + 3 =