Реализации, использующие переполнение буфера, как правило, включают некоторый код в массив, записываемый в стек, и возвращаемый адрес устанавливается на этот код. Этот прием позволяет взломщику запускать любой произвольно выбранный код с теми правами доступа, которыми обладает атакуемая программа. Если эта программа является сетевым демоном, работающим как root, то любой удаленный пользователь получает доступ root к локальной системе!
Обработка строк — не единственное место, в котором встречается переполнение буфера (хотя, пожалуй, наиболее распространенное). Еще одним уязвимым моментом является чтение файлов. Файловые форматы нередко сохраняют размер элемента данных, за которым следуют сами данные. Если размер сохранения используется для выделения буфера, а конец поля данных определяется каким-то другим способом, может произойти ошибка переполнения буфера. Этот тип ошибки сделал возможным для Web-сайтов обращение к файлам, которые искажены так, чтобы предоставить удаленное пользование.
Чтение данных через сетевое соединение предоставляет еще одну возможность для переполнения буфера. Многие сетевые протоколы указывают максимальный размер для полей данных. Например, протокол ВООТР[160] фиксирует для всех пакетов размер 300 байтов. Это, однако, не мешает другой машине передать через сеть 350-байтовый пакет ВООТР. Если в сети работают программы с дефектами, то они попытаются скопировать этот нестандартный 350-байтовый пакет в пространство, выделенное для корректного 300-байтового пакета ВООТР, тем самым вызовут переполнение буфера.
Локализация и трансляция служат еще двумя побудителями переполнения буфера. Если программа написана для английского языка, то без сомнения для хранения названия месяца, загружаемого из таблицы, будет достаточно десятисимвольной строки. Когда эта программа переводится на испанский, "September" превращается в "Septiembre" и может произойти переполнение буфера. Всякий раз, когда программа поддерживает различные языки и локали, большинство первоначально статических строк становятся динамическими, и внутренние строковые буферы должны это учитывать.
Теперь уже очевидно, что переполнение буфера представляет собой критическую проблему в системе безопасности. Ее очень легко упустить из виду во время программирования (в конце концов, кто должен волноваться о файловых именах, длина которых превышает _POSIX_PATH_MAX
?), и этим чрезвычайно легко воспользоваться.
Существует несколько приемов для устранения из кода возможности переполнения буфера. Хорошо продуманные программы используют множество способов для внимательного выделения буферов соответствующих размеров.
Лучшим способом распределения памяти для объектов является метод malloc()
, который устраняет проблемы, возникающие из-за перезаписывания возвращаемого адреса, поскольку malloc()
не выделяет память из стека. Аккуратное применение функции strlen()
для вычисления необходимого размера и динамическое выделение буфера в программной куче обеспечивает хорошую защиту от переполнения. К сожалению, при этом также расходуется память, поскольку каждый вызов метод malloc()
требует вызова метода free()
. В главе 7 обсуждалось несколько способов отслеживания ненужных расходов памяти, однако даже с описанными инструментами трудно точно знать, когда можно освободить память, занимаемую объектом. Особенно в том случае, если динамическое распределение памяти для объекта подстроено под уже существующий код. Функция alloca()
предлагает альтернативу malloc()
.
#include
void * alloca(size_t size);
Подобно malloc()
, alloca()
выделяет область памяти длиной size
байтов и возвращает указатель на начало этой области. Вместо использования памяти из программной кучи этот метод распределяет память из вершины стека, из того же места, где хранятся локальные переменные. Первое преимущество данной функции перед локальными переменными состоит в том, что необходимое количество байтов точно вычисляется в программе, а не определяется приблизительно. Превосходство над malloc()
заключается в том, что при завершении работы функции память освобождается автоматически. Все это позволяет охарактеризовать alloca()
как легкий способ распределения памяти, которая требуется только временно. До тех пор, пока размер буфера вычисляется должным образом (не забудьте учесть '\0'
в конце каждой строки С!), можно не бояться переполнения буфера[161].
Есть еще также несколько других функций, которые помогают избежать переполнения буфера. Библиотечные методы strncpy()
и strncat()
легко предотвращают перегрузки буфера при копировании строк.
#include
char * strncpy (char * dest, const char * src, size_t max);
char * strncat (char * dest, const char * src, size_t max);