Переполнение буфера
Переполнение буфера (англ. buffer overflow; также выход за границы буфера или переполнение массива) — это аномалия, возникающая при работе программы, когда новые данные записываются за пределы выделенной памяти для буфера, приводя к перезаписи данных в соседних областях памяти[1].
Буфер — это область памяти, выделенная для временного хранения данных при их передаче между частями программы или между программами. Переполнение буфера часто вызывается некорректно сформированными входными данными: если предполагается, что всё, что попадёт в буфер, не превысит некоторое ограничение, и выделяется память только этого объёма, то большие данные могут быть записаны за его пределы. Если это приводит к перезаписи соседних данных или исполняемого кода, программа может вести себя ошибочно — возникнут ошибки доступа к памяти, неправильные результаты или даже сбой приложения[2].
Некорректная обработка переполнения буфера широко используется для проведения эксплойтов при атаках на информационную безопасность. На многих системах организация памяти программ предсказуема; отправив данные, специально вызывающие переполнение буфера, можно изменить содержимое областей памяти, предназначенных для выполнения кода — заменив его на вредоносный, либо модифицировать критически важные служебные данные программы. Буферы повсеместно используются в коде операционных систем, что позволяет с помощью атак добиться повышения привилегий и получить неограниченный доступ к ресурсам компьютера. Известный червь Морриса в 1988 году использовал переполнение буфера как одну из техник проникновения[3].
Языки программирования, чаще всего связанные с переполнением буфера, — это C и C++, поскольку они не обеспечивают встроенной защиты при обращении к памяти или её перезаписи и не выполняют автоматических проверок границ при работе с массивами (тип буфера в языке). Проверка границ может предотвратить переполнение, но требует дополнительного кода и затрат ресурсов. Современные операционные системы используют разнообразные техники для борьбы с вредоносным эффектом переполнения буфера, включая рандомизация адресного пространства и введение специальных «канареек» между буферами для обнаружения некорректных записей.
Техническое описание
Переполнение буфера возникает, когда записываемые в буфер данные также повреждают значения, хранящиеся по соседним адресам памяти, вследствие отсутствия или недостатка проверки границ[1]. Это особенно характерно при копировании данных в буфер без предварительной проверки, помещаются ли они полностью в целевой буфер.
В следующем примере на языке C программа содержит две переменные, расположенные рядом в памяти: строковый буфер длиной 8 байт a и двухбайтовое целое число (без знака, в big-endian) b:
char a[8] = "";
unsigned short b = 1979;
Изначально a заполнен нулями, а b содержит число 1979.
| Имя переменной | a
|
b
| ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Значение | [нулевая строка] | 1979 | ||||||||
| Шестнадцатеричный вид | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 00 | 07 | BB |
Далее программа пытается записать в буфер a строку "excessive" в кодировке ASCII:
strcpy(a, "excessive");
"excessive" состоит из 9 символов и занимает 10 байт с учётом завершающего нуля, в то время как a может содержать только 8 байт. Без проверки длины строки происходит перезапись значения b:
| Имя переменной | a
|
b
| ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| Значение | 'e' | 'x' | 'c' | 'e' | 's' | 's' | 'i' | 'v' | 25856 | |
| Шестнадцатеричный вид | 65 | 78 | 63 | 65 | 73 | 73 | 69 | 76 | 65 | 00 |
Теперь содержимое b непреднамеренно изменено на число, составленное из части строкового значения. В данном случае буквенный символ «e», за которым следует нулевой байт, даст значение 25856.
Запись данных за пределами выделенной памяти может быть обнаружена операционной системой и вызвать ошибку доступа к памяти, что приведёт к аварийному завершению процесса.
Для предотвращения переполнения буфера в примере вызов strcpy можно заменить на strlcpy, который принимает максимальный размер a (включая завершающий ноль) и не позволит записать больше, чем выделено буферу:
strlcpy(a, "excessive", sizeof(a));
При наличии функции strlcpy она предпочтительна перед strncpy, которая не гарантирует нулевого завершения буфера, если длина копируемой строки равна или превышает размер назначенного буфера. Следовательно, a может не быть завершающимся нулём, и работа с ним как со строкой стандарта C будет некорректна.
Эксплуатация уязвимости
Методы эксплуатации уязвимости переполнения буфера различаются в зависимости от архитектуры, операционной системы и области памяти. Например, атаки на кучу (динамически выделяемая память) коренным образом отличаются от атак на стек вызова. Для эксплуатации на куче важно, какой менеджер памяти используется на целевой системе; для стека важны соглашения о вызовах, применяемые компилятором и архитектурой.
Есть несколько способов нарушения работы программы с помощью атак через переполнение буфера на стеке:
- Изменение поведения программы путём перезаписи локальных переменных, расположенных рядом с уязвимым буфером на стеке;
- Перезапись адреса возврата в кадр стека для перехода на код, выбранный атакующим (называемый шеллкодом); после возврата из функции управление переходит к шеллкоду атакующего;
- Перезапись указателя на функцию[4] или обработчика исключения для запуска шеллкода;
- Перезапись локальной переменной либо указателя в другом кадре стека, который позже будет использован соответствующей функцией[5].
Атакующий формирует данные, вызывающие данную ошибку, и размещает их в буфере, предоставленном пользователю уязвимым кодом. Если адрес пользовательских данных, применяемых для атаки, непредсказуем — эксплуатация переполнения стека для выполнения произвольного кода становится намного сложнее. Одним из способов атаки в таких случаях является техника «трамплинирования» (англ. trampolining): атакующий находит указатель на уязвимый буфер стека, вычисляет расположение шеллкода относительно этого указателя и с помощью перезаписи организует переход на существующую в памяти инструкцию, далее происходит относительный переход к шеллкоду. Такие инструкции часто встречаются в большом по размеру коде. Например, проект Metasploit поддерживает базу подходящих опкодов, однако только для Windows[6].
Переполнение буфера в области кучи называется переполнением кучи и технически реализуется иначе, чем стековые переполнения. Память кучи управляется приложением во время выполнения и обычно хранит данные программы. Атака производится, как правило, путём целенаправленного искажения этих данных так, чтобы приложение начало перезаписывать внутренние структуры, например указатели связанных списков. Классический способ состоит в перезаписи связей выделения памяти (например, метаданных malloc) и последующем использовании изменённых указателей для переписи указателей на функции.
Пример опасности атак на кучу — уязвимость Microsoft GDI+ при обработке изображений JPEG[7].
Модификации буфера до исполнения могут привести к срыву попытки эксплуатации. Такие манипуляции позволяют снизить угрозу, но не гарантируют её отмену. Например, это могут быть приведения к единому регистру, удаление метасимволов, фильтрация неалфавитных строк. Однако существуют методы обхода таких фильтров, например алфавитный шеллкод, полиморфный код, самоизменяющийся код и атака return-to-libc. Те же методы применяются для сокрытия атак от систем обнаружения вторжений. В ряде случаев, например при переводе кода в Unicode[8], угроза уязвимости недостоверно трактовалась раскрывающими как DDoS, хотя фактически была возможна удалённая передача и исполнение произвольного кода.
На практике для надёжного проведения атак требуется преодолеть множество трудностей: наличие нулевых байтов в адресах, различные расположения кода, различия в окружениях и работа защитных механизмов.
NOP-sled — древнейшая и наиболее известная техника эксплуатации переполнения стека. Её цель — увеличить область поиска точного адреса буфера: большая часть стека заполняется командами no-op (нет операции), за которыми размещается инструкция перехода к вершине буфера — там находится шеллкод. Эта последовательность no-op называется «NOP-скользящей дорожкой»: если адрес возврата перезаписан любым указателем в область NOP-сегмента, выполнение просто «скользит» по no-op до перехода к вредоносному коду. Эта техника требует догадки, где именно на стеке расположен сегмент, но увеличивает вероятность успеха.
Ввиду популярности подхода многие IDS/IPS распознают подобные шаблоны (массивы команд no-op) как признаки шеллкода. Однако для обхода обнаружения аттаки часто заменяют классические no-op на любую инструкцию, не портящую состояние виртуальной машины.
Хотя техника повышает шанс успешной атаки, она не гарантирует успех: угадать смещение стека всё же приходится. Ошибка приводит к сбою программы и, возможно, оповещает администратора. Ещё одна проблема — ограниченный размер буфера стека (слишком мал для NOP-сегмента требуемой длины, либо малкий стек). Несмотря на это, этот метод часто оказывается единственно работоспособным для конкретной платформы или среды, и остаётся важной техникой.
Техника «прыжок (jump) по регистру» позволяет надёжно атаковать переполнение стека без NOP-секций и угадывания смещений. Суть — перезаписать адрес возврата таким образом, чтобы программа перешла по заранее известному указателю, находящемуся в регистре и указывающему на подконтрольный атакующему буфер (шеллкод). Например, если регистр A содержит адрес начала буфера, то любая инструкция перехода — jump/call на этот регистр — может быть использована для захвата управления.
На практике программа может не содержать специальных инструкций перехода, однако возможно найти случайно подходящую видимую последовательность байт (опкоды) где-то в памяти программы, например, в вызове стандартных библиотечных функций.
Если этот путь возможен, степень опасности уязвимости резко возрастает: эксплуатация становится надёжной и легко автоматизируется, что используется червями для массового заражения.
Метод позволяет помещать шеллкод после адреса возврата, особенно на платформе Microsoft Windows, но есть ограничения: из-за особенностей адресации и порядка байтов размер шеллкода ограничивается объёмом буфера. Библиотеки (DLL) часто располагаются по адресам, не содержащим нулей (ограничение для строковых операций), что дало рождение технике «трамплинирования через DLL».
Защитные меры
Используется целый ряд техник для обнаружения и предотвращения переполнений буфера, есть компромисс между защитой и производительностью. Ниже приведены основные подходы.
Программирование на ассемблер, C и C++ подвержено переполнениям буфера, поскольку эти языки не имеют строгой типизации и позволяют прямое обращение к памяти[2]. C не проверяет, не происходит ли выход за границу буфера. В стандарте C++ есть внедряемые механизмы безопасной работы с контейнерами (например, метод vector::at() выбрасывает исключение out_of_range при ошибке границ[9]), но в случае отсутствия прямого указания на проверку границ, поведение идентично C. Для C существуют техники предотвращения переполнений.
Языки со строгой типизацией и без прямого доступа к памяти, такие как COBOL, Java, Eiffel, Python, обычно не допускают переполнения буфера[2]. Многие современные языки (например, Ada, Eiffel, Lisp, Modula-2, Smalltalk, OCaml, а также производные от C — Cyclone, Rust, D) предоставляют проверки во время выполнения и/или компиляции. Для Java и .NET Framework проверки границ массивов обязательны. Почти все интерпретируемые языки обеспечивают защиту от переполнения и выдают однозначную ошибку. Опции компилятора иногда позволяют включать или выключать проверки границ: это компромисс между безопасностью и производительностью.
Переполнения буфера характерны для C/C++, поскольку эти языки предоставляют работу с примитивными структурами и позволяют напрямую работать с представлением данных. Для предотвращения таких уязвимостей рекомендуется избегать стандартных функций без проверки границ, например gets, scanf, strcpy. Червь Морриса, например, использовал уязвимость в gets в сервисе fingerd[10].
Для уменьшения рисков необходимо использовать и внедрять специальные библиотеки и типы данных, автоматически реализующие безопасное управление буферами и контроль границ. Большая часть уязвимостей приходится на строки и массивы, так что покрытие этих типов снижает большинство рисков. Однако ошибки в самих библиотеках или неправильное обращение с ними могут привести к уязвимостям. Среди безопасных реализаций известных стандартных библиотек: «The Better String Library»[11], Vstr[12], Erwin[13]. OpenBSD реализовал функции strlcpy и strlcat, однако они менее универсальны, чем полноценные библиотеки управления строками.
В 2007 году был опубликован отчёт Международного комитета по стандартам C (ISO TR 24731)[14], описывающий набор функций с дополнительными параметрами для проверки размера буфера. Эффективность применения таких функций для устранения уязвимости спорна, так как требуют той же дисциплины, что и ручная проверка стандартных функций[15].
Механизмы обнаружения переполнения буфера заключаются в контроле, чтобы стек не был изменён при выходе из функции. Если обнаруживается несоответствие, программа завершается с ошибкой доступа к памяти. Существуют системы Libsafe[16], StackGuard[17], ProPolice[18] (патчи для GNU Compiler Collection).
У Microsoft защита Data Execution Prevention (DEP) предотвращает перезапись указателя обработчика структурированных исключений (SEH)[19].
Более сильная защита — раздельное хранение стека для данных и возвратов (имеется, например, в языке Forth), хотя такой раздел не обеспечивает абсолютной защиты — могут быть перезаписаны другие чувствительные данные.
Данный тип защиты не гарантирует обнаружения всех атак, но эффективен по производительности, поскольку ориентирован на характер поведения атак[20].
Переполнение буфера основывается на подмене указателей, включая адреса возврата. Технология PointGuard реализует кодирование указателей через их XOR-перевод компилятором[21]. Идея — при перезаписи указателя атакующий не может предсказать значение после декодирования. PointGuard не был реализован полноценно, но Microsoft внедрила аналогичный подход в Windows XP SP2, Windows Server 2003 SP1[22]: механизм вызова защиты доступен через АПИ.
XOR-линейность потенциально позволяет атакующему управлять кодомируемым указателем частично, методом многократных попыток или выбора одного из допустимых вариантов (например, адрес в зоне NOP-сегмента)[23]. Для борьбы с этим разработчики Microsoft ввели случайные смещения при кодировании[24].
Ограничение исполнения кода в памяти — подход к защите, при котором стек и куча отмечаются как неисполняемые (например, с помощью NX bit, XD bit). При атаке попытка запуска кода из этих сегментов приводит к срабатыванию исключения.
Некоторые UNIX-системы (например, OpenBSD, macOS) по умолчанию используют защиту от исполнения кода (например, W^X). Также применяются такие расширения, как PaX[25], Exec Shield[26], Openwall[27].
Новые версии Microsoft Windows поддерживают защиту от исполнения кода — Data Execution Prevention[28]. Среди проприетарных решений — BufferShield[29], StackDefender[30].
Защита памяти от исполнения затрудняет, но не исключает такие методы, как атака return-to-libc. На 64-битных системах с ASLR защита от исполнения делает атаки ещё менее выполнимыми.
CHERI — процессорная технология, предназначенная для аппаратной защиты памяти через внедрение специального типа ссылок (capability). Вместо обычных указателей используются адреса с метаданными, ограничивающие области допустимого доступа.
ASLR — функция информационной безопасности, динамически изменяющая расположение ключевых областей данных (базы исполняемых файлов, положения библиотек, кучи и стека) в адресном пространстве процесса.
Рандомизация усложняет эксплуатацию переполнений буфера — атакующему приходится настраивать эксплойт под конкретную машину, что делает автоматические «черви» малоэффективными[31]. Похожие эффекты достигаются с помощью перестройки (rebasing) библиотек и процессов.
Использование глубокого анализа пакетов (DPI) позволяет обнаруживать на уровне сети попытки атак, используя сигнатуры эксплойтов и эвристический анализ. Этот метод блокирует_packets, соответствующие известным шаблонам атак, например длинным цепочкам no-op (NOP-сегментам) или частично переменным нагрузкам.
Но DPI-защита легко обходится — шеллкод может быть алфавитным, полиморфным, само-модифицирующимся. Такие методы позволяют скрыть атаку от IDS/IPS.
Проверка наличия переполнений буфера на этапе тестирования и исправление ошибок, их вызывающих, помогает предотвратить уязвимости. Наиболее распространённый автоматизированный способ обнаружения — фазз-тестирование[32]. Граничные тесты и статический анализ кода также позволяют выявлять уязвимости[33]. Для новых программ тестирование эффективно, но для устаревших не поддерживаемых решений — ограниченно.
История
Переполнения буфера были исследованы и частично задокументированы уже в 1972 году в отчёте Computer Security Technology Planning Study, где указан принцип: «Код, выполняющий данную функцию, не проверяет корректность адресов источника и назначения, позволяя пользователю перезаписывать части монитора. Это можно использовать для внедрения кода, позволяющего захватить контроль над системой»[34]. Монитор здесь — аналог современного ядра.
Первое хорошо задокументированное использование переполнения буфера с целью атаки — червь Морриса в 1988 году: он использовал уязвимость службы finger в Unix[3][35]. В 1995 году Томас Лопатик независимо переоткрыл уязвимость и опубликовал на Bugtraq. В 1996 году Elias Levy (Aleph One) опубликовал в журнале Phrack статью «Smashing the Stack for Fun and Profit»[36], ставшую классическим введением в эксплуатацию стековых переполнений буфера.
Позднее, крупные эпидемии червей использовали подобные уязвимости: в 2001 году Code Red эксплуатировал переполнение буфера в Microsoft IIS 5.0[37], в 2003 году SQL Slammer поражал Microsoft SQL Server 2000[38].
C 2003 года переполнения буфера в коммерческих играх для Xbox также позволили запускать на консолях неавторизованное ПО без аппаратных модификаций[39]; аналогичные уязвимости использовались для PlayStation 2 (PS2 Independence Exploit) и Wii (Twilight hack).
См. также
Примечания
Литература
- Vangelis (8 декабря 2004). “Stack-based Overflow Exploit: Introduction to Classical and Advanced Overflow Technique” [англ.]. Wowhacker via Neworder. Архивировано из оригинала (text) 2007-08-18. Используется устаревший параметр
|url-status=(справка) - Balaban, Murat. “Buffer Overflows Demystified” [англ.]. Enderunix.org. Архивировано из оригинала (text) 2004-08-12. Используется устаревший параметр
|url-status=(справка) - Akritidis, P.; Evangelos P. Markatos; M. Polychronakis; Kostas D. Anagnostakis (2005). “STRIDE: Polymorphic Sled Detection through Instruction Sequence Analysis.” (PDF). Proceedings of the 20th IFIP International Information Security Conference (IFIP/SEC 2005) [англ.]. IFIP International Information Security Conference. Архивировано из оригинала (PDF) 2012-09-01. Дата обращения 2024-06-27. Используется устаревший параметр
|url-status=(справка) - Klein, Christian (сентябрь 2004). “Buffer Overflow” (PDF) [англ.]. Архивировано из оригинала (PDF) 2007-09-28. Используется устаревший параметр
|url-status=(справка); Проверьте дату в|date=(справка на английском) - Shah, Saumil (2006). “Writing Metasploit Plugins: from vulnerability to exploit” (PDF). Hack In The Box [англ.]. Куала-Лумпур. Дата обращения 2024-06-27.
- Intel 64 and IA-32 Architectures Software Developer's Manual Volume 2A: Instruction Set Reference, A-M : [англ.]. — Intel Corporation, май 2007. — P. 3–508.
- Alvarez, Sergio (5 сентября 2004). “Win32 Stack BufferOverFlow Real Life Vuln-Dev Process” (PDF) [англ.]. IT Security Consulting. Дата обращения 2024-06-27.
- Ukai, Yuji; Soeder, Derek; Permeh, Ryan (2004). “Environment Dependencies in Windows Exploitation”. BlackHat Japan [англ.]. Япония: eEye Digital Security. Дата обращения 2024-06-27.
Ссылки
- Обнаружение и эксплуатация удалённого переполнения буфера на FTP-сервере (англ.)
- Aleph One — Smashing the Stack for Fun and Profit (англ.)
- Gerg, Isaac (2 мая 2005). “An Overview and Example of the Buffer-Overflow Exploit” (PDF). IAnewsletter [англ.]. Information Assurance Technology Analysis Center. 7 (4): 16—21. Архивировано из оригинала (PDF) 2006-09-27. Дата обращения 2024-06-27. Используется устаревший параметр
|url-status=(справка) - Стандарты безопасного программирования CERT (англ.)
- Инициатива CERT по безопасному программированию (англ.)
- Secure Coding in C and C++ (англ.)
- SANS: inside the buffer overflow attack (англ.)
- Advances in adjacent memory overflows (англ.)
- A Comparison of Buffer Overflow Prevention Implementations and Weaknesses (англ.)
- Сборник whitepapers по переполнению буфера (англ.)
- Writing Exploits III. Книга: Sockets, Shellcode, Porting & Coding: Reverse Engineering Exploits and Tool Coding for Security Professionals (англ., James C. Foster, ISBN 1-59749-005-9)
- Computer Security Technology Planning Study (англ., James P. Anderson. ESD-TR-73-51, ESD/AFSC, Hanscom AFB, Bedford, MA 01731, октябрь 1972)
- Buffer Overflows: Anatomy of an Exploit (англ.)
- Secure Programming with GCC and GLibc (англ., Marcel Holtmann, 2008)
- Criação de Exploits com Buffer Overflor – Parte 0 – Um pouco de teoria (порт.)


