четвъртък, 28 февруари 2008 г.

BugTraq, r00t, и Underground.Org
ви представят:

XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
Smashing The Stack For Fun And Profit
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

от Aleph One
aleph1@underground.org

(Българска Версия)
(преведено от |Razor|)
Заб. Текста ограден в (* ... *) е пояснение вмъкнато от мен

/ Част 1 \

`разбиване на стека` - [определение от програмирането на C]
В много дистрибуции на C е възможно да се промени съдържа-
нието на стека чрез записване на повече от отделената в него
информация. Кода, който прави това може да промени хода на из-
пълнение на програмата като е пренасочи към случаен адрес.
Това може да причини един от най-опасните бъгове в системите...
препълване на буфера


Въведение
-----------

На последно време се наблюдава голямо нарастване на уязвимостите в системите
получени от препълвания на буфера. Примери са следните апликации(програми):
syslog, splitvt, sendmail, Linux/FreeBSD mount и други. Този документ обяснява
принципа на действие на препълванията и съответно работата на експлоитите към тях

За документа са необходими начални знания в асемблер и C. Познаването на стру-
ктурата на RAM паметта в компютъра и работата с `gdb` (*debug програма за Linux*)
са също полезни. И разбира се трябва да имате Intel x86 процесор с операционна
система Linux.

В началото малко основна терминология:
Буфера е съвкупност от поредни клетки в паметта на компютъра, които съдържат
данни от един и същи тип (*в нашия случая използуваме char*). Статичните промен-
ливи се заделят по време на зареждането на програмата,а динамичните се заделят в
стека по време на изпълнението.
Препълването е да запишем повече информация в стека отколкото е отделено от про-
менливата


Организация на паметта
------------------------

За да разберете какво са стек буферуте първо трябва да се запознаем как процесите
са организирани в паметта. Те са разделени на три региона: текст, данни и стек. Ще
се концентрираме на стек-региона, но първо ще разгледаме бегло другите два.

Текстовият регион е фиксиран от програмата и включва инструкции(кодове) и данни,
които единствено могат да бъдат четени(при опит за запис възниква грешка).

Регионна с данни съдържа инициализирани и не-инициализирани данни. Статичните про-
менливи са заделени в тази област

/------------------\ памет с
| | по-нисък
| Текст | адрес
| |
|------------------|
| (Инициализирани) |
| Данни |
|(Неинизиализирани)|
|------------------|
| |
| Стек | памет с
| | по-висок
\------------------/ адрес

Фиг. 1 Региони в паметта


Какво е стек
--------------

Стека е част от паметта където могат да бъдат временно съхранявани различ-
ни типове данни.
(* Стека е организиран със структура LIFO (Last In First Out). Това означава, че
елементът вкаран последен, излиза първи:

|X| връх \
|O| - стек
|O| /
|_| дъно /

Така ако последователно сме поставяли "O","O" и "X" редът на изваждането им ще е
"X","O","O" *)


За работа със стека се използуват предимно два оператора:
PUSH - слага елемент на върха на стека
POP - извлича елемента, който е на върха и променя големината на стека с 1


Защо използуваме стек?
------------------------

Сегашните компютърни езици използуват под-програми (функции), които поз-
воляват едно и също действие да се изпълнява много пъти само чрез извикване-
то на името на функцията (без кода да се пренаписва отново). Функцията се из-
пълнява чрез пренасочване на програмата към адреса, на който е записана. Това
на пръв поглед наподобява нормален безусловен (* инструкциа jmp*) преход, но
за разлика от него след приключване на функцията, програмата продължава изпъл-
нението си след мястото, от което е била пренасочена (* инструкция ret*).
Именно тука взема участие стека. В него се записват параметрите, които тря-
бва да получи функцията. (*Тези параметри се записват на стека в обратен ред,
за да може функцията да ги "вземе" в правилен (понеже както казахме стека има
LIFO структура) *)

Региона на стека
------------------

Стека представлява поредни клетки от паметта съдържащи различни данни. Регис-
търът SP (stack pointer) винаги съдържа адреса на върха на стека. Дъното има фик-
сиран адрес. Големината на стека се променя непрекъснато от kernel-а по време на
изпълнението на програмата.

След извикване на функция, параметрите записани в стека играят ролята на ло-
кални променливи в самата функция. Освен данните за лоакалните променливи, в
него е записана и информация, за възстановяване на предишния адрес на програмата
(на който трябва да се прехвърли след като изпълнението на подпрограмата при-
ключи). Регистъра който сочи адреса към следващата инструкция, която програмата
трябва да изпълни се нарича IP (instruction pointer)

В зависмост от ОС-а стека може да нараства към по-висок или към по-нисък ад-
рес на паметта. В този документ ще използуваме стек, който се разширява към нис-
ки адреси. По този начин действат повечето компютри като Interl, Motorolla, SPARC.
Указателя към стека (SP) също се различава в отделните дистрибуции. Той може да
сочи към последния адрес на стека или към адреса, след последната свободна клетка

(*
Вариант 1 Вариант 2
<- SP - по-нисък адрес
|X| <- SP |X|
|X| |X|
|X| |X|
|_| |_| - по-висок адрес
*)

Освен регистъра SP, който сочи върха на стека (по-ниският адрес) често се из-
ползува и указател към клетка (FP - Frame Pointer), който съдържа фиксирана по-
зиция в дадена клетка. Някои текстове също се посочват от локален базов указа-
тел (LB - local base pointer). По принцип локалните променливи могат да бъдат
посочени като се знае отместването им в стека спрямо регистъра SP, но понеже
броят на данните в стека непрекъснато се сменя, това може да стане и спрямо BP
(base pointer). Много компилатори използуват втори регистър, FP, като указател
към локалните променливи и параметрите, понеже те не се отместват спрямо адреса
от този регистър при използуването на POP или PUSH.

Първото нещо, което трябва да направи една функция(процедура), когато бъде из-
викана е да запомни текущия FP, за да може изпълнението на програмата да се вър-
не след приключване на процедурата. Тогава тя записва съдържанието на SP върху
FP, за да създаде нов FP, и разширява SP, за да може в стека да бъде отделено
място за локалните променливи. Тази част от кода се нарича "процедурен пролог".
След приключването работата на процедурата, стека се изчиства в старото му със-
тояние. Тази част се нарича "процедурен епилог".

Нека видим какво представлява стека в проста C програма

primer1.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
}

void main() {
function(1,2,3);
}
------------------------------------------------------------------------------

За да разберем какво точно прави програмата, за да извика function() е ком-
пилираме с `gcc` използвайки параметър -S, който изисква генерирането на асем-
блиран код.

$ gcc -S -o primer1.s primer1.c
$ pico primer1.s

Поглеждайки в кода на асемблер виждаме че извикването на function() е пред-
ставено със следните редове:

pushl $3
pushl $2
pushl $1
call function

Функцията приема 3 аргумента (* както споменах те се слагат в стека в обра-
тен ред*). Инструкцията `call` слага указателя IP (instruction pointer) върху
стека. Ще наричаме запомният IP `връщан адрес` (RET). Първото нещо което се
прави от функцията е процедурният пролог:

pushl %ebp
movl %esp,%ebp
subl $20,%esp

Нека разгледаме този код. Първо EBP, (frame pointer), се слага на върха на
стека. Тогава се копира съдържанието на SP върху EBP, правейки го нов FP указа-
тел. Ще наричаме запаметеният FP указател SFP. Най-накрая се създава място за
локалните променливи (buffer1 и buffer2) като тяхната големина се изважда от SP.

Трябва да се помни че паметта може да бъде адресирана на части с големина не
по-малки от една `дума` (word). `Думата` в случая е 4 байта или 32 бита. Така че
нашият 5-байтов buffer1 ще заеме 8 байта в паметта(2 думи), а 10-байтовият
buffer2 ще заеме 12 байта (2 думи). Именно затова от SP се изважда 20.
Имайки това предвид нашият стек ще изглежда по следният начин:
(*Цифрата в клетката показва големината й в байтове*)

нисък адрес на висок адрес на
паметта паметта
buffer2 buffer1 sfp ret a b c
<----- [ 12 ][ 8 ][ 4 ][ 4 ][ 4 ][ 4 ][ 4 ]

връх на дъно на
стека <--- стека

(* Показано по-нагледно:
BP - base pointer - базово отместване

| |
BP+16 | 3 аргумент | стек - връх
BP+12 | 2 аргумент |
BP+8 | 1 аргумент |
BP+4 |връщан адрес|
BP |съхранено BP|
BP-8 | buffer1 |
BP-12 | buffer2 | стек - дъно
------------
*)

Препълвания на буфера
-----------------------

Препълване на буфера се получава когато се опитаме да му сложим повече дан-
ни от колкото може да побере. Но как това помага на хакерите да изпълнят соб-
ствен код в програмата? Нека погледнем следния пример:

primer2.c
------------------------------------------------------------------------------
void function(char *str) {
char buffer[16];

strcpy(buffer,str);
}

void main() {
char large_string[256];
int i;

for( i = 0; i < 255; i++)
large_string[i] = 'A';

function(large_string);
}
------------------------------------------------------------------------------

$ gcc -o primer2 primer2.c
$ ./primer2
Segementation fault (Core dumped)

Това е програма имаща функция с ясно изразена грешка с препълване на буфера.
Тази функция копира зададеният и низ в буфер използувайки strcpy() вместо
strncpy() (* strncpy копира N байта от изходния низ, до като strcpy() копира
низа до като не достигне краят му - chr(0) *).
Ако стартирате програмата ще получите грешка в сегментацията (* програмата се
опитва да чете от адрес, от който не и е позволено *). След извикване на функ-
цията стека изглежда по следният начин:

нисък адрес висок адрес
на паметта на паметта
buffer sfp ret *str
<----- [ ][ ][ ][ ]

връх на дъно на
стека стека

Какво всъщност става тука и от къде идва тази грешка в сегментацията?
strcpy() копира съдържанието на larger_string[256] в buffer[16] както казах
без никаква проверка дали това практически е възможно. Съвсем ясно се вижда
че buffer[] е много по-малък от *str. В buffer[] се записват само първите 16
байта от larger_string, а останалата част се презаписва върху стека. Презапи-
саните части на стека включват SFP, RET и дори самият *str!. Нашият large_string
съдържа 256 символа `A` с ASCII 0x41 (шестнадесетично). Това означава, че връ-
щаният адрес вече е 0x41414141. Това е извън адресното пространство на процеса.
Функцията се връща и се опитва да прочете следващата инструкция от този адрес
(0x41414141). При този опит kernel-а прекъсва изпълнението на програмата и връ-
ща `Segmentation fault` (грешка в сегментацията).

Значи препълването на буфера ни позволява да променим връщаният от функция
адрес. По този начин ние можем да променим хода на изпълнението на програмата.
Нека се върнем към първия пример и се опитаме да видим как изглеждаше стека:

нисък адрес висок адрес
на паметта на паметта
buffer2 buffer1 sfp ret a b c
<----- [ 12 ][ 8 ][ 4 ][ 4 ][ 4 ][ 4 ][ 4 ]

връх на дъно на
стека стека


Нека се опитаме на променим този първи пример, така че той да променя връ-
щаният адрес и да покажем как можем да изпълнем някакъв код. Точно преди
buffer1[] в стека се намира SFP, а преди него връщаният адрес. Това означава
4 байта след края на buffer1[] (запомнете че големината на buffer1[] е всъ-
щност 8 байта, така че връщаният адрес се получава на 12 байта отместване от
началото). Ще променим връщаният адрес(ret), така че редът:
x = 1;
да бъде прескочен от програмата. За да направим това добавяме 8 байта към стой-
ността записана ret. Кодът би изглеждал по следният начин:


primer3.c:
------------------------------------------------------------------------------
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
int *ret;

ret = buffer1 + 12;
(*ret) += 8;
}

void main() {
int x;

x = 0;
function(1,2,3);
x = 1;
printf("%d\n",x);
}
------------------------------------------------------------------------------

Сега компилирайте и стартирайте програмата и... хоп - вместо 1 програмата из-
вежда резултат 0!
Всъщност добавихме 12 към адреса на buffer1[] в паметта.
ret = buffer1 + 12;
На този нов адрес е записан адреса, на който функцията трябва да се върне.
Целта ни беше да прескочим редът `x = 1` и да отидем направо на процедурата
printf(). Но как разбрахме, че трябва да добавим 8 към стойността на връщаният
адрес? За начало използуваме пробна стойност (например 1), компилираме програ-
амта и използуваме `gdb`:

------------------------------------------------------------------------------
$ gdb primer3
GDB is free software and you are welcome to distribute copies of it
under certain conditions; type "show copying" to see the conditions.
There is absolutely no warranty for GDB; type "show warranty" for details.
GDB 4.15 (i586-unknown-linux), Copyright 1995 Free Software Foundation, Inc...
(no debugging symbols found)...
(gdb) disassemble main
Dump of assembler code for function main:
0x8000490
: pushl %ebp
0x8000491 : movl %esp,%ebp
0x8000493 : subl $0x4,%esp
0x8000496 : movl $0x0,0xfffffffc(%ebp)
0x800049d : pushl $0x3
0x800049f : pushl $0x2
0x80004a1 : pushl $0x1
0x80004a3 : call 0x8000470
0x80004a8 : addl $0xc,%esp
0x80004ab : movl $0x1,0xfffffffc(%ebp)
0x80004b2 : movl 0xfffffffc(%ebp),%eax
0x80004b5 : pushl %eax
0x80004b6 : pushl $0x80004f8
0x80004bb : call 0x8000378
0x80004c0 : addl $0x8,%esp
0x80004c3 : movl %ebp,%esp
0x80004c5 : popl %ebp
0x80004c6 : ret
0x80004c7 : nop
------------------------------------------------------------------------------

Лесно можем да видим че след извикването на функцията RET ще бъде 0x80004a8,
а ние искаме да прескочим инструкцията на 0x80004ab. Следователно следващата ин-
струкция, която ни интересува е на 0x80004b2. Използувайки шестнадесетичен кал-
кулатор изчисляваме:

80004b2
- 80004a8
-------------
8

По този начин намираме отместването до следващия адрес.

-------------------------------------- Край на част 1 ---------------------------------

Няма коментари: