Одним из способов проникновения в компьютерные системы и нарушения их безопасности является использование различных уязвимостей программного обеспечения. Такие уязвимости могут принимать разнообразные формы: несовершенство протоколов, недостаточная обработка информации перед включением ее в запросы к базам данных и др. Особым типом программных уязвимостей является переполнение буфера. При переполнении буфера происходит выход за пределы выделенной памяти, и возможна перезапись важной управляющей информации: адресов возврата, указателей на функции и т. д., что может повлечь за собой выполнение уязвимой программой постороннего кода. В статье анализируются различные ситуации переполнения буфера, представлена общая информация по эксплуатации данного вида уязвимости.
Ключевые слова: компьютерная безопасность, переполнение буфера, эксплойты
Уязвимости переполнения буфера.
Одной из самых опасных уязвимостей компьютерных программ является уязвимость переполнения буфера. Этот тип уязвимости характерен для программ, написанных на таких языках программирования, как C, C++, Ассемблер. Данные языки позволяют программисту задавать фиксированные размеры выделяемых областей памяти. В случае ошибки в обработке данных существует возможность выхода за пределы границ буферов. Следствием такого переполнения может быть как ситуация отказа в обслуживании (DoS), так и выполнение произвольного кода уязвимой программой, что означает компрометацию системы, исполняющей эту программу.
Переполнение буфера в стеке.
Рассмотрим простейшую программу на языке C (рисунок 1).
Рис. 1. Уязвимая программа на языке C
Данная программа принимает на входе строку, которую затем отображает на экран с помощью определенной функции display. Функция display, принимающая в качестве параметра строковый указатель, содержит уязвимость. В начале данной функции определяется ограниченный по размеру буфер buf. Далее, путем вызова функции strcpy, в локальный буфер копируется строка, на которую указывает параметр функции display. В данном месте и содержится уязвимость. Дело в том, что библиотечная функция strcpy не проверяет размеры буфера-приемника, копирование строки при этом продолжается до тех пор, пока в строке-источнике не встретиться нулевой символ, обозначающий конец строки. Таким образом, возможна ситуация переполнения локального буфера, если строка-источник, указатель на которую поступает в функцию display, будет достаточной длины.
Рассмотрим, как выглядит переполнение со стороны отладчика. Снимки экрана с программой, запушенной под отладчиком Windbg, показаны на рисунках 2 и 3.
Рис. 2. Начало функции display и состояние стека в момент входа
Рис. 3. Состояние стека к моменту исполнения инструкции ret
На рисунке 2 (до команды отладчика dd esp) представлен дизассемблированный код начала функции display. Видно, что по адресу 0040134d происходит выполнение команды lea, которая в данном случае загружает в регистр eax указатель на область памяти в стеке. Далее этот указатель используется в качестве параметра строки-приемника функции strcpy, которая получает управление после исполнения инструкции call по адресу 00401353 (через прыжок по адресу 00401с00).
На рисунках 2 и 3 показаны два изображения состояния стековой области памяти, в момент входа в функцию display и перед самым выходом — исполнением инструкции ret. В данном случае программе передавалась строка, состоящая из 12h символов «A», четырех символов «B» и четырех символов «C». Хорошо видно, что важная управляющая информация — адрес возврата из функции — к моменту исполнения инструкции ret оказывается перезаписанной значением 43434343. Данное значение представляет собой четыре байта с ascii кодом символа «C». Таким образом, мы получаем здесь классическое переполнение буфера в стеке. Поскольку появляется возможность контролировать адрес возврата, то становится вероятным появление функциональности в уязвимой программе, которая не была запланирована ее разработчиком.
Переполнение буфера не в стеке.
Явление переполнение буфера может возникать не только в стековой памяти. Пусть у нас имеются некоторые переменные в программе (на языке C), которые объявлены как статические и не инициализированы. В том случае, если среди этих переменных есть буферы фиксированного размера, которые принимают входные данные без соответствующих проверок и объявляются перед некоторыми статическими указателями на функции, то возможно переполнение буфера в сегменте.bss. При перезаписи указателей на функции последующий их вызов способен привести к исполнению программой постороннего кода.
Другим типом уязвимости переполнения является переполнение буфера, выделенного в куче [1, с. 119]. Куча — специальная область памяти, которую программа способна запрашивать динамически во время выполнения. Кроме того, что сами блоки памяти могут содержать важную для программы информацию, в выделенных из кучи участках содержатся служебные данные, перезапись которых может спровоцировать в конечном итоге выполнение произвольного кода.
На некоторых системах [2, с. 291] при наличии переполнения используются особенности алгоритма управления памятью из кучи. Все свободные куски памяти (области, освобожденные с помощью функции free()) объединяются в двухсвязные списки. На рисунке 4 схематично показан фрагмент одного из таких списков.
Рис. 4. Двухсвязный список свободных блоков кучи
В заголовках свободных блоков в списке содержится информация о размере блока (size), размере предыдущего блока (prev_size) в случаях, если он свободный, указатель на следующий блок (fd), указатель на предыдущий блок (bk). Если при освобождении блока он граничит со свободным куском, то освобождаемый кусок будет слит с этим свободным куском. При этом возникает необходимость удаления свободного блока из двухсвязного списка, для этого функция free() вызывает макрос unlink(). Этот макрос имеет следующий вид:
#define unlink(P, BK, FD) {
FD = P->fd;
BK = P->bk;
FD->bk = BK;
BK->fd = FD;
}
Вначале в unlink() происходит извлечение указателей на следующий FD и предыдущий BK блоки из удаляемого из списка блока P. Далее, указателем предыдущего блока bk для последующего блока FD становится указатель на блок, предыдущий блоку P. Аналогично, указателем следующего блока fd для предыдущего блока BK становится указатель на блок, следующий за P блоком.
Когда возникает переполнение в буфере, то возможна перезапись указателей fd и bk произвольными значениями. Фактически это означает, что макрос unlink() способен при своем выполнении записать 4 произвольных байта по произвольному адресу. В качестве адреса выбирается какой-либо элемент таблицы GOT, соответствующий функции, которая будет в дальнейшем вызываться программой. После этого возможно исполнение постороннего кода программой.
Эксплуатация уязвимостей.
Рассмотренные выше ситуации используются специальными программами, которые называются эксплойты. В соответствии с [2, с. 221], эксплойт — это «программа, которая использует уязвимость в программном обеспечении для выполнения заранее подготовленного кода».
Эксплойты могут иметь различный вид в зависимости от конкретных условий уязвимости программы. В общем виде эксплойт можно представить как программу, подающую на вход уязвимой программы специально сформированные данные. Также, эксплойтом можно назвать сами такие данные. В этих данных можно выделить некоторую часть — назовем её триггер — то, благодаря чему путь программы меняется от заданного ее разработчиком, и программа начинает выполнять код, не предусмотренный ее изначальной функциональностью. Например, перезаписанный адрес возврата в стеке будет являться триггером — точкой, с которой исполнение программы отклониться от нормального. В свою очередь, то, что будет исполняться после срабатывания триггера — внедренный код. Его называют полезной нагрузкой, или шеллкод (если в результате исполнения кода вызывается командная оболочка системы).
В самом простейшем случае шеллкод может разместиться в буфере уязвимой программы. Однако, в настоящее время, если в результате срабатывания триггера исполнение передастся в область стека, практически наверняка шеллкод не сработает, а программа завершится аварийно. Дело в том, что разработчики защитных механизмов программ и операционных систем давно обратили внимание на проблему эксплуатации программных уязвимостей. В результате были разработаны и разрабатываются в настоящий момент разнообразные техники и методы для противодействия и детектирования использования уязвимостей программного обеспечения. Так, в системе Windows эксплойт с шеллкодом в стеке, на который передается управление сразу после срабатывания триггера, не сработает по причине наличия технологии DEP — Data Execution Prevention. Данная технология использует (в случае поддержки со стороны процессора) NX/XD биты элементов директорий и таблиц страниц [3, с. 751] и предотвращает выполнение кода в секции данных, стеке и куче.
Тем не менее, если эксплойт построен иным образом, возможен обход технологии DEP. Существует класс атак, основанных на использовании имеющегося в программе кода. К данным техникам можно отнести атаки типа ret-to-libc, возвратно-ориентированное программирование в целом. Эксплойт может иметь в своем составе часть, отвечающую за изменение атрибутов памяти (помечать ее как исполняемую) и копирование в эту область шеллкода. После скопированный код получит управление.
Для противодействия таким случаям существует другая технология — Address Space Layout Randomization — рандомизация на уровне расположения в адресном пространстве. В результате исполняемые образы получают возможность загружаться в адресное пространство процесса по случайным (в определенном смысле) адресам, что делает атаки на основе возвратно-ориентированного программирования практически невозможными с использованием этих модулей.
В целом, имеется множество различных деталей при эксплуатации уязвимостей повреждения памяти (переполнения буфера). В свою очередь, и технологии защиты не ограничиваются DEP и ASLR, могут принимать разнообразные формы — надстройки для компилятора, отдельные программы и др. Эксплойты, использующие уязвимости, представляют собой один из наиболее действенных способов компрометации безопасности компьютерных систем. Можно утверждать, что в настоящий момент вопросы эксплуатации уязвимостей и противодействия этому является актуальной областью компьютерной безопасности.
Литература:
1. Фостер Дж., Лю В. Разработка средств безопасности и эксплойтов / Пер. с англ. — М.: Издательство «Русская Редакция»; СПб.: Питер, 2007. — 432 стр.: ил.
2. Скляров И. Программирование боевого софта под Linux / И. Скляров. — СПб.: БХВ-Петербург, 2007. — 416 с.: ил.
3. Касперски К., Рокко Е. Искусство дизассемблирования / К. Касперски, Е. Рокко. — СПб.: БХВ-Петербург, 2008. — 891 с.: ил.