Переповнення стекового буфера

Матеріал з Вікіпедії — вільної енциклопедії.
Перейти до навігації Перейти до пошуку

В програмах, переповнення буфера стеку трапляється, коли програма пише за адресами в програмному стеку виклику поза призначеною структурою даних; це зазвичай буфер фіксованої довжини.[1][2] Баг переповнення буфера стеку трапляється, коли програма пише більше даних в буфер розміщений в стеку, ніж було фактично виділено місця для буфера. Це майже завжди призводить до псування прилеглих даних в стеку, і в разі якщо переповнення було зроблено помилково, часто призводить до краху програми або некоректної роботи. Цей тип переповнення є одним з випадків загальнішого класу багів програмування, знаних як переповнення буфера.[1]

Якщо атакована програма виконується з спеціальними привілеями, або приймає дані з недовірених хостів мережі (напр. вебсерверів), тоді баг є потенційною вразливістю безпеки. Якщо стековий буфер залитий даними, які надійшли від недовіреного користувача, тоді цей користувач може пошкодити стек в такий спосіб, що в стеку опиняється виконуваний код, інжектований ним, відтак він отримує управління процесом. Це один з найстаріших і найнадійніших методів для зловмисників отримати неавторизований доступ до комп'ютера.[3][4][5]

Використання переповнень стекових буферів[ред. | ред. код]

Канонічний метод експлуатації переповнення стекового буфера є затирання адреси повернення в функцію, яка викликала (caller) поточну функцію (callee) вказівником на контрольовані хакером дані (зазвичай на той же стек).[3][6] Це ілюструється в прикладі нижче:

Приклад з strcpy
#include<string.h>

void foo(char* bar){
	char c[12];

	strcpy(c, bar);	// перевірки межі масиву немає...
}

int main(int argc, char** argv){
	foo(argv[1]);
	return 0;
}

Цей код бере аргумент з командного рядка і копіює його в локальну стекову змінну c. Все працює добре для аргументів командного рядка, меншими за 12 символів (як ви можете бачити з малюнка Б нижче). Будь-які аргументи більші за 11 символів в довжину спричинять пошкодження стеку. (Максимальне число символів, що будуть безпечними є на один менше ніж розмір буфера, бо рядки в мові C обмежуються нульовим байтом, який теж треба враховувати. Рядок з дванадцяти однобайтних символів насправді вимагатиме тринадцятибайтного вмістилища. Нульовий байт тоді потре один байт ділянки пам'яті поза кінцем буфера.)

Програмний стек foo() з різними введеннями.
А. - Перед тим як дані
скопійовано.
Б. - "hello" це перший аргумент
командного рядка.
В. - "A​A​A​A​A​A​A​A​A​A​A​A​A​A​A​A​A​A​A​A​\x08​\x35​\xC0​\x80" це перший аргумент командного рядка.

Примітка. На малюнку вказівник char* bar чомусь лежить поверх адреси повернення і навіть збереженого вказівника на стековий кадр попередньої функції (регістр ebp), але як аргумент функції foo(char*) мав би бути якраз перед адресою повернення.

На малюнку 'В' вище, коли аргумент взятий з командного рядка - більший за 11 байтів, foo() (точніше, strcpy(), викликана foo()) затирає локальні дані стеку, збережений вказівник на стековий кадр (ebp), і що найголовніше, - адресу повернення. Коли foo() повертається (виконання в ній доходить до інструкції ret), вона знімає зі стеку адресу повернення, і передає на неї управління (команду ret можна трактувати як pop eip). Як ви можете бачити на малюнку 'В' вище, атакер потер адресу повернення вказівником на стековий буфер char c[12], який тепер містить вставлені ним дані. У випадку справжнього експлуатування переповнення стекового буфера, рядок багатьох "А" міг би містити шелкод (shellcode) для цієї платформи і потрібної функції. Якщо програма має особливі привілеї (напр. біт SUID виставлений для виконання на правах суперкористувача), тоді хакер може використати вразливість щоб дістати права суперкористувача на атакованій машині.[3]

Хакер також може модифікувати внутрішні змінні для експлуатації деяких багів. Той же приклад:

#include<stdio.h>
#include<string.h>

void foo(char* bar){
	float MyFloat = 10.5f;	// Адреса = 0x0023FF4C
	char c[12];		// Адреса = 0x0023FF30

	// Друкуватиме 10.500000
	printf("My float value = %f\n",MyFloat);

	/* ---------------------------------------
		Карта пам'яти:
		@: пам'ять виділена c
		#: пам'ять виділена MyFloat
		-: інша пам'ять

		*c				*MyFloat
		0x0023FF30			0x0023FF4C
		 |				|
		 @@@@@@@@@@@@----------------####
	 foo("My string is too long !!!!! XXXX");

		memcpy потре послідовністю байтів 0x10 0x10 0xC0 0x42
		значення змінної MyFloat. Як плаваючий тип, ця послідовність
		є числом 96.0313720703125 (байт 0x42 - старший!)
		------------------------------------------ */
		memcpy(c,bar,strlen(bar)); // не перевіряється межа масиву...

		// надрукує 96.031372
		printf("My Float value = %f\n", MyFloat);
}

int main(int argc, char** argv){
	foo("my string is too long !!!!! \x10\x10\xC0\x42");
	return 0;
}

Платформозалежні відмінності[ред. | ред. код]

Різні платформи мають тонкі відмінності в своїй реалізації стеку виклику, що може завадити експлуатації переповнення стекового буфера. Деякі машинні архітектури зберігають верхньорівневу адресу повернення стеку виклику в регістрі. Це означає, що будь-яка потерта адреса повернення не використовуватиметься до пізнішого розгортання стеку викликів. Інший приклад машинноспецифічної деталі, яка може завадити використати цю техніку - це факт, що більшість RISC архітектур не дозволяють невирівняний (unaligned) доступ до пам'яті. Тобто, адреси інструкцій мають бути завжди числами, кратними до якогось числа (наприклад - 4).[7] В поєднанні з фіксованою довжиною машинної команди, це машинне обмеження може зробити техніку jump esp майже неможливою для втілення (за винятком, коли програма дійсно містить малоймовірний код для точного переходу на значення стекового регістра).[8][9]

Стеки, які ростуть вгору[ред. | ред. код]

В темі переповнень стекових буферів, є часто обговорювана архітектура яка рідко зустрічається на практиці, в якій стек росте в протилежному напрямку. Тобто, верх стеку рухається в бік старших адрес, коли стек заповнюється, а не навпаки, як в звичайному стеку. Ця зміна архітектури часто пропонується як рішення проблеми переповнення стекового буфера, бо будь-яке переповнення стекового буфера, яке трапиться всередині того самого стекового кадру, не зможе перетерти вказівник повернення. Подальший розгляд цього заявленого захисту, виявляє що це не відповідає дійсності. Будь-яке переповнення, що трапляється в буфері з попереднього стекового кадру все одно тертиме адресу повернення і дозволятиме шкідливу експлуатацію бага.[10] Наприклад, в коді вгорі, вказівник повернення з foo() не буде потерто, бо переповнення справді станеться в стековому кадрі strcpy(). Однак, через те, що буфер, який переповнюється під час виклику strcpy() міститься в попередньому стековому кадрі, вказівник повернення з strcpy() матиме старшу адресу пам'яті, ніж у буфера. Це означає, що замість затирання адреси повернення foo(), тертиметься адреса повернення з strcpy(). Найбільше, що це означатиме, це те, що зростання стеку в протилежному (до звичного) напрямі, змінить деякі деталі того, як експлуатуватиметься переповнення стекового буфера, але це помітно не зменшить кількості експлуатованих багів.

Схеми захисту[ред. | ред. код]

За роки було розроблено чимало схем для перешкоджання експлуатації переповнення стекового буфера. Вони зазвичай мають одну з двох форм. Перший метод - це детектувати переповнення буфера і запобігти передаванню керування на шкідливий код. Другий метод намагається запобігти виконанню шкідливого коду зі стека без прямої детекції переповнення стекового буфера.[11]

Стекові канарки[ред. | ред. код]

Стекові канарки - названі так через те, що вони діють як канарки в вугільній шахті - використовуються для детекції переповнення стекового буфера перед тим, як виконання шкідливого коду може трапитися. Цей метод полягає в розміщенні невеликого цілого - величини, яка довільно вибирається на старті програми - в пам'яті, прямо перед стековим вказівником повернення. Найбільше буферних переповнень затирають пам'ять від молодших до старших адрес, тож, під час затирання адреси повернення (і отримання відтак контролю в процесі), величина "канарки" також зітреться. Ця величина перевіряється перед тим як підпроцедура використає адресу повернення на стеку, щоб переконатися, що її не змінено.[2] Ця техніка має ускладнити експлуатування переповнення стекового буфера, бо змушує хакера отримувати контроль над вказівником команд якимись нетрадиційнішими способами, такими як псування інших важливих змінних на стеку.[2]

Невиконуваний стек[ред. | ред. код]

Інший підхід до запобігання переповнення стекового буфера полягає в забороні виконання даних зі стекової області пам'яті. Це означає, що для виконання шелкоду зі стеку хакер має або знайти шлях скасувати захист від виконання з цієї області пам'яті, або знайти шлях перенести корисне навантаження свого шелкоду в незахищену (від виконання) область пам'яті. Цей метод стає популярніший зараз, через те, що апаратна підтримка флажка "не виконувати" (no-execute) доступна на більшості десктопних процесорів. Хоча цей метод став фактично традиційним в протидії експлуатації переповнення стекового буфера, хоча й має певні недоліки. По-перше нескладно знайти спосіб зберегти шелкод в незахищених областях пам'яті, таких як купа (heap).[12] Навіть якщо це буде не так, є інші шляхи. Найбільш вбивчий - це так званий метод "return to libc" для створення шелкоду. В цій атаці шкідливе навантаження заноситиметься в стек не шелкодом, а правильним стеком викликів, тож виконання буде рознесене по ланцюжку викликів функцій стандартної бібліотеки, зазвичай з ефектом зняття захисту від виконання і дозволом виконання шелкоду як нормального.[13] Це працює того що, в цій атаці, код не заноситься власне в стек. Замість розміщення коду в стеці і затирання адреси повернення в функцію, адресою верха стеку, адреса повернення затирається адресою (точкою входу) якої-небудь потрібної для атаки бібліотечної функції (напр. system() за допомогою якої можна запустити наприклад bash), яка майже гарантовано вивантажена в простір процесу. Однак, якщо невиконуваний стек використати в поєднанні з техніками, такими як ASLR, коли адреси блоків стеку, купи, та динамічних бібліотек рандомізуються (тобто щоразу в системі вони розміщуються в іншій адресі віртуального адресного простору процесу), це може ускладнити "return to libc" атаки, і тому може сильно підвищити безпеку програми.

Відомі приклади[ред. | ред. код]

  • Хробак Моріса поширювався, серед іншого, використовуючи переповнення стекового буфера в сервері Unix finger. Доповідь щодо інтернет-хробака.
  • Хробак Witty поширювався експлуатуючи переповнення стекового буфера в Internet Security Systems BlackICE Desktop Agent.[1]
  • Хробак Slammer поширювався, використовуючи переповнення стекового буфера в SQL сервері Microsoft.[2]
  • Хробак Blaster поширювався, використовуючи переповнення стекового буфера в службі DCOM Microsoft.
  • Експлойт Twilight hack був створений для ігрової консолі Wii. Занадто довге ім'я для коня ("Epona") в The Legend of Zelda: Twilight Princess спричиняло переповнення стекового буфера, дозволяючи виконання довільного коду.

Див. також[ред. | ред. код]

Посилання[ред. | ред. код]

  1. а б Fithen, William L; Seacord, Robert (27 березня 2007). VT-MB. Violation of Memory Bounds. US CERT.
  2. а б в Dowd, Mark; McDonald, John; Schuh, Justin (November 2006). The Art Of Software Security Assessment. Addison Wesley. с. 169–196. ISBN 0-321-44442-6.
  3. а б в Levy, Elias (8 листопада 1996). Smashing the stack for fun and profit. Phrack. 1 (49): 14. Архів оригіналу за 27 жовтня 2012. Процитовано 3 лютого 2010.
  4. Pincus, Jonathan; Baker, Brandon (July-August 2004). Beyond Stack Smashing: Recent Advances in Exploiting Buffer Overruns ([недоступне посилання з 01.06.2008]Scholar search). IEEE Security & Privacy. 2 (4): 20—27. doi:10.1109/MSP.2004.36.
  5. Burebista. Stack Overflows (PDF). Архівовано з джерела 28 вересня 2007. Процитовано 2010-02-03.
  6. Bertrand, Louis (2002). OpenBsd: Fix the Bugs, Secure the System. MUSESS '02: McMaster University Software Engineering Symposium. Архів оригіналу за 30 вересня 2007. Процитовано 3 лютого 2010.
  7. pr1. Exploiting SPARC Buffer Overflow vulnerabilities (HTML). Архівовано з джерела 5 лютого 2012. Процитовано 2010-02-03.
  8. Curious (8 січня 2005). Reverse engineering - PowerPC Cracking on Mac OS X with GDB. Phrack. 11 (63): 16.
  9. Sovarel, Ana Nora. Where’s the FEEB? The Effectiveness of Instruction Set Randomization (HTML).
  10. Zhodiac (28 грудня 2001). HP-UX (PA-RISC 1.1) Overflows. Phrack. 11 (58): 11. Архів оригіналу за 1 травня 2008. Процитовано 18 липня 2019.
  11. Ward, Craig E. (13 червня 2005). C/C++ Buffer Overflows (PDF). Unix Users Association of Southern California. Orange County, California. Архів оригіналу (PDF) за 26 грудня 2010. Процитовано 3 лютого 2010.
  12. Foster, James C.; Osipov, Vitaly; Bhalla, Nish; Heinen, Niels (2005). Buffer Overflow Attacks: Detect, Exploit, Prevent (PDF). United States of America: Syngress Publishing,Inc. ISBN 1-932266-67-4.
  13. Nergal (28 грудня 2001). The advanced return-into-lib(c) exploits: PaX case study. Phrack. 11 (58): 4.