Введение Переполнение буфера
Переполнение буфера, пожалуй, самый коварный тип атак. Переполнение буфера происходит, когда в область памяти помещается слишком много данных для выделенного пространства, и память переполняется. Это переполнение позволяет злоумышленнику выполнить свой собственный, специально созданный код. Код злоумышленника часто представляет собой руткит или другой шелл-код, позволяющий злоумышленнику удалённо управлять системой.
В этой статье я попытаюсь дать вам элементарное понимание очень сложной темы. Надеюсь, в будущем я смогу предложить более продвинутый курс по переполнению буфера здесь, на hackers-arise.com, где мы могли бы посвятить больше ресурсов только этой теме. Но здесь, в нескольких уроках, я хочу, чтобы вы хотя бы немного познакомились с:
1. Что такое переполнение буфера?
2. Риски переполнения буфера
3. Основной механизм переполнения буфера
4. Как использовать фаззинг для поиска переполнений буфера
5. Основная терминология и понятия в данной области
6. Инструменты, используемые для анализа и отладки кода
7. Написание простого переполнения буфера
8. Написание шелл-кода
Некоторые определения
Чтобы понять, что такое переполнение буфера, необходимо определить несколько терминов. Понимание переполнения буфера и разработка эксплойтов требуют более глубокого понимания внутренней архитектуры и работы процессора, его регистров памяти и того, как они обрабатывают инструкции и данные. Вот лишь некоторые термины, которые вам необходимо знать для успешного освоения этого модуля.
API – Интерфейс прикладного программирования
Код ассемблера – низкоуровневый язык программирования с несколькими очень простыми операциями.
Big Endian – старший байт сохраняется первым
Буфер – выделенная область памяти фиксированного размера
Байт-код – код, написанный на языке высокого уровня
Компилятор – программа, преобразующая язык высокого уровня в машинный код.
Отладчик — программное обеспечение, позволяющее нам отлаживать проблемы в нашем коде, либо подключаясь к среде выполнения приложения, либо запуская его на виртуальной машине.
Дизассемблер – программный инструмент для преобразования скомпилированных программ в машинный код.
DLL – программный компонент в системах Windows, содержащий функциональные возможности, используемые многими программами.
GDB – отладчик GNU (GDB) – фактический отладчик в системах Unix и Linux. Мы будем использовать его далее в модуле.
Куча – область памяти, выделяемая динамически
Интерпретатор — читает и выполняет программный код построчно, не сохраняя его для повторного использования. Интерпретаторы облегчают достижение платформенной независимости.
Little Endian – наименее значимый бит сохраняется первым. Системы x86 используют метод little endian.
Машинный язык – код, который может читать и понимать процессор
Malloc – вызов функции, которая выделяет n байтов в куче
Memset/Memcpy – memset – это функция, используемая для заполнения кучи указанным количеством байтов, в то время как memcpy копирует определенное количество байтов из одного буфера в другой.
printf – самая распространённая функция LIBC для вывода данных
Песочница — контролируемая среда для выполнения кода, не позволяющая коду влиять на внешние системы.
Шелл-код (shellcode) – традиционно байт-код, запускающий оболочку. Сейчас он имеет более широкое значение. В целом, он относится к любому коду, который выполняется для взлома системы.
Знаковые – целые числа со знаком имеют знаковый бит, обозначающий наличие знака. Данные могут быть как положительными, так и отрицательными.
Стек – область памяти, предназначенная для хранения временных данных.
strcpy/strncpy – обе эти функции LIBC имеют проблемы с безопасностью. strcpy копирует данные из одного буфера в другой без каких-либо ограничений по размеру, гарантируя возможность переполнения буфера. Функция strncpy добавляет параметр размера к strcpy, но может быть неправильно рассчитан при динамической генерации.
Беззнаковые – беззнаковые типы данных могут быть положительными или нулевыми. Отрицательные значения невозможны.
Теория памяти
Прежде чем углубляться в переполнение буфера, необходимо рассмотреть основы теории памяти. Знаю, знаю… теория вам не нужна, и я, как правило, стараюсь её избегать, но в данном случае она неизбежна. Без понимания теории практическое применение будет бессмысленным. Это как пытаться наладить сеть, не понимая TCP/IP.
Чтобы выполнить переполнение буфера, нам потребуется манипулировать памятью таким образом, чтобы заставить процессор выполнить наш код. Начнём с переполнения буфера в стеке, которое заключается в переполнении стека памяти и последующей перезаписи смежных со стеком областей памяти.
Сначала давайте рассмотрим, как организована память программы.
Стек растёт вниз. Куча растёт вверх. Текстовый сегмент содержит программный код и сегменты данных, содержащие глобальную информацию. Старшие адреса совместно используются стеком и кучей, которые система выделяет во время выполнения. Стек имеет фиксированный размер, тогда как куча — динамическая.
Процессоры Intel имеют регистры общего назначения, которые используются для хранения данных. К ним относятся:
EIP – указатель инструкций
ESP – указатель стека
EBP – базовый указатель
ESI – индекс источника
EDI – индекс назначения
EAX – аккумулятор
EBX – база
ECX – счетчик
EDX – данные
Все эти регистры будут важны для нашего анализа, но ESP, EBP и EIP имеют решающее значение для понимания и выполнения переполнения буфера.
Анатомия переполнения буфера
ESP указывает на вершину стека (на схеме ниже это зелёная область вверху) по самому низкому
адресу памяти, а EBP — на самый высокий адрес внизу стека (зелёная область в нижней части схемы справа). EIP содержит адрес следующей инструкции, которую должен выполнить процессор (ниже EBP). Чтобы выполнить переполнение буфера, нам нужно манипулировать EIP и заставить его указывать на наш вредоносный код.
Стек представляет собой структуру FIFO. Для добавления данных в любой стек необходимо использовать инструкцию PUSH в ассемблерном коде. PUSH помещает следующий элемент данных в стек. Данные в стеке хранятся сверху вниз, поэтому новые данные добавляются наверх, а все существующие данные перемещаются вниз. Затем ESP (указатель стека) перемещается на меньший адрес памяти.
При запуске программы создаётся стековый фрейм с локальными переменными, который помещается на вершину стека с помощью команды PUSH. Ключ к атаке с переполнением буфера — получить доступ к EIP (красная область на схеме) или адресу возврата. Получив к нему доступ, мы можем указать на наш вредоносный код.
При вводе данных они попадают в стек в обратном порядке. Стек растёт вниз, к EBP. Если нам удастся успешно записать в стек больше данных, выделенных для него, мы, возможно, сможем продвинуться мимо EBP в EIP (красный), перезаписав EIP и указав на наш код.
Затем EIP укажет на наш код и начнёт его выполнение. Вуаля! Мы выполнили переполнение буфера и получили контроль над системой.
Конечно, это слишком упрощённая концепция переполнения буфера, но с чего-то же надо начинать. В новых операционных системах используются новые методы для ограничения атак на переполнение буфера (хотя они всё ещё возможны), такие как ASLR (рандомизация адресного пространства) и DEP (предотвращение выполнения данных). В этом курсе мы будем считать, что ASLR и DEP отключены или никогда не были реализованы, и выделим переполнения буфера в таких средах в специальный курс, который я надеюсь вскоре здесь представить.
Спасибо Марку Карвалью за прекрасную графику, иллюстрирующую переполнение буфера в стеке.
Угроза переполнения буфера
При исследовании безопасности вы часто будете встречать упоминания об «удалённом выполнении кода» или «произвольном выполнении кода». Практически в каждом случае речь идёт об уязвимости, основанной на переполнении буфера того или иного типа. Например, если обратиться к бюллетеням безопасности Microsoft на Technet, можно найти множество бюллетеней безопасности с предупреждениями об «удалённом выполнении кода». Это почти наверняка переполнение буфера. Хотя в последние годы таких бюллетеней безопасности становится всё меньше, они всё ещё появляются довольно регулярно и почти всегда оцениваются Microsoft как критические.
Давайте перейдём на сайт www.securityfocus.com. Security Focus, пожалуй, моя любимая база данных об уязвимостях, просто потому, что информация хорошо структурирована и охватывает все типы уязвимостей и поставщиков программного обеспечения.
Среди наиболее проблемных программных продуктов последних лет с точки зрения переполнения буфера оказался Adobe Flash Player. Похоже, что в Flash Player почти каждый день обнаруживаются новые уязвимости. В тот день, когда я зашёл на сайт www.securityfocus.com и выбрал Adobe в качестве поставщика программного обеспечения, а Flash Player в качестве заголовка, я обнаружил следующие уязвимости, представленные ниже. В этот день, 7 мая 2015 года, было обнаружено четыре (4) новых уязвимости переполнения буфера. Видно, что названия каждой из них немного отличаются: от «Уязвимости удалённого выполнения кода» до «Уязвимости переполнения буфера» и «Уязвимости переполнения буфера на основе кучи», но все они являются переполнениями буфера. Все они очень опасны и попадают в ту критическую категорию, которую мы рассматриваем в этом модуле.
Далее
В следующих нескольких статьях мы воспользуемся фаззерами для обнаружения переполнений буфера, а затем разработаем несколько базовых переполнений буфера на архитектуре Intel x86, чтобы продемонстрировать принципы и коварство этого типа уязвимостей.
Переполнение буфера, пожалуй, самый коварный тип атак. Переполнение буфера происходит, когда в область памяти помещается слишком много данных для выделенного пространства, и память переполняется. Это переполнение позволяет злоумышленнику выполнить свой собственный, специально созданный код. Код злоумышленника часто представляет собой руткит или другой шелл-код, позволяющий злоумышленнику удалённо управлять системой.
В этой статье я попытаюсь дать вам элементарное понимание очень сложной темы. Надеюсь, в будущем я смогу предложить более продвинутый курс по переполнению буфера здесь, на hackers-arise.com, где мы могли бы посвятить больше ресурсов только этой теме. Но здесь, в нескольких уроках, я хочу, чтобы вы хотя бы немного познакомились с:
1. Что такое переполнение буфера?
2. Риски переполнения буфера
3. Основной механизм переполнения буфера
4. Как использовать фаззинг для поиска переполнений буфера
5. Основная терминология и понятия в данной области
6. Инструменты, используемые для анализа и отладки кода
7. Написание простого переполнения буфера
8. Написание шелл-кода
Некоторые определения
Чтобы понять, что такое переполнение буфера, необходимо определить несколько терминов. Понимание переполнения буфера и разработка эксплойтов требуют более глубокого понимания внутренней архитектуры и работы процессора, его регистров памяти и того, как они обрабатывают инструкции и данные. Вот лишь некоторые термины, которые вам необходимо знать для успешного освоения этого модуля.
API – Интерфейс прикладного программирования
Код ассемблера – низкоуровневый язык программирования с несколькими очень простыми операциями.
Big Endian – старший байт сохраняется первым
Буфер – выделенная область памяти фиксированного размера
Байт-код – код, написанный на языке высокого уровня
Компилятор – программа, преобразующая язык высокого уровня в машинный код.
Отладчик — программное обеспечение, позволяющее нам отлаживать проблемы в нашем коде, либо подключаясь к среде выполнения приложения, либо запуская его на виртуальной машине.
Дизассемблер – программный инструмент для преобразования скомпилированных программ в машинный код.
DLL – программный компонент в системах Windows, содержащий функциональные возможности, используемые многими программами.
GDB – отладчик GNU (GDB) – фактический отладчик в системах Unix и Linux. Мы будем использовать его далее в модуле.
Куча – область памяти, выделяемая динамически
Интерпретатор — читает и выполняет программный код построчно, не сохраняя его для повторного использования. Интерпретаторы облегчают достижение платформенной независимости.
Little Endian – наименее значимый бит сохраняется первым. Системы x86 используют метод little endian.
Машинный язык – код, который может читать и понимать процессор
Malloc – вызов функции, которая выделяет n байтов в куче
Memset/Memcpy – memset – это функция, используемая для заполнения кучи указанным количеством байтов, в то время как memcpy копирует определенное количество байтов из одного буфера в другой.
printf – самая распространённая функция LIBC для вывода данных
Песочница — контролируемая среда для выполнения кода, не позволяющая коду влиять на внешние системы.
Шелл-код (shellcode) – традиционно байт-код, запускающий оболочку. Сейчас он имеет более широкое значение. В целом, он относится к любому коду, который выполняется для взлома системы.
Знаковые – целые числа со знаком имеют знаковый бит, обозначающий наличие знака. Данные могут быть как положительными, так и отрицательными.
Стек – область памяти, предназначенная для хранения временных данных.
strcpy/strncpy – обе эти функции LIBC имеют проблемы с безопасностью. strcpy копирует данные из одного буфера в другой без каких-либо ограничений по размеру, гарантируя возможность переполнения буфера. Функция strncpy добавляет параметр размера к strcpy, но может быть неправильно рассчитан при динамической генерации.
Беззнаковые – беззнаковые типы данных могут быть положительными или нулевыми. Отрицательные значения невозможны.
Теория памяти
Прежде чем углубляться в переполнение буфера, необходимо рассмотреть основы теории памяти. Знаю, знаю… теория вам не нужна, и я, как правило, стараюсь её избегать, но в данном случае она неизбежна. Без понимания теории практическое применение будет бессмысленным. Это как пытаться наладить сеть, не понимая TCP/IP.
Чтобы выполнить переполнение буфера, нам потребуется манипулировать памятью таким образом, чтобы заставить процессор выполнить наш код. Начнём с переполнения буфера в стеке, которое заключается в переполнении стека памяти и последующей перезаписи смежных со стеком областей памяти.
Сначала давайте рассмотрим, как организована память программы.
Стек растёт вниз. Куча растёт вверх. Текстовый сегмент содержит программный код и сегменты данных, содержащие глобальную информацию. Старшие адреса совместно используются стеком и кучей, которые система выделяет во время выполнения. Стек имеет фиксированный размер, тогда как куча — динамическая.
Процессоры Intel имеют регистры общего назначения, которые используются для хранения данных. К ним относятся:
EIP – указатель инструкций
ESP – указатель стека
EBP – базовый указатель
ESI – индекс источника
EDI – индекс назначения
EAX – аккумулятор
EBX – база
ECX – счетчик
EDX – данные
Все эти регистры будут важны для нашего анализа, но ESP, EBP и EIP имеют решающее значение для понимания и выполнения переполнения буфера.
Анатомия переполнения буфера
ESP указывает на вершину стека (на схеме ниже это зелёная область вверху) по самому низкому
адресу памяти, а EBP — на самый высокий адрес внизу стека (зелёная область в нижней части схемы справа). EIP содержит адрес следующей инструкции, которую должен выполнить процессор (ниже EBP). Чтобы выполнить переполнение буфера, нам нужно манипулировать EIP и заставить его указывать на наш вредоносный код.
При запуске программы создаётся стековый фрейм с локальными переменными, который помещается на вершину стека с помощью команды PUSH. Ключ к атаке с переполнением буфера — получить доступ к EIP (красная область на схеме) или адресу возврата. Получив к нему доступ, мы можем указать на наш вредоносный код.
При вводе данных они попадают в стек в обратном порядке. Стек растёт вниз, к EBP. Если нам удастся успешно записать в стек больше данных, выделенных для него, мы, возможно, сможем продвинуться мимо EBP в EIP (красный), перезаписав EIP и указав на наш код.
Затем EIP укажет на наш код и начнёт его выполнение. Вуаля! Мы выполнили переполнение буфера и получили контроль над системой.
Конечно, это слишком упрощённая концепция переполнения буфера, но с чего-то же надо начинать. В новых операционных системах используются новые методы для ограничения атак на переполнение буфера (хотя они всё ещё возможны), такие как ASLR (рандомизация адресного пространства) и DEP (предотвращение выполнения данных). В этом курсе мы будем считать, что ASLR и DEP отключены или никогда не были реализованы, и выделим переполнения буфера в таких средах в специальный курс, который я надеюсь вскоре здесь представить.
Спасибо Марку Карвалью за прекрасную графику, иллюстрирующую переполнение буфера в стеке.
Угроза переполнения буфера
При исследовании безопасности вы часто будете встречать упоминания об «удалённом выполнении кода» или «произвольном выполнении кода». Практически в каждом случае речь идёт об уязвимости, основанной на переполнении буфера того или иного типа. Например, если обратиться к бюллетеням безопасности Microsoft на Technet, можно найти множество бюллетеней безопасности с предупреждениями об «удалённом выполнении кода». Это почти наверняка переполнение буфера. Хотя в последние годы таких бюллетеней безопасности становится всё меньше, они всё ещё появляются довольно регулярно и почти всегда оцениваются Microsoft как критические.
Давайте перейдём на сайт www.securityfocus.com. Security Focus, пожалуй, моя любимая база данных об уязвимостях, просто потому, что информация хорошо структурирована и охватывает все типы уязвимостей и поставщиков программного обеспечения.
Среди наиболее проблемных программных продуктов последних лет с точки зрения переполнения буфера оказался Adobe Flash Player. Похоже, что в Flash Player почти каждый день обнаруживаются новые уязвимости. В тот день, когда я зашёл на сайт www.securityfocus.com и выбрал Adobe в качестве поставщика программного обеспечения, а Flash Player в качестве заголовка, я обнаружил следующие уязвимости, представленные ниже. В этот день, 7 мая 2015 года, было обнаружено четыре (4) новых уязвимости переполнения буфера. Видно, что названия каждой из них немного отличаются: от «Уязвимости удалённого выполнения кода» до «Уязвимости переполнения буфера» и «Уязвимости переполнения буфера на основе кучи», но все они являются переполнениями буфера. Все они очень опасны и попадают в ту критическую категорию, которую мы рассматриваем в этом модуле.
Далее
В следующих нескольких статьях мы воспользуемся фаззерами для обнаружения переполнений буфера, а затем разработаем несколько базовых переполнений буфера на архитектуре Intel x86, чтобы продемонстрировать принципы и коварство этого типа уязвимостей.