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

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

Проверяем версию Python
Проверяем версию Python

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 за пределами окружения контейнера

Этот второй 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 процесса внутри пространства имён и вне него

Исправляем уязвимость с помощью pivot_root

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

Создание пространства имён и монтирование procfs и dockerfs
Создание пространства имён и монтирование procfs и dockerfs

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

pivot_root в действии
pivot_root в действии

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

Изменения в новом пространстве имён mount, старый корень «помещён»  в .oldroot
Изменения в новом пространстве имён mount, старый корень «помещён»  в .oldroot

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

Действия в старом корне, требующие привилегий, будут отклонены
Действия в старом корне, требующие привилегий, будут отклонены

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

Отмонтируйте старую корневую файловую систему и выполните chroot на корень текущей файловой системы
Отмонтируйте старую корневую файловую систему и выполните chroot на корень текущей файловой системы

В таблице монтирования теперь будут только эти две записи (нет никакого способа — по крайней мере, известного мне — вернуться к старым точкам монтирования):

/dev/nvme0n1p3 on / type ext4 (rw,noatime)
proc on /proc type proc (rw,relatime)

Заключение

Подведем итог. Команда chroot изменяет корневую директорию для активного процесса и его дочерних процессов, но не изменяет точку монтирования корневой директории и таблицу монтирования в глобальном пространстве имён, что создаёт угрозу безопасности. pivot_root изменяет корневую директорию в новом пространстве имён и перемещает старый корень за пределы пространства имён, что обеспечивает лучшую изоляцию и безопасность.

P. S.

Читайте также в нашем блоге:

Комментарии (0)