wyscig pl, Haking
[ Pobierz całość w formacie PDF ]
Artykuł pochodzi z czasopisma Hakin9.
Do ściągnięcia bezpłatnie ze strony:
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-
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
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
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
[ Pobierz całość w formacie PDF ]