wyscig_pl.pdf

(462 KB) Pobierz
4302597 UNPDF
Artykuł pochodzi z czasopisma Hakin9.
Do ściągnięcia bezpłatnie ze strony:
http://www.hakin9.org
Bezpłatne kopiowanie i rozpowszechanie
artykułu dozwolone pod warunkiem
zachowania jego obecnej formy i treści.
Sytuacje wyścigu
Michał Wojciechowski
Do sytuacji wyścigu
(ang. race condition ) dochodzi
wówczas, gdy wiele procesów
wykonuje operacje na tych
samych danych, a rezultat tych
operacji jest zależny od kolejno-
ści, w jakiej procesy zostaną
wykonane.
dek dwóch procesów zapisujących
dane do tego samego pliku. Jeśli ich
praca nie jest w jakiś sposób zsynchronizo-
wana, wówczas nie wiadomo, który proces
wygra wyścig , czyli zapisze swoje dane jako
pierwszy.
AAAAAAAAAA
BBBBBBBBBB
devil@hell$ ./race
BAAAAAAAAAA
BBBBBBBBB
Jak widać, efekty mogą być różne – nie da się
przewidzieć, jaki napis ukaże się na wyjściu.
Nie wiadomo nawet, który z procesów rozpocz-
nie wypisywanie jako pierwszy – czasem jest to
rodzic, czasem potomek.
Powyższy przykład ma charakter wyłącz-
nie demonstracyjny, jednak nietrudno wyobra-
zić sobie sytuację, w której tego typu zdarzenie
mogłoby stać się problemem. Spójrzmy na ko-
lejny program, przedstawiony na Listingu 2. Je-
Dwa proste przykłady
Listing 1 przedstawia przykładowy program
obrazujący taką sytuację. Program tworzy dwa
procesy, z których każdy wypisuje ciąg zna-
ków; proces macierzysty – AAAAAAAAAA ,
proces potomny – BBBBBBBBBB . W obu pro-
cesach wyłączone jest buforowanie wyjścia,
więc wypisywanie przebiega znak po znaku
(dzięki czemu można rzeczywiście zaobserwo-
wać sytuację wyścigu).
Skompilujmy ten program i wykonajmy go
kilka razy.
Uwaga
Przykładowe programy i skrypty towarzyszące
temu artykułowi testowane były pod Linuksem
i FreeBSD, jednak starałem się pisać je w taki spo-
sób, by dały się uruchomić pod dowolnym syste-
mem zgodnym ze standardem POSIX (w jednym
przypadku wymagany jest system plików /proc ).
Nie dotyczy to oczywiście linuksowego eksploita
luki ptrace/kmod , omawianego na końcu.
devil@hell$ gcc -Wall -orace race.c
devil@hell$ ./race
BAAAAAAAAAA
BBBBBBBBB
devil@hell$ ./race
BAAABBBBBBBBBAAAAAAA
devil@hell$ ./race
26
www.hakin9.org
Hakin9 Nr 3
N ajprostszym przykładem jest przypa-
4302597.004.png 4302597.005.png
Sytuacje wyścigu
Listing 1. race.c – prosty
przykład sytuacji wyścigu
Po uruchomieniu program wczytuje
ów numer, zwiększa go o jeden i za-
pisuje z powrotem.
Zobaczmy, jak wygląda to w
praktyce. Na początek umieścimy
zero w pliku sequence :
Zwykle wyróżnia się blokady dwu
typów – odczytu i zapisu. W zależno-
ści od tego, jakie operacje wykony-
wane będą na pliku, proces zakłada
blokadę odpowiedniego typu. Bloka-
dy odczytu nazywane są także dzie-
lonymi (ang. shared lock ), ponieważ
wiele procesów może w tym samym
czasie założyć tego rodzaju bloka-
dę na pliku i odczytywać z niego da-
ne, nie przeszkadzając sobie nawza-
jem. Z kolei blokady zapisu nazywa
się wyłącznymi (ang. exclusive lock ),
gdyż tylko jeden proces może za-
blokować plik do zapisu – inne pro-
cesy, chcąc uzyskać dostęp do pli-
ku, muszą poczekać na zakończe-
nie zapisu.
Istnieje kilka odmian mechani-
zmu blokowania, wywodzących się
z różnych wersji Uniksa. W wyda-
niach opartych na Systemie V wy-
stępuje funkcja lockf , natomiast w
BSD – l ock . Standard POSIX zale-
ca realizację blokowania przy uży-
ciu funkcji fcntl i tę właśnie funk-
cję zastosujemy za chwilę. Dla wy-
gody w większości systemów unik-
sowych jest także implementowana
funkcja l ock .
Na Listingu 3 przedstawiony zo-
stał program seq _ lock , który jest po-
prawioną wersją programu seq . Przed
odczytaniem zawartości pliku pro-
gram blokuje go. Parametry zakłada-
nej blokady dei niowane są w struktu-
rze l ock ; najbardziej interesuje nas typ
blokady – F _ WRLCK , czyli blokada za-
pisu. Pozostałe pola struktury związa-
ne są z możliwością zablokowania do-
wolnego fragmentu pliku (rekordu).
Blokada zakładana jest w wyni-
ku wywołania funkcji fcntl z para-
metrem F _ SETLK lub F _ SETLKW . Róż-
nica między nimi polega na tym, że w
przypadku, gdy inny proces zabloko-
wał wcześniej dostęp do pliku, fcntl
wywołana z F _ SETLK zwróci błąd,
natomiast z F _ SETLKW będzie cze-
kać na udostępnienie pliku. Zwolnie-
nie blokady przeprowadza się w taki
sam sposób jak założenie, podając
jako jej typ wartość F _ UNLCK .
Sprawdźmy więc, jak seq _ lock
poradzi sobie w warunkach bojo-
wych. Podobnie jak wcześniej seq ,
wywołujemy go pięciokrotnie w tle:
#include <stdio.h>
#include <unistd.h>
int main ()
{
char * s ;
devil@hell$ echo 0 > sequence
/*Wyłączenie buforowania stdout*/
setbuf ( stdout , NULL );
if ( fork ())
/* To wypisuje potomek... */
s = "BBBBBBBBBB \n " ;
else
/* ...a to rodzic */
s = "AAAAAAAAAA \n " ;
Po jednokrotnym uruchomieniu pro-
gramu seq wygenerowany zostanie
numer 1 . Zaaranżujmy jednak sytu-
ację, w której program wykonywany
jest kilka razy w tym samym czasie.
Kilka procesów seq będzie się wów-
czas ścigać o dostęp do pliku sequen-
ce – spójrzmy, jaki będzie tego efekt.
for (; * s != ' \0 ' ; s ++)
putc (* s , stdout );
devil@hell$ ./seq & ./seq & \
./seq & ./seq & ./seq
Moj numer: 1
Moj numer: 1
Moj numer: 1
Moj numer: 2
Moj numer: 2
exit ( 0 );
}
go zadaniem jest generowanie kolej-
nych numerów sekwencyjnych – za
każdym uruchomieniem programu
otrzymujemy liczbę o jeden większą
od poprzedniej. Program tego rodza-
ju mógłby posłużyć do generowania
unikatowych identyi katorów użyt-
kowników w serwisie WWW (działa-
jąc jako CGI), mógłby także po pro-
stu pełnić rolę licznika.
Ostatnio wygenerowany numer
zapisywany jest w pliku sequence .
Najwyraźniej trzy pierwsze proce-
sy wczytały z pliku zero, następnie
jeden z nich wpisał do niego nową
wartość 1 . Odczytały ją dwa kolej-
ne procesy. Program nie zadziałał
tak jak powinien, ponieważ nie prze-
widziano w nim możliwości wystą-
pienia sytuacji wyścigu. Co prawda
została ona sprowokowana, jednak
także w rzeczywistości wcale o nią
nietrudno. Jeśli powyższy program
byłby wykorzystywany do genero-
wania unikatowych numerów na po-
trzeby witryny WWW, wówczas wy-
ścig mógłby zostać spowodowany
jednoczesną obsługą wielu żądań.
Świadomy takiej sytuacji użytkow-
nik mógłby spreparować serię żą-
dań i doprowadzić do wygenerowa-
nia błędnych numerów.
Listing 2. seq.c
#include <stdio.h>
#include <unistd.h>
int main ()
{
FILE * fp ;
int c ;
fp = fopen ( "sequence" , "r+" );
fscanf ( fp , "%d" , & c );
Blokady
Dostęp do danych, z których korzy-
sta wiele procesów, musi być koor-
dynowany. W przypadku dostępu do
plików stosowany jest zwykle me-
chanizm blokowania (ang. locking ).
Proces, który wykonuje operacje na
pliku, na pewien czas zakłada na nim
blokadę, uniemożliwiając dostęp in-
nym procesom.
c ++;
printf ( "Moj numer: %d \n " , c );
/*Zapis nowego numeru do pliku*/
rewind ( fp );
fprintf ( fp , "%d \n " , c );
fclose ( fp );
return 0 ;
}
Hakin9 Nr 3
www.hakin9.org
27
4302597.006.png
Listing 3. seq_lock.c
myślnie. Dostęp do pliku jest zatem
możliwy mimo blokady – można się
o tym przekonać np. wywołując za-
raz po fcntl funkcję sleep , a pod-
czas drzemki procesu wydając na
innej konsoli polecenie echo 31337
> sequence . Taki stan rzeczy wynika
z faktu, że blokady, o których mówi-
my, są z założenia zalecane (ang.
advisory ), w odróżnieniu od obo-
wiązkowych (ang. mandatory – patrz
Ramka). Blokady zalecane mają
wpływ jedynie na te procesy, które
sprawdzają ich obecność.
Czy w takim razie stosowanie
blokad ma jakikolwiek sens, sko-
ro w gruncie rzeczy nie zapewnia-
ją one plikom żadnej ochrony? Tak,
ponieważ ich rola jest inna – chro-
nią one pliki nie przed dostępem ze
strony programów i użytkowników
nieuprawnionych, lecz przed jedno-
czesnym wykonywaniem operacji
przez programy upoważnione. Słu-
żą zatem do synchronizacji dostępu
do danych, a nie ich ochrony (od te-
go są prawa dostępu do plików). Plik
sequence z poprzedniego przykładu
powinien zatem mieć takie prawa do-
stępu, by jedynie procesy seq_lock
mogły go odczytywać i zapisywać.
Pliki są najpowszechniejszym
i najlepiej znanym mechanizmem
dostępu do danych, dlatego też wy-
stepują w głównej roli w większości
przedstawianych tu przykładów. Sy-
tuacje wyścigu mogą jednak mieć
miejsce także w innych przypadkach
– zawsze wtedy, gdy kilka procesów
ma dostęp do tych samych zasobów
systemu.
ków, gdy proces w sposób nieza-
mierzony ingerował w działanie inne-
go procesu. Mówi się, że są to sytu-
acje wyścigu pomiędzy współpracu-
jącymi procesami – możemy nazwać
je przypadkowymi. Oprócz nich ist-
nieją zamierzone sytuacje wyścigu
– gdy jeden z procesów celowo za-
kłóca pracę innego. Najczęściej są
one znacznie bardziej groźne w skut-
kach, a co za tym idzie – ciekawsze.
Jeżeli jakiś program nie jest na-
pisany w sposób bezpieczny i po-
woduje wystąpienie sytuacji wyści-
gu, wówczas można napisać inny
program, który będzie się starał wy-
grać wyścig i w jakiś sposób na tym
skorzystać. Mamy zatem do czynie-
nia z programem-oi arą i z progra-
mem-napastnikiem. Jak nietrudno
zgadnąć, te ostatnie to najczęściej
eksploity pisane przez hakerów.
Wiele sytuacji wyścigu opisywa-
nych jest regułą nazywaną w skró-
cie TOCTTOU (ang. time of check
to time of use – czas sprawdzenia
a czas użycia). Dotyczy ona pewne-
go mechanizmu: program sprawdza,
czy wystąpił określony warunek,
i w zależności od wyniku owego
sprawdzenia wykonuje następną
operację. Oto najbardziej popularny
przykład takiej sytuacji:
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
int main ()
{
FILE * fp ;
int c ;
struct l ock l ;
/* Blokada zapisu */
l . l_type = F_WRLCK ;
l . l_whence = SEEK_SET ;
l . l_start = 0 ;
l . l_len = 0 ;
fp = fopen ( "sequence" , "r+" );
/* Oczekiwanie na
* uzyskanie blokady */
fcntl ( i leno ( fp ) , F_SETLKW ,
& l );
fscanf ( fp , "%d" , & c );
c ++;
printf ( "Moj numer: %d \n " , c );
/* Zapis nowego numeru
* do pliku */
rewind ( fp );
fprintf ( fp , "%d \n " , c );
/* Zwolnienie blokady */
l . l_type = F_UNLCK ;
fcntl ( i leno ( fp ) , F_SETLK , & l );
fclose ( fp );
return 0 ;
}
if (access( "plik" , R_OK) == 0)
fp = fopen( "plik" , "r" );
devil@hell$ ./seq_lock & ./seq_lock & \
./seq_lock & ./seq_lock & ./seq_lock
Moj numer: 1
Moj numer: 2
Moj numer: 3
Moj numer: 4
Moj numer: 5
Wywołując funkcję access program
sprawdza, czy użytkownik, który
uruchomił program, dysponuje pra-
wem odczytu określonego pliku. Je-
śli tak – plik jest otwierany przy po-
mocy funkcji fopen .
Z pozoru wygląda to na jak naj-
bardziej prawidłowy fragment pro-
gramu; mamy tu jednak do czynienia
Wykorzystywanie
sytuacji wyścigu
Omówione do tej pory przykłady sy-
tuacji wyścigu dotyczyły przypad-
Jak widać, dzięki wprowadzeniu me-
tody synchronizacji dostępu do pliku
sytuacja wyścigu została zażegnana.
Każdy z procesów wykonał odczyt i
zapis pliku nie kolidując z pozostałymi.
Zwróćmy uwagę, że dopie-
ro funkcja fcntl sprawdza, czy plik
nie został wcześniej zablokowany.
Oznacza to, że wywołanie fopen na
zablokowanym pliku przebiega po-
Blokady obowiązkowe
O ile właściwe funkcjonowanie blokad zalecanych wymaga ich przestrzegania przez
procesy, to w przypadku blokad obowiązkowych funkcję tę przejmuje jądro. Nadzoruje
ono wywołania funkcji open , read i write , i jeśli wskutek wykonania którejś z nich mo-
głoby dojść do naruszenia blokady, wówczas funkcja kończy się błędem. Blokadom
obowiązkowym podlegają zatem wszystkie procesy.
Blokady obowiązkowe wymagają wsparcia ze strony systemu plików. W Linuksie
ich obsługę włącza się poprzez zamontowanie systemu plików z opcją mand .
28
www.hakin9.org
Hakin9 Nr 3
4302597.007.png 4302597.001.png 4302597.002.png
Sytuacje wyścigu
Mechanizmy blokowania w Perlu i PHP
Zarówno w Perlu jak i w PHP obsługa blokad realizowana jest przez funkcję l ock .
Wywołuję się ją tak samo jak jej odpowiednik systemowy, czyli podając jako argumen-
ty deskryptor pliku i typ blokady. Dopuszczalne są następujące typy:
Skompilujmy program i ustawmy
mu bit SUID (naturalnie korzystając
z konta root):
root@hell# gcc -Wall -oshow show.c
root@hell# chmod u+s show
LOCK_SH – blokada dzielona,
LOCK _ EX – blokada wyłączna,
LOCK _ UN – zwolnienie blokady.
Sprawdźmy teraz, jak działa zabez-
pieczenie realizowane przez funkcję
access . Spróbujmy odczytać zawar-
tość pliku /etc/shadow posługując się
kontem zwykłego użytkownika:
Oto przykład otwierania pliku do zapisu oraz zakładania blokady wyłącznej w Perlu:
open (F, “> plik.txt” );
l ock (F, LOCK_EX);
...oraz to samo w PHP:
devil@hell$ ./show /etc/shadow
/etc/shadow: Permission denied
$fp = fopen ( “plik.txt” , “w” );
l ock ( $fp , LOCK_EX);
Zgodnie z założeniami, użytkownik
może otwierać tylko te pliki, do któ-
rych faktycznie ma dostęp. Ponie-
waż jednak, jak już wiemy, w pro-
gramie występuje klasyczna sytu-
acja wyścigu, postaramy się ją wy-
korzystać.
Rozpoczynamy atak. Na wybra-
nej konsoli tworzymy pusty plik:
z klasyczną sytuacją wyścigu. Po-
między wywołaniami access i fopen
plik może bowiem ulec zmianie (na
przykład może zostać usunięty). Jest
to konsekwencją wielozadaniowości
systemu operacyjnego, a właściwie
sposobu jej realizacji. System opera-
cyjny może przerwać działanie pro-
cesu w dowolnej chwili – jest zatem
możliwe, że proces zostanie wstrzy-
many pomiędzy wywołaniami access
i fopen , a włączony zostanie inny pro-
ces, który modyi kuje otwierany plik.
Po powrocie do pierwszego proce-
su funkcja fopen będzie pracować na
niewłaściwym pliku.
Argumentem funkcji access i fopen
jest nazwa pliku. Program zakłada, że
przy obu wywołaniach nazwa odno-
si się do tych samych danych na dys-
ku. Jest to jednak błędne założenie,
ponieważ powiązanie (ang. binding )
między nazwą pliku a danymi nie jest
stałe. Nazwa pliku stanowi jedynie ro-
dzaj etykiety, którą można w każdej
chwili przenieść w inne miejsce.
Niektóre operacje wykonywane
przez proces nazywane są atomo-
wymi (ang. atomic ). System opera-
cyjny nie może przerwać procesu
przed zakończeniem działania takiej
operacji – jest ona zatem niepodziel-
na. Sprawdzenie dostępu do pliku
( access ), a następnie jego otwarcie
( fopen ), nie jest operacją atomową,
dlatego pojawia się sytuacja wyści-
gu, polegająca na możliwości zmia-
ny powiązania między nazwą a da-
nymi pliku.
Funkcja access bywa używana
w programach z ustawionym bitem
SUID w celu stwierdzenia, czy plik
może zostać otwarty przez użyt-
kownika uruchamiającego program
(brany jest pod uwagę rzeczywisty,
a nie efektywny identyi kator użyt-
kownika). Ma to zapewnić ochronę
przed sytuacją, gdy użytkownik wy-
korzystuje program uprzywilejowa-
ny do uzyskania dostępu do pliku,
który normalnie jest dla niego nie-
dostępny.
Przykładowy program, widoczny
na Listingu 4, służy do wyświetlenia
zawartości pliku o nazwie podanej ja-
ko argument. Zakładamy, że program
ten będzie działać z prawem SUID, a
jego właścicielem będzie root. Przy
pomocy tego programu każdy użyt-
kownik mógłby zatem otworzyć do-
wolny plik w systemie. Aby umożli-
wić otwieranie jedynie tych plików, do
których użytkownik faktycznie ma do-
stęp, wprowadzamy funkcję access .
devil@hell$ touch pusty
Następnie uruchamiamy jednowier-
szowy skrypt powłoki:
devil@hell$ while [ 1 ]; do ln -sf \
pusty plik; ln -sf /etc/shadow plik;
done
Skrypt ten tworzy dowiązanie sym-
boliczne o nazwie plik , prowadzące
do utworzonego wcześniej pustego
pliku. Nastepnie zmienia plik doce-
lowy dowiązania na /etc/shadow , po-
tem z powrotem na pusty , i tak w nie-
skończoność. Dowiązanie takie mo-
żemy nazwać migoczącym , ponie-
waż jego plik docelowy nieustannie
się zmienia.
Teraz druga część ataku – na in-
nej konsoli wpisujemy następujące
polecenia:
Procesy a wątki
Obok procesów w systemie operacyj-
nym funkcjonuje mechanizm wątków.
Procesy i wątki mają wiele cech po-
dobnych; jedną z nich jest zagrożenie
sytuacjami wyścigu. Także w przypad-
ku wątków istnieje potrzeba synchroni-
zacji dostępu do danych – jest ona re-
alizowana poprzez mechanizm mutek-
sów, analogiczny do blokad.
devil@hell$ touch wynik
devil@hell$ while [ ! -s wynik ]; \
do ./show plik > wynik \
2> /dev/null; done
Wywołujemy tu raz za razem pro-
gram show , przekierowując wyni-
ki jego pracy do utworzonego za-
Hakin9 Nr 3
www.hakin9.org
29
4302597.003.png
Zgłoś jeśli naruszono regulamin