Чтение онлайн

ЖАНРЫ

Разработка приложений в среде Linux. Второе издание

Троан Эрик В.

Шрифт:

Возвращаясь к нашему примеру переполнения буфера, следует отметить, что для переменной

path
выделяется память на верхушке стека. Байт
path[0]
находится на самом верху, затем следующий байт —
path[1]
и так далее. Если наша программа-пример записывает в
path
более _
POSIX_PATH_MAX
байтов, то начинается перезапись остальных элементов стека. Если этот процесс продолжается, то происходит попытка записи за пределами верхушки стека, что вызывает ошибку сегментации.

Существенная проблема возникает, если программа записывает возвращаемый адрес вне пределов стека, но не порождает ошибку сегментации. Это позволяет изменять адрес, возвращаемый из работающей функции, на любой случайный адрес в памяти. Когда функция возвращает управление,

программа переходит к данному случайному адресу и продолжает выполнение с этой точки.

Реализации, использующие переполнение буфера, как правило, включают некоторый код в массив, записываемый в стек, и возвращаемый адрес устанавливается на этот код. Этот прием позволяет взломщику запускать любой произвольно выбранный код с теми правами доступа, которыми обладает атакуемая программа. Если эта программа является сетевым демоном, работающим как root, то любой удаленный пользователь получает доступ root к локальной системе!

Обработка строк — не единственное место, в котором встречается переполнение буфера (хотя, пожалуй, наиболее распространенное). Еще одним уязвимым моментом является чтение файлов. Файловые форматы нередко сохраняют размер элемента данных, за которым следуют сами данные. Если размер сохранения используется для выделения буфера, а конец поля данных определяется каким-то другим способом, может произойти ошибка переполнения буфера. Этот тип ошибки сделал возможным для Web-сайтов обращение к файлам, которые искажены так, чтобы предоставить удаленное пользование.

Чтение данных через сетевое соединение предоставляет еще одну возможность для переполнения буфера. Многие сетевые протоколы указывают максимальный размер для полей данных. Например, протокол ВООТР [160] фиксирует для всех пакетов размер 300 байтов. Это, однако, не мешает другой машине передать через сеть 350-байтовый пакет ВООТР. Если в сети работают программы с дефектами, то они попытаются скопировать этот нестандартный 350-байтовый пакет в пространство, выделенное для корректного 300-байтового пакета ВООТР, тем самым вызовут переполнение буфера.

160

ВООТР — это предшественник DHCP, позволяющий компьютерам автоматически узнавать свои IP-адреса при запуске сетевых интерфейсов.

Локализация и трансляция служат еще двумя побудителями переполнения буфера. Если программа написана для английского языка, то без сомнения для хранения названия месяца, загружаемого из таблицы, будет достаточно десятисимвольной строки. Когда эта программа переводится на испанский, "September" превращается в "Septiembre" и может произойти переполнение буфера. Всякий раз, когда программа поддерживает различные языки и локали, большинство первоначально статических строк становятся динамическими, и внутренние строковые буферы должны это учитывать.

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

_POSIX_PATH_MAX
?), и этим чрезвычайно легко воспользоваться.

Существует несколько приемов для устранения из кода возможности переполнения буфера. Хорошо продуманные программы используют множество способов для внимательного выделения буферов соответствующих размеров.

Лучшим способом распределения памяти для объектов является метод

malloc
, который устраняет проблемы, возникающие из-за перезаписывания возвращаемого адреса, поскольку
malloc
не выделяет память из стека. Аккуратное применение функции
strlen
для вычисления необходимого размера и динамическое выделение буфера в программной куче обеспечивает хорошую защиту от переполнения. К сожалению, при этом также расходуется память, поскольку каждый вызов метод
malloc
требует вызова метода
free
. В главе 7 обсуждалось несколько способов отслеживания ненужных расходов памяти, однако даже с описанными инструментами трудно точно знать, когда можно освободить память, занимаемую объектом. Особенно в том случае, если динамическое распределение памяти для объекта подстроено под уже существующий код. Функция
alloca
предлагает альтернативу
malloc
.

#include <alloca.h>

void * alloca(size_t size);

Подобно

malloc
,
alloca
выделяет область памяти длиной
size
байтов и возвращает указатель на начало этой области. Вместо использования памяти из программной кучи этот метод распределяет память из вершины стека, из того же места, где хранятся локальные переменные. Первое преимущество данной функции перед локальными переменными состоит в том, что необходимое количество байтов точно вычисляется в программе, а не определяется приблизительно. Превосходство над
malloc
заключается в том, что при завершении работы функции память освобождается автоматически. Все это позволяет охарактеризовать
alloca
как легкий способ распределения памяти, которая требуется только временно. До тех пор, пока размер буфера вычисляется должным образом (не забудьте учесть
'\0'
в конце каждой строки С!), можно не бояться переполнения буфера [161] .

161

Функция

alloca
не является стандартным средством языка С, однако компилятор gcc предоставляет
alloca
в большинстве поддерживаемых операционных систем. В более старых версиях gcc (до версии 3.3)
alloca
не всегда должным образом взаимодействовала с массивами динамических размеров (другое расширение GNU), поэтому примите во внимание, что использовать нужно только одну из версий.

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

strncpy
и
strncat
легко предотвращают перегрузки буфера при копировании строк.

#include <string.h>

char * strncpy (char * dest, const char * src, size_t max);

char * strncat (char * dest, const char * src, size_t max);

Обе функции ведут себя как их родственники, называемые аналогично,

strcpy
и
strcat
, но они возвращают за один раз только max байт, копируемые в строку назначения. Если достигнут предел, то результирующая строка не завершается
'\0'
, поэтому обычные строковые функции не смогут с ней работать. По этой причине необходимо явно завершить строку после вызова одной из подобных функций.

strncpy(dest, src, sizeof(dest));

dest[sizeof(dest) - 1] = '\0';

Частой ошибкой при использовании

strncat
является передача общего размера
dest
в качестве параметра
max
. Это приводит к потенциальному переполнению буфера, так как
strncat
добавляет до
max
байт в
dest
; она не прекращает копировать байты, когда общая длина
dest
достигает
max
байтов.

Несмотря на то что эти функции могут сделать выполнение программы некорректным при передаче длинных строк (из-за усечения этих строк), данный прием хорошо предотвращает перегрузки в буферах статических размеров. Во многих случаях это приемлемый компромисс (во всяком случае, при этом не произойдет ничего хуже того, что может случиться из-за переполнения буфера).

Функция

strncpy
решает проблему копирования строки в статический буфер без переполнения его. А функции
strdup
автоматически выделяют буфер, достаточный для хранения строки, до начала копирования в него исходной строки.

#include <string.h>

char * strdup(const char * src);

char * strdupa(const char * src);

char * strndup(const char * src, int max);

char * strndupa(const char * src, int max);

Поделиться с друзьями: