Информационная безопасность
[RU] switch to English


Атаки на переполнение буфера

Андрей Колищак

ноябрь, 1999

[email protected]

Атаки на переполнение буфера

 

Переполнение буфера (buffer overflows) - название самой распространенной уязвимости в области безопасности программного обеспечения. Первая атака с применением данной уязвимости использовалась в вирусе-черве Морриса в 1988 году. С тех пор их число увеличивается с каждым годом. В настоящее время можно говорить, что уязвимости, связанные с переполнение буфера являются доминирующими при удаленных атаках, где обычный пользователь сети получает частичный или полный контроль над атакуемым хостом. Анализ атак и обнаруженных уязвимостей последних лет показывает, что данная проблема является первостепенной. Так, например, 9 из 13 выпусков CERT (Computer Emergency Response Team site) в 1998 году и по крайней мере половина выпусков 1999 года связаны с переполнением буфера [1]. Информационный обзор популярного списка рассылки Bugtraq показывает, что примерно 2/3 респондентов считает переполнение буфера основной причиной нарушения сетевой безопасности [2]. Отметим, что переполнение буфера присуще также программному обеспечению ряда аппаратных средств. Примером может служить уязвимость принтера HP LaserJet 4500 [20]. Очевидно, что эффективное решение данной проблемы позволит исключить большую долю самых серьезных угроз компьютерной безопасности.

Основа атак с использованием этой уязвимости - принцип функционирования операционных систем, где программа получает привилегии и права запустившего ее пользователя или процесса. Таким образом, менее привилегированный пользователь или процесс, который взаимодействует с данной программой может использовать ее права в своих целях. Штатные средства программного обеспечения не позволяют выполнять такие действия. Однако “переполнение буфера” все же делает это возможным. Использование данной уязвимости подразумевает изменение хода выполнения привилегированной программы, например, запуск командной оболочки с правами администратора. Реализации атаки требует решения двух подзадач.

  • Подготовка кода, который будет выполнятся в контексте привилегированной программы.
  • Изменение последовательности выполнения программы с передачей управления подготовленному коду.

Методы решения этих двух подзадач могут служить основой классификации атак, связанных с переполнением буфера [3].

Рассмотрим пути решения подзадачи подготовки кода.

  • Подготавливаемый код представляет собой исполняемый машинные инструкции соответствующего процессора и может передаваться в программу в качестве ее параметра или команды. При этом параметр или команда сохраняется программой в отведенном для этого буфере. Буфер может находится в любой области памяти: в стеке (локальные, автоматические переменные), в динамически распределяемой памяти, в области статических данных. Например, программе, запрашивающей ввод строки, под видом строки может быть передан нужный атакующему код, которому в дальнейшем будет передано управление.
  • Нужный код не передается в программу, так как он уже присутствует в ней самой или в ее адресном пространстве и требуется лишь его параметризация. Например, подготовка параметра для функции запуска программы. В данном случае атакующему требуется изменить или сформировать нужный параметр (для нашего примера указатель на строку с названием программы), а не сам код. Параметр также может находится в любой области памяти.
  • Если параметризированный код уже присутствует в адресном пространстве программы, то подготовки кода не требуется.

Далее рассмотрим способы передачи управления подготовленному коду. В основе этих способов лежит переполнение буфера, т. е. блока памяти, выделенного под переменную. Переполнение возникает при отсутствии проверки выхода за границы буфера. Таким образом, искажается содержимое других переменных состояния и параметров программы, которые входят в область переполнения буфера. Типы искажаемых объектов-переменных определяет способ передачи управления коду атакующего и могут быть следующими.

Искажение адреса возврата из функции

Так как вызову функции сопутствует занесение адреса возврата в стек, то при его подмене атакующим, управление передается по заданному им адресу. Здесь используется переполнение буфера локальных переменных функции, которые также создаются в стеке. Простым примером служит следующий фрагмент программы на Си.

int namelen (void) {

char name[21];

gets(name);

return strlen(name);

}

Из примера видно, что при вводе имени размером более 20 символов частью строки будет замещен адрес возврата из функции. Далее, при выполнении инструкции возврата из подпрограммы, управление будет передано по адресу, который образуют соответствующие позиции введенной строки и в обычной ситуации будет получено сообщение об ошибке операционной системы. Описанный процесс изображен на рисунке 1.

Рисунок 1. Схема атаки “срыв стека”

Такие атаки на переполнение буфера получили название “атаки срыва стека” (stack smashing attack) [4, 5, 6, 7].

Искажение указателя функции

В данном случае атаке подвергаются переменные, содержащие указатели на функции. Эти переменные могут располагаться в любой области памяти, не только в стеке но и в области динамически и статически выделяемых данных. Атакующий организовывает переполнение буфера, которое искажает данные указатели, и далее при вызове функций по этим указателям управление передается подготовленному коду. Приведем пример уязвимости указателей в виде следующего фрагмента программы на Си.

void dummy(void) {

printf("Hello world!\n");

}

int main(int argc, char **argv) {

void (*dummyptr)();

char buffer[100];

dummyptr=dummy;

strcpy(buffer, argv[1]); // Уязвимость

(*dummyptr)();

}

Здесь переполнение буфера buffer приводит к подмене указателя dummyptr и последующему изменению хода выполнения программы.

Искажение таблиц переходов

В результате компиляции часто создаются так называемые таблицы переходов, которые могут использоваться в целях оптимизации, динамического связывания кода, обработки исключений и т.п. Искажение таких таблиц вызванное переполнением буфера позволяет передать управление подготовленному коду.

 

Искажение указателей данных

Данный способ не предусматривает явной передачи управления подготовленному коду, но предполагает, что передача происходит на основании оригинального алгоритма программы. Другими словами, подготовленный код запускается в ходе обычной, не искаженной, последовательности исполнения программы.

Рассмотрим следующий фрагмент программы на C.

foo(char * arg) {

char * p = arg; // уязвимый указатель

char a[40]; // переполняемый буфер

gets(a); // применение gets() реализует уязвимость

gets(p); // искажение кода

}

Здесь переполнение буфера a вызывает подмену указателя p и последующую запись строки по адресу искаженного указателя. Вводимая строка содержит код атакующего. Такая схема атаки часто используется для корректировки (patch) части кода программы или кода динамических и статических библиотек, располагающихся в памяти по фиксированным адресам. Например, корректировка-подмена системных функции выхода из программы или запуска процесса.

Другой пример атаки подобного рода - искажение указателя кадра стека локальных переменных (frame pointer overwrite attack) [8]. Эта атака основана на стандартных операциях пролога и эпилога подпрограмм, в результате чего подменяется указатель базы кадра локальных переменных.

На мой взгляд, комбинация всех методов подготовки кода и целей переполнения буфера (типа искажаемых структур) определяет виды всех возможных атак по переполнению буфера, что позволяет их классифицировать. Результат комбинации приведен в следующей таблице.

 

Таблица 1. Классификация атак по переполнению буфера

Подготовка кода

Цель переполнения

Внедрение кода

Внедрение параметров

Не требуется

Искажение адреса возврата из функции

Атака “срыв стека”

Атака “срыв стека” с параметризацией

Атака “срыв стека” с передачей управления

Искажение указателей функций

Атака на указатели функций

Атака на указатели функций с параметризацией

Атака на указатели функций с передачей управления

Искажение таблиц переходов

Атака на таблицы переходов

Атака на таблицы переходов с параметризацией

Атака на таблицы переходов с передачей управления

Искажение указателей данных

Атака с искажением указателей данных

Атака с искажением указателей данных с параметризацией

Атака с искажением указателей данных с оригинальным кодом

Определив возможные виды атак по переполнению буфера рассмотрим методы защиты от них.

Корректировка исходных кодов программы для устранения уязвимостей

Переполнение буфера происходит прежде всего из-за неправильного алгоритма работы программы, который не предусматривает проверок выхода за границы буферов. Также особую роль здесь играет язык программирования Си и его стандартные библиотеки. Так как Си не содержит средств контроля соответствия типов, то в переменную одного типа можно занести значение другого типа. Стандартные функции Си такие как strcpy, sprintf, gets работают со строками символов и не имеют в качестве аргументов их размеров, что, как видно из приведенных выше примеров, легко приводит к переполнению буфера. Сложившийся годами стиль программирования более ориентированный на производительность программ, без выполнения дополнительных проверок также является причиной распространения данной уязвимости. В результате чего, для программистов выработано ряд методик и указаний по написанию программ не содержащих уязвимости [9]. Сформированы рекомендации по исправлению уже существующих программ (например, замена уязвимых функций: strcpy, spritnf на их аналоги strncpy, snprintf, в параметры которых входит размер строки). Созданы и постоянно возникают новые команды-объединения программистов по аудиту и исправлению кода существующих программ [10]. Существуют гибкие средства автоматически выполняющие действия имитирующие переполнение буфера на этапе отладки программы [11]. Также следует упомянуть об утилитах автоматического поиска уязвимостей в исходном коде программы. Указанные методы и средства позволяют создавать более защищенные программы, но не решают проблему в принципе, а лишь минимизируют число уязвимостей по переполнению буфера. К недостаткам следует отнести и то, что

данный подход ориентирован непосредственно на разработчиков программного обеспечения и не является инструментом конечного пользователя или системного администратора.

Использование неисполнимых буферов

Суть метода заключается в запрещении исполнения кода в сегментах данных и стека, т.е. параметры сегментов данных и стека содержат только атрибуты записи и чтения, но не исполнения. Например, для реализации неисполняемого стека существуют “заплаты” для ОС Solaris и Linux [12, 13]. Однако ограничение на исполнение данных приводит к проблеме несовместимости. Исполняемый стек необходим для работы многим программам, так как на его основе генерируется код компиляторами, реализуются системные функции операционных систем, реализуется автоматическая генерация кода. Защита с использованием неисполнимых буферов предотвратит только атаки с внедрением кода, но не поможет при других видах атак.

Применение проверок выхода за границы

В основе данного метода лежит выполнение проверок выхода за границы переменной при каждом обращении к ней. Это предотвращает все возможные атаки по переполнению буфера, так как полностью исключает само переполнение. Проверки выхода за границы переменной опционально реализованы в некоторых компиляторах Си, например, Compaq C, cc в Tru64 Unix, cc в Alpha Linux [14]. Следует отметить, что реализованные проверки ограничены только точными ссылками на элементы массивов, но не производятся для указателей. Существует также “заплата” для gcc, которая позволяет компилировать программы с полностью реализованной (включая проверку указателей) проверкой выхода за границы массивов [15]. Однако, у этого решения есть существенный недостаток - значительное (до 30 раз) снижение производительности программы. Другие системы осуществляют проверки при доступе к памяти, выполняя вставки дополнительного объектного кода проверок во все места программы, где есть обращения к памяти [16]. Вставки могут производится как до сборки объектных файлов (Purify) так и после (Pixie). Такие проверки сказываются на производительности с ее уменьшением от 2 до 5 раз и скорее подходят для отладки.

Применение проверок целостности

Решение, основанное на данном методе, получено благодаря проекту Synthetix [17]. Цель Synthetix - специализация кода для увеличения производительности операционных систем. При этом вводится понятие так называемого квази-постоянства (Quasi-invariant), т.е. состояния среды, которое неизменно в определенных рамках. Такое квази-постоянство позволяет устранить ряд избыточного кода проверки выполнения различных условий. В рамках проекта реализован набор утилит, в том числе обеспечивающих контроль и защиту квази-постоянных состояний среды. К их числу относятся StackGuard и PointGuard [3, 18, 19]. StackGuard предназначен для защиты от всех атак по переполнению буфера с изменением адреса возврата из функции и реализован в виде “заплаты” к gcc. Данная заплата изменяет пролог и эпилог всех функций с целью проверки целостности адреса возврата из функции при помощи так называемого "canary word". Схема защиты изображена на рисунке 2.

Рисунок 2.

Измененный пролог каждой функции выполняет занесение в стек “canary word”, а эпилог проверку содержимого стека, занесенного ранее и, в случае, нарушения останавливает программу с предупреждающим сообщением. При атаке с искажением адреса возврата неизбежно произойдет искажение “canary word”, что и будет признаком нарушения целостности. Таким образом, целостность адреса возврата определяется целостностью “canary word”. В терминологии Synthetix, нарушение целостности является нарушением квази-постоянства среды. При известном значении “canary word” атакующий может организовать подмену адреса возврата без нарушения целостности. Поэтому “canary word” формируется StackGuard особым образом [19]:

  1. имеет значения 0, CR, LF, EOF, что не позволит провести атаку при переполнении буфера в библиотечных функциях Си, так как данные значения являются признаками конца строки;
  2. имеет псевдослучайное значение, генерируемое при каждом запуске программы.

Авторами системы распространяется защищенная версия Red Hat Linux 5.1 [19], скомпилированная при помощи StackGuard.

Продукт PointGuard предназначен для защиты от атак на указатели функций. Он также реализован в виде дополнения к компилятору gcc и осуществляет защиту путем помещения “canary word” перед каждым указателем функции и таблицей переходов. Существует ряд трудностей с реализацией данного алгоритма защиты [3]: 1) размещение “canary word” должно выполнятся одновременно с выделением памяти под переменную, 2) инициализация одновременно с инициализацией переменной, 3) проверка целостности должна производится при каждом обращении к защищаемой переменной. Поэтому PointGuard ограничивается лишь статическими указателями на функции, которые не являются агрегативными. В дальнейшем авторы намерены реализовать полнофункциональную версию, которая будет оперировать указателями на функции различных видов. PointGuard не сможет защитить от атак с искажением указателей данных. Хотя для исключения этих видов атак, программисту предоставляется возможность самому создавать переменные, из специальных защищенных классов.

Проверки целостности StackGuard и PointGuard выполняются практически не сказываясь на производительности защищаемых программ, в отличие от других систем, так как не отслеживают динамически каждое обращение к переменным, но функционируют по адаптивной схеме с проверкой сохранения квази-постоянства среды.

Механизм проверки целостности используется также другой системой защиты от атак по переполнению буфера - StackShield [23]. StackShield реализован в виде процессора ассемблерного кода, генерируемого gcc и выполняет защиту от атак с искажением адреса возврата и указателей функций. Для предотвращения подмены адреса возврата в прологе каждой функции выполняется сохранение этого адреса во вторичном (дополнительном) стеке, а эпилог восстанавливает его значение. В случае переполнения буфера и искажения адреса возврата он будет восстановлен эпилогом без выдачи дополнительных сообщений, что впоследствии может привести к аварийному завершению. Атака с подменой указателей функций пресекается путем вставки специального кода перед каждой инструкцией вызова подпрограммы по указателю. Специальный код выполняет проверку того, в каком сегменте расположен адрес, вызываемой подпрограммы. Если это область данных или стека то программа завершается с ненулевым кодом ошибки. Однако, при такой схеме защиты встает проблема несовместимости с программами, которые содержат исполняемый код в области данных и стека. Защита StackShield также практически не сказывается на производительности программы.

Также следует отметить реализацию рассматриваемого метода для FreeBSD [21], которая выполнена в виде “заплат” с проверками целостности адреса возврата внутри базовой библиотеки libc. При этом защищаются только библиотечные функции, но не сама программа.

Рассмотренные методы противодействия атакам по переполнению буфера не выполняют полную автоматическую защиту от всех возможных атак описанных в таблице 1. Ряд атак с искажением указателей данных носит логический характер и не могут быть выявлены в автоматическом режиме. Как ни странно, самая первая атака по переполнению буфера в вирусе-черве Морриса носила именно такой характер [3]. Программистам также следует обратить свой взор на языки, обеспечивающие проверку и сохранение типов, такие как Java и Паскаль, исключающие переполнение буфера. Однако, не следует забывать, что виртуальная машина Java написана на Си и, таким образом, может иметь уязвимости [22].

Литература

  1. Fred B. Schneider, Steven M. Bellovin, Martha Branstad, J.Randall Catoe, StephenD. Crocker, Charlie Kaufman, Stephen T. Kent, John C. Knight, Steven McGeady, Ruth R. Nelson, Allan M. Schiffman, George A. Spix, and Doug Tygar. Trust in Cyberspace. National Acadamy Press, 1999. Committee on Information Systems Trustworthiness, National Research Council.
  2. Steve Bellovin. Buffer Overflows and Remote Root Exploits. Personal Communications, октябрь 1999.
  3. Crispin Cowan, Perry Wagle, Calton Pu, Steve Beattie, and Jonathan Walpole. Buffer Overflows: Attacks and Defenses for the Vulnerability of the Decade., Department of Computer Science and Engineering, Oregon Graduate Institute of Science & Technology, ноябрь 1999.
  4. "Aleph One". Smashing The Stack For Fun And Profit. Phrack, 7(49), ноябрь 1996.
  5. "Mudge". How to Write Buffer Overflows. http://l0pht.com/advisories/bufero.html, 1997.
  6. Nathan P. Smith. Stack Smashing vulnerabilities in the UNIX Operating System. http://millcomm.com/nate/machines/security/stack-smashing/nate-buffer.ps, 1997.
  7. Вадим Колонцов. Переполнение буфера. WDL magazine., http://www.tversu.ac.ru/wdl/articles/overflows.html, 1996.
  8. klog. The Frame Pointer Overwrite. Phrack Magazine, выпуск 55, сентябрь 1999.
  9. M.Bishop. How to Write a Setuid Program. :login;,12(1), январь/февраль http://olympus.cs.ucdavis.edu/bishop/scriv/index.html, 1986.
  10. Anon. Linux Security Audit Project. http://lsap.org/.
  11. AnupK Ghosh, Tom O’Connor, and Gary McGraw. An Automated Approach for Identifying Potential Vulnerabilities in Software. In Proceedings of theIEEE Symposium on Security and Privacy, Oakland, CA, май 1998.
  12. "Solar Designer". Non-Executable User Stack. http://www.false.com/security/linux-stack/.
  13. Casper Dik. "Non-Executable Stack for Solaris". Posting to comp.security.unix,
  14. Compaq. ccc C Compiler for Linux. http://www.unix.digital.com/linux/compaq_c/ , 1999.
  15. Richard Jones and Paul Kelly. Bounds Checking for C. http://www-ala.doc.ic.ac.uk/phjk/BoundsChecking.html, июль 1995.
  16. Reed Hastings and Bob Joyce. "Purify: Fast Detection of Memory Leaks and Access Errors". Winter USENIX Conference, январь 1992.
  17. Synthetix: Tools for Adapting Systems Software. http://www.cse.ogi.edu/DISC/projects/synthetix.
  18. Crispin Cowan, Calton Pu, Dave Maier, Heather Hinton, Peat Bakke, Steve Beattie, Aaron Grier, Perry Wagle, and Qian Zhang. StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks. 7-я конференция USENIX Security Conference, San Antonio, TX, январь 1998.
  19. Crispin Cowan, Steve Beattie, RyanFinnin Day, Calton Pu, Perry Wagle, and Erik Walthinsen. Protecting Systems from Stack Smashing Attacks with StackGuard. Linux Expo, Raleigh, NC, май 1999.
  20. Tobias Haustein. Buffer overflow in HP JetDirect module, рассылка Bugtraq, 19 ноября 1999.
  21. Alexander Snarskii. FreeBSD Stack Integrity Patch ftp://ftp.lucky.net/pub/unix/local/libc-letter, 1997.
  22. Drew Dean, Edward W. Felten, and Dan S. Wallach. Java Security: From HotJava to Netscape and Beyond. IEEE Symposium on Security and Privacy, Oakland, CA, 1996, http://www.cs.princeton.edu/sip/pub/secure96.html.
  23. "Vendicator". Stack Shield: A "stack smashing" technique protection tool for Linux. октябрь 1999, http://www.angelfire.com/sk/stackshield.
О сайте | Условия использования
© SecurityVulns, 3APA3A, Владимир Дубровин
Нижний Новгород