Системный вызов chroot()
Chroot — сокращение от change root, то есть «изменить корень». Как можно понять из названия, это механизм изоляции процессов в специальной части файловой системы, то есть формирование отдельной от всей системы иерархии каталогов для работы конкретного приложения.
Системный вызов chroot() был добавлен еще в Unix Version 7 по меркам ИТ достаточно давно — 45 лет назад, в 1979 году, и с тех пор используется в UNIX-подобных операционных системах для создания безопасной (безопасной для остальной части системы) среды выполнения приложений.
$ man 2 chroot
Chroot-изированный процесс, например сетевая служба, может подвергаться сетевым атакам извне, и в случае успешности этих атак возможность эскалации и распространения атаки внутри системы через обращение к файлам будет ограничена той частью файловой системы, где за счет chroot заперт атакованный процесс.
Механизм chroot-изации (изоляции процесса на ФС) является предшественником таких технологий в ядре Linux, как namespaces и cgroups, лежащих в основе современной контейнеризации, а именно docker, CRI-O, lxc/lxd.
Также механизм chroot-изации применяется в решениях по созданию изолированных окружений — «песочниц» (sanbox, sandboxing), подходящих для запуска недоверенных приложений и вообще для исследования вредоносного программного обеспечения. Примеры таких sandbox-решений для ОС на основе ядра Linux — firejail и bubblewrap.
Кроме контейнеризации и «песочниц», chroot-изация оказалась полезной при создании изолированных от системы сред для независимого от системы развертывания приложений со всеми их зависимостями. В ОС на основе ядра Linux мы можем увидеть их в таких решениях, как snap и flatpak.
В других UNIX-подобных операционных системах наличие системного вызова chroot() помогло реализовать их собственные контейнеры и песочницы, например FreeBSD jail, Solaris Zones.
Использование системного вызова chroot()
Давайте подготовим отдельную chroot-среду для запуска приложения и запустим там специальное приложение, позволяющее оценить особенности этой среды. Создадим chroot-каталог с двумя подкаталогами. По имени подкаталога мы легко узнаем, что попали в него.
$ mkdir -p /tmp/chroot/{subdir1,subdir2}
Имя каталога chroot-изации произвольно, каталог /tmp выбран для его размещения исключительно ради удобства.
Код приложения (chroot.c) на языке C представлен ниже:
#include <stdio.h>
#include <unistd.h>
#include <dirent.h>
int main(void) {
// Текущий каталог процесса до chroot()
char t_cwd[PATH_MAX];
getcwd(t_cwd, sizeof(t_cwd));
printf("Текущий каталог до chroot(): %sn", t_cwd);
// Выполнение chroot()
chdir("/tmp/chroot/");
if (chroot("/tmp/chroot/") != 0) {
perror("chroot /tmp/chroot/");
return 1;
}
// Текущий каталог процесса после chroot()
char a_cwd[PATH_MAX];
getcwd(a_cwd, sizeof(a_cwd));
printf("Текущий каталог после chroot(): %sn", a_cwd);
// Получаем указатель на корень файловой системы
struct dirent *de;
DIR *dr = opendir("/");
// Смотрим содержание корня ФС
while ((de = readdir(dr)) != NULL)
printf("%sn", de->d_name);
// Пытаемся открыть /etc/passwd хостовой ФС
FILE *f;
f = fopen("/etc/passwd", "r");
if (f == NULL) {
perror("/etc/passwd");
return 1;
} else {
char buf[100];
while (fgets(buf, sizeof(buf), f)) {
printf("%s", buf);
}
}
return 0;
}
Сборка и запуск указанного примера выполняется командами:
$ make chroot
$ sudo ./chroot
Запуск должен производиться под учетной записью суперпользователя, так как использование системного вызова chroot() требует наличие системной привилегии CAP_SYS_CHROOT, по умолчанию отсутствующей у процессов обычных пользователей.
Данное приложение:
- Выводит текущий каталог, в котором было запущено приложение.
- Выполняет системный вызов chroot().
- Выводит текущий каталог после выполнения системного вызова chroot().
- Пытается перейти в корневой каталог (“/”).
- Выводит содержимое текущего каталога.
- Пытается открыть файл с базой учетных записей пользователей по его абсолютному пути (/etc/passwd).
После сборки и выполнения приложения по результату в пунктах 4–7 вы увидите, что приложение оказалось изолировано в каталоге /tmp/chroot, видит только его содержимое (два созданных подкаталога) и не имеет возможности выхода за пределы каталога chroot-изации, а также не может открывать файлы по абсолютным путям за пределами этого каталога.
Использование chroot() разработчиками сетевых приложений
Изоляцией chroot-изированных процессов активно пользуются разработчики сетевых служб для повышения безопасности своих продуктов. Основной процесс службы обычно запускается системой стандартно, без chroot-изации, выполняет необходимые ему действия, работая с системной иерархией каталогов, например считывает свою конфигурацию из каталога /etc. После чего переходит системным вызовам chroot() в отдельный, специально подготовленный каталог и уже в нем, изолированно от основной части системы, начинает потенциально уязвимое для сетевых атак взаимодействие с внешним миром.
Кстати, порожденные процессы наследуют каталог chroot-изации от родительского процесса, поэтому, когда сетевое приложение использует для обработки пользовательских подключений порожденные процессы, а это достаточно часто используемый подход при разработке сетевых приложений, то порожденные процессы также остаются в установленном их родителем каталоге chroot-изации.
Финальные соображения
Создаваемая при помощи chroot() изоляция процессов не является абсолютной. Известны работающие методы выхода из chroot с использованием обращений к специальным файлам устройств и с рядом других возможностей системы.
Именно поэтому полагаться безопасность приложения исключительно по причине его работы в chroot-окружении не стоит. Необходимо помнить и о других уровнях обеспечения информационной безопасности системы, например о своевременной установке обновлений безопасности.