Эта статья является переводом. В ней рассматриваются различия в результатах применения команд chroot
«в лоб» и chroot
, применённой после pivot_root
. Это поможет разобраться, почему именно pivot_root
используется в контейнеризации. Передаём слово автору.
Привет всем! В одной из предыдущих статей о безопасности Linux я упоминал о проблемах с chroot
и о том, как злоумышленник может ими воспользоваться с помощью «техники двойного chroot'а». Команда разработчиков Docker решила отказаться от chroot
и перейти на pivot_root
, поскольку иногда при отладке в контейнере требуются привилегии суперпользователя (или разрешённый CAP_SYS_CHROOT), что делает chroot
неподходящим вариантом.
Что такое pivot_root?
При запуске контейнеров в разных пространствах имён необходимо следить за безопасностью, чтобы два процесса, запущенные в разных пространствах имён, не конфликтовали друг с другом. Поскольку chroot
применяется к активному процессу и его дочерним процессам, но не изменяет точку монтирования корневой директории (root) и таблицу монтирования в глобальном пространстве имён, из его «тюрьмы» легко вырваться и получить доступ к файловой системе хоста. Я рассказывал об этом в своей предыдущей статье о выходе за пределы chroot
. Рекомендую с ней ознакомиться.
Изменение корневой директории во внешнем пространстве имён чревато кучей проблем, поскольку процессы обычно не рассчитывают на внезапное изменение своей корневой директории. Я не проверял это на практике и не совсем уверен, но всё же полагаю, что именно так и будет.
runc также создаёт новое пространство имён mount, после чего выполняет в нём pivot_root
, меняя корневую директорию. Точнее, он изменяет корень в новом пространстве имён на new_root
и перемещает старый корень за пределы пространства имён в каталог put_old
. То есть если поискать корневую директорию процесса вне пространства имён, то она будет ссылаться на /
, а не на более глубокую иерархию в файловой системе, подобную той, которая получилась бы с chroot.
Не волнуйтесь, если пока не понимаете, о чём речь. Это лишь теоретическая часть. Уверен, вы во всём разберётесь, когда я расскажу о проблемах с chroot
.
Проблемы с chroot в контейнерах
В заметке «Управление ресурсами Docker в деталях» я показал, как с помощью chroot
можно создавать контейнеры без использования runc или containerd. Но достаточно ли этого? А как насчёт безопасности и побега из chroot-окружения? Что ж, давайте посмотрим.
Создайте файл secret вне пространства имён и сохраните его в /bin/secret
. В него можно записать что угодно. Я воспользовался $RANDOM$RANDOM
, чтобы не тратить время на размышления:
![Создайте файл secret за пределами пространства имён Создайте файл secret за пределами пространства имён](https://habrastorage.org/getpro/habr/upload_files/349/2ee/4d4/3492ee4d4dfd443c1748fd138eb27c81.png)
Теперь переключитесь в chroot-окружение и попробуйте вывести список файлов в директории /bin
. В нём не будет никакого файла secret. Это совершенно логично, поскольку для текущего процесса и его дочерних процессов корневой каталог изменён, /bin/
в chroot
больше не указывает на оригинальный /bin/
за пределами пространства имён.
![Проверяем версию Python Проверяем версию Python](https://habrastorage.org/getpro/habr/upload_files/10f/aeb/293/10faeb293c110bf35bf37d69f3a3da4e.png)
Python уже установлен в пространстве имён, а оболочка запущена от имени суперпользователя. Теперь скопируйте эксплойт из моей статьи и вставьте его в новый файл:
import os
if not os.path.exists("chroot"):
os.mkdir("chroot")
os.chroot("chroot")
for _ in range(1000):
os.chdir("..")
os.chroot(".")
os.system("/bin/bash")
Эксплойт запустит Bash в корневой директории вне пространства имён, несмотря на ограничения chroot
. Результат будет похож на скриншот ниже:
![Доступ к файлу secret за пределами окружения контейнера Доступ к файлу secret за пределами окружения контейнера](https://habrastorage.org/getpro/habr/upload_files/73c/684/9ad/73c6849ad21e723ffe8795ecfc3003d6.png)
Этот второй chroot
возможен, потому что пользователь по сути работает под UID суперпользователя из-за флага -r/--map-root-user
в unshare
. Он нужен, чтобы примонтировать procfs
в /proc
.
Я поленился всё настраивать в AttackDefense и воспользовался следующей командой для создания уязвимого пространства имён (без контрольных групп):
unshare --mount --pid --user --map-root-user --fork \
--mount-proc chroot dockerfs sh -c "mount -t proc proc /proc && bash"
Дочерний процесс в этом пространстве имён, который является самым первым процессом sh, естественно, наследует текущую рабочую директорию и корневую директорию от родительского процесса. Но если посмотреть на тот же процесс снаружи пространства имён, то и корень, и текущая рабочая директория (cwd) будут привязаны к /home/terabyte/dockerfs
, а не к /
.
![Корень и cwd процесса внутри пространства имён и вне него Корень и cwd процесса внутри пространства имён и вне него](https://habrastorage.org/getpro/habr/upload_files/c11/dc2/a47/c11dc2a4720160c200fb1008b42ac9c6.png)
Исправляем уязвимость с помощью pivot_root
Для успешного выполнения pivot_root
необходимо запустить новый Bash в пространстве имён без chroot
, настроить procfs и сделать bind mount для dockerfs, заменив корень внутри пространства имён mount.
![Создание пространства имён и монтирование procfs и dockerfs Создание пространства имён и монтирование procfs и dockerfs](https://habrastorage.org/getpro/habr/upload_files/1ef/35e/af1/1ef35eaf11e193d0f612654bce0c4501.png)
Также необходимо, чтобы и new_root
, и put_old
были директориями в файловой системе, причем директория, ссылающаяся на put_old
, должна быть дочерней для new_root
. Можно создать директорию в dockerfs, а затем сделать pivot_root
, как показано ниже. После этого текущая директория станет корневой в таблице монтирования внутри пространства имён.
![pivot_root в действии pivot_root в действии](https://habrastorage.org/getpro/habr/upload_files/ad1/e71/100/ad1e711002b4898ff230c294b6f3c157.gif)
Если заглянуть в директорию .oldroot, то она теперь будет содержать старую корневую файловую систему, и эта информация также будет отражена в таблице монтирования внутри пространства имён, причём корневая директория будет заменена на /.oldroot. Разве это не странно?
![Изменения в новом пространстве имён mount, старый корень «помещён» в .oldroot Изменения в новом пространстве имён mount, старый корень «помещён» в .oldroot](https://habrastorage.org/getpro/habr/upload_files/4c8/162/874/4c81628745bd0da6c300edba6d6aa79e.png)
Процесс вне этого пространства имён работает от имени пользователя с низкими привилегиями (terabyte), поэтому любые действия с высокими привилегиями будут запрещены, но он всё равно сможет выполнять чтение и запись в домашнюю директорию пользователя (/home/terabyte). Это всё ещё не та истинная изоляция, к которой мы стремимся.
![Действия в старом корне, требующие привилегий, будут отклонены Действия в старом корне, требующие привилегий, будут отклонены](https://habrastorage.org/getpro/habr/upload_files/d5e/66f/8cc/d5e66f8cc185bf5540448ca920ea5bb8.png)
Поскольку новая rootfs уже готова, можно отмонтировать .oldroot, в результате чего директория станет пустой. Чтобы всё заработало, перемонтируйте procfs
в директорию /proc
— она будет использоваться для получения таблицы монтирования внутри пространства имён. После этого можно безопасно выполнить chroot
в текущую файловую систему, и на этот раз старый эксплойт не сработает.
![Отмонтируйте старую корневую файловую систему и выполните chroot на корень текущей файловой системы Отмонтируйте старую корневую файловую систему и выполните chroot на корень текущей файловой системы](https://habrastorage.org/getpro/habr/upload_files/c77/289/470/c77289470937ec007f611a1e910a4ba6.png)
В таблице монтирования теперь будут только эти две записи (нет никакого способа — по крайней мере, известного мне — вернуться к старым точкам монтирования):
/dev/nvme0n1p3 on / type ext4 (rw,noatime)
proc on /proc type proc (rw,relatime)
Заключение
Подведем итог. Команда chroot
изменяет корневую директорию для активного процесса и его дочерних процессов, но не изменяет точку монтирования корневой директории и таблицу монтирования в глобальном пространстве имён, что создаёт угрозу безопасности. pivot_root
изменяет корневую директорию в новом пространстве имён и перемещает старый корень за пределы пространства имён, что обеспечивает лучшую изоляцию и безопасность.
P. S.
Читайте также в нашем блоге: