PL Mission 00

5 minute read

Example image

Mission 00

“Zaawansowane wirówki do wzbogacania uranu zostały znowu uruchomione. Iran zapewnia, że jego działania są pokojowe…” – pojawiło się na wiodących portalach informacyjnych w kraju. Czy ktoś to w ogóle zarejestrował? Dla wielu przeczytanie o wtrysku gazu uranowego do 164 wirówek IR-6 będzie tylko co najwyżej jakimś mglistym wspomnieniem z lekcji fizyki w liceum. Mózg uzna tą informację za nieistotną i tuż po chwilowej przerwie w pracy na czytanie internetu wyrzuci ją ze swojej, krótkotrwałej pamięci raz na zawsze. Z resztą, w końcu zaraz będzie weekend i rzeczy takie jak “gdzie jutro balet” są generalnie bardziej istotne. Ale nie dla Ciebie.

Wiadomość: “Dzisiaj wyjazd, zbiórka 17.00, baza.”. Szybkie pakowanie, prysznic, double chceck sprzętu, uber. Zamiast latte ze Starbucksa – puszka pepsi na pokładzie An-26B. W drodze na lotnisko powoli uświadamiasz sobie, że jutro o tej godzinie razem ze swoją grupą bojową będziesz już w Natanz. Twoim zadaniem jest odłączenie systemu monitoringu tak zwanej “Pierwszej Bramy” a następnie otworzenie włazu N do tunelu podziemnego. To tyle i aż tyle. Resztę załatwią koledzy z Twojego teamu. Odbywasz ostatnią, szczegółową odprawę na dzień przed operacją i jazda. Powodzenia.

Odprawa

System, który mamy do złamania składa się z trzech funkcji. Pierwsza z nich – main() odpowiada za uruchomienie aplikacji. Dwie kolejne – setMonitoringDown() oraz openDoor() musimy wywołać samodzielnie aby wykonać zadanie.

Zobacz kod poniżej:

void setMonitoringDown()
{
  // ...
}

void openDoor()
{
  // ...
}

int main(int argc, char *argv[])
{
  printf("Welcome to the TOP Secret System v1.0\n");

  // ...
 
  char buff[12];

  strcpy(buff, argv[1]);  

  // ...
}

Realizacja

Jako że badany przez nas system przetwarza pewne dane to wyślemy do niego taki o to ciąg znaków (payload) i zobaczymy co się dalej stanie:

Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8…

Coś się wydarzyło. Na szczęście poniżej mamy podłączony, mocno rozbudowany debug:

[PC] = “aaaa”

Każda aplikacja ma różne mechanizmy, które są odpowiedzialne są za jej działanie. Nie jest inaczej z naszą badaną aplikacją. Gdzieś w aplikacji istnieje taki schowek odpowiadający za przechowywanie informacji o następnej instrukcji wykonywanej w programie. Jeśli nasza przykładowa apka wygląda tak:

  • zainicjuj liczbę 5
  • dodaj liczbę 5 do liczby 10

To w momencie wykonywania pierwszej operacji (pobierz liczbę od użytkownika) schowek ten będzie wskazywał na operację kolejną “dodaj liczbę 5 do liczby 10” – to tak w dużym uproszczeniu. Schowek ten będziemy określać mianem rejestr PC. Super.

I teraz wygląda na to, że nasz rejestr został nadpisany powyższym wygenerowanym ciągiem znaków. Można powiedzieć, że przejęliśmy kontrolę nad działaniem programu ponieważ chce on wykonać instrukcje znajdujące się pod “adresem” danych, które nadpisały rejestr PC. Naszych danych. Fajne co? Tylko co możemy z tym teraz zrobić? Zastanów się przez moment.

Example image

Każda ze wspomnianych funkcji posiada swoją lokalizację – adres, pod którą jest ona dostępna w aplikacji. To może zróbmy w ten sposób, że wyślemy nasz powyższy ciąg znaków a w miejsce, w którym ten ciąg nadpisuje rejestr PC wstawimy adres funkcji setMonitoringDown() i tym samym skierujemy flow programu w to miejsce? Da radę! Zobacz drugi przykład na obrazku powyżej.

Na tą chwilę jest git ale czeka nas jeszcze rozwiązanie dwóch problemów. Pierwszy problem dotyczy tak zwanego “bajtu zerowego”. Co to jest ten bajt zerowy? Zobacz na adres funkcji setMonitoringDown() – jego pierwszy bajt od lewej ma postać dwóch zer. Przesyłając w ciągu znaków cały adres funkcji, w której jest bajt zerowy wywalimy tylko apkę i wraz z nią całą naszą operację.

Trzeba to jakoś obejść. Będziemy musieli pobrudzić się trochę w assembly. Wystraszyłem Cię trochę? Gdzieś kiedyś usłyszałem, że programowanie jest dla kozaków. Jak można pisać lub czytać w asmie skoro wygląda on jak chiński? Ano można i na dodatek to nie boli.

Plan jest taki, że zamiast do funkcji skoczymy do kawałka kodu assembly, który wykona nam operację “wsadzenia” (hehe) adresu funkcji setMonitoringDown() do rejestru PC. Aha, będzie też jeszcze pewna operacja matematyczna ale bardzo prosta. Zanim przejdziemy do roboty to musisz coś jeszcze poznać. Oprócz rejestru PC będziemy mieli do dyspozycji inne “schowki na dane” nazwane kolejno: R0, R1, R2.. aż do R7. Są to rejestry tak zwanego ogólnego przeznaczenia. Teraz zobacz obrazek poniżej:

Example image

ROP_1

Zamiast do funkcji skaczemy pod adres.. instrukcji POP (0x76f6b6ec), która pobiera z przesłanego payloadu dane i umieszcza je kolejno do rejestrów R4, R5, R6 oraz PC. To wszystko. Co się dzieje dalej? Flow programu wykonuje skok pod adres z rejestru PC.

Example image

ROP_2

Instrukcja SUB wykonuje proste matematyczne działanie – od wartości rejestru R5 jest odejmowana wartość rejestru R6 a wynik umieszczany w rejestrze R3. Wartości rejestrów R5 oraz R6 musiałem dobrać w taki sposób aby przy odejmowaniu otrzymać wartość 00400608 (2262282A – 22222222) czyli adres funkcji setMonitoringDown(). Rejestr PC uzupełniony z payloadu wskazuje na kolejną instrukcję..

Example image

ROP_3

Tu się rozwiązuje przy okazji nasz kolejny problem. Wartości rejestrów R4, R5 oraz R6 są dla nas nieistotne natomiast musieliśmy skorzystać z tej instrukcji ponieważ obsługuje ona rejestr LR, który jest dla nas bardzo ważny. W rejestrze LR upakujemy adres drugiej funkcji – openDoor(). Instrukcja BX wykonuje skok pod adres z rejestru R3 czyli naszej funkcji setMonitoringDown(). Pierwszy cel osiągnięty. Zostaje jeszcze tylko otworzyć wrota.

Example image

ROP_4

Co się dzieje po wykonaniu funkcji setMonitoringDown()? Program musi przecież kontynuować swoją pracę powracając z funkcji w jakiejś miejsce. I tutaj właśnie z pomocą przychodzi nam rejestr LR (Link register), który uzupełniliśmy w kroku ROP_3. Rejestr ten przechowuje adres, pod który program “skacze” po zakończeniu wykonywania danej funkcji. My wcześniej nadpisaliśmy ten rejestr adresem openDoor() zatem po skończeniu wykonania funkcji setMonitoringDown() program skoczy właśnie tam.

Game over. Misja udana, żadnych strat w ludziach, można wracać do domu.

Synteza

Za pomocą tego tekstu chciałem wywołać u Ciebie pewną reakcję na temat architektury ARM działającej dziś w tak wielu urządzeniach otaczających nas każdego dnia a przede wszystkim w urządzeniach typu IoT na całym Świecie. Oczywiście kontekst całego artykułu jest nakierowany na tematykę bezpieczeństwa bo przecież docelowo to nas z tego wszystkiego interesuje najbardziej. Jeśli chociaż trochę się zaciekawiłeś to czytaj dalej.

ROP Chain, który stworzyłeś pokazuje koncept obejścia jednego z zabezpieczeń jakim jest ustawiany przy kompilacji NX Bit ale po kolei. Jeśli temat Cię naprawdę zaciekawił to zacznij od zrozumienia na czym polegają błędy dotyczące naruszeń ochrony pamięci (zaczynając od sławnego Buffer Overflow) bez żadnych zabezpieczeń. Dalej zapoznaj się z mechanizmami bezpieczeństwa jak już wspomniany NX Bit, ASLR, RELRO, PIE, Pointer Authentication no i Kanarek. Później dobrze by było ogarnąć stertę (Heap), błędy z nią związane a na końcu powtórzyć to wszystko jeszcze raz w 64 bitach.

Działaj.