Функция sprintf() также входит в число тех, которые часто вызывают переполнение буфера. Так же как strcat() и strcpy(), функция sprintf() имеет разновидность, позволяющую облегчить защиту от перегрузок.
#include <stdio.h>
int snprintf(char * str, size_t max, char * format, ...);
Попытки определить размер буфера, необходимый для sprintf(), могут оказаться слишком сложными. Он зависит от таких элементов, как значения всех форматируемых чисел (для которых могут быть нужны или не нужны знаки чисел), используемые аргументы форматирования и длины всех строк, которые были затронуты форматированием. Для того чтобы избежать переполнения буфера, функция snprintf() помещает в str не более чем max символов, включая замыкающий ' '. В отличие от strcat() и strncat(), функция snprintf() корректно завершает строку, при необходимости пренебрегая символом из форматируемой строки. Она возвращает количество символов, которые будет занимать конечная строка при наличии доступного пространства. Также сообщается, нужно ли усекать строку до max символов (не считая последний ' ')[162]. Если возвращаемое значение меньше чем max, значит, функция успешно завершила свою работу. Если же равно или больше, значит, предел max превышен.
Функция vsprintf() несет те же проблемы, a vsnprintf() предлагает способ их преодоления.
22.3.2. Разбор имен файлов
Абсолютно обычным действием для привилегированных приложений является предоставление доступа к файлам ненадежным пользователям и разрешение этим пользователям передавать имена файлов, к которым необходим доступ. Хорошим примером служит Web-сервер. URL-адрес HTTP содержит имя файла, полученное сервером как запрос на передачу удаленному (ненадежному) пользователю. На Web-сервере необходимо убедиться, что возвращаемый файл — это именно тот, который был сконфигурирован на отправку, а также внимательно проверить правильность имен файлов.
Представьте Web-сервер, обслуживающий файлы из home/httpd/html, выполняющий это посредством простого добавления имени файла из URL, который требуется предоставить, концу /home/httpd/html. Такой процесс дает правильный файл, однако это также позволяет удаленным пользователям увидеть любой файл системы, к которой Web-сервер имеет доступ, просто запросив, к примеру, файл ../../.. /etc/passwd. Подобные каталоги .. необходимо явно проверять и отклонять. Системный вызов chroot() предоставляет хороший способ, позволяющий сделать обработку имен файлов в программах более простой.
Если имена файлов передаются в другие программы, то необходима еще более тщательная проверка. Например, если в имени файла используется начальный символ -, то весьма вероятно, что другая программа интерпретирует его как опцию командной строки.
22.3.3. Переменные окружения
В программах, работающих с возможностями setuid или setgid, нужно проявлять особую осторожность с установками окружения. Эти переменные определяются пользователем, активизировавшим программу, тем самым открывается путь для атак. Самая явная атака может пройти через переменную окружения PATH, изменяющую те каталоги, в которых функции execlp() и execvp() отыскивают программы. Если привилегированная программа запускает другие программы, то она должна убедиться, что это именно те программы, которые нужны! Пользователь, который имеет возможность подменить программный путь поиска, легко может подвергнуть программу опасности.
Существуют и другие переменные окружения, которые могут оказаться опасными. Например, переменная LD_PRELOAD позволяет пользователю указать некоторую библиотеку для загрузки до стандартной библиотеки С. Это может быть полезным, но одновременно и очень опасным в привилегированных приложениях (по этой же причине переменная окружения игнорируется, если реальное и эффективное универсальные имена совпадают).
Если программа локализована, то переменная NLSPATH также становится проблемной. Она позволяет пользователю переключать используемый программой языковой каталог, который определяет способ перевода строк. Это означает, что в переводных программах пользователь имеет возможность указать значение для любой переводимой строки. Строку можно сделать сколько угодно длинной, вынуждая программу быть крайне осторожной при выделении буфера. Еще более опасным является то, что при переводе форматирующих строк для таких функций, как printf(), можно изменить формат. Например, строка Hello World, today is %s может превратиться в Hello World, today is %c%d%s. Трудно предсказать, какое воздействие могут оказать подобные изменения на функционирование программы!
Все это сводится к тому, что наилучшим решением для переменных окружения setuid- или setgid-программ является исключение этих переменных. Функция clearenv()[163] стирает все значения из окружения, оставляя его пустым. После этого программа может заполнить любые необходимые ей переменные окружения известными значениями.
22.3.4. Запуск командной оболочки
Запуск системной командной оболочки из любой программы, в которой важен вопрос безопасности, является плохой идеей. При этом защита от тех проблем, которые мы уже обсуждали, становится еще более трудной.
Каждую строку, передаваемую в оболочку, необходимо очень тщательно проверять на достоверность. К примеру, символ 'n' или ;, вставленный в строку, может привести к тому, что оболочка примет две команды вместо одной. Если строка содержит символы ` или последовательность $(), оболочка запускает другую программу для построения аргумента командной строки. Может также иметь место обычное расширение оболочки, при этом переменные окружения и универсализация файловых имен становятся доступными для взломщиков. Переменная IFS позволяет указать символы (отличные от пробела и табуляции) для разделения полей при анализе командных строк при помощи символов, тем самым, открывая новые бреши для атак. Другие специальные символы, такие как <, > и |, предоставляют еще больший простор для построения командных строк, которые ведут себя не так, как подразумевает программа.
Очень трудно выполнить полную проверку всех этих возможностей. Наилучшим способом предотвращения всех возможных атак против командной оболочки служит в первую очередь уклонение от ее запуска. Функции вроде pipe(), fork(), exec() и glob() позволяют достаточно легко выполнять большинство тех задач, для которых обычно используется оболочка. При этом проблемы расширения командной строки оболочки не возникают.
22.3.5. Создание временных файлов
Довольно часто в программах применяются временные файлы. Система Linux даже предусматривает для этой цели особые каталоги (/tmp и /var/tmp). К сожалению, использование временных файлов в безопасном режиме — дело очень ненадежное. Лучшим решением будет создание временных файлов в каталоге, который доступен только через эффективный uid программы. Неплохим выбором, например, может стать домашний каталог данного пользователя. При таком подходе употребление временных файлов становится простым и безопасным. Однако большинство программистов не любят этот способ, так как он загромождает каталоги, причем вполне возможно, что эти файлы никогда не будут удалены, если программа неожиданно выйдет из строя.
Давайте представим программу, активизированную пользователем root, которая создает основной сценарий во временном файле и затем запускает его. Для разрешения одновременного запуска нескольких экземпляров программы, возможно, сценарий включает программный идентификатор как часть имени файла и создает файл со следующим кодом:
char fn[200];
int fd;
sprintf(fn, "/tmp/myprogram.%d", getpid());
fd = open(fn, O_CREAT | O_RDWR | O_TRUNC, 0600);
Программа создает уникальное имя файла и усекает любой существующий файл с таким именем перед записью в него. Хотя на первый взгляд этот способ может показаться рациональным, фактически им легко воспользоваться для атак. Если файл, который программа пытается создать, уже существует как символическая ссылка, то открытый запрос следует по такой ссылке и открывает произвольный указываемый файл. Первым примером эксплуатации в такой ситуации является создание символических ссылок в /tmp с использованием многих (или всех) возможных программных идентификаторов, указывающих на файл типа /etc/passwd. При запуске данной программы это приводит к перезаписыванию системного файла паролей, результатом чего становится атака отказа в обслуживании.
Еще более опасной является атака, при которой символические ссылки указывают на собственный файл взломщика (или когда в /tmp создаются нормальные файлы со всеми возможными именами). При открытии файла целевой файл искажается, но во временной промежуток между открытием файла и выполнением программы атакующий (который все еще владеет файлом) может записать в него все, что угодно (добавление строки типа chmod u+s /bin/sh определенно будет полезным в основном сценарии, работающим как root!). Может показаться трудным точно угадать время, однако, режимы состязаний такого типа часто эксплуатируются, подвергая риску безопасность программы. Если программа была setuid, а не запущенная как root, то эксплуатация фактически становится еще легче, так как пользователь может передать SIGSTOP в программу сразу после открытия файла, а затем после эксплуатации этого режима состязаний послать SIGCONT.