Анализ атак переполнения буфера
Что вызывает состояние переполнения буфера? Вообще говоря, переполнение буфера происходит всякий раз, когда программа записывает в буфер больше информации, чем выделено в памяти. Это позволяет злоумышленнику перезаписать данные, которые контролируют путь выполнения программы, и перехватить управление программой для выполнения кода злоумышленника вместо кода процесса. Для тех, кому интересно посмотреть, как это работает, мы сейчас попытаемся более подробно рассмотреть механизм этой атаки, а также наметить некоторые превентивные меры.
Из опыта мы знаем, что многие слышали об этих атаках, но мало кто действительно понимает их механику. Другие имеют смутное представление или вообще не имеют представления о том, что такое атака с переполнением буфера. Есть и те, кто считает эту проблему подпадающей под категорию тайных премудростей и умений, доступных лишь узкому кругу специалистов. Однако это не что иное, как проблема уязвимости, созданная нерадивыми программистами.
Программы, написанные на языке C, в которых больше внимания уделяется эффективности программирования и длине кода, чем аспектам безопасности, наиболее подвержены этому типу атак. На самом деле, с точки зрения программирования, язык C считается очень гибким и мощным, но кажется, что хотя этот инструмент является активом, он может стать головной болью для многих начинающих программистов. Достаточно упомянуть вызов на основе указателя в режиме прямой ссылки на память или подход с использованием текстовой строки. Последнее подразумевает ситуацию, когда даже среди библиотечных функций, работающих с текстовыми строками, действительно есть такие, которые не могут контролировать длину реального буфера, тем самым становясь восприимчивыми к переполнению объявленной длины.
Прежде чем приступить к дальнейшему анализу механизма развития атаки, давайте ознакомимся с некоторыми техническими аспектами, касающимися выполнения программ и функций управления памятью.
Память процесса
Когда программа выполняется, ее различные единицы компиляции отображаются в памяти хорошо структурированным образом. На рис. 1 представлена схема памяти.
сегмент содержит в основном программный код, т. е. ряд исполняемых программных инструкций. Следующий сегмент представляет собой область памяти, содержащую как инициализированные, так и неинициализированные глобальные данные. Его размер предоставляется во время компиляции. Если углубиться в структуру памяти в направлении более высоких адресов, у нас есть часть, совместно используемая стеком и , которые, в свою очередь, выделяются во время выполнения. используется для хранения аргументов вызова функции, локальных переменных и значений выбранных регистров, что позволяет получить состояние программы. содержит динамические переменные. Для выделения памяти куча использует функцию или оператор.
Для чего используется стек?
Стек работает по модели LIFO (Last In First Out). Поскольку пространство в стеке выделяется на время жизни функции, там могут находиться только те данные, которые активны в течение этого времени жизни. Только этот тип структуры является результатом сущности структурного подхода к программированию, когда код разбивается на множество разделов кода, называемых функциями или процедурами. Когда программа работает в памяти, она последовательно вызывает каждую отдельную процедуру, очень часто беря одну из другой, тем самым создавая многоуровневую цепочку вызовов. По завершении процедуры требуется, чтобы программа продолжила выполнение, обработав инструкцию, следующую сразу за инструкцией CALL. Кроме того, поскольку вызывающая функция не была завершена, все ее локальные переменные, параметры и состояние выполнения необходимо «заморозить», чтобы позволить оставшейся части программы возобновить выполнение сразу после вызова. Реализация такого стека гарантирует, что описанное здесь поведение будет точно таким же.
Вызовы функций
Программа работает, последовательно выполняя инструкции процессора. Для этой цели ЦП имеет расширенный счетчик команд (регистр EIP) для поддержания порядка последовательности. Он контролирует выполнение программы, указывая адрес следующей выполняемой инструкции. Например, выполнение перехода или вызов функции приводит к соответствующей модификации указанного регистра. Предположим, что EIP вызывает сам себя по адресу своего собственного раздела кода и продолжает выполнение. Что произойдет тогда?
При вызове процедуры в стек помещается адрес возврата для вызова функции, который необходим программе для возобновления выполнения. Если посмотреть на это с точки зрения злоумышленника, это ключевая ситуация. Если злоумышленнику каким-то образом удалось перезаписать адрес возврата, хранящийся в стеке, после завершения процедуры он будет загружен в регистр EIP, что потенциально позволит выполнить любой код переполнения вместо кода процесса, возникающего в результате нормального поведения процедуры. программа. Мы можем увидеть, как ведет себя стек после выполнения кода листинга 1.
Листинг1
пустота f (int a, int b)
{
символ buf[10];
// <– здесь просматривается стек
}
пустая функция()
{
ф(1, 2);
}
После ввода функции f() стек выглядит так, как показано на рисунке 2.
Во-первых, аргументы функции перемещаются в стеке назад (в соответствии с правилами языка C), после чего следует адрес возврата. С этого момента функция использует адрес возврата, чтобы использовать его. помещает текущее содержимое EBP (EBP будет обсуждаться ниже), а затем выделяет часть стека под свои локальные переменные. Стоит обратить внимание на две вещи. Во-первых, стек растет вниз в памяти по мере его увеличения. Это важно помнить, потому что такое утверждение:
саб сп, 08h
Это приводит к увеличению стека, что может показаться запутанным. На самом деле, чем больше ESP, тем меньше размер стека и наоборот. Очевидный парадокс.
Во-вторых, в стек помещаются целые 32-битные слова. Следовательно, 10-символьный массив действительно занимает три полных слова, т. е. 12 байтов.
Как работает стек?
Есть два регистра ЦП, которые имеют жизненно важное значение для функционирования стека и содержат информацию, необходимую при вызове данных, находящихся в памяти. Их имена ESP и EBP. ESP (указатель стека) содержит адрес верхнего стека. ESP поддается изменению и может быть изменена прямо или косвенно. Напрямую — так как здесь выполняются прямые операции, например, добавить esp, 08h. Это вызывает сжатие стека на 8 байт (2 слова). Косвенно – путем добавления/удаления элементов данных в/из стека при каждой последующей операции PUSH или POP стека. Регистр EBP — это базовый (статический) регистр, указывающий на дно стека. Точнее, содержит адрес дна стека как смещение относительно исполняемой процедуры. Каждый раз, когда вызывается новая процедура, старое значение EBP первым помещается в стек, а затем новое значение ESP перемещается в EBP. Это новое значение ESP, хранящееся в EBP, становится базой ссылок на локальные переменные, необходимые для извлечения раздела стека, выделенного для вызова функции {1}.
Поскольку ESP указывает на вершину стека, он часто изменяется во время выполнения программы, и использование его в качестве ссылочного регистра смещения очень громоздко. Вот почему EBP используется в этой роли.
Угроза
Как распознать, где может произойти приступ? Мы просто знаем, что адрес возврата хранится в стеке. Кроме того, данные обрабатываются в стеке. Позже мы узнаем, что происходит с обратным адресом, если мы рассмотрим комбинацию, при определенных обстоятельствах, обоих фактов. Имея это в виду, давайте попробуем использовать этот простой пример приложения, используя листинг 2.
Листинг 2
#включают
char *code = «AAAABBBBBCCCCDDD»; //включая символ '