Утилита времен «динозавров»: история системного вызова chroot и его применение в современности

в 11:19, , рубрики: C, chroot, linux, UNIX, Блог компании Selectel
Утилита времен «динозавров»: история системного вызова chroot и его применение в современности - 1

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

Этот простой системный вызов подменяет местонахождение «корня» файловой системы, «заключая» программу в специально созданное ограниченное окружение. Самая распространенная ситуация — восстановление загрузки операционной системы с помощью live-образа. Но при создании chroot о таком применении не задумывались.

Чтобы найти истоки появления chroot в *NIX-подобных операционных системах, нужно пройти немалый путь по истории IT. В этой статье я расскажу про появление chroot и его применение в современном мире. А еще покажу проекты, которые позволяют прикоснуться к операционным системам эпохи, когда Интернета не было.

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

Что ты такое?


Сперва познакомимся с «героем» рассказа. Чаще всего администраторы сталкиваются с утилитой chroot, которая позволяет подменить местонахождение корня для дочерних программ.

Представим ситуацию: перед системным администратором стоит задача восстановить ОС после сбоя. Администратор достает из кармана загрузочную флешку и начинается магия. Провайдеры инфраструктуры для экстренных случаев предоставляют заготовленный образ для восстановления — Rescue. Для выделенных серверов Selectel загрузить образ восстановления можно прямо по сети.

Утилита времен «динозавров»: история системного вызова chroot и его применение в современности - 2


Из 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 <torvalds@ppc970.osdl.org>
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

Утилита времен «динозавров»: история системного вызова chroot и его применение в современности - 3

Генеалогическое древо 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 есть команда man, а у 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 и его применение в современности - 4

Так как chroot не меняет каталога, то после выполнения этого системного вызова попытка получить текущий рабочий каталог будет заканчиваться ошибкой «нет такого каталога», но это не помешает выполнить переход в родительский каталог. Излишнее количество попыток перейти в родительский каталог не помеха: корень сам себе родитель.

Тем не менее, для эксплуатации этой уязвимости нужны права суперпользователя, чтобы сделать вложенный chroot, что значительно усложняет эксплуатацию.

Заключение


Мы узнали, что chroot имеет большую историю и множество применений, но изначально появился во благо удобства программиста. История Unix — сложное и запутанное переплетение гениальных инженерных мыслей, коммерции и энтузиастов, продвигающих принцип открытости.

Несмотря на множество негативных факторов, у нас есть возможность прикоснуться к истокам семейства операционных систем UNIX и буквально «потрогать» историю на своих современных компьютерах.

Автор: Владимир

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js