Preorder drugiego tomu książki sekuraka: Wprowadzenie do bezpieczeństwa IT. -15% z kodem: sekurak-book

Jak się w końcu zabrać za tego assemblera? Czyli MIPS w kontekście bezpieczeństwa

14 lutego 2020, 10:29 | Teksty | komentarzy 19

Cześć. Rozumiem, że trafiłeś na ten mini kurs ponieważ chcesz rozpocząć swoją przygodę z MIPS’em w kontekście bezpieczeństwa, poznać assembly i zagłębić się w proces reverse engineeringu różnych aplikacji opartych o tą właśnie architekturę. No i super, w takim razie czeka Cię trochę wiedzy do przyswojenia i masa ciekawych, nowych rzeczy do odkrycia ;)

Mam nadzieję, że ta seria mini tutoriali (obecnie czytasz część pierwszą) będzie przede wszystkim odpowiedzią na pytanie czy ten temat w ogóle jest dla Ciebie, chociaż liczę na to, że tak i że nie odpuścisz ;) Zarezerwuj sobie trochę spokojnego czasu, odpal terminal i jedziemy.

Środowisko

Na początek system operacyjny, na którym będziemy sobie działać. Tutorial ten będzie oparty o system Debian chociaż jeśli masz już doświadczenie z linuksem to nie powinieneś mieć kłopotu z innym systemem tego typu.

Konfigurując swoje środowisko mamy tutaj do wyboru co najmniej dwa wyjścia. Instalacja systemu operacyjnego wybranej architekturze lub też trochę prostsze rozwiązanie jakim jest pakiet Buildroot – super fajne narzędzie do budowania oprogramowania dla systemów wbudowanych bardzo różnych architektur. Na nim będziemy działać. Do tego jeszcze będziemy potrzebowali emulatora o nazwie Qemu.

Instalacja Qemu

Bierzemy się za instalację Qemu:

~$ apt-get install qemu-user-static

Git. Mamy to.

Instalacja & konfiguracja Buildroot

Pobierz sobie najnowszego Buildroor’a. Na dzień, w którym to pisałem najnowszą wersją była ta z dnia 2019.02.9.

~$ wget https://buildroot.org/downloads/buildroot-2019.02.9.tar.gz

~$ tar zxf buildroot-2019.02.9.tar.gz

~$ cd buildroot-2019.02.9

Teraz musisz skonfigurować ten pakiet a następnie go skompilować. Odpalasz konfigurator:

~$ make menuconfig

Buildroot wymaga niektórych bibliotek do działania (jak np. ncurses,bc czy unzip). Jeśli ich nie posiadasz, możesz je zainstalować za pomocą apt-geta (“apt-get libncurses5-dev libncursesw5-dev bc unzip”).

Jeśli po wykonaniu polecenia “make menuconfig” zobaczysz konfigurator jak poniżej na obrazku to znaczy, że się udało.

Konfiguracja

Z menu startowego Buildroot’a wybierz “Target options”. Konfigurator ma wybraną standardową architekturę (jak na obrazku poniżej) dlatego musimy ją zmienić na MIPSa.

Klikasz enter i jesteś już w menu z dostępnymi opcjami. Powinieneś teraz zaznaczyć opcję “MIPS (little endian)” a później < Select > jak na obrazku poniżej. UWAGA – w tej części kursu zostawiamy na razie opcję 64-bitową tak więc zrób to uważnie i NIE zaznaczaj “MIPS64”. 

Po dokonaniu wyboru powinna widnieć nazwa “Target architecture (MIPS (little endian))” zamiast i386 tak jak to widać na obrazku powyżej. Teraz kliknij w < Exit > aby powrócić do głównego menu.

Okej. Wejdź teraz do zakładki “Toolchain” z głównego menu (obrazek poniżej):

Tutaj musisz wybrać opcję “uClibc-ng” dla “C library”. Powinna być ustawiona standardowo (obrazek poniżej). Gdyby jednak było inaczej to wejdź w opcje “C library” i wybierz pozycję “uClibc-ng”.

Nie wychodź jeszcze do głównego menu. Zjedź kursorem na dół i poszukaj opcji “Build cross gdb for the host”. Będziemy potrzebowali debuggera dla MIPS’a tak więc i ta opcja musi być zaznaczona (obrazek poniżej).

Super. Masz już skonfigurowanego Buildroot’a. Przejdź teraz do menu głównego (< Exit > i jesteś). Zapisz konfigurację wybierając z menu na dole opcję “< Save >”. Powinien pojawić Ci się formularz z potwierdzeniem zapisania konfiguracji, którą stworzyłeś (obrazek poniżej). Klikasz “OK“ i lądujesz w terminalu.

W terminalu podajesz “make” i enter. To wszystko. Aha, jeszcze jedno. Proces kompilacji może zająć dłuższą chwilę tak więc możesz iść sobie po kawę czy coś – tak zawsze radzą programiści geeki jak im się coś będzie długo kompilować. Niech tak będzie.

Jeśli wszystko się zrobiło prawidłowo bez błędów to masz już gotowe środowisko do pracy! W katalogu “buildroot-2019.02.9/output/host/usr/bin” znajdziesz wszystkie potrzebne Tobie narzędzia.

Mamy już wszystko co potrzeba tak więc w dalszej części kursu przejdziemy już do omawiania kodu.

Zaczynamy

Instrukcje warunkowe

Stały i podstawowy element programowania, który pozwala wykonywać konkretne operacje w zależności od wcześniej spełnionego warunku oraz jeden z głównych tematów naszej dzisiejszej lekcji. Jeśli jakimś cudem nie spotkałeś się z tym terminem, ogólną koncepcję masz poniżej:

Jeśli mi się teraz kompiluje

     to idę po kawę

Programuję sobie dalej

Może być też taka sytuacja:

Jeśli mi się teraz kompiluje

    to idę po kawę

w innym przypadku:

    to idę tylko po cukierki ;)

Programuję sobie dalej

W obu przypadkach “jeśli mi się teraz kompiluje” to na pewno jestem teraz w drodze, bo “idę po kawę” a później “Programuję sobie dalej”. Jeśli jednak “mi się NIE kompiluje” – to w pierwszym przykładzie przejdę odrazu do “Programuję sobie dalej” natomiast w drugim najpierw “idę po cukierki” a dopiero później “Programuję sobie dalej”. Uff.

Jak to teraz technicznie wygląda w C. Kod poniżej:

#include <stdio.h>

int main() {

	int d = 4;
	
	if(d == 4)
	{
		puts("warunek spelniony"); 
	}

	return 0;
}

Kompilacja:

~$ mipsel-linux-gcc -static -o lesson01 lesson01.c

Proces debugowania

W tym rozdziale nauczysz się jak taki program możesz sam debugować. Rozumiem, że masz już odpalony jeden terminal. Potrzebny będzie Ci także drugi.

W pierwszym uruchamiamy debugger gdb, ustawiamy breakpointa na funkcję “main” oraz uruchamiamy tryb nasłuchiwania na połączenie:

~$ mipsel-linux-gdb lesson01
…
(gdb) break main
(gdb) target remote 127.0.0.1:7331

[Pierwszy terminal]

Dobra, teraz nasz debugger nasłuchuje na porcie 7331 i oczekuje, aż badany program zgłosi się do niego.

W drugim terminalu uruchom skompilowany program ale zanim to zrobisz, skopiuj sobie emulator qemu do bieżącego folderu aby było Tobie łatwiej:

~$ cp `which qemu-mipsel-static` .
~$ sudo chroot . ./qemu-mipsel-static -g 7331 lesson01

[Drugi terminal]

Teraz zerknij na pierwszy terminal. Debugger powinien odebrać połączenie z emulatora:

(gdb) target remote 127.0.0.1:7331
Remote debugging using 127.0.0.1:7331
0x00400190 in __start ()

Wygląda dobrze. Zobaczmy razem jak wygląda funkcja “main” oraz przedstawiona w niej instrukcja warunkowa. Do tego posłużymy się poleceniem “disassemble main” w gdb.

Rejestry

Rozpoczniemy naszą analizę kodu od zaznajomienia się z rejestrami. Rejestr jest taką komórką w pamięci procesora, która przechowuje pewne istotne wartości. MIPS standardowo posiada 32 (32 bitowych) rejestrów do ogólnych zastosowań oraz 32 rejestry zmiennoprzecinkowe.

Masz je przedstawione w tabelce poniżej:

Rejestr Nazwa w assembly Opis
r0 $zero Zawsze zawiera 0
r1 $at Zarezerwowany
r2 – r3 $v0 – $v1 Wartości zwracane przez funkcje
r4 – r7 $a0 – $3 Argumenty funkcji
r8 – r15 $t0 – $t7 Rejestry ogólnego przeznaczenia
r16 – r23 $s0 – $s7 Rejestry ogólnego przeznaczenia tzw. “Saved registers”
r24 – r25 $t8 – $t9 Rejestry ogólnego przeznaczenia
r26 – r27 $k0 – $k1 Rejestry zarezerwowane dla kernela
r28 $gp Rejestr wskaźnika danych globalnych
r29 $sp Rejestr wskaźnika stosu
r30 $fp Rejestr wskaźnika ramki
r31 $ra Adres powrotu

 

Zwróć uwagę na to, że do niektórych rejestrów programista nie ma dostępu ($k0 – $k1). Istnieją także rejestry od $f0 do $f31, które są przeznaczone do liczb zmiennoprzecinkowych.

Analiza kodu

Zapoznałeś się już z rejestrami zatem możemy przystąpić do analizy. Wyjaśnienie znajdziesz pod obrazkiem, tymczasem zerknij na kod funkcji “main” poniżej:

Trochę się tutaj dzieje. Dla przypomnienia Twój program napisany w języku C ma zadeklarowaną zmienną typu integer z wartością “4”. Jeśli zmienna “d” ma wartość “4” co jest w tym przypadku prawą – zostanie wyświetlony komunikat.

Zobacz teraz jak to robi MIPS:

li v0, 4 – (load immediate) wrzuca wartość typu integer (naszą “4”) do tak zwanego rejestru “v0“

Poznałeś właśnie instrukcję z rodziny instrukcji “load”. Instrukcji tego typu jest znacznie więcej (np. “lb” – load byte, “lh” – load half word czy “lw” – load word). Ważną rzeczą, którą musisz zapamiętać to to, że instrukcje typu “load” biorą wartość z prawej strony po przecinku (w naszym przypadku będzie to “4”) i zapisują do lewej (u nas będzie to rejestr “v0”). Tak jak zadeklarowałeś zmienną “d” w języku C za pomocą zapisu: “int d = 4;” tak samo MIPS zapisał ją sobie do rejestru “v0”. O instrukcji “li” więcej poczytasz tutaj.

sw v0, 24(s8) – (store word) zapisuje 4 bajtową wartość (word – słowo) z rejestru “v0” do pamięci pod adres rejestru “s8” + offset 24.

 

Teraz MIPS umieszcza naszą wartość “4” w pewne miejsce w pamięci. I tutaj pojawia nam się kolejny rejestr “s8”, który wskazuje na pewne miejsce w pamięci. Dodając do wartości rejestru offset “24” uzyskamy miejsce gdzie Twoja “4” zostanie zapisana.

Instrukcji typu “store” jest także znacznie więcej. Do niektórych możemy zaliczyć: “sb” – store byte, “sh” – store halfword czy “sd” – store doubleword. Instrukcje te dla odmiany działają w odwrotną stronę (co do instrukcji “load”): biorą wartość/rejestr z lewej strony a następnie zapisują ją po stronie prawej po przecinku.

Zagadnienie pamięci, stosu będzie omawiane w dalszych częściach kursu tak więc na tą chwilę wyobraź sobie tylko że pamięć to taki duży schowek z wieloma przegródkami z góry na dół. O instrukcji “sw” więcej poczytasz tutaj.

lw v1, 24(s8) – wrzuca wartość ”4” z pamięci, pod którą wcześniej była zapisana tym razem do rejestru “v1”. Dlaczego? Ponieważ do rejestru..

li v0, 4 – ..wędruje druga wartość integer “4” z instrukcji “if ( 4 == ” w Twoim programie.I w końcu przychodzi czas na instrukcję warunkową:

bne v1, v0, 0x4003e0 <main+80> – Branch if Not Equal to immediate – w tłumaczeniu na nasze:


bne
x, y, [skocz pod adres tu podany jeśli v0 nie jest równe v1]

W miejsce “x” oraz “y” mamy wstawione rejestry “v0” i “v1”. Procesor będzie działał tak:

Sprawdzę “v0” z “v1” i sprawdzę czy są równe.

Jeśli są -> idę dalej (obrazek “funkcja main” – linia +48)
Jeśli NIE są -> skoczę pod adres 0x4003e0 (obrazek “funkcja main” – linia +80)

Jeśli kiedyś programowałeś w językach takich jak C lub Perl to spotkałeś się na pewno z taką instrukcją jak “goto etykieta;”, która powoduje skok w dowolne miejsce w programie.

MIPS posiada całą gamę takich instrukcji, które możemy podzielić na trzy grupy:

  • Instrukcje odnoszące się do rejestru PC*: rozpoczynające się od “b” czy “j” (np. “jr”) oraz adresu docelowego, pod który następuje skok. Można je porównywać właśnie z omawianym wcześniej “goto”.W obrazku “funkcja main” w linijce 140 występuje taki właśnie skok pod adres rejestru “ra”.
  • Instrukcje typu “jump and link” lub “branch and link”, zakończone są końcówką “al”, o których pomówimy sobie jeszcze nieco więcej w kolejnych częściach kursu.
  • Instrukcje warunkowe takie jak “beq”, “bgez”, “bne” itp, które pozwalają na skok tylko po spełnieniu określonych warunków. Taki właśnie przykład spotkaliśmy w naszym kodzie (“bne v1, v0, 0x4003e0”). Oczywiście instrukcja “bne” nie jest odosobniona i takich instrukcji jest więcej jak chociażby:
    • beq rejestr1, rejestr2, adres_skoku
      branch of equal – skok jeśli wartości rejestrów są równe
    • bgez rejestr1, adres_skoku
      branch on greater or equal zero – skok jeśli wartość rejestru jest równa lub większa od zera
    • bgtz rejestr1, adres_skoku
      branch on greater than zero – skok jeśli wartość rejestru jest większa od zerai tak dalej.

    PC* inaczej “program counter” jest specjalnym rejestrem, który przechowuje adres następnej wykonywanej instrukcji w programie (następna po tej, którą wykonuje w danym momencie).

    Analizując kod często spotkasz się z sytuacją, kiedy to przy skoku do funkcji będą występowały dwie instrukcje takie jak:

  • załadowanie adresu funkcji do rejestru “$t9” za pomocą instrukcji “lw” np. “lw  t9, adres”
  • skok pod adres funkcji za pomocą instrukcji “jalr t9” lub “jr t9”

Na dzisiaj tyle. Teraz zadania domowe ;)

Zadania domowe

1. Dla leniuchów:

Linków do manuala nie będzie. Musisz odnaleźć resztę instrukcji warunkowych dla MIPSa i przeanalizować jak one działają.

2. Dla komandosów:

Zrozumieć i wytłumaczyć jak działa poniższy kod:

Małe hinty:

  • załóżmy, że tam gdzie jest nazwa funkcji “printf” – program wypisze literkę “X” lub “Y” 
  • traktuj linię od +132 w dół (do +156) jak koniec działania programu
  • nie skupiaj się na instrukcjach “bal” czy “nop”, najważniejsze jest to, co program robi w operacjach warunkowych
  • sprawdź sobie jak działa instrukcja “b”, będzie Tobie łatwiej
  • czerwony kolor nic nie znaczy – po prostu zakryłem kod, który na tą chwilę jest zbędny do analizowania i mógłby odciągać od głównego rozwiązania
  • nie tylko instrukcje rozpoczynające się od “b” są tutaj kluczowe ale też pewna instrukcja.. ;)

Ajj, chyba za dużo podpowiedzi jak na to zadanie. Działaj i nie poddawaj się ;)

  • Mateusz Wójcik – Squadron31

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



Komentarze

  1. Wójt

    Bardzo fajny tutorial :D Aż mi się łezka w oku zakręciła na wspomnienie pisaniu programu w assemblerze właśnie na MIPSa.. :’)

    Ale szczerze nie spotkałem jeszcze takich programów na żywo.. gdzie można się z takimi spotkać?
    (niekoniecznie z wykluczeniem konkretnych firm zajmujących się m.in. reversem :D)

    Odpowiedz
    • Pyth0n

      @MateuszWójcik

      Jak czytam kod, to oczy mi krwawią od nieoptymalizacji. Wciśnięcie czegoś do pamięci, po czym natychmiastowy odczyt do tego samego (pół biedy innego) rejestru? To ma jakieś uzasadnienie w (powalonej) architekturze MIPS?

      Czekam z Colą i orzeszkami na tłumaczenie nop po skokach :)

      Odpowiedz
  2. Katus40

    Świetny tutorial – proponuję na początek do testów użyć wersji online https://godbolt.org/

    Trzeba wybrać kompilator MIPS gcc 5.4 (a nie MIPS64 ….) . Przy okazji ładnie pokoloruje fragmenty kody vs. odpowiednie fragmenty kodu asemblera

    Odpowiedz
  3. Kacper

    Kiedy bedzie wiecej? nop ;)

    Odpowiedz
  4. KhadaJhin

    Witam!
    Bardzo podoba mi się Twój poradnik, ale niestety jestem troche jeszcze zielony z linuxem i nie wiem jak rozwiązać ten problem(błąd skopiowany prosto z konsoli):

    checking whether mknod can create fifo without root privileges… configure: error: in `/root/buildroot-2019.02.9/output/build/host-tar-1.29′:
    configure: error: you should not run configure as root (set FORCE_UNSAFE_CONFIGURE=1 in environment to bypass this check)
    See `config.log’ for more details
    make: *** [package/pkg-generic.mk:231: /root/buildroot-2019.02.9/output/build/host-tar-1.29/.stamp_configured] Błąd 1

    No i już próbowałem uruchamiając komendę „make” jako „nieroot” ale nic nie dało. Brak narazie mi pomysłów. Dziękuję z góry za pomoc.

    Odpowiedz
    • Mike

      Komunikat błędu mówi, że wiecej szczegółów znajdziesz w pliku 'config.log’. Zaglądałeś tam?

      Odpowiedz
    • Wójt

      Jeśli w 100% ufasz Makefile’owi oraz podprogramom przez niego uruchamianymi, to mam dla Ciebie NIEBEZPIECZNIE rozwiązanie:
      1. przejdź na roota
      2. $> export FORCE_UNSAFE_CONFIGURE=1
      3. $> make

      Jeśli nie chcesz NIEBEZPIECZNEGO rozwiązania stosować, to już musisz pogrzebać czemu jako nie-root nie dało się tego uruchomić.

      Odpowiedz
    • Paweł

      Ten sam problem u mnie :/

      Odpowiedz
  5. Klaudyn

    cd /root/buildroot-2019.02.9
    export FORCE_UNSAFE_CONFIGURE=1 && make

    I będzie działać :)

    Odpowiedz
  6. Paweł

    make[1]: Leaving directory '/home/pawel/buildroot-2019.02.9/output/build/host-gcc-initial-7.4.0/build’
    package/pkg-generic.mk:238: recipe for target '/home/pawel/buildroot-2019.02.9/output/build/host-gcc-initial-7.4.0/.stamp_built’ failed
    make: *** [/home/pawel/buildroot-2019.02.9/output/build/host-gcc-initial-7.4.0/.stamp_built] Error 2

    co jest nie tak?

    Odpowiedz
    • Hej Paweł :)

      Napisz proszę na jakim linuksie działasz?

      Odpowiedz
  7. Paweł

    Czy nie wkradł się błąd?:

    bne x, y, [skocz pod adres tu podany jeśli v0 nie jest równe v1]

    Sprawdzę “v0” z “v1” i sprawdzę czy są równe.

    Jeśli są -> skoczę pod adres 0x4003e0 (obrazek “funkcja main” – linia +80) – TUTAJ BEZ SKOKU??
    Jeśli NIE są -> idę dalej (obrazek “funkcja main” – linia +48) – CHYBA TUTAJ SKOK??

    Odpowiedz
    • Paweł, zgadza się, dzięki za czujność, poprawimy :)

      Odpowiedz
  8. grammarnaziol

    s/Tobie/Ci/g
    hxxps://sjp.pwn.pl/poradnia/haslo/ci-czy-tobie;5696.html

    Odpowiedz
  9. Maciek

    li v1, 24(s8) – wrzuca wartość ”4” z pamięci, pod którą wcześniej była zapisana tym razem do rejestru “v1”. Dlaczego? Ponieważ do rejestru..

    Tutaj chyba powinno być lw a nie li :)

    Odpowiedz
  10. W pierwszej lekcji jest podany program w C
    Nie zainstalowało się środowisko programistyczne
    chociaż wszystko jakby zgodnie z planem bo kompilacja przebiegła pomyślnie
    i pierwszy terminal zadziałał poprawnie, a drugi nie widzi pliku programu.
    Jeśli powinienem przepisać ten program w C, to brakuje mi platformy.
    pozdrawiam

    Odpowiedz
  11. 2 razy prosiłem o pomoc
    Nie mogę ruszyć z miejsca bo pomocy nie otrzymałem
    Wszystko zainstalowałem kompilacja zakończona
    Pierwszy terminal oczekuje drugi error
    przypuszczam że nie mam skompilowanego programu napisanego w c, bo nie zainstalowało się chyba środowisko programistyczne, jeśli poprawnie to nazywam i nie miałem w czym przepisać programu, a więc skompilować
    Bardo proszę o wsparcie
    Dziękuję

    Odpowiedz
    • dkmn

      kompilacja jest tutaj: mipsel-linux-gcc -static -o lesson01 lesson01.c
      póżniej sprawdź czy powstał plik lesson01: ls -al
      póżniej kopiujesz qemu-mipsel-static
      i dalej zgodnie z poradnikiem…

      Tak w zasadzie to wystarczy tylko dobrze skopiować polecenia do konsoli.

      Odpowiedz
  12. Wojciech Pronoza

    Czy potrzeba drogiego sprzetu laptopa by moc zaczac sprobowac swoich sil?!

    Odpowiedz

Odpowiedz