[LinuxFocus-icon]
Домой  |  Карта  |  Индекс  |  Поиск

Новости | Архивы | Ссылки | Про LF
[an error occurred while processing this directive]
[image of the authors]
автор Frйdйric Raynal, Christophe Blaess, Christophe Grenier
<pappy(_at_)users.sourceforge.net, ccb(_at_)club-internet.fr, grenier(_at_)nef.esiea.fr>

Об авторе:

Christophe Blaess - независимый инженер по аэронавтике. Он почитатель Linux и делает большую часть своей работы на этой системе. Заведует координацией переводов man страниц, публикуемых Linux Documentation Project.

Christophe Grenier - студент 5 курса в ESIEA, где он также работает сисадмином. Страстно увлекается компьютерной безопасностью.

Frйdйric Raynal много лет использует Linux, потому что он не загрязняет окружающую среду, не использует ни гармоны, ни MSG, ни костяную муку из животных ... только тяжелый труд и хитрости.



Перевод на Русский:
Kolobynin Alexey <alexey_ak0(_at_)mail.ru>

Содержание:

 

Как избежать дыр в безопасности при разработке приложения - Часть 5: условия перехвата (race conditions)

[article illustration]

Резюме:

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

_________________ _________________ _________________

 

Введение

Основной принцип, определяющий условие перехвата, следующий: процесс желает получить монопольный доступ к системному ресурсу. Он проверяет, что ресурс уже не используется другим процессом, и затем использует его по своему усмотрению. Условие перехвата возникает, когда другой процесс пытается использовать тот же ресурс в промежуток времени между проверкой первым процессом этого ресурса и фактическим его использованием. Побочные эффекты, возникающие при этом, могут быть различными. Классическая ситуация в теории ОС - блокировка обоих процессов. Чаще это приводит к сбою в программе или даже к дыре в безопасности, когда один процесс незаконно пользуется привилегиями другого.

Под тем, что мы ранее назвали ресурсом, может подразумеваться разное. Очень часто ситуации перехвата обнаруживаются и исправляются в самом ядре Linux при конкурентном досупе к памяти. Мы здесь будем рассматривать системные приложения и под ресурсами будем понимать узлы файловой системы. Это относится не только к обычным файлам, но и к прямому доступу к устройствам при помощи специальных точек входа из директории /dev/.

Чаще всего, атаки, направленные на нарушение безопасности системы, производятся против Set-UID приложений, так как атакующий может воспользоваться привилегиями владельца исполняемого файла. Однако, в отличие от рассмотреных ранее дыр в безопасности (переполнение буфера, строки формата, ...), условия перехвата обычно не допускают выполнения "нужного" кода. При помощи такого типа атак возможно использовать ресурсы программы, пока она выполняется. Этот тип атаки также может быть направлен и на "нормальные" программы (не Set-UID), взломщик как бы лежит в засаде на другого пользователя, главным образом на root, и ждет, когда он запустит нужное приложение, чтобы получить доступ к его ресурсам. Это, например, запись в файлы (к примеру ~/.rhost, где строка "+ +" предоставит прямой доступ с любой машины без пароля) или чтение конфеденциального файла (засекреченные комерческие данные, личная медецинская информация, файл паролей, личный ключ, ...)

В отличие от дыр в безопасности, рассмотреных в предыдущих статьях, данная проблема имеет отношение ко всем приложениям, а не только к Set-UID утилитам и системным серверам или демонам.

 

Первый пример

Рассмотрим поведение Set-UID программы, которой нужно сохранить данные в файле, принадлежащем пользователю. Мы могли бы, например, рассмотреть случай почтовой транспортной программы, такой как sendmail. Предположим, что пользователь может задать как имя файла, так и сообщение для записи в файл, что достаточно правдоподобно при некоторых обстоятельствах. В этом случае приложение должно проверить, принадлежит ли файл лицу, запустившему программу. Оно также проверит, что файл не является символической ссылкой на системный файл. Не будем забывать, что наша программа Set-UID root, и ей позволено изменять любой файл на машине. Соответственно она сравнит владельца файла со своим истинным UID. Давайте напишем что-нибудь вроде:

1     /* ex_01.c */
2     #include <stdio.h>
3     #include <stdlib.h>
4     #include <unistd.h>
5     #include <sys/stat.h>
6     #include <sys/types.h>
7
8     int
9     main (int argc, char * argv [])
10    {
11        struct stat st;
12        FILE * fp;
13
14        if (argc != 3) {
15            fprintf (stderr, "использование : %s файл сообщение\n", argv [0]);
16            exit(EXIT_FAILURE);
17        }
18        if (stat (argv [1], & st) < 0) {
19            fprintf (stderr, "не найден %s\n", argv [1]);
20            exit(EXIT_FAILURE);
21        }
22        if (st . st_uid != getuid ()) {
23            fprintf (stderr, "не владелец %s \n", argv [1]);
24            exit(EXIT_FAILURE);
25        }
26        if (! S_ISREG (st . st_mode)) {
27            fprintf (stderr, "%s не является обычным файлом\n", argv[1]);
28            exit(EXIT_FAILURE);
29        }
30
31        if ((fp = fopen (argv [1], "w")) == NULL) {
32            fprintf (stderr, "Невозможно открыть\n");
33            exit(EXIT_FAILURE);
34        }
35        fprintf (fp, "%s\n", argv [2]);
36        fclose (fp);
37        fprintf (stderr, "Запись сделана\n");
38        exit(EXIT_SUCCESS);
39    }

Как мы объясняли в нашей первой статье, было бы лучше для Set-UID приложения временно понизить свои привилегии и открыть файл, используя UID пользователя, запустившего его. Фактически, вышеописанная ситуация соответствует демону, предоставляющему услуги каждому пользователю. Всегда выполняясь под ID root-а, он должен бы делать проверку, используя UID вместо собственного истинного UID. И все же, мы оставим пока все как и есть, даже если это не так уж и правдоподобно, это поможет нам понять проблему, легко воспользовавшись дырой в безопасности.

Как мы можем видеть, программа начинает выполнение, проделывая все необходимые проверки, то есть: файл существует, принадлежит пользователю и это обычный файл. Далее она открывает файл и записывает сообщение. Вот где находится дыра! Или, более точно, она в промежутке времени между чтением атрибутов файла при помощи stat() и его открытием при помощи fopen(). Этот промежуток часто черезвычайно мал, однако атакующий может воспользоватся им и поменять параметры файла. Чтобы сделать нашу атаку попроще, добавим строку, которая заставит процесс ждать между двумя операциями, таким образом мы получим время, чтобы сделать работу вручную. Поменяем строку 30 (ранее пустую) и вставим:

30        sleep (20);

Теперь давайте выполним программу. Сначала сделаем ее Set-UID root. Сделаем, что очень важно, резервную копию нашего файла паролей /etc/shadow:

$ cc ex_01.c -Wall -o ex_01
$ su
Password:
# cp /etc/shadow /etc/shadow.bak
# chown root.root ex_01
# chmod +s ex_01
# exit
$ ls -l ex_01
-rwsrwsr-x 1 root  root    15454 Jan 30 14:14 ex_01
$

Все готово для атаки. Мы в дериктории, принадлежащей нам. Мы имеем Set-UID root утилиту (здесь ex_01), содержащую дыру в безопасности, и желаем заменить строку, соответствующую root из файла паролей /etc/shadow на строку, содержащую пустой пароль.

Сначала мы создаем файл fic, принадлежащий нам:

$ rm -f fic
$ touch fic

Далее, мы запускаем наше приложение в фоновом режиме. Мы просим его записать строку в этот файл. Приложение проверяет что следует и "засыпает" на некоторое время перед настоящим доступом к файлу.

$ ./ex_01 fic "root::1:99999:::::" &
[1] 4426

Содержимое строки root взято из man страницы shadow(5), самое важное - то что второе поле пусто (нет пароля). Пока процесс спит, у нас есть около 20 секунд, чтобы удалить файл fic и заменить его ссылкой (символической или жесткой - обе сработают) на файл /etc/shadow. Вспомним, что любой пользователь может создавать ссылку на файл в приндлежащей ему директории, даже если он не может прочитать его содержимое (или в /tmp, что мы увидим чуть попозже). Однако невозможно создать копию такого файла, так как это требует его полного прочтения.

$ rm -f fic
$ ln -s /etc/shadow ./fic

Далее мы попросим оболочку вернуть процесс ex_01 в приоритетный режим при помощи команды fg и подождем пока он завершится:

$ fg
./ex_01 fic "root::1:99999:::::"
Write Ok
$

Вуаля! Готово. Файл /etc/shadow содержит только одну строку, которая говорит, что root не имеет пароля. Вы не верите?

$ su
# whoami
root
# cat /etc/shadow
root::1:99999:::::
#

Закончим наш эксперимент, вернув старый файл паролей:

# cp /etc/shadow.bak /etc/shadow
cp: replace `/etc/shadow'? y
#
 

Будем реалистичнее

Мы использовали условие перехвата в Set-UID root утилите. Конечно, очень помогло, что программа подождала 20 секунд, чтобы мы успели тайно от нее изменить файл. В настоящем приложении условие перехвата доступно для использования только на очень короткое время. Как же мы воспользуемся им?

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

Метод, позволяющий нам воспользоваться дырой в безопасности, основанный на условии перехвата, достаточно скучный и нудный, однако он работает! Попробуем найти наиболее эффективное решение проблемы.

 

Возможное усовершенствование

Проблема, которая обсуждалась выше, основана на возможности изменить характеристики объекта в промежуток времени между двумя операциями, не трогая остального. В предыдущей ситуации изменение не касалось самого файла. Между прочим, обычному пользователю, было бы достаточно сложно изменить или даже прочитать файл /etc/shadow. Фактически, изменение касалось связи между существующей файловой записью в дереве имен и самим файлом, как физической сущностью. Вспомним, что большинство системных команд (rm, mv, ln и т.д.) воздействуют на имя файла, а не на его содержимое. Даже если вы удаляете файл (используя rm и системный вызов unlink()), по-настоящему сожержимое его удаляется, когда последняя физическая связь - последняя жесткая ссылка - будет удалена.

Ошибка, которая была сделана в предыдущей программе, происходит из-за того, что мы полагали, что ассоциация между именем файла и его содержимым неизменна, или, как минимум, постоянна в промежуток времени между операциями stat() и fopen(). Примера жесткой ссылки достаточно, чтобы удостовериться, что это соответствие совсем не постоянно. Приведем пример, используя такой тип ссылок. В директории, которая принадлежит нам, мы создаем новую ссылку на системный файл. Естественно, владелец и права доступа к файлу сохраняются. Опция -f команды ln заставляет сделать ссылку даже если имя уже существует:

$ ln -f /etc/fstab ./myfile
$ ls -il /etc/fstab myfile
8570 -rw-r--r--   2 root  root  716 Jan 25 19:07 /etc/fstab
8570 -rw-r--r--   2 root  root  716 Jan 25 19:07 myfile
$ cat myfile
/dev/hda5   /                 ext2    defaults,mand   1 1
/dev/hda6   swap              swap    defaults        0 0
/dev/fd0    /mnt/floppy       vfat    noauto,user     0 0
/dev/hdc    /mnt/cdrom        iso9660 noauto,ro,user  0 0
/dev/hda1   /mnt/dos          vfat    noauto,user     0 0
/dev/hda7   /mnt/audio        vfat    noauto,user     0 0
/dev/hda8   /home/ccb/annexe  ext2    noauto,user     0 0
none        /dev/pts          devpts  gid=5,mode=620  0 0
none        /proc             proc    defaults        0 0
$ ln -f /etc/host.conf ./myfile
$ ls -il /etc/host.conf myfile 
8198 -rw-r--r--   2 root  root   26 Mar 11  2000 /etc/host.conf
8198 -rw-r--r--   2 root  root   26 Mar 11  2000 myfile
$ cat myfile
order hosts,bind
multi on
$

Опция -i команды /bin/ls выводит номер индексного дескриптора в начале строки. Мы видим, что одно имя указывает на различные физические индексные дескрипторы.

Фактически, нам хотелось бы, чтобы функции, которые делают проверку и осуществляют доступ к файлу, всегда обращались с одним и тем же содержимым, и с одним и тем же индексным дескриптором. И это возможно! Ядро само автоматически управляет этим соответствием, когда предоставляет нам файловый дескриптор. Когда мы открываем файл для чтения, системный вызов open() возвращает целое число, которое является дескриптором, асоциированым с физическим файлом во внутренней таблице. Все операции чтения, которые мы затем будем производить, будут относиться к содержимому этого файла, не учитывая, что происходит с именем, которое было использовано при операции открытия.

Давайте выделим этот момент: если файл был открыт, любая операция над именем файла, включая его удаление, не будет влиять на содержимое файла. Пока есть еще процесс, работающий с дескриптором файла, содержимое файла не удаляется с диска, даже если его имя исчезает из директории, где оно находилось. Ядро поддерживает ассоциацию с содержимым файла между системным вызовом open(), который предоставляет файловый дескриптор, и освобождением этого дескриптора при вызове close() или завершением процесса.

Итак мы имеем наше решение! Мы можем открыть файл, а затем проверять права доступа, изучая характеристики дескриптора, а не какого-то имени файла. Это можно сделать используя системный вызов fstat() (работает так же как и stat()), который проверяет файловый дескриптор, а не путевое имя. Чтобы получить доступ к содержимому файла, используя дескриптор, мы будем использовать функцию fdopen() (которая работает так же как и fopen()), так как она использует дескриптор, а не имя файла. Вот какая получается программа:

1    /* ex_02.c */
2    #include <fcntl.h>
3    #include <stdio.h>
4    #include <stdlib.h>
5    #include <unistd.h>
6    #include <sys/stat.h>
7    #include <sys/types.h>
8
9     int
10    main (int argc, char * argv [])
11    {
12        struct stat st;
13        int fd;
14        FILE * fp;
15
16        if (argc != 3) {
17            fprintf (stderr, "использование : %s файл сообщение\n", argv [0]);
18            exit(EXIT_FAILURE);
19        }
20        if ((fd = open (argv [1], O_WRONLY, 0)) < 0) {
21            fprintf (stderr, "Невозможно открыть %s\n", argv [1]);
22            exit(EXIT_FAILURE);
23        }
24        fstat (fd, & st);
25        if (st . st_uid != getuid ()) {
26            fprintf (stderr, "%s не владелец !\n", argv [1]);
27            exit(EXIT_FAILURE);
28        }
29        if (! S_ISREG (st . st_mode)) {
30            fprintf (stderr, "%s не является обычным файлом\n", argv[1]);
31            exit(EXIT_FAILURE);
32        }
33        if ((fp = fdopen (fd, "w")) == NULL) {
34            fprintf (stderr, "Невозможно открыть\n");
35            exit(EXIT_FAILURE);
36        }
37        fprintf (fp, "%s", argv [2]);
38        fclose (fp);
39        fprintf (stderr, "Запись сделана\n");
40        exit(EXIT_SUCCESS);
41    }

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

 

Основные принципы

При работе с файлом, важно обеспечить постоянство соответствия между внутренним представлением и реальным содержимым файла. Мы советуем отдавать предпочтение использованию следующих системных вызовов для работы с физическими файлами через открытые дескрипторы, а не их эквивалентов, использующих путь к файлу:

Системный вызов Использование
fchdir (int fd) Переходит в дерикторию, представленную fd.
fchmod (int fd, mode_t mode) Изменяет права доступа к файлу.
fchown (int fd, uid_t uid, gid_t gif) Изменяет владельца файла.
fstat (int fd, struct stat * st) Обращается за информацией, содержащейся в индексном дескрипторе физического файла.
ftruncate (int fd, off_t length) Обрезает существующий файл.
fdopen (int fd, char * mode) Инициализирует ввод-вывод из уже открытого дескриптора. Это процедура библиотеки stdio, а не системный вызов.

Для этого, естественно, вы должны открыть файл в нужном режиме, вызвав open() (не забудте про третий аргумент при создании нового файла). Подробнее о вызове open() поговорим позже, когда будем обсуждать проблемы с временными файлами.

Мы настаиваем на необходимости проверять возвращаемый код системных вызовов. Например, сошлемся (даже если это не имеет ничего общего с условиями перехвата) на проблему, обнаруженную в старых реализациях /bin/login, где пренебрегали проверкой кода ошибки. Данное приложение автоматически предоставляло доступ с правами root, если не находило файл /etc/passwd. Такое поведение может показаться приемлемым, если дело касается восстановления поврежденной файловой системы. Однако, проверка, что невозможно открыть файл, вместо проверки на существование файла, оказалась менее приемлема. Вызов /bin/login после открытия максимально возможного количества дескрипторов, позволял получить любому пользователю доступ с правами root... Закончим с этим отступлением, которое показывает, насколько важно проверять не только успешно или нет прошел системный вызов, но и код ошибки, перед тем как предпринимать какие-либо действия, связанные с безопасностью системы.

 

Условия перехвата и содержимое файла

Программа, занимающаяся системной безопасностью, не должна надеяться на монопольный доступ к содержимому файла. Более точно, важно должным образом учитывать риск возникновения условий перехвата при работе с одним файлом. Основная опасность исходит от пользователя, одновременно запускающего несколько копий Set-UID root приложения или одновременно устанавливающего несколько соединений с одним демоном, надеясь создать условие перехвата, во время которого содержимое системного файла может быть модифицировано нестандартным образом.

Чтобы программа не была восприимчива к такого рода ситуациям, необходимо ввести механизм монопольного доступа к файлу данных. Это та же проблема, которая возникает в базах данных, когда различным пользователям позволено одновременно делать запрос и изменять содержимое файла. Принцип блокировки файла решает эту проблему.

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

На самом деле, система умнее, чем мы описали: ядро различает блокировки, требуемые для чтения и для записи. Различные процессы могут одновременно держать блокировку для чтения, так как ни один из них не попытается изменить содержимое файла. Однако, в данный промежуток времени только один процесс может держать блокировку для записи, и в это время не могут быть предоставлены никакие другие блокировки, даже по чтению.

Существует два типа блокировок (по большей части друг с другом не совместимых). Первый, из BSD, основан на системном вызове flock(). Первый аргумент вызова - дескриптор файла, к которому вы желаете получить монопольный доступ, второй - именованая константа, задающая операцию, которая будет производиться. Она может принимать различные значения: LOCK_SH (блокировка для чтения), LOCK_EX (для записи), LOCK_UN (снятие блокировки). Система блокирует процесс на время, пока запрошенная операция невозможна. Однако, вы можете произвести операцию ИЛИ | второго аргумента с константой LOCK_NB, чтобы функция неудачно завершалась вместо того, чтобы приостанавливаться.

Второй тип блокировки, из System V, основан на системном вызове fcntl(), немного сложном. Существует библиотечная функция lockf() близкая к системному вызову, но не до конца. Первый аргумент fcntl() - дескриптор файла для блокировки. Второй - представляет необходимую операцию: F_SETLK и F_SETLKW управляют блокировками, вторая команда приостанавливает процесс пока операция не становится доступной, в то время как первая сразу же возвращает управление при неудачной попытке. F_GETLK проверяет состояние блокировки файла (что бесполезно для рассматриваемых приложений). Третий аргумент - указатель на переменную типа struct flock, которая описывает блокировку. Важные поля структуры flock следующие:

Имя Тип Значение
l_type int Ожидаемое действие: F_RDLCK (блокировка для чтения), F_WRLCK (для записи) and F_UNLCK (для снятия блокировки).
l_whence int Тип смещения поля l_start (обычно SEEK_SET).
l_start off_t Позичия начала блокировки (обычно 0).
l_len off_t Длина блокировки, 0 для достижения конца файла.

Мы можем видеть, что fcntl() способна блокировать ограниченные части файла, она может делать много чего по сравнению с flock(). Давайте взглянем на программу, которая запрашивает блокировку для чтения для файлов, имена которых указаны как аргументы, и ожидает пока пользователь нажмет Ввод перед выходом (и снятием блокировок).

1    /* ex_03.c */
2    #include <fcntl.h>
3    #include <stdio.h>
4    #include <stdlib.h>
5    #include <sys/stat.h>
6    #include <sys/types.h>
7    #include <unistd.h>
8
9    int
10   main (int argc, char * argv [])
11   {
12     int i;
13     int fd;
14     char buffer [2];
15     struct flock lock;
16
17     for (i = 1; i < argc; i ++) {
18       fd = open (argv [i], O_RDWR | O_CREAT, 0644);
19       if (fd < 0) {
20         fprintf (stderr, "Невозможно открыть %s\n", argv [i]);
21         exit(EXIT_FAILURE);
22       }
23       lock . l_type = F_WRLCK;
24       lock . l_whence = SEEK_SET;
25       lock . l_start = 0;
26       lock . l_len = 0;
27       if (fcntl (fd, F_SETLK, & lock) < 0) {
28         fprintf (stderr, "Невозможно заблокировать %s\n", argv [i]);
29         exit(EXIT_FAILURE);
30       }
31     }
32     fprintf (stdout, "Нажмите Ввод для снятия блокировки(ок)\n");
33     fgets (buffer, 2, stdin);
34     exit(EXIT_SUCCESS);
35   }

Вначале мы запускаем эту программу с первой консоли, где она будет ожидать:

$ cc -Wall ex_03.c -o ex_03
$ ./ex_03 myfile
Нажмите Ввод для снятия блокировки(ок)
>С другого терминала...
    $ ./ex_03 myfile
    Невозможно заблокировать myfile
    $
Нажимая Ввод на первой консоли, мы снимаем блокировки.

При помощи механизма блокировок, вы можете избежать появления условий перехвата в директориях и очередях печати, как демон lpd, который использует flock() для блокировки файла /var/lock/subsys/lpd, таким образом допуская только один экземпляр для выполнения. Вы также можете безопасно управлять доступом к системным файлам, также как в библиотеке pam файл /etc/passwd блокируется при помощи fcntl(), при изменении данных пользователя.

Однако, такие действия защитят вас только от вмешательства корректно работающих приложений, то есть приложений, которые запрашивают у ядра разрешения на требуемый доступ, перед чтением или записью в важный системный файл. То есть мы говорим о совместном блокировании - такие приложения ответственно относятся к досупу к данным. К сожалению, плохо написаная программа может изменить содержимое файла, даже если другой процесс, с правильным поведением, заблокировал его для записи. Вот пример. Мы записываем несколько букв в файл и блокируем его, используя предыдущую программу:

$ echo "FIRST" > myfile
$ ./ex_03 myfile
Нажмите Ввод для снятия блокировки(ок)
> С другой консоли мы изменяем файл:
    $ echo "SECOND" > myfile
    $
Возвращаясь на первую консоль, мы проверяем "повреждения":
(Ввод)
$ cat myfile
SECOND
$

Чтобы решить эту проблему, ядро Linux предоставляет системному администратору механизм блокирования из System V. Поэтому вы можете использовать этот механизм только при помощи fcntl(), но не flock(). Администратор может сказать ядру, что блокировки fcntl() строгие, использующие специальную комбинацию прав доступа. Тогда, если процесс блокирует файл для записи, другой процесс не сможет изменить этот файл (даже root). Специальная комбинация прав, которая используется, - это установленный бит Set-GID при снятом бите выполнения для группы. Этого можно добиться командой:

$ chmod g+s-x myfile
$
Однако этого не достаточно. Чтобы автоматически пользоваться строгими совместными блокировками при обращении к файлу, должен быть установлен атрибут mandatory для раздела, где находится этот файл. Обычно, вам надо изменить файл /etc/fstab и добавить туда опцию mand в 4-ую колонку или ввести команду:
# mount
/dev/hda5 on / type ext2 (rw)
[...]
# mount / -o remount,mand
# mount
/dev/hda5 on / type ext2 (rw,mand)
[...]
#
Теперь мы можем проверить, что изменение с другой консоли невозможно:
$ ./ex_03 myfile
Нажмите Ввод для снятия блокировки(ок)
>С другого терминала:
    $ echo "THIRD" > myfile
    bash: myfile: Resource temporarily not available
    $
И возвращаясь на первую консоль:
(Ввод)
$ cat myfile
SECOND
$

Администратору-не программисту следует принять решение об использовании строгих блокировок файлов (например /etc/passwd или /etc/shadow). Программисту необходимо котролировать способы доступа к данным, что обеспечит его приложению при чтении правильную работу с данными, а при записи оно не создаст помех другим процессам, если будет правильно настроено окружение для выполнения.

 

Временные файлы

Очень часто программе необходимо временно сохранить данные во внешнем файле. Типичный пример - вставка записи в середину последовательно упорядоченного файла, что предполагает создание копии исходного файла во временном файле при добавлении новой информации. Затем, системный вызов 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 (чтение/запись для владельца, чтение для остальных), может случится неприятность. Функция

	#include <sys/types.h>
	#include <sys/stat.h>

        mode_t umask(mode_t mask);
позволяет определить права доступа к файлу в момент создания. Поэтому, после вызова umask(077), файл будет открыт с режимом 600 (чтение/запись для владельца, накаких прав другим).

Обычно создание временного файла происходит в три этапа:

  1. создание уникального имени (случайного) ;
  2. открытие файла, используя O_CREAT | O_EXCL с самыми ограниченными правами доступа;
  3. проверка результата открытия файла с последующей соответствующей реакцией (повтор или выход).

Как создать временный файл? Функции

      #include <stdio.h>

      char *tmpnam(char *s);
      char *tempnam(const char *dir, const char *prefix);

возвращают указатели на случайно созданные имена.

Первая функция принимает аргумент 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() поддерживает различное количество иксов в зависимости от реализации.  

Вывод

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

 

Ссылки


 

Страница отзывов

У каждой заметки есть страница отзывов. На этой странице вы можете оставить свой комментарий или просмотреть комментарии других читателей :
 talkback page 

Webpages maintained by the LinuxFocus Editor team
© Frйdйric Raynal, Christophe Blaess, Christophe Grenier, FDL
LinuxFocus.org

Click here to report a fault or send a comment to LinuxFocus
Translation information:
fr --> -- : Frйdйric Raynal, Christophe Blaess, Christophe Grenier <pappy(_at_)users.sourceforge.net, ccb(_at_)club-internet.fr, grenier(_at_)nef.esiea.fr>
fr --> en: Georges Tarbouriech <georges.t(_at_)linuxfocus.org>
en --> en: Lorne Bailey <sherm_pbody(_at_)yahoo.com>
en --> ru: Kolobynin Alexey <alexey_ak0(_at_)mail.ru>

2002-09-20, generated by lfparser version 2.30