В новом выпуске рубрики Unix-гуру нашего журнала Игорь Облаков предлагает вниманию читателей несколько курьезов из разных операционных систем.
В первом номере «Открытых систем» за 1999 год мы начали тему «кунсткамера» - сборник курьезов из разных операционных систем. Она оказалась слегка заброшенной, но новые письма читателей вновь пробудили ее к жизни.
Двуликий Янус
Каждая версия ОС Unix обязательно содержит какую-то изюминку, которую не найти в других ее версиях. Мне давно хотелось рассказать об одной из них. В версии HP-UX 9.x (увы, увы, даже документацию мне не удалось разыскать на фоне распространившихся десятой и одиннадцатой версий) имелось такое нетривиальное образование, как «скрытый» каталог.
Может ли быть так, что при обращении к одному и тому же файлу в Unix разные пользователи получали совершенно разное содержимое, при просмотре каталога видели разный размер и т.п.? Конечно, если это происходит в момент, когда кто-то туда пишет, то возможно. А если не пишет? Именно так могло быть в девятой версии HP-UX. Такая возможность оказывалась полезной в самых разнообразных ситуациях. Например, при загрузке различных компьютеров по сети все они могли в каталоге bin найти собственные двоичные файлы вне зависимости от типа архитектуры процессора, ОС и т.п. Система определяла тип архитектуры вызывающего процессора и «подсовывала» ему соответствующие файлы.
Как это происходило? Вспомните набор характеристик, которые входят в состав окружения процесса. Можно обнаружить, что он слегка отличается в разных версиях Unix. Основа-то, конечно, общая, но у каждого свое маленькое деревце в палисаднике. В частности, в HP-UX 9.x каждый процесс сопровождал набор дополнительных текстовых характеристик, которые описывали, что пользователь является локальным или нет, каким является тип архитектуры машины, с которой стартовал telnet или ftp, имя хоста и т.п.
И как же это использовалось? Довольно просто. Предположим, что мы хотели бы, чтобы файл /etc/ ogogo.config читался с разных машин в соответствующем каждой машине виде. Для этого создается каталог /etc/ogogo.config, в этом каталоге размещаются файлы с названиями, которые должны совпадать с соответствующими характеристиками процессов (например, имена хостов) и, возможно, вариант по умолчанию - default. Последнее, что необходимо сделать, это установить для каталога бит H, т. е. сделать его «скрытой» (hidden). И наша цель будет достигнута - каждый любитель потаскать подобные файлы сможет получить почти личную копию. Если же мы не предусмотрим варианта по умолчанию (default) варианта, то... для кого-то файл может и не существовать вообще.
Увы, увы, все это пропало, и в новых версиях операционной системы этого механизма нет? Отнюдь. Он не был забыт. Зачем же он опять понадобился?
Для этого нам придется немного поговорить о безопасности *. Может ли быть «обычный» Unix достаточно безопасным? Это, конечно, зависит от того, что вы понимаете под безопасностью.
Одним из требований к «безопасным» системам является возможность работы с «мандатами». В операционной системе хотелось бы иметь какой-то механизм, который бы позволял говорить в терминах уровня секретности («а начихать», «жутко секретно» и т.п.) и групп - «реклама», «поставки», «разработка» изделия. Если «изделие» достаточно серьезно, то группы должны иметь доступ как к общедоступным данным (минимальные общие сведения) так и специфичным (кто будет лоббировать - для одних, технические секреты для других), руководство же соответствующего уровня, возможно, должно иметь доступ к данным двух и более групп. Как кажется, все можно было бы покрыть механизмами Unix - поделим всех на группы по уровню секретности и специализации (а это в общем случае требует количества групп равное произведению количества уровней на количество специализаций), естественно, предполагая возможность наличия кого-либо в нескольких группах. Достаточно ли этого? Может и хватит, но при одном предположении: все действующие лица постоянно сами отслеживают права на создаваемые файлы и не забывают их менять в соответствии с требованиями организации. А это, заметим, труд и четкие требования к сотрудникам. Попробуйте после этого оценить ответ банковского работника на вопрос об обучении вопросам секретности: «А у нас первый отдел есть. Зачем же обучение?».
Что плохо в таком подходе? Ясно, что возможно преднамеренное или непреднамеренное раскрытие секретов. Если один пользователь просто копирует секретный файл, то новый файл будет создан с текущими параметрами процесса; в общем случае никто не сумеет поймать пользователя в такой ситуации и не оповестит его о возможности понижения уровня секретности.
Вот в этот момент и возникает необходимость в мандатной системе. Каждый процесс в системе должен в своем контексте содержать не раздробленную информацию о принадлежности пользователя к каким-то группам, а целостное представление о секретности и доступности данной работы. Соответственно сам процесс будет помнить, что он работает с «жутко секретными данными о поставках» и не позволит вам породить файл с меньшим уровнем секретности. Более точное описание может быть следующим: процессу присваивается уровень секретности и набор характеристик (чем плохи строки?) по поводу принадлежности к каким-либо группам. Точно также поступаем с файлами: у каждого уровень и набор строк. Процесс может читать некоторый файл только в том случае, когда его уровень не ниже уровня файлов и в его контексте встречаются все строки, которые подвязаны к файлу. Процесс может писать только при полном совпадении уровня и строк. Второе правило автоматически не позволяет рассекретить какой-либо документ. В самом деле, для того чтобы прочитать документ процесс (а значит и пользователь) должен обладать определенными правами секретности. Куда бы он не скопировал свои данные, вне зависимости от его желания, они останутся на его уровне секретности, и никакой chmod не позволит ему объявить их туфтой. Заметим тот забавный факт, что пользователь не властен изменить (как минимум вниз) права на свой (МОЕ!) файл. Явное отличие от механизма, имеющего хождения в традиционных разновидностях Unix.
Как бы нам это обмануть? А кто мешает воспользоваться механизмом конвейеров, сообщений, разделяемой памятью, да и вообще всей мощью IPC и сетевыми службами для взлома. Вот тут-то и проявляется следующий уровень наколки - все механизмы связи используют мандатный доступ. Задачка: а это-то как обмануть?
Какие неудобства тут возникают? Вернемся к вопросу о том, как скрестить лань (Unix) и новые требования. Что-то тут не должно работать. Что именно?
Вспоминаем, что масса Unix-программ используют текущую, домашнюю или какую-либо специфическую (/tmp, /var/tmp и т.п.) директорию для хранения временных файлов. А что, если пользователь, возможно, достаточно высокого ранга, может работать с данными разного уровня секретности? Процессы, которые будут запускаться данным пользователем тоже будут иметь различные права секретности и ... (приготовились?) не могут писать в одну и ту же директорию, т.к. для этого они должны иметь права доступа эквивалентные правам директории. Приплыли? Не быть Unix-у достаточно секретным пока весь не перепишут.
Вернемся теперь к нашему деревцу - CDF. А что, если каталог, в который мы попадаем, может быть CDF-каталогом и в зависимости от уровня секретности выбирается соответствующий ее подкаталог? Ура, один из вариантов найден. Вот один из шагов по дороге, которую ведет из Unix в систему класса защищенности B.
Бледнолицый и грабли
Когда-то я написал статью о некоторых фокусах Си-библиотек (см. «Практическое использование разделяемых библиотек в ОС UNIX», «Открытые системы», 1994, №3, http://www.osp.ru/os/1994/03/5.htm) в одном из вариантов Unix. В частности, там был пример с процедурой sscanf. Это было что-то в стиле:
sscanf(«123 456», «%d %d», &i, &j);
Оказывалось, что gcc размещал текстовую строку в секции кода и она оказывалась защищенной от записи. Все бы ничего, но алгоритм sscanf разбивал строку на отдельные компоненты и подменял отдельные символы на ноль перед вызовом преобразователей в целое и т.п. В результате в принципе строка не изменялась, но должна была лежать в секции не защищенной от записи. Специально для этих целей gcc имеет ключ -fwritable-strings, который периодически приходилось ставить для того, чтобы программа не «грохалась» по защите памяти.
Дело былое. Но вот этим летом Д. Самборский прислал мне следующий пример: «Предлагаю экспонат в вашу кунсткамеру... Это пример из книги «UNIX System Programming» Keith Haviland, Arthur Anderson, Marcus Gray, Ben Salama». Текст программы взят мною из его письма без изменений:
#include #include #include main (int argc, char **argv) { int input, output; size_t filesize; void *source, *target; char endchar = ? ?; /* Проверка числа входных параметров */ if (argc != 3) { fprintf (stderr, «Синтаксис: copyfile источник цель »); exit (1); } /* Открыть файлы ввода и вывода */ if ((input = open (argv[1], O_RDONLY)) == -1) { fprintf (stderr, «Ошибка при открытии файла %s », argv[1]); exit (1); } if ((output = open (argv[2], O_RDWR | O_CREAT | O_TRUNC, 0666)) == -1) { close (input); fprintf (stderr, «Ошибка при открытии файла %s », argv[2]); exit (2); } /* Создать второй файл того же размера, что и первый. */ filesize = lseek (input, 0, SEEK_END); lseek (output, filesize - 1, SEEK_SET); write (output, &endchar, 1); /* Отобразить в память файлы ввода и вывода. */ if ((source = mmap (0, filesize, PROT_READ, MAP_SHARED, input, 0)) == (void *) -1) { fprintf(stderr,«Ошибка отображения файла 1 в память »); exit (1); } if ((target = mmap (0, filesize, PROT_WRITE, MAP_SHARED, output,0)) == (void *) -1) { fprintf (stderr, « Ошибка отображения файла 2 в память »); exit (2); } /* Копирование */ #if defined(__linux__) /* В Linux memcpy приводит к промаху по памяти */ { int i; for (i = 0; i < filesize; i++) *((char *) target + i) = *((char *) source + i); } #else memcpy (target, source, filesize); #endif /* Отменить отображение обоих файлов */ munmap (source, filesize); munmap (target, filesize); /* Закрыть оба файла */ close (input); close (output); exit (0); }
Пройти мимо этого нельзя. Обвинение «memcpy() приводит к промахам в памяти» слишком серьезно. Отключаем define и пытаемся стартовать программу - не работает. SIGSEGV с соответствующими последствиями.
Запускаем отладчик и видим, что ошибка происходит в подпрограмме memcpy. Адрес обращения корректен. В чем же дело? Вглядимся в код (благо текст memcpy.S из glibc есть!) рядом с генерируемой ошибкой:
subl $32, %ecx js L(2) /* Read ahead to make sure we write in the cache since the stupid i586 designers haven?t implemented read-on-write-miss. */ movl (%edi), %eax <———————— место ошибки L(3): movl 28(%edi), %edx /* Now correct the loop counter. Please note that in the following code the flags are not changed anymore. */
Что же это происходит в указанной точке? В этот момент делается чтение из области... результата. Почему это делается, читатели могут понять по приведенному фрагменту программы самостоятельно. Что же не сложилось? Здесь важен тот момент, что, вообще говоря, программа memcpy предполагает, что такого рода чтение всегда допустимо. А вот этого-то предположения она и не должна делать. Мало ли куда мы копируем! Может быть, это некэшируемая зона для регистров внешних устройств в памяти PCI и т.п. В данном случае нам не разрешен доступ к данной области на чтение (PROT_WRITE!), что и приводит к неудаче.
Обратите внимание и на такой достаточно любопытный факт. В данном случае делается явная (и не очень удачная) попытка поддержать скорость работы программы за счет кэша. Но нужны ли эти данные в кэше? Чаще всего массовый переброс данных просто засоряет кэш ненужными данными. Так, в приведенной выше программе мы никогда не обратимся вновь к скопированным данным. В некоторых архитектурах наоборот при выполнении программ типа memcpy делается все (не в ущерб, конечно, скорости копирования) для того, чтобы копируемые данные не засоряли кэш.
Присылайте свои экспонаты по адресу oblakov@bigfoot.com
* Заметим, что коротенькое описание данного механизма (он называется также CDF - context dependent files) можно найти в пятой главе книги «Practical UNIX & Internet Security», изданной O?Reilly & Associates.