Эта статья является переводом. В ней рассматриваются различия в результатах применения команд 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
, чтобы не тратить время на размышления:

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

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
. Результат будет похож на скриншот ниже:

Этот второй 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
, а не к /
.

Исправляем уязвимость с помощью pivot_root
Для успешного выполнения pivot_root
необходимо запустить новый Bash в пространстве имён без chroot
, настроить procfs и сделать bind mount для dockerfs, заменив корень внутри пространства имён mount.

Также необходимо, чтобы и new_root
, и put_old
были директориями в файловой системе, причем директория, ссылающаяся на put_old
, должна быть дочерней для new_root
. Можно создать директорию в dockerfs, а затем сделать pivot_root
, как показано ниже. После этого текущая директория станет корневой в таблице монтирования внутри пространства имён.

Если заглянуть в директорию .oldroot, то она теперь будет содержать старую корневую файловую систему, и эта информация также будет отражена в таблице монтирования внутри пространства имён, причём корневая директория будет заменена на /.oldroot. Разве это не странно?

Процесс вне этого пространства имён работает от имени пользователя с низкими привилегиями (terabyte), поэтому любые действия с высокими привилегиями будут запрещены, но он всё равно сможет выполнять чтение и запись в домашнюю директорию пользователя (/home/terabyte). Это всё ещё не та истинная изоляция, к которой мы стремимся.

Поскольку новая rootfs уже готова, можно отмонтировать .oldroot, в результате чего директория станет пустой. Чтобы всё заработало, перемонтируйте procfs
в директорию /proc
— она будет использоваться для получения таблицы монтирования внутри пространства имён. После этого можно безопасно выполнить chroot
в текущую файловую систему, и на этот раз старый эксплойт не сработает.

В таблице монтирования теперь будут только эти две записи (нет никакого способа — по крайней мере, известного мне — вернуться к старым точкам монтирования):
/dev/nvme0n1p3 on / type ext4 (rw,noatime)
proc on /proc type proc (rw,relatime)
Заключение
Подведем итог. Команда chroot
изменяет корневую директорию для активного процесса и его дочерних процессов, но не изменяет точку монтирования корневой директории и таблицу монтирования в глобальном пространстве имён, что создаёт угрозу безопасности. pivot_root
изменяет корневую директорию в новом пространстве имён и перемещает старый корень за пределы пространства имён, что обеспечивает лучшую изоляцию и безопасность.
P. S.
Читайте также в нашем блоге:
VADemon
https://www.man7.org/linux/man-pages/man2/pivot_root.2.html