ХакерДом: Seminar/20072008/BufferOverflow ...

Null Page | Каталог | Изменения | НовыеКомментарии | Пользователи | Регистрация | Вход:  Пароль:  

Переполнение буфера (Вuffer Overflow) — это явление, возникающее, когда компьютерная программа записывает данные за пределами выделенного в памяти буфера.
Переполнение буфера обычно возникает из-за неправильной работы с данными полученными извне и памятью, при отсутствии жесткой защиты со стороны подсистемы программирования (компилятор или интерпретатор) и операционной системы. В результате переполнения могут быть испорчены данные расположенные следом за буфером (или перед ним).
Переполнение буфера является наиболее популярным способом взлома компьютерных систем, так как большинство языков высокого уровня используют технологию стекового кадра — размещение данных в стеке процесса, смешивая данные программы с управляющими данными (в том числе адреса начала стекового кадра и адреса возврата из исполняемой функции).
Переполнение буфера может вызывать аварийное завершение или зависание программы, ведущее к отказу обслуживания (denial of service, Do S?). Отдельные виды переполнений, например переполнение в стековом кадре, позволяют злоумышленнику загрузить и выполнить произвольный машинный код от имени программы и с правами учетной записи, от которой она выполняется.


Первая атака с применением данной уязвимости использовалась в вирусе-черве Морриса в 1988 году. К сожалению, точно такая же атака повторилась и в наши дни. 9 из 13 выпусков CERT (Если есть какая-нибудь проблема с защитой, вы обязательно, в конце концов, узнаете о ней, когда она появится в CERT advisory. Координационный центр CERT (Computer Emergency Response Team) был организован большим количеством университетов и DARPA в ответ на червь Морриса в 1988.) в 1998 году и из 17 бюллетеней безопасности CERT за 1999 год, 10 были непосредственно вызваны ошибкам в программном обеспечении, связанным с переполнениями буфера. Информационный обзор популярного списка рассылки Bugtraq показывает, что примерно 2/3 респондентов считает переполнение буфера основной причиной нарушения сетевой безопасности.Отметим, что переполнение буфера присуще также программному обеспечению ряда аппаратных средств. Примером может служить уязвимость принтера HP LaserJet 4500(Принтер имеет встроенный Web-сервер для удаленного администрирования. Слишком длинный адрес URL при обращении к встроенному вызывает печать принтером страницы диагностики с указанием содержимого регистров. ).
Например, представим гипотетическую программу, которая исполняется с привилегиями суперпользователя и выполняет некоторые функции системного администрирования — к примеру, изменение пароля пользователя. Если программа не проверяет длину введённого нового пароля, то любые данные, длина которых превышает размер выделенного для их хранения буфера, будут просто записаны поверх того, что находилось после буфера. Если в дальнейшем программа передаст управление в эту область памяти, то есть исполнит находящийся в этой области памяти машинный код, то злоумышленник может вставить в неё инструкции на машинном языке, которые будут выполнять любые действия с привилегиями суперпользователя — добавлять и удалять учётные записи пользователей, изменять пароли, изменять или удалять любые файлы и т. д.


Рассмотрим эту проблему с 2-х сторон:

1. Атаки


Самые современные вычислительные системы используют стек для передачи аргументов процедурам и сохранения локальных переменных. Стек является буфером типа LIFO (последним вошел первым вышел) в верхней части области памяти процесса. Когда программа вызывает функцию, создается новая «граница стека». Эта граница состоит из аргументов, переданных в функцию, а также динамического количества пространства локальных переменных. «Указатель стека» является регистром, хранящим текущее положение вершины стека. Так как это значение постоянно меняется вместе с помещением новых значений на вершину стека, многие реализации также предусматривают «указатель границы», который расположен около начала стека, так что локальные переменные можно легко адресовать относительно этого значения. Адрес возврата из функции также сохраняется в стеке, и это является причиной нарушений безопасности, связанных с переполнением стека, так как перезаписывание локальной переменной в функции может изменить адрес возврата из этой функции, потенциально позволяя злоумышленнику выполнить любой код.
Основа атак с использованием этой уязвимости – принцип функционирования операционных систем, где программа получает привилегии и права запустившего ее пользователя или процесса. Таким образом, менее привилегированный пользователь или процесс, который взаимодействует с данной программой может использовать ее права в своих целях. Штатные средства программного обеспечения не позволяют выполнять такие действия. Однако “переполнение буфера” все же делает это возможным. Использование данной уязвимости подразумевает изменение хода выполнения привилегированной программы, например, запуск командной оболочки с правами администратора.
Как и во многих других языках программирования, в C не выполняется автоматической проверки границ в массивах или указателях. Кроме того, стандартная библиотека C полна очень опасных функций.
Реализации атаки требует решения двух подзадач.

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



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



Очевидно, что для выполнения реальных инструкций (таких, как exec(/bin/sh)), может быть придуман более вредоносный ввод.
Такие атаки на переполнение буфера получили название “атаки срыва стека” (stack smashing attack).
Искажение указателя функции
В данном случае атаке подвергаются переменные, содержащие указатели на функции. Эти переменные могут располагаться в любой области памяти, не только в стеке но и в области динамически и статически выделяемых данных. Атакующий организовывает переполнение буфера, которое искажает данные указатели, и далее при вызове функций по этим указателям управление передается подготовленному коду. Приведем пример уязвимости указателей в виде следующего фрагмента программы на Си.



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


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


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


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


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


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



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


Внедрение кода 1
Внедрение параметров
Не требуется
Искажение адреса возврата из функции 2
Атака “срыв стека”
Атака “срыв стека” с параметризацией
Атака “срыв стека” с передачей управления
Искажение указателей функций
Атака на указатели функций
Атака на указатели функций с параметризацией
Атака на указатели функций с передачей управления
Искажение таблиц переходов
Атака на таблицы переходов
Атака на таблицы переходов с параметризацией
Атака на таблицы переходов с передачей управления
Искажение указателей данных
Атака с искажением указателей данных
Атака с искажением указателей данных с параметризацией
Атака с искажением указателей данных с оригинальным кодом

1 Столбцы – подготовка кода
2 Строки – цель переполнения

2. Защита

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


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

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

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

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

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

Системы обнаружения вторжения

С помощью систем обнаружения вторжения (СОВ) можно обнаружить и предотвратить попытки удалённого использования переполнения буфера. Так как в большинстве случаев данные, предназначенные для переполнения буфера, содержат длинные массивы инструкций No Operation (NOP или NOOP), СОВ просто блокирует все входящие пакеты, содержащие большое количество последовательных NOP-ов. Этот способ, в общем, неэффективен, так как такие массивы могут быть записаны с использованием большого разнообразия инструкций языка ассемблера. В последнее время крэкеры начали использовать коды оболочки с шифрованием, самомодифицирующимся кодом, полиморфным кодом и алфавитно-цифровым кодом а также атаки возврата в стандартную библиотеку для проникновения через эти СОВ.

Защита пространства исполняемого кода для UNIX-подобных систем

Защита пространства исполняемого кода может смягчить последствия переполнений буфера, делая большинство действий злоумышленников невозможными. Это достигается рандомизацией адресного пространства (ASLR) и/или запрещением одновременного доступа к памяти на запись и исполнение. Неисполняемый стек предотвращает большинство эксплойтов кода оболочки.
Существует два исправления для ядра Linux, которые обеспечивают эту защиту — PaX и exec-shield. Ни один из них ещё не включен в основную поставку ядра. OpenBSD с версии 3.3 включает систему, называемую W^X, которая также обеспечивает контроль исполняемого пространства.
Заметим, что этот способ защиты не предотвращает повреждение стека. Однако он часто предотвращает успешное выполнение «полезной нагрузки» эксплойта. Программа не будет способна вставить код оболочки в защищённую от записи память, такую как существующие сегменты исполняемого кода. Также будет невозможно выполнение инструкций в неисполняемой памяти, такой как стек или куча.
ASLR затрудняет для взломщика определение адресов функций в коде программы, с помощью которых он мог бы осуществить успешную атаку, и делает атаки типа ret2libc очень трудной задачей, хотя они всё ещё возможны в контролируемом окружении, или если атакующий правильно угадает нужный адрес.
Некоторые процессоры, такие как Sparc фирмы Sun, Efficeon фирмы Transmeta, и новейшие 64-битные процессоры фирм AMD и Intel предотвращают выполнение кода, расположенного в областях памяти, помеченных специальным битом NX. AMD называет своё решение NX (от англ. No eXecute), а Intel своё — XD (от англ. eXecute Disabled).

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

Решение, основанное на данном методе, получено благодаря проекту Synthetix. Цель Synthetix – специализация кода для увеличения производительности операционных систем. При этом вводится понятие так называемого квази-постоянства (Quasi-invariant), т.е. состояния среды, которое неизменно в определенных рамках. Такое квази-постоянство позволяет устранить ряд избыточного кода проверки выполнения различных условий. В рамках проекта реализован набор утилит, в том числе обеспечивающих контроль и защиту квази-постоянных состояний среды. К их числу относятся StackGuard и PointGuard. StackGuard предназначен для защиты от всех атак по переполнению буфера с изменением адреса возврата из функции и реализован в виде “заплаты” к gcc. Данная заплата изменяет пролог и эпилог всех функций с целью проверки целостности адреса возврата из функции при помощи так называемого “canary word”.
Измененный пролог каждой функции выполняет занесение в стек “canary word”, а эпилог проверку содержимого стека, занесенного ранее и, в случае, нарушения останавливает программу с предупреждающим сообщением. При атаке с искажением адреса возврата неизбежно произойдет искажение “canary word”, что и будет признаком нарушения целостности. Таким образом, целостность адреса возврата определяется целостностью “canary word”. В терминологии Synthetix, нарушение целостности является нарушением квази-постоянства среды. При известном значении “canary word” атакующий может организовать подмену адреса возврата без нарушения целостности. Поэтому “canary word” формируется StackGuard особым образом:
имеет значения 0, CR, LF, EOF, что не позволит провести атаку при переполнении буфера в библиотечных функциях Си, так как данные значения являются признаками конца строки;
имеет псевдослучайное значение, генерируемое при каждом запуске программы.
Авторами системы распространяется защищенная версия Red Hat Linux 5.1, скомпилированная при помощи StackGuard.
Продукт PointGuard предназначен для защиты от атак на указатели функций. Он также реализован в виде дополнения к компилятору gcc и осуществляет защиту путем помещения “canary word” перед каждым указателем функции и таблицей переходов. Существует ряд трудностей с реализацией данного алгоритма защиты: 1) размещение “canary word” должно выполнятся одновременно с выделением памяти под переменную, 2) инициализация одновременно с инициализацией переменной, 3) проверка целостности должна производится при каждом обращении к защищаемой переменной. Поэтому PointGuard ограничивается лишь статическими указателями на функции, которые не являются агрегативными. В дальнейшем авторы намерены реализовать полнофункциональную версию, которая будет оперировать указателями на функции различных видов. PointGuard не сможет защитить от атак с искажением указателей данных. Хотя для исключения этих видов атак, программисту предоставляется возможность самому создавать переменные, из специальных защищенных классов.
Проверки целостности StackGuard и PointGuard выполняются практически, не сказываясь на производительности защищаемых программ, в отличие от других систем, так как не отслеживают динамически каждое обращение к переменным, но функционируют по адаптивной схеме с проверкой сохранения квази-постоянства среды.
Механизм проверки целостности используется также другой системой защиты от атак по переполнению буфера – StackShield. StackShield реализован в виде процессора ассемблерного кода, генерируемого gcc и выполняет защиту от атак с искажением адреса возврата и указателей функций. Для предотвращения подмены адреса возврата в прологе каждой функции выполняется сохранение этого адреса во вторичном (дополнительном) стеке, а эпилог восстанавливает его значение. В случае переполнения буфера и искажения адреса возврата он будет восстановлен эпилогом без выдачи дополнительных сообщений, что впоследствии может привести к аварийному завершению. Атака с подменой указателей функций пресекается путем вставки специального кода перед каждой инструкцией вызова подпрограммы по указателю. Специальный код выполняет проверку того, в каком сегменте расположен адрес, вызываемой подпрограммы. Если это область данных или стека то программа завершается с ненулевым кодом ошибки. Однако, при такой схеме защиты встает проблема несовместимости с программами, которые содержат исполняемый код в области данных и стека. Защита StackShield также практически не сказывается на производительности программы.
Также следует отметить реализацию рассматриваемого метода для FreeBSD, которая выполнена в виде “заплат” с проверками целостности адреса возврата внутри базовой библиотеки libc. При этом защищаются только библиотечные функции, но не сама программа.


Рассмотренные методы противодействия атакам по переполнению буфера не выполняют полную автоматическую защиту от всех возможных атак описанных в таблице 1. Ряд атак с искажением указателей данных носит логический характер и не могут быть выявлены в автоматическом режиме. Как ни странно, самая первая атака по переполнению буфера в вирусе-черве Морриса носила именно такой характер. Программистам также следует обратить свой взор на языки, обеспечивающие проверку и сохранение типов, такие как Java и Паскаль, исключающие переполнение буфера. Однако, не следует забывать, что виртуальная машина Java написана на Си и, таким образом, может иметь уязвимости.
Многие языки программирования, например, Java и Lisp, управляют выделением памяти автоматически, и используют комбинацию статического анализа и проверки корректности действий программы во время выполнения. Это делает ошибки, связанные с переполнением буфера, маловероятными или невозможными. Perl для избежания переполнений буфера обеспечивает автоматическое изменение размера массивов. Однако, системы времени выполнения и библиотеки для таких языков всё равно могут быть подвержены переполнениям буфера, вследствие возможных внутренних ошибок в реализации этих систем проверки. В Windows доступны некоторые программные решения, которые предотвращают выполнение кода за пределами переполненного буфера, если такое переполнение было осуществлено. Среди этих решений — DEP в Windows XP SP2, OSsurance и Anti-Execute.


 
Файлов нет. [Показать файлы/форму]
Комментариев нет. [Показать комментарии/форму]