Утилита времен «динозавров»: история системного вызова chroot и его применение в современности
В мире победившей контейнеризации и виртуализации об утилите chroot вспоминают лишь брутальные админы суровых физических серверов, а про лежащий в основе системный вызов, кажется, забыли как страшный сон.
Этот простой системный вызов подменяет местонахождение «корня» файловой системы, «заключая» программу в специально созданное ограниченное окружение. Самая распространенная ситуация — восстановление загрузки операционной системы с помощью live-образа. Но при создании chroot о таком применении не задумывались.
Чтобы найти истоки появления chroot в *NIX-подобных операционных системах, нужно пройти немалый путь по истории IT. В этой статье я расскажу про появление chroot и его применение в современном мире. А еще покажу проекты, которые позволяют прикоснуться к операционным системам эпохи, когда Интернета не было.
Предполагается, что читатель имеет как минимум базовое умение работать с операционными системами семейства Linux.
Что ты такое?
Сперва познакомимся с «героем» рассказа. Чаще всего администраторы сталкиваются с утилитой chroot, которая позволяет подменить местонахождение корня для дочерних программ.
Представим ситуацию: перед системным администратором стоит задача восстановить ОС после сбоя. Администратор достает из кармана загрузочную флешку и начинается магия. Провайдеры инфраструктуры для экстренных случаев предоставляют заготовленный образ для восстановления — Rescue. Для выделенных серверов Selectel загрузить образ восстановления можно прямо по сети.
Из Rescue администратор имеет доступ ко всем файлам на диске и может вносить исправления. В случае загрузчика недостаточно просто обновить файлы конфигурации, необходимо применить изменения. Для загрузчика GNU GRUB2, например, следует выполнить утилиту grub2-install.
Возникает проблема: утилита сканирует доступные ядра текущей операционной системы и сохраняет результат в подкаталогах /boot. В результате обновляется загрузчик «спасительной» флешки, а не пострадавшей системы. Нужно «обмануть» утилиту, чтобы она считала корнем накопитель сервера, а не флешку.
На этом моменте в игру вступает chroot. Администратор выполняет команду chroot /mnt, где /mnt — это точка монтирования накопителя с ОС, и происходит чудо: отныне все программы, включая интерпретатор, действуют от лица «неисправной» системы. Теперь grub2-install отработает корректно.
Утилита chroot в современных дистрибутивах Linux является частью проекта GNU Coreutils, а исходный код умещается в один файл. Утилита выполняет следующие действия:
- разбирает аргументы командной строки;
- выполняет системный вызов chroot;
- выполняет переход в другой каталог;
- изменяет пользователя, от имени которого необходимо выполнить действия;
- выполняет заданную команду, а если команды нет, то запускает интерпретатор командной строки по умолчанию.
Утилита основана на одноименном системном вызове, который делает единственную вещь: меняет один из компонентов разрешения имен файлов. Назначение других системных вызовов, связанных с файловыми системами, очевидно: программы хотят уметь работать с файлами и каталогами. Но кому и зачем пришла в голову идея подменять корень?
Чтобы ответить на вопрос, нужно плавно погрузиться в историю. Восстановление исторических событий — сложное дело, так как вместо фактов можно получить мнения. К счастью, у нас история программного обеспечения, а значит, мы можем обратиться к исходным кодам, комментариям и копирайтам.
Linux
Начнем с чего-то привычного широкой аудитории, что не требует дополнительного представления: Linux.
Исходный код ядра Linux доступен на нескольких ресурсах, в том числе на Github. На текущий момент в репозитории более миллиона коммитов, но, вопреки ожиданиям, история репозитория начинается в 2005 году с версии 2.6.12-rc2. В первом коммите Линус Торвальдс отмечает, что не обеспокоен историей и, если потребуется, будет создан «исторический» репозиторий.
commit 1da177e4c3f41524e886b7f1b8a0c1fc7321cac2 (tag: v2.6.12-rc2)
Author: Linus Torvalds
Date: Sat Apr 16 15:20:36 2005 -0700
Linux-2.6.12-rc2
Initial git repository build. I'm not bothering with the full history,
even though we have it. We can create a separate "historical" git
archive of that later if we want to, and in the meantime it's about
3.2GB when imported into git - space that would just make the early
git days unnecessarily complicated, when we don't have a lot of good
infrastructure for it.
Let it rip!
Исторический репозиторий был создан командой Linux History group. Он содержит коммиты начиная с версии 0.01. К сожалению, наиболее интересные коммиты в репозитории имеют сбитые даты. Так, тэг 0.10 имеет время 23 ноября 2007 года, а в описании указано 11 ноября 1991 года. Переключаемся на самый ранний коммит, соответствующий версии 0.01, и ищем упоминания chroot.
git checkout fa1ec1000cf9954b8e78216c11b0c3f86336d488
grep -rn chroot
Для наших задач хватит и исторического репозитория, но, если хочется посмотреть на историю ядра Linux от начала и до конца, есть проект по слиянию нескольких репозиториев в один.
В файле fs/open.c находится функция sys_chroot, которая реализует логику системного вызова. Современная реализация обросла макросами и дополнительными проверками, но по сей день находится в том же файле. К сожалению, первая версия ядра Linux сделана одним коммитом, комментариев в коде практически нет, а release notes не содержит таких тонкостей.
Тем не менее, при создании Linux Линус Торвальдс вдохновлялся операционной системой MINIX, которая была создана профессором Эндрю Таненбаумом для обучения своих студентов. Кроме того, в man-странице chroot есть ссылки на System V Release 4 (SVr4), 4.4BSD и спецификацию Single Unix Specification (SUSv2).
Здесь начинается наш путь по UNIX-системам.
UNIX
Генеалогическое древо UNIX (источник: wikipedia.org)
Первая версия ядра Linux появилась в 1991 году. Один беглый взгляд на генеалогическое дерево ОС Unix, и становится понятно, что упомянутая в man-странице BSD 4.4 была позже, а System V Release 4 является проприетарной и ее исходный код недоступен. Остается два кандидата: Minix и ранние версии BSD.
Minix
Minix — это Unix-подобная операционная система, разработанная профессором Эндрю Таненбаумом в качестве «иллюстрации» для учебника «Операционные системы: Разработка и реализация». Первая версия Minix увидела свет в 1987 году, а исходные коды распространялась в печатном виде в учебнике.
Технический прогресс не стоял на месте, и «учебная» операционная система развивалась и адаптировалась к современным реалиям. На данный момент актуальная версия Minix — 3.3.0. Исходные коды третьей мажорной версии Minix можно найти с третьим изданием учебника Таненбаума, но теперь на компакт-диске.
Учебник 1987 года найти затруднительно, но, к счастью, существует официальный ресурс с исходными кодами Minix, где можно скачать не только актуальную версию, но и предыдущие. Самая ранняя версия на сайте — 1.1. Возможно, с учебником распространялась версия 1.0, но файлы версии 1.1 датированы 1987 годом — этого нам достаточно.
В исторических исходниках Minix есть неконсистентность. Так, сборка версии 1.3 сделана сторонним человеком, и один образ дискеты имеет битый 512-байтовый сектор, а версия 1.6 распространяется в виде хитрых sh-патчей и требует установленной версии 1.5. Версия 1.5, в свою очередь, поставляется в виде десятка бинарных файлов без инструкции по применению.
Беглый поиск по исходникам разных версий привел к следующим фактам:
- в Minix 1.1 (1987) системного вызова chroot не было;
- исходя из п.1 в Minix 1.0, допустимо предположение, что его тоже не было;
- в Minix 1.7 (1988) имеется простая реализация chroot;
- в Minix 3.3.0 в man-файлах, посвященных chroot, имеется копирайт, указывающий на Университет Калифорнии, Беркли (Berkeley), и 1983 год.
Так как Linux появился позднее (1991) и Линус создал Linux совместимым с Minix, то, вероятно, системный вызов «переехал» из одной системы в другую.
Что касается копирайта, то в 1983 году существовал проект BSD (Berkeley Software Distribution), созданный для обмена опытом между учебными заведениями. У проекта была операционная система BSD-UNIX, известная сейчас как BSD.
В 1983 году вышли 2.9BSD и 4.2BSD.
BSD-UNIX
История BSD началась, когда один из профессоров университета Калифорнии, Боб Фабри (Bob Fabry), входящий в программный комитет симпозиума по принципам операционных систем (Symposium on Operating Systems Principles), заинтересовался впервые представленной операционной системой Unix.
Для запуска новой операционной системы был приобретен компьютер PDP-11/45, но из экономических соображений доступ к нему был также у математиков и статистиков. Разделение доступа привело к тому, что Unix работал всего 8 часов в сутки.
В 1975 году Кен Томпсон (Ken Thompson) из Bell Labs взял длительный отпуск и приехал в Беркли в качестве приглашенного профессора. Он помог установить Unix v6 и начал работу над реализацией Pascal, а студенты занимались другими компонентами, в том числе улучшенным текстовым редактором — ex. Другие университеты заинтересовались разработками в Беркли и в 1977 году появилась идея сделать первую «сборку» — 1BSD. Она увидела свет 9 марта 1978 года.
Фактически, 1BSD не является «клоном» Unix, это и есть Unix, но распространяемый с расширениями. Спустя несколько мажорных версий BSD столкнулась с юридическими трудностями, так как Unix — проприетарная система. К версии 2.79 BSD был полностью переписан, чтобы не использовать код Unix. Это значительно замедлило разработку новых версий, а вскоре привело к закрытию проекта.
После завершения судебных тяжб исходный код BSD-UNIX доступен по лицензии BSD, которая накладывает минимальные ограничения на использование кода. Это позволило появиться множеству проектов, основанных на BSD.
Именно в BSD впервые появилась такая абстракция, как сокет Беркли (Berkeley Socket). Сокеты с этим интерфейсом до сих пор используются в *NIX системах, и даже в Microsoft Windows есть частички кода BSD.
Исходный код BSD открыт, но во время его создания не было привычных систем контроля версий, поэтому искать нужно у энтузиастов, которые чтут историю. Существует общество The Unix Heritage Society, которое предоставляет доступ к «древнему UNIX» (Ancient UNIX) и тематическим wiki-страницам.
У этого общества есть географически распределенные «зеркала», в том числе в России. Зеркала предоставляют исходные коды в виде архивов, что упрощает поиск. Заходим в каталог Distributions и выбираем подкаталог UCB, что означает University of California, Berkeley.
Поиск по нескольким архивам разных версий привел к следующим выводам:
- в 1BSD (1978) системный вызов chroot не упоминается;
- в 2BSD (1979) также нет упоминаний;
- в «переписанной» 2.79BSD (приблизительно 1983 год) присутствуют два бинарных файла с расширением v7, которые содержат список системных вызовов, в том числе chroot. Упоминаний в man-страницах или исходниках отсутствуют.
- в 2.9BSD (1983) chroot уже присутствует, но описан в man-странице другого системного вызова, chdir.
Мне удалось связаться с архитектором из Computer Systems Research Group, который занимался 2BSD и 4.4BSD, и задать ему несколько вопросов. Как оказалось, в 2.9BSD chroot появился как порт из 4BSD. Тем не менее, в 1980-х, когда архитектор принялся за работу над BSD, chroot уже являлся частью системы.
Как отмечалось ранее, 1BSD была основана на Unix v6, а значит, стоит заглянуть к «прародителю» всех *NIX-подобных систем.
UNIX
Unix зародился в Bell Labs, подразделении AT&T, в конце 1960-х. Авторство названия приписывают Брайану Кернигану (Brian Kernighan), который придумал игру слов Unics — Uniplexed Information and Computing Service — c отсылкой на многозадачную систему Multics — Multiplexed Information and Computer Services.
Первая версия (Unix v1) была попыткой воссоздать Multics на менее мощном железе — PDP-7. Эта ОС написана на ассемблере и не имеет встроенного компилятора высокоуровневого языка. К 1973 году была выпущена третья редакция Unix, которая содержала ранний компилятор языка Си, а позже в том же году вышла четвертая редакция с ядром на языке Си.
В Unix v1 «начало» отсчета системного времени «прибили» к 1 января 1970 года 00:00:00 UTC. Это описание времени прижилось и используется до сих пор во всех NIX-подобных операционных системах.
С 1974 года Unix стал распространяться по учебным заведениям, а спустя год появились сторонние модификации Unix. В 1975 году вышла шестая редакция Unix, которая стала основой для BSD. Исходники Unix доступны в каталоге Research хранилища The Unix Heritage Society.
В отличие от BSD, старый Unix распространяется чаще в виде «образов» магнитной ленты, чем в архивах, поэтому быстро пройтись по исходникам не получится. На этом этапе стоит начать знакомство с проектом SimH (History Simulator). Многие исторические версии ПО были проприетарными, но после множества юридических процессов исходный код был открыт для изучения.
На главной странице проекта можно скачать сам симулятор и программное обеспечение к нему. Среди подготовленных образов есть и три версии Unix: пятая, шестая и седьмая редакции. У проекта также есть документация, в частности, Sample Software Documentation позволит запустить подготовленный образ желаемой операционной системы.
Скачиваем исходники SimH и собираем. Некоторым симуляторам нужны дополнительные библиотеки, например, для поддержки сетевых операций. Собираем симулятор.
# Инициирует сборку всех доступных симуляторов, займет некоторое время
make
# Если хочется только один симулятор
make pdp11
В каталоге BIN находятся скомпилированные симуляторы. Для запуска Unix v6 нужно выполнить следующие действия:
BIN/pdp11
set cpu u18
att rk0 unix0_v6_rk.dsk
att rk1 unix1_v6_rk.dsk
att rk2 unix2_v6_rk.dsk
att rk3 unix3_v6_rk.dsk
boot rk0
unix
$ BIN/pdp11
PDP-11 simulator V4.0-0 Current git commit id: 9f5e40e2
sim> set cpu u18
Disabling XQ
sim> att rk0 unix0_v6_rk.dsk
%SIM-INFO: RK0: Amount of data in use in disk container 'unix0_v6_rk.dsk' cannot be determined, skipping autosizing
sim> att rk1 unix1_v6_rk.dsk
%SIM-INFO: RK1: Amount of data in use in disk container 'unix1_v6_rk.dsk' cannot be determined, skipping autosizing
sim> att rk2 unix2_v6_rk.dsk
%SIM-INFO: RK2: Amount of data in use in disk container 'unix2_v6_rk.dsk' cannot be determined, skipping autosizing
sim> att rk3 unix3_v6_rk.dsk
%SIM-INFO: RK3: Amount of data in use in disk container 'unix3_v6_rk.dsk' cannot be determined, skipping autosizing
sim> boot rk0
@unix
login:
Для запуска Unix v7 нужно выполнить следующие команды:
BIN/pdp11
set cpu u18
set rl0 RL02
att rl0 unix_v7_rl.dsk
boot rl0
boot
rl(0,0)rl2unix
^D
$ BIN/pdp11
PDP-11 simulator V4.0-0 Current git commit id: 9f5e40e2
sim> set cpu u18
Disabling XQ
sim> set rl0 RL02
sim> att rl0 unix_v7_rl.dsk
%SIM-INFO: RL0: Amount of data in use in disk container 'unix_v7_rl.dsk' cannot be determined, skipping autosizing
sim> boot rl0
@boot
New Boot, known devices are hp ht rk rl rp tm vt
: rl(0,0)rl2unix
mem = 177856
# Restricted rights: Use, duplication, or disclosure
is subject to restrictions stated in your contract with
Western Electric Company, Inc.
Thu Sep 22 07:56:23 EDT 1988
login:
Unix v6 имеет суперпользователя (root) без пароля, у Unix v7 есть пользователь dmr без пароля, а у суперпользователя пароль совпадает с логином.
Первое открытие командного интерпретатора Unix создает впечатление, что старые системы суровы и не прощают ошибок. Так, попытка стереть символ клавишей backspace приведет к полному сбросу строки. Для того, чтобы стереть последний символ, нужно отправить символ #. При этом вместо стирания отобразится символ решетки.
# echo Hello, World#####Habr!
Hello, Habr!
Дальнейшее использование Unix только подтверждает первое впечатление. Единственный интерактивный текстовый редактор, ed, по современным меркам имеет плохой UX. Редактор не задает лишних вопросов для подтверждения действия, а если и задает, то весь вопрос умещается в один символ — вопросительный знак.
У Unix v7 есть команда, а у Unix v6 ее нет. Поэтому проще всего воспользоваться компилятором языка С и проверить наличие системного вызова chroot. Если его нет, то компилятор нам об этом скажет. К слову, первый стандарт языка С, называемый ANSI C, принят в 1989 году, а «на дворе» нашей операционной системы 1979 год.
Симулятор PDP-11 запускается с системным временем 22 сентября 1988 года. Внутреннее представление времени хранится в секундах в 32-битном типе данных. Таким образом, Unix v7 способен корректно обрабатывать дату вплоть до 7 февраля 2106 года.
Сделаем простую программу, которая вызовет chroot. По умолчанию компилятор производит компиляцию и линковку, что позволяет использовать функции без включения заголовочных файлов с определением функций. Хотя данная программа написана в совсем старом стиле, современные компиляторы способны ее «переварить».
main(argc, argv)
int argc;
char* argv[];
{
chroot("/tmp");
}
В Unix v7 сработало, а вот компилятор Unix v6 выводит ошибку:
# cc test.c
Undefined:
_chroot
Получается, что впервые chroot появился в Unix v7, примерно в 1979 году. В документации jail (8) к 4.4BSD есть сноска, которая гласит, что 18 марта 1982 года, примерно за полтора года до релиза 4.2BSD, Билл Джой (Bill Joy) добавил системный вызов chroot в BSD для удобства тестирования новых сборок.
Билл Джой — основатель Computer Systems Research Group, которая занималась ранними версиями Unix и BSD. Вероятно, что в Unix v7 системный вызов появился также благодаря Биллу с той же целью: удобство тестирования новых версий ОС.
Системный вызов появился достаточно давно. Рассмотрим, какие ему нашли применения.
Применения
Первое применение освещалось в начале статьи: системный администратор может «переключиться» в каталог с другой системой с целью восстановления. Это похоже на оригинальное применение chroot при тестировании новых сборок ОС, когда необходимо изолироваться от файлов текущей операционной системы.
Одно из возможных применений — ограничение программы в заданном каталоге. Так, популярный ftp-сервер VSFTPD позволяет «заточить» пользователя в его домашнем каталоге.
Более редкий случай — отслеживание злых намерений других пользователей интернета. Уильям Чесвик (William Cheswick) использовал chroot для создания «ловушки», в которую попался хакер.
Хотя системный вызов chroot влияет только на уровне файловых систем, он стал толчком к разработке утилиты jail в FreeBSD. Она ограничивала доступ не только к файловой системе, но также к сети, общей памяти и видимым переменным ядра. Таким образом, софт в «тюрьме» никак не мог навредить основной системе или другим программам в аналогичных клетках.
Вскоре концепт «тюрьмы» переехал в Linux, затем появилась реализация Linux Namespaces, а потом появились привычные контейнеры.
В современной документации к системному вызову отмечено, что его не следует использовать для изоляции программ и других решений, связанных с изоляцией программ.
Безопасность
Системный вызов chroot меняет один компонент системы разрешения имен на файловых системах. Рассмотрим абсолютный путь до абстрактного файла habr.png:
/home/web/html/static/habr.png
Операционная система разбирает путь по компонентам. Путь абсолютный, это значит, что он начинается с символа /. В обычных условиях магии не происходит. Но если сделать chroot (»/home/web/html»), то абсолютный путь до файла будет выглядеть так:
/static/habr.png
Если упростить, то при разрешении такого пути внутри chroot корень «подменится» на /home/web/html и файл будет найден. Однако это приводит нас к первой проблеме в безопасности, так как программы внутри chroot не могут знать о подмене окружения.
Рассмотрим гипотетическую ситуацию, что администратор сервера решил зачем-то «ограничить» своих пользователей и настроил делать chroot в домашний каталог пользователя при логине. Умный пользователь может принести с собой файл /etc/passwd и исполняемый файл su со своего личного компьютера и разместить их в домашнем каталоге.
Файл /etc/password использован в качестве простейшего примера. В современных системах используется /etc/shadow и более сложный механизм.
Пользователь находится в своем домашнем каталоге, где имеет максимальные права, поэтому он может создать любую структуру каталогов. Пользователь создает собственный файл /etc/password, который содержит хэш пароля от суперпользователя. Далее с помощью утилиты su, которая проверяет пароль с помощью ранее указанного файла, пользователь получает права суперпользователя. Именно из-за этого «фокуса» с повышением прав системный вызов chroot может выполнять только суперпользователь.
В 1999 году впервые была затронута тема побега из тюрьмы (jailbreak). Некорректная обработка вложенных chroot приводит к возможности выбраться из chroot в «оригинальный» корень. Несмотря на то, что эта уязвимость особенность осталась в ядре Linux по сей день, использовать ее затруднительно.
Уязвимость основана на том, что chroot подменяет корень, но не изменяет текущий каталог. Так, если сделать chroot (»/home/web/static/»), находясь в каталоге /home, то текущий рабочий каталог останется вне нового корня. В этом случае обработка ядром относительных путей не задействует новый корень и есть возможность добраться до корня системы. Общая последовательность выглядит так:
- получить права суперпользователя;
- создать каталог tmp, но не переходить в него;
- выполнить chroot («tmp»);
- выполнить переход в каталог »…» максимально возможное количество раз;
- выполнить chroot (».»);
- готово.
Рассмотрим в виде таблицы эти действия.
Так как chroot не меняет каталога, то после выполнения этого системного вызова попытка получить текущий рабочий каталог будет заканчиваться ошибкой «нет такого каталога», но это не помешает выполнить переход в родительский каталог. Излишнее количество попыток перейти в родительский каталог не помеха: корень сам себе родитель.
Тем не менее, для эксплуатации этой уязвимости нужны права суперпользователя, чтобы сделать вложенный chroot, что значительно усложняет эксплуатацию.
Заключение
Мы узнали, что chroot имеет большую историю и множество применений, но изначально появился во благо удобства программиста. История Unix — сложное и запутанное переплетение гениальных инженерных мыслей, коммерции и энтузиастов, продвигающих принцип открытости.
Несмотря на множество негативных факторов, у нас есть возможность прикоснуться к истокам семейства операционных систем UNIX и буквально «потрогать» историю на своих современных компьютерах.