https://chacker.pl/
Najpierw krótko podsumujmy technikę modyfikacji punktu wejścia ELF. W poniższym przykładzie przekażę kontrolę do sekcji kodu wstrzykniętej za pomocą elfinject, ale zamiast używać elfinject do aktualizacji samego punktu wejścia, użyję edytora heksadecymalnego. Pokażę, jak uogólnić tę technikę na kod wstrzykiwany na różne sposoby. Listing przedstawia instrukcje asemblera dla kodu, który wstrzyknę. Jest to przykład „hello world” użyty w poprzedniej sekcji.
Listing : hello.s
(1)BITS 64
SEKCJA .text
global main
main:
(2) push rax ; zapisz wszystkie zablokowane rejestry
push rcx ; (rcx i r11 zniszczone przez jądro)
push rdx
push rsi
push rdi
push r11
(3) mov rax,1 ; sys_write
mov rdi,1 ; stdout
lea rsi,[rel $+hello-$] ; hello
mov rdx,[rel $+len-$] ; len
(4) wywołanie systemowe
(5) pop r11
pop rdi
pop rsi
pop rdx
pop rcx
pop rax
(6) push 0x4049a0 ; przejdź do oryginalnego punktu wejścia
ret
(7) hello: db „hello world”,33,10
(8) len: dd 13
Kod jest w składni Intela i przeznaczony do asemblacji za pomocą asemblera nasm w trybie 64-bitowym (1). Pierwsze kilka instrukcji asemblera zapisuje rejestry rax, rcx, rdx, rsi i rdi poprzez umieszczenie ich na stosie (2). Rejestry te mogą zostać uszkodzone przez jądro i należy przywrócić je do pierwotnych wartości po zakończeniu wstrzykiwania kodu, aby uniknąć kolizji z innym kodem. Następne instrukcje konfigurują argumenty dla wywołania systemowego sys_write (3), które wyświetli na ekranie tekst „hello world!”. (Więcej informacji na temat wszystkich standardowych numerów i argumentów wywołań systemowych Linuksa znajdziesz w podręczniku systemowym syscall). W przypadku sys_write numer wywołania systemowego (umieszczony w rax) wynosi 1, a argumenty to: deskryptor pliku, do którego ma zostać zapisany (1 dla stdout), wskaźnik do ciągu, który ma zostać wydrukowany, oraz długość ciągu. Po przygotowaniu wszystkich argumentów instrukcja systemowa (4) wywołuje właściwe wywołanie systemowe, drukując ciąg znaków. Po wywołaniu wywołania systemowego sys_write kod przywraca rejestry do ich wcześniej zapisanego stanu (5). Następnie umieszcza adres 0x4049a0 oryginalnego punktu wejścia (który znalazłem za pomocą readelf, jak wkrótce zobaczysz) i wraca pod ten adres, rozpoczynając wykonywanie oryginalnego programu (6). Ciąg „hello world” (7) jest deklarowany po instrukcjach asemblera wraz z liczbą całkowitą zawierającą długość ciągu znaków (8), które są używane w wywołaniu systemowym sys_write.
Aby kod nadawał się do wstrzyknięcia, należy go zaasemblować do surowego pliku binarnego, który zawiera jedynie binarne kodowanie instrukcji asemblera i danych. Dzieje się tak, ponieważ nie chcemy tworzyć pełnoprawnego pliku binarnego ELF zawierającego nagłówki i inne dane narzutowe niepotrzebne do wstrzyknięcia. Aby zmontować plik hello.s do surowego pliku binarnego, można użyć opcji -f bin asemblera NASM, jak pokazano na listingu . Plik Makefile dla tego rozdziału zawiera cel hello.bin, który automatycznie uruchamia to polecenie.
Listing : Asemblacja pliku hello.s do pliku hello.bin za pomocą nasm
$ nasm -f bin -o hello.bin hello.s
W ten sposób tworzony jest plik hello.bin, który zawiera surowe instrukcje binarne i dane nadające się do wstrzyknięcia. Teraz użyjmy elfinject do wstrzyknięcia tego pliku i przekierujmy punkt wejścia ELF za pomocą edytora szesnastkowego, aby wstrzyknięty kod uruchamiał się podczas uruchamiania pliku binarnego. Listing pokazuje, jak to zrobić.
Listing 7-14: Wywołanie wstrzykniętego kodu poprzez nadpisanie punktu wejścia ELF
(1) $ cp /bin/ls ls.entry
(2) $ ./elfinject ls.entry hello.bin „.injected” 0x800000 -1
$ readelf -h ./ls.entry
Nagłówek ELF:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Klasa: ELF64
Dane: uzupełnienie do 2, little endian
Wersja: 1 (bieżąca)
OS/ABI: UNIX – System V
Wersja ABI: 0
Typ: EXEC (plik wykonywalny)
Maszyna: Advanced Micro Devices X86-64
Wersja: 0x1
Adres punktu wejścia: (3)0x4049a0
Początek nagłówków programu: 64 (bajtów w pliku)
Początek nagłówków sekcji: 124728 (bajtów w pliku)
Flagi: 0x0
Rozmiar tego nagłówka: 64 (bajtów)
Rozmiar nagłówków programu: 56 (bajtów)
Liczba nagłówków programu: 9
Rozmiar nagłówków sekcji: 64 (bajtów)
Liczba nagłówków sekcji: 29
Indeks tabeli ciągów nagłówków sekcji: 28
$ readelf –wide -S code/chapter7/ls.entry
Istnieje 29 nagłówków sekcji, począwszy od przesunięcia 0x1e738:
Nagłówki sekcji:
[Nr] Nazwa Typ Adres Wył. Rozmiar ES Flg Lk Inf Al
…
[27] .injected PROGBITS (4)0000000000800e78 01ee78 00003f 00 AX 0 0 16
…
(5) $ ./ls.entry
elfinject elfinject.c hello.s hello.bin ls Makefile
(6) $ hexedit ./ls.entry
$ readelf -h ./ls.entry
Nagłówek ELF:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Klasa: ELF64
Dane: uzupełnienie do 2, little endian
Wersja: 1 (bieżąca)
OS/ABI: UNIX – System V
Wersja ABI: 0
Typ: EXEC (Plik wykonywalny)
Maszyna: Advanced Micro Devices X86-64
Wersja: 0x1
Adres punktu wejścia: (7)0x800e78
Początek nagłówków programu: 64 (bajtów w pliku)
Początek nagłówków sekcji: 124728 (bajtów w pliku)
Flagi: 0x0
Rozmiar tego nagłówka: 64 (bajtów)
Rozmiar nagłówków programu: 56 (bajtów)
Liczba nagłówków programu: 9
Rozmiar nagłówków sekcji: 64 (bajtów)
Liczba nagłówków sekcji: 29
Indeks tabeli ciągów nagłówków sekcji: 28
(8) $ ./ls.entry
Witaj świecie!
elfinject elfinject.c hello.s hello.bin ls Makefile
Najpierw skopiuj plik binarny /bin/ls do pliku ls.entry (1). Będzie on służył jako plik binarny hosta dla wstrzyknięcia. Następnie możesz użyć elfinject do wstrzyknięcia właśnie przygotowanego kodu do pliku binarnego z adresem ładowania 0x800000 (2), dokładnie tak, jak omówiono w sekcji 7.3.2, z jedną istotną różnicą: ustaw ostatni argument elfinject na -1, aby elfinject pozostawił punkt wejścia niezmieniony (ponieważ nadpiszesz go ręcznie). Za pomocą readelf możesz zobaczyć oryginalny punkt wejścia pliku binarnego: 0x4049a0 (3). Zauważ, że jest to adres, na który wstrzyknięty kod przeskakuje po zakończeniu drukowania komunikatu „hello world”, jak pokazano na Listingu 7-12. Za pomocą readelf możesz również zobaczyć, że wstrzyknięta sekcja faktycznie zaczyna się od adresu 0x800e78 (4), a nie od adresu 0x800000. Dzieje się tak, ponieważ elfinject nieznacznie zmienił adres, aby spełnić wymagania wyrównania formatu ELF, co omawiam bardziej szczegółowo w Dodatku B. Ważne jest, aby 0x800e78 był nowym adresem, którym chcesz nadpisać adres punktu wejścia. Ponieważ punkt wejścia jest nadal niezmodyfikowany, jeśli teraz uruchomisz ls.entry, będzie ono zachowywać się jak normalne polecenie ls bez dodanego komunikatu „hello world” na początku (5). Aby zmodyfikować punkt wejścia, otwórz plik binarny ls.entry w hexedit (6) i wyszukaj oryginalny adres punktu wejścia. Przypomnij sobie, że możesz otworzyć okno dialogowe wyszukiwania w hexedit za pomocą klawisza /, a następnie wpisać adres do wyszukania. Adres jest przechowywany w formacie little-endian, więc będziesz musiał wyszukać bajty a04940 zamiast 4049a0. Po znalezieniu punktu wejścia nadpisz go nowym, ponownie z odwróconą kolejnością bajtów: 780e80. Teraz naciśnij CTRL+X, aby wyjść, i naciśnij Y, aby zapisać zmiany. Możesz teraz zobaczyć w readelf, że punkt wejścia został zaktualizowany do 0x800e78 (7), wskazując na początek wstrzykniętego kodu. Teraz po uruchomieniu ls.entry, wyświetli się „hello world” przed wyświetleniem listy katalogów (8). Pomyślnie nadpisałeś punkt wejścia!