stackoverflow_pl.pdf

(1224 KB) Pobierz
2653650 UNPDF
Przepełnianie stosu
pod Linuksem x86
Piotr Sobolewski
Artykuł opublikowany w numerze 4/2004 magazynu Hakin9
Wszelkie prawa zastrzeżone. Bezpłatne kopiowanie i rozpowszechnianie artykułu dozwolone
pod warunkiem zachowania jego obecnej formy i treści.
Magazyn Hakin9 , Wydawnictwo Software, ul. Lewartowskiego 6, 00-190 Warszawa, hakin9@hakin9.org
2653650.043.png
Przepełnianie stosu
pod Linuksem x86
Piotr Sobolewski
Nawet bardzo prosty program,
na pierwszy rzut oka
wyglądający całkiem poprawnie,
może zawierać błędy, które
mogą zostać wykorzystane do
wykonania dostarczonego kodu.
Wystarczy, że program będzie
umieszczał w tablicy dane,
nie sprawdzając wcześniej ich
długości.
szych trików stosowanych w celu prze-
jęcia kontroli nad dziurawym progra-
mem. Choć technika znana jest od dawna, pro-
gramiści nadal robią błędy umożliwiające jej
zastosowanie przez intruzów. Przyjrzyjmy się
dokładnie, na czym polega zastosowanie tej
techniki do przepełnienia bufora na stosie.
Zacznijmy od przedstawionego na Listingu 1
programu stack_1.c . Jego działanie jest proste
– funkcja fn pobiera jeden argument (wskaźnik
do łańcucha znaków char *a ) i kopiuje jego za-
wartość do tablicy znaków char buf[10] . Funkcja
ta wywoływana jest w pierwszej linii programu
( fn(argv[1]) ), jako argument funkcji fn podawany
jest pierwszy argument z linii poleceń ( argv[1] ).
Kompilujemy i uruchamiamy program:
Spróbujmy teraz sypnąć piach w tryby. Za-
uważmy, że tablica buf może pomieścić tylko
dziesięć znaków ( char buf[10] ), zaś umiesz-
czany w niej ciąg tekstowy może mieć dowol-
ną długość. Przykład:
$ ./stack_1 AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Tak uruchomiony program będzie usiłował umie-
ścić trzydzieści znaków w dziesięcioznakowej ta-
Z artykułu nauczysz się...
• na czym polega technika przepełniania stosu
( stack overlow ),
• jak rozpoznać, że program jest na tę technikę
podatny,
• jak zmusić podatny program do wykonania do-
starczonego kodu,
• jak debugować programy za pomocą debugge-
ra gdb .
Co powinieneś wiedzieć...
• znać podstawy języka C,
• znać podstawy pracy w systemie Linux (linia
poleceń).
$ gcc -o stack_1 stack_1.c
$ ./stack_1 AAAA
Program zaczyna działanie od wywołania funk-
cji fn . Jako argument funkcja otrzymuje ciąg
AAAA . Ciąg ten jest kopiowany do tablicy buf , po
czym program wypisuje dwa komunikaty: o za-
kończeniu działania funkcji i dojściu do końca
programu. Następnie kończy działanie.
2
www.hakin9.org
Hakin9 Nr 4/2004
P rzepełnienie bufora to jeden z najstar-
2653650.044.png 2653650.045.png 2653650.046.png 2653650.001.png 2653650.002.png
Przepełnianie stosu pod Linuksem
Listing 1. stack_1.c
– przykładowy program
37
89
89
void fn ( char * a ) {
char buf [ 10 ];
strcpy ( buf , a );
printf ( "koniec funkcji fn \n " );
}
push
push
89
pop
37
37
37
24
24
24
24
main ( int argc , char * argv []) {
fn ( argv [ 1 ]);
printf ( "koniec \n " );
}
254
254
254
254
31
31
31
31
173
173
173
173
blicy, po czym zakończy pracę z błę-
dem, wyświetlając segmentation fault .
Zwróćmy uwagę, że nie pojawia się
żaden komunikat w stylu tablica buf
jest zbyt mała , zamiast tego widzi-
my informację o błędzie segmentacji
(ang. segmentation fault ). Oznacza to,
że program próbował uzyskać dostęp
(pisać lub czytać) do pamięci, do któ-
rej nie ma praw.
Może nam się wydawać, że
program umieścił pierwsze dziesięć
liter A w tablicy, po czym została wy-
kryta próba pisania poza dozwolonym
obszarem i to spowodowało wszczę-
cie alarmu. Nic bardziej mylnego. Pro-
gram bez żadnych przeszkód zapisał
trzydziestobajtowy ciąg w dziesięcio-
znakowej tablicy, w ten sposób nad-
pisując dwadzieścia bajtów pamięci
znajdującej się za obszarem zajmo-
wanym przez tablicę buf[10] . Błąd,
o którym poinformował nas komunikat
segmentation fault , powstał dużo póź-
niej, a jego przyczyną było uszkodze-
nie pamięci spowodowane nadpisa-
niem tych dwudziestu bajtów.
stos
stos
stos
stos
Rysunek 1. Podstawowe operacje na stosie to odkładanie liczb na jego
wierzchołek i zdejmowanie z wierzchołka. W sytuacji przedstawionej na
rysunku najpierw odkładamy na stos (ang. push) wartość 37 (traia na
wierzchołek), następnie kładziemy liczbę 89. Kiedy następnie zdejmujemy
wartość ze stosu (ang. pop), otrzymujemy ostatnio nań odłożoną liczbę 89.
Aby dostać się do liczby 37 musielibyśmy jeszcze raz wykonać operację
zdjęcia wartości ze stosu
��������������������������������
����������������������������������
��
��
���
��
���
�����������������
Rysunek 2. W przypadku Linuksa na x86 stos rośnie w dół (objaśnienia
w tekście)
Kilka ważnych pojęć
Zanim dowiemy się, co dzieje się
w czasie między nadpisaniem dwu-
dziestu bajtów pamięci a pojawie-
niem się informacji o błędzie seg-
mentacji, musimy przypomnieć so-
bie kilka podstawowych faktów.
Bugtraq – bardzo popularna lista dyskusyjna, na której publikowane są informacje o
wykrytych błędach związanych z bezpieczeństwem. Archiwa bugtraqa można zna-
leźć w Internecie pod adresem http://www.securityfocus.com/ .
nop – w asemblerze większości procesorów istnieje polecenie, które nic nie robi
– polecenie nop . Mogłoby się wydawać, że istnienie takiego polecenia nie ma sen-
su, ale – jak widać w artykule – czasem jest ono bardzo przydatne.
• Debugger – narzędzie służące do kontrolowanego uruchamiania programów. De-
bugger umożliwia zatrzymywanie i wznawianie działania badanego programu, wy-
konywanie go krok po kroku, sprawdzanie i modyikowanie zawartości zmiennych,
oglądanie zawartości pamięci, rejestrów procesora itp.
• Segmentation fault – błąd, który oznacza, że program usiłował dokonać odczytu
lub zapisu w obszarze pamięci, do którego nie ma dostępu.
Co powinniśmy
wiedzieć o stosie
Każdy uruchomiony program uzy-
skuje od systemu operacyjnego frag-
ment pamięci. Pamięć ta składa się
z różnych sekcji – w jednej umiesz-
czone są biblioteki dzielone, w dru-
giej kod programu, w jeszcze innej
Hakin9 Nr 4/2004
www.hakin9.org
3
2653650.003.png 2653650.004.png 2653650.005.png
 
2653650.006.png 2653650.007.png 2653650.008.png 2653650.009.png 2653650.010.png 2653650.011.png 2653650.012.png 2653650.013.png 2653650.014.png 2653650.015.png 2653650.016.png 2653650.017.png
Listing 2. Wywołanie funkcji
– listing do Rysunku 3
Listing 3. stack_2.c – listing do
Rysunku 4
Listing 4. Zmodyikowana
wersja programu z Listingu 3,
działanie tego programu
prześledzimy za pomocą
debuggera
main () {
int a ;
int b ;
fn ();
}
void fn ( int arg1 , int arg2 ) {
int x ;
int y ;
printf ( "jesteśmy w fn \n " );
}
void fn ( int arg1 , int arg2 ) {
int x ;
int y ;
x = 3 ; y = 4 ;
printf ( "jesteśmy w fn \n " );
}
void fn () {
int x ;
int y ;
printf ( "jesteśmy w fn \n " );
}
main () {
int a ;
int b ;
fn ( a , b );
}
main () {
int a ;
int b ;
a = 1 ; b = 2 ;
fn ( a , b );
}
jego dane. Sekcją, której przyjrzymy
się bliżej, jest stos .
Stos jest strukturą służącą do
tymczasowego przechowywania da-
nych. Dane na stos możemy odkła-
dać (ang. push ) – traiają one wtedy
na jego wierzchołek, możemy też je
z wierzchołka stosu zdejmować (ang.
pop ). Przedstawia to Rysunek 1.
W praktyce stos używany jest
przez programy do przechowywa-
nia (między innymi) zmiennych lokal-
nych. Ważne jest, by program korzy-
stający ze stosu znał dwa istotne ad-
resy. Pierwszy to adres wierzchołka
stosu – jego znajomość jest potrzeb-
na, aby móc odkładać wartości (mu-
simy wiedzieć, pod jakim adresem
umieścić odkładaną wartość). Drugi
ważny adres to tzw. wskaźnik ramki ,
czyli początek obszaru zawierające-
go zmienne lokalne aktualnie wyko-
nywanej funkcji. W przypadku, któ-
ry rozważamy (Linux na platformie
x86) adres wierzchołka stosu prze-
chowywany jest w rejestrze %esp ,
zaś wskaźnik ramki – %ebp .
Inną kwestią charakterystyczną
dla omawianej platformy jest fakt,
że stos rośnie w dół. Oznacza to, że
wierzchołkiem stosu jest należąca
do niego komórka pamięci o najniż-
szym adresie (patrz Rysunek 2). Ko-
lejno odkładane na stos wartości tra-
iają pod coraz niższe adresy.
cji. Ponieważ nowowywołana funk-
cja ma własne zmienne lokalne, a po-
przednie zmienne lokalne (należące
do funkcji wywołującej) nie mogą zo-
stać usunięte ze stosu (będą potrzeb-
ne po powrocie z funkcji wywoływa-
nej), rejestr %ebp (wskaźnik ramki) mu-
si zacząć wskazywać na miejsce bę-
dące w chwili wywołania funkcji wierz-
chołkiem stosu, od którego to miejsca
zaczną być odkładane na stos nowe
zmienne lokalne. Dokładniej przedsta-
wia to Rysunek 3, będący ilustracją te-
go, co dzieje się, kiedy wykonywany
jest kod przedstawiony na Listingu 2.
Jak widać na lewej części ilu-
stracji, przedstawiającej stan sto-
su pod koniec funkcji main() , na sto-
sie umieszczone są dwie zmienne lo-
kalne – int a i int b . Wskaźnik ram-
ki (rejestr %ebp ) wskazuje na początek
obszaru zajmowanego przez zmien-
ne lokalne funkcji main() , wierzcho-
łek zaś na koniec tego obszaru. Po
wywołaniu fn() (prawa część rysun-
ku), na stosie, za obszarem zmien-
nych lokalnych funkcji main() , znajdu-
je się obszar, w którym umieszczone
są zmienne lokalne funkcji fn() . Po-
czątkiem ramki jest teraz początek
obszaru zmiennych funkcji fn() , zaś
wierzchołkiem – jego koniec. Ten opis
jest jednak tylko uproszczeniem: fak-
tycznie podczas wywoływania funkcji
dzieje się trochę więcej.
Kiedy funkcja fn() zakończy swo-
je działanie, kontrola musi zostać
Co dzieje się na stosie
podczas wywoływania funkcji
Ciekawe rzeczy dzieją się na sto-
sie, kiedy następuje wywołanie funk-
���������
��������
��������
�������
�����������
��������
��������
��������������� ś ������������
���������
��������
��������
�������
�����������
��������
��������
��������������� ś ������������
��������������
�����
�����
�����
�����������������
�����
��������������
�����
�����
�����������������
Rysunek 3. Zmienne lokalne na stosie (w uproszczeniu) – ilustracja do
Listingu 2
4
www.hakin9.org
Hakin9 Nr 4/2004
2653650.018.png 2653650.019.png 2653650.020.png
 
2653650.021.png
 
 
2653650.022.png 2653650.023.png 2653650.024.png 2653650.025.png 2653650.026.png 2653650.027.png 2653650.028.png
Przepełnianie stosu pod Linuksem
przekazana z powrotem do funkcji
main() . Aby było to możliwe, przed
wywołaniem funkcji fn() musi zo-
stać zapisany adres skoku powrot-
nego z funkcji fn() do funkcji main() .
Po powrocie do main() program po-
winien kontynuować działanie tak,
jakby wykonywanie main() nigdy nie
było przerywane: stos powinien więc
wrócić do stanu sprzed wywołania
fn() . W tym celu oprócz adresu po-
wrotnego należy też zachować ad-
res początku ramki. W zaprezento-
wanym przykładzie funkcja fn() nie
przyjmowała żadnych argumentów.
W przypadku programu stack_2.c ,
którego źródła przedstawia Listing
3, funkcja fn() przyjmuje dwa argu-
menty będące liczbami naturalnymi.
Podczas wywoływania fn() z main()
argumenty te muszą zostać w jakiś
sposób przekazane.
Wszystkie wspomniane wartości
(adres powrotu z funkcji, adres po-
przedniego początku ramki oraz ar-
gumenty) zachowywane są na sto-
sie. Rysunek 4 przedstawia co dzieje
się, kiedy main() wywołuje fn() .
Pierwsza część rysunku przed-
stawia sytuację, która ma miejsce,
kiedy wykonywanie programu do-
chodzi do linii int b (na rysunku ta
linia kodu zaznaczona jest strzał-
ką). Jak widać na stosie odłożo-
ne są dwie zmienne lokalne funkcji
main() : int a i int b . Strzałka niebie-
ska wskazuje spód stosu, czerwona
– jego wierzchołek.
Druga część rysunku przedsta-
wia stan stosu w momencie, kiedy
wykonywana jest linia fn(a, b) . Jak
widać wykonanie tej linii spowodo-
wało odłożenie na stos argumentów
funkcji fn() , czyli zmiennych a i b .
Kolejny krok przedstawiony jest
na trzeciej części rysunku. W kro-
ku tym na stos odkładany jest adres
powrotu po zakończeniu funkcji fn() .
Adresem tym jest adres kolejnej po
fn(a, b) instrukcji z main() .
Następnie wykonywany jest skok
do początku funkcji fn() , przechodzimy
do czwartej części rysunku. Jak widać
na stos zostaje odłożona aktualna
wartość początku ramki, zaś nowym
wskaźnikiem ramki zostaje aktualny
�����
�����������������������������
��������
��������
����������������������������
�����
��������
��������
��������
�����������
�����
�����������������������������
��������
��������
����������������������������
�����
������������
������������
��������
��������
��������
�����������
�����
�����������������������������
��������
��������
����������������������������
�����
������������
������������
�������������
��������
��������
��������
�����������
�����
�����������������������������
��������
��������
����������������������������
�����
������������
������������
�������������
��������
��������
��������
�����������
������������������
��������������
�����
�����
�����������������������������
��������
��������
����������������������������
������������
������������
�������������
������������������
��������������
��������
��������
��������
�����������
�����
�����
Rysunek 4. Operacje na stosie podczas wywoływania funkcji – ilustracja do
Listingu 2 (objaśnienia w tekście)
Hakin9 Nr 4/2004
www.hakin9.org
5
2653650.029.png 2653650.030.png 2653650.031.png
 
2653650.032.png 2653650.033.png 2653650.034.png
 
2653650.035.png 2653650.036.png 2653650.037.png 2653650.038.png 2653650.039.png
 
2653650.040.png
 
2653650.041.png 2653650.042.png
Zgłoś jeśli naruszono regulamin