Оф, видяло се е че ще се разказва

'>
Значи при 32-битовите x86 архитектури има определен набор от инструкции и процесорни ресурси. Тези ресурси се наричат регистри, побират 32 бита данни (4 байта) и се използват за временно съхранение на данни. Такива регистри са EAX, EBX, etc etc. Същевременно, с оглед на реализирането на разни подфункции се налага по някакъв начин хардуерно да се реализират LIFO (last-in-first-out) структури. Това е защото може да имаш извикване на подфункция от подфункция, при което някъде на удобно място трябва да се пазят данни, свързани с "предишната" функция. Тази структура се нарича "стек". Съответно има инструкции PUSH/POP с които се пъхат/вадят данни в/от стека.
Сега преминаваме на това как изглежда един процес в паметта. Всеки процес си има свой "изглед" към физическата памет (РАМ+суоп). За процеса, това е непрекъснат регион от памет, който се адресира линейно (адресите са от 0х0000000 до 0xffffffff). Ядрото има грижата да мап-ва паметта на процеса върху реалната физическа такава и как става това е една много дълга и сложна работа, която не е предмет на тази лекция (а и аз не съм компетентен лектор)

'>
Адресното пространство на един процес понастоящем изглежда така:
0х00000000 (начало)--------------------------------
I
I0x0804000 (програмен код, статични променливи, library функции)
I
--------------------------------
I
I HEAP (динамично заделена памет)
I
---------------------------------
I/////////////////////////////////////
I////////(свободна памет)//
I/////////////////////////////////////
-----------------------------------
I Стек (расте нагоре)
-----------------------------------
I [vdso] - новост от 2.6 - допълнителен код, свързан с рандомизацията на адресното пространство
I
0xffffffff---------------------------------------------------------(край)
Общо 4 гигабайта адресно пространство.
Сега за стека: използват се 2 процесорни регистъра (ESP, EBP), които пазят адреса съответно на началото на стека и на отместването (т.е адреса на последния елемент в него). Нещо много важно - при всяко извикване на функция, в стека се "пъхат" освен параметрите, които функцията връща, също и адреса на който програмата трябва да продължи изпълнението си след завършването на тази функция. Съответно, когато се срещне RET инструкция, този адрес се "вади" от стека и се прескача на този адрес. Също така понеже всяка функция може да има различни параметри и всяко такова "напъхване" в стека може да вкарва различен обем данни в него, в стека се "пъха" и последната стойност на EBP (която се въстановява след края на функцията).
Трябва да се помни, че стека расте обратно, т.е следващите елементи напъхани в него ще са с по-"нисък" адрес.
Значи как изглежда нашият стек (извиквайки функцията gets()):
<край-EBP сочи тук>***(големина на буфера)*****<"стар" EBP><return addr><начало-ESP сочи тук> ------->...край на адресното пространство
Нашата цел е да презапишем буфера с данни по начин по който да "замажем" return addr така че след изпълнението на gets() изпълнението да продължи на място където ние искаме. Това е мястото, където разполагаме шелкода (изпълняващ шела).
Как изглежда успешно "замазания" стек? Ей така:
<край-EBP сочи тук>***(ПЪЛНЕЖ)*****<"стар" EBP (ЗАМАЗАН)><return addr (ТАКА ЧЕ ДА СОЧИ КЪДЕТО ТРЯБВА)><начало-ESP сочи тук, това е адреса, който ни трябва>SHELLCODE ------->...край на адресното пространство/
Сега сигурно някой се чуди няма ли да е най-добре да дебъгнем програмата с gdb, да видим стойността на ESP и да я използваме за return адрес. Ами допреди 2.6 това беше добра идея, сега не е. И това е защото вкараха известно "рандомизиране" на адресното пространство. Т.е стойността на ESP ще бъде различна при всяко изпълнение на програмата и вместо да скочим на нашия шелкод, ще скочим някъде на майната си.
Как се оправяме с това? За щастие злите хакери са открили, че във всички binary-та изпълнени на 2.6, ELF loader-а вмъква определен код на една и съща локация на края на адресното пространство ([vdso], linux-gate.so, etc). Оказва се че в този код някъде съществува винаги една и съща стойност \xff\xe4 (на асемблер преведено - JMP ESP). И това винаги се намира на един и същ адрес. Значи какво става ако използваме този адрес за нашия хакерски return adres?
Изпълняваме програмата, тя вика gets(), препълваме както трябва буфера с нашия return address и шелкод и се случва следното:
1) gets() завършва когато въведем "лошия" низ и изпълнението се прехвърля на нашият "хакерски адрес" който се намира след началото на стека (в обратна посока).
2) На този адрес има последователност, идентична с инструкцията JMP ESP. Изпълнението на програмата се прехвърля на адреса-начало на стека.
3) Там пък се намира нашия зъл shellcode. Бух! spawn-ва се шел. Ако програмата, която чупим се е изпълнявала с по-високи привилегии (както в случая daemon), значи имаме и шел с такива привилегии.
....ииии тук някъде ми писна да се правя на лектор щото ме заболяха пръстите

'>
Уф мамка му сигурно някои неща звучат малко странно, просто проблемът е че стекът расте в обратна посока на адресното пространство и логически ми е трудно да пояснявам кое и защо. И да, EBP не сочи края на стека, а последния stack frame, но не искам да пояснявам и това, просто в случая няма значение, приемаме че EBP не е интересен регистър и ненужно много ще се усложни цялата тарапана ако трябва да го вземаме предвид

'> Така че ако има забележки по отношение на това ще ги приема без възражения

'>