Очень часто программе необходимо временно сохранить данные во внешнем файле. Типичный пример - вставка записи в середину последовательно упорядоченного файла, что предполагает создание копии исходного файла во временном файле при добавлении новой информации. Затем, системный вызов unlink() удаляет исходный файл и rename() переносит временный файл на место исходного.
Открытие временного файла, если оно не сделано должным образом, часто становится начальным пунктом ситуации условия перехвата для злонамеренного пользователя. Дыры в безопасности, основанные на временных файлах, были недавно обнаружены в таких приложениях как Apache, Linuxconf, getty_ps, wu-ftpd, rdist, gpm, inn и т.д. Вспомним несколько принципов, которые помогают избежать такого рода проблем.
Как правило, создание временного файла происходит в директории /tmp. Это позволяет системному администратору знать, где хранятся данные, необходимые на короткий промежуток времени. Также это позволяет производить периодическую чистку ненужных данных (используя cron), а также использовать для этих файлов отдельный раздел, форматируемый при загрузке и т.д. Обычно администратор определяет местоположение для временных файлов в файлах <paths.h> и <stdio.h> в определении именованных констант _PATH_TMP и P_tmpdir. На самом деле, использование директории по умолчанию, отличной от /tmp, не очень удобно, так как это потребует перекомпиляции всех приложений, включая библиотеку C. Однако, напомним, что поведение подпрограммы из GlibC по отношению к директории с временными файлами может быть изменено используя переменную окружения TMPDIR. Поэтому пользователь может сделать так, чтобы временные файлы сохранялись в директории, принадлежащей ему, а не в /tmp. Это иногда необходимо, например, когда раздел, выделенный для /tmp, слишком мал, чтобы запустить приложение требующее много места для хранения временных данных.
Системная директория /tmp вещь специфическая из-за прав доступа:
$ ls -ld /tmp
drwxrwxrwt 7 root root 31744 Feb 14 09:47 /tmp
$
Sticky-Bit, представленный буквой t в конце или восьмеричным способом 01000, при применении к дериктории имеет специальное назначение: только владелец директории (root) и владелец файла, находящегося в этой директории, могут удалить этот файл. Директория имеет полные права для записи, любой пользователь может разместить тут свои файлы, будучи уверенным, что они защищены - как минимум до следующей чистки, устроенной сисадмином.
Тем не менее, использование директории для временного хранения данных может создать некоторые проблемы. Начнем с тривиального случая Set-UID root приложения, взаимодействующего с пользователем. Скажем, программы передачи почты. Если этот процесс получит сигнал, говорящий, что необходимо немедленно завершить работу, например, SIGTERM или SIGQUIT при останове системы, он попытается налету сохранить почту, уже написанную, но не отправленную. В старых версиях это делалось в /tmp/dead.letter. Поэтому, пользователю всего лишь надо было создать (так как он может писать в /tmp) для почтовой программы (выполняющейся с действующим UID-ом root-а) жесткую ссылку на /etc/passwd с именем dead.letter, чтобы записать в этот файл содержимое незаконченного письма (случайно содержащего строку "root::1:99999:::::").
Первая проблема такой работы программы - предсказуемость имени файла. Вы можете один раз понаблюдать за программой и вывести, что она будет использовать имя файла /tmp/dead.letter. Поэтому первый шаг - использовать имя файла, уникальное для каждого экземпляра программы. Существуют различные библиотечные функции, которые могут предоставить нам индивидуальное имя временного файла.
Предположим, у нас есть подобная функция, которая дает нам уникальное имя для нашего временного файла. Открытое программное обеспечение доступно вместе с исходным кодом (то же верно и для библиотеки C), и поэтому, имя файла предсказуемо, хотя это сделать намного сложнее. Атакующий может создать символическую ссылку с именем, предоставляемым библиотекой C. Наша первая реакция - проверить, существует ли файл перед его открытием. Наивно мы напишем что-нибудь вроде:
if ((fd = open (filename, O_RDWR)) != -1) {
fprintf (stderr, "%s уже существует\n", filename);
exit(EXIT_FAILURE);
}
fd = open (filename, O_RDWR | O_CREAT, 0644);
...
Очевидно, это типичная ситуация условия перехвата, где дыра в безопасности открывает простор для действий пользователю, преуспевшему в создании ссылки на /etc/passwd в промежуток между первым и вторым open(). Эти две операции должны быть сделаны слитно, чтобы невозможно было произвести подделку между ними. Это возможно, если использовать специальную опцию системного вызова open(). Она называется O_EXCL и используется вместе с O_CREAT. С этой опцией, open() неудачно завершается, если файл уже существует, причем проверка на существование слито с созданием.
Между прочим, расширение Gnu 'x' для режимов открытия в функции fopen() требует обязательного создания, функция неудачно завершается, если файл уже существует:
FILE * fp;
if ((fp = fopen (filename, "r+x")) == NULL) {
perror ("Невозможно создать файл.");
exit (EXIT_FAILURE);
}
Права доступа к временным файлам также весьма важная вещь. Если вы запишете тайную информацию в файл с режимом 644 (чтение/запись для владельца, чтение для остальных), может случится неприятность. Функция
позволяет определить права доступа к файлу в момент создания. Поэтому, после вызова umask(077), файл будет открыт с режимом 600 (чтение/запись для владельца, накаких прав другим).
Обычно создание временного файла происходит в три этапа:
создание уникального имени (случайного) ;
открытие файла, используя O_CREAT | O_EXCL с самыми ограниченными правами доступа;
проверка результата открытия файла с последующей соответствующей реакцией (повтор или выход).
Первая функция принимает аргумент NULL, в этом случае она возвращает адрес статического буфера. Его содержимое будет изменено при следующем вызове tmpnam(NULL). Если аргумент - строка, для которой выделена память, имя копируется в нее, что требует, чтобы строка состояла минимум из L-tmpnam байт. Будте осторожны с переполнением буфера! Страница man предупреждает о проблемах, когда функция используется с параметром NULL и определены _POSIX_THREADS или _POSIX_THREAD_SAFE_FUNCTIONS.
Функция tempnam() возвращает указатель на строку. Директория dir должна быть "подходящей" (страница man поясняет правильное значение слова "подходящая (suitable)"). Эта функция проверяет, что файл не существует, перед возвратом его имени. Однако, опять, страница man не рекомендует использовать dir, так как под "подходящей" директорией может подразумеваться разное в зависимости от реализации функции. Упомянем, что Gnome советует использовать функцию следующим образом:
char *filename;
int fd;
do {
filename = tempnam (NULL, "foo");
fd = open (filename, O_CREAT | O_EXCL | O_TRUNC | O_RDWR, 0600);
free (filename);
} while (fd == -1);
Цикл, использованный здесь, уменьшает опасность, но создает еще одну. Что случится, если раздел, где мы хотим создать временный файл заполнен, или система уже открыла максимально допустимое количество файлов...
Функция
#include <stdio.h>
FILE *tmpfile (void);
создает уникальное имя файла и открывает его. Этот файл автоматически удаляется при закрытии.
В GlibC-2.1.3 эта функция использует похожую технику как и в tmpnam() для создания имени файла, а затем открывает соответствующий дескриптор. После этого файл удаляется, однако Linux реально удаляет его, если он больше не используется никаким способом, то есть при освобождении дескриптора при помощи системного вызова close().
FILE * fp_tmp;
if ((fp_tmp = tmpfile()) == NULL) {
fprintf (stderr, "Невозможно создать временный файл\n");
exit (EXIT_FAILURE);
}
/* ... использование временного файла ... */
fclose (fp_tmp); /* реальное удаление из системы */
В простейших случаях нам не нужно изменять имя файла или передавать его другому процессу, только хранить в нем данные и перечитывать их во временной области. Поэтому нам не нужно знать имя временного файла, а только необходимо иметь доступ к содержимому. Функция tmpfile() именно это и делает.
Страница man никак не комментирует ее, однако Secure-Programs-HOWTO не рекомендует ее использование. По словам автора, спецификация на гарантирует создание файла, а мы не можем проверять каждую реализацию. Несмотря на это замечание, эта функция наиболее эффективна.
И последнее, функции
#include <stdlib.h>
char *mktemp(char *template);
int mkstemp(char *template);
создают уникальное имя по шаблону, который заканчивается "XXXXXX". Эти иксы заменяются, так чтобы получить уникальное имя файла.
В зависимости от версии, mktemp() заменяет первые пять 'X' на идентификатор процесса (PID)... что позволяет легко угадать это имя: только последние иксы случайные. Некоторые версии позволяют использование более шести иксов.
mkstemp() - рекомендованая функция в Secure-Programs-HOWTO. Вот метод использования:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
void failure(msg) {
fprintf(stderr, "%s\n", msg);
exit(1);
}
/*
* Создает временный файл и возвращает его.
* Эта процедура удаляет имя файла из файловой системы, поэтому
* оно не видно при просмотре директории.
*/
FILE *create_tempfile(char *temp_filename_pattern)
{
int temp_fd;
mode_t old_mode;
FILE *temp_file;
/* Создание файла с ограниченными правами доступа */
old_mode = umask(077);
temp_fd = mkstemp(temp_filename_pattern);
(void) umask(old_mode);
if (temp_fd == -1) {
failure("Невозможно открыть временный файл");
}
if (!(temp_file = fdopen(temp_fd, "w+b"))) {
failure("Невозможно создать дескриптор временного файла");
}
if (unlink(temp_filename_pattern) == -1) {
failure("Невозможно удалить временный файл");
}
return temp_file;
}
Все эти функции показывают проблемы, связанные с абстракцией и переносимостью. То есть, от функций стандартой библиотеки ожидается предоставление некоторых общих возможностей (абстракция)... однако способ их реализации различен в зависимости от системы (переносимость). Например, функция tmpfile() открывает временный файл по разному (в некоторых версиях не используется O_EXCL), или mkstemp() поддерживает различное количество иксов в зависимости от реализации.