Chapter 12. Подсистема Jail

Table of Contents
12.1. Архитектура
12.2. Ограничения
12.3. Jail NG
Evan SarmientoCopyright © 2001 by Evan Sarmiento

Перевод на русский язык: Сергей Любка ()

На большинстве UNIX систем, пользователь root имеет неограниченные права. Это потенциально небезопасно. Если атакующий сумеет получить права root, любая функция системы будет под его контролем. FreeBSD имеет ряд параметров ядра, ограничивающих безраздельные права root для уменьшения возможного ущерба от атакующего. Например, одним из таких параметров является уровень защиты (secure level). Начиная с версии FreeBSD 4.0, другим таким параметром является jail(8). Jail (jail в переводе - тюрьма, заключение) делает chroot окружения, и накладывает определенные ограничения на процессы, которые в нем порождены. Например, jailed процесс (т.е. процесс, сделавший вызов jail) не может влиять на процессы вне jail, делать некоторые системные вызовы, или каким-либо образом повреждать другие части ОС.

Jail становится новой моделью защиты. Администраторы запускают потенциально уязвимые сервисы, такие как Apache, BIBD и sendmail, внутри jail; и если атакующий даже и получит права root внутри Jail, то это не приведет к краху всей системы. Эта статья концентрируется на внутренней реализации Jail и Jail NG, а также предложит некоторые улучшения к теперешней реализации. Если Вас интересует административная сторона установки Jail, я рекомендую просмотреть мою статью в Sys Admin Magazine за май 2001, под заглавием "Securing FreeBSD using Jail".

12.1. Архитектура

Jail состоит из двух частей: непривилегированной (user-space) программы, jail, и кода в ядре: системного вызова jail и соответствующих ограничений. Сначала я рассмотрю user-space утилиту jail, а затем то, как jail реализованa в ядре.

12.1.1. Утилита jail

Исходный код утилиты jail находится в каталоге /usr/src/usr.sbin/jail, в файле jail.c. Утилита принимает следующие аргументы командной строки: путь к jail, имя хоста, ip адрес, и команду.

12.1.1.1. Структуры данных

В файле jail.c первой вещью, которую я хотел бы отметить, это декларация важной структуры, struct jail j, которая включена из файла /usr/include/sys/jail.h.

Вот определение это структуры:

    /usr/include/sys/jail.h:
    
    struct jail {
            u_int32_t       version;
            char            *path;
            char            *hostname;
            u_int32_t       ip_number;
    };

Как можно заметить, в ней есть поле для каждого из аргументов, передаваемого утилите jail, и они выставляются при запуске jail.

    /usr/src/usr.sbin/jail.c
    j.version = 0;
    j.path = argv[1];
    j.hostname = argv[2];

12.1.1.2. Сеть

Одним из аргументов, передаваемых jail, есть ip адрес, по которому jail может быть доступен из сети. Jail переводит переданный ip адрес в big endian формат (network byte order), и сохраняет его в j (структуре jail).

    /usr/src/usr.sbin/jail/jail.c:
    struct in.addr in;
    ...
    i = inet.aton(argv[3], &in);
    ...
    j.ip.number = ntohl(in.s.addr);

Функция inet_aton(3) интерпретирует переданную ей строку как интернет адрес, и сохраняет его по переданному указателю на структуру in_addr. Функция ntohl() изменяет порядок следования байтов на сетевой (big endian), и получившееся значение сохраняется в поле ip адреса структуры jail.

12.1.1.3. Заключение (jailing) процесса

Наконец, утилита jail выполняет системный вызов jail, чем ``заключает в тюрьму'' сама себя, и порождает дочерний процесс, который затем выполняет команду, указанную в командной строке jail, используя вызов execv(3):

    /usr/src/sys/usr.sbin/jail/jail.c
    i = jail(&j);
    ...
    i = execv(argv[4], argv + 4);

Системному вызову jail, как видно из листинга, передается указатель на заполненную структуру jail. Теперь я собираюсь обсудить, как Jail реализован в ядре.

12.1.2. Код jail в ядре

Заглянем в файл /usr/src/sys/kern/kern_jail.c. Это файл, в котором определены системный вызов jail, соответствующие параметры ядра (sysctls), и сетевые функции.

12.1.2.1. Параметры ядра (sysctls)

В файле kern_jail.c определены следующие параметры ядра:

    /usr/src/sys/kern/kern_jail.c:
    
    int     jail_set_hostname_allowed = 1;
    SYSCTL_INT(_jail, OID_AUTO, set_hostname_allowed, CTLFLAG_RW,
        &jail_set_hostname_allowed, 0,
        "Processes in jail can set their hostnames");
    
    int     jail_socket_unixiproute_only = 1;
    SYSCTL_INT(_jail, OID_AUTO, socket_unixiproute_only, CTLFLAG_RW,
        &jail_socket_unixiproute_only, 0,
        "Processes in jail are limited to creating UNIX/IPv4/route sockets only
    ");
    
    int     jail_sysvipc_allowed = 0;
    SYSCTL_INT(_jail, OID_AUTO, sysvipc_allowed, CTLFLAG_RW,
        &jail_sysvipc_allowed, 0,
        "Processes in jail can use System V IPC primitives");

Каждый из этих параметров доступен пользователю через утилиту sysctl. В ядре эти параметры распознаются по уникальному имени; например, имя первого параметра будет jail.set.hostname.allowed.

12.1.2.2. Системный вызов jail(2)

Как и все другие системные вызовы, вызов jail(2) принимает два аргумента, struct proc *p и struct jail_args *uap. p - это указатель на структуру proc текущего процесса, а uap - это аргумент, переданный утилитой jail системному вызову jail(2):

    /usr/src/sys/kern/kern_jail.c:
    int
    jail(p, uap)
            struct proc *p;
            struct jail_args /* {
                    syscallarg(struct jail *) jail;
            } */ *uap;

Таким образом, uap->jail будет указывать на структуру jail, переданную системному вызову. Далее, структура jail копируется в адресное пространство ядра с помощью функции copyin(), которая принимает три аргумента: данные, которые необходимо скопировать (uap->jail), где их сохранить (j) и размер копируемых данных. Структура jail, переданная как параметр системному вызову, копируется в пространство ядра и сохраняется в другой структуре jail, j.

    /usr/src/sys/kern/kern_jail.c: 
    error = copyin(uap->jail, &j, sizeof j);

В файле jail.h определена другая важная структура - структура prison (pr). Эта структура используется исключительно ядром. Системный вызов jail(2) копирует все из структуры jail в структуру prison. Вот определение структуры prison:

    /usr/include/sys/jail.h:
    struct prison {
            int             pr_ref;
            char            pr_host[MAXHOSTNAMELEN];
            u_int32_t       pr_ip;
            void            *pr_linux;
    };

Далее, системный вызов jail() выделяет память для структуры prison и копирует данные:

    /usr/src/sys/kern/kern_jail.c:
     MALLOC(pr, struct prison *, sizeof *pr , M_PRISON, M_WAITOK);
     bzero((caddr_t)pr, sizeof *pr);
     error = copyinstr(j.hostname, &pr->pr_host, sizeof pr->pr_host, 0);
     if (error)
             goto bail;

Наконец, jail() делает вызов chroot(2) для указанного пути. Вызову chroot() передаются два аргумента: первый - это p, который представляет вызвавший процесс, а второй - указатель на структуру chroot args. Структура chroot args содержит путь, который станет новым корнем. Путь, указанный в структуре jail, копируется в структуру chroot args и используется далее:

    /usr/src/sys/kern/kern_jail.c:
    ca.path = j.path;
    error = chroot(p, &ca);

Следующие три строки кода очень важны, поскольку указывают, как ядро распознает jailed процесс. Каждый процесс в UNIX описывается своей структурой proc. Определение структуры proc можно посмотреть в /usr/include/sys/proc.h. К примеру, аргумент p, передаваемый в каждый системный вызов есть на самом деле указатель на эту структуру, как было указано выше. Структура proc содержит поля, описывающие владельца процесса (p_cred), лимиты ресурсов (p_limit), и так далее. В определении структуры есть также указатель на структуру prison (p_prison):

    /usr/include/sys/proc.h: 
    struct proc {
    ...
    struct prison *p_prison;
    ...
    };

В файле kern_jail.c, вызов jail() копирует структуру pr, заполненную в соответствии с информацией из структуры jail, в структуру p->p_prison. Затем делается побитовое OR p->p_flag и константы P_JAILED, что в дальнейшем даст возможность распознать этот процесс как jailed. Родительский процесс для любого дочернего процесса внутри jail есть программа jail. Когда дочерний процесс делает execve(), он наследует поля структуры proc от родительского, и, таким образом, также имеет в поле p->p_flag выставленный флаг P_JAILED, и заполненную структуру p->p_prison.

    /usr/src/sys/kern/kern_jail.c
    p->p.prison = pr;
    p->p.flag --= P.JAILED;

Когда процесс порождает дочерний процесс, системный вызов fork(2) работает несколько иначе для процесса в jail. fork(2) принимает два аргумента - указателя на структуру proc, p1 и p2. Первый указатель, p1, указывает на структуру proc родительского процесса, а второй, p2 - дочернего. Структура, на которую указывает p2, не заполнена. После копирования всех необходимых данных между структурами, fork(2) проверяет, не заполнена ли структура p->p_prison у дочернего процесса, и если заполнена, то увеличивает счетчик pr.ref на единицу и выставляет флаг P_JAILED в поле p_flag:

    /usr/src/sys/kern/kern_fork.c:
    if (p2->p_prison) {
            p2->p_prison->pr_ref++;
            p2->p_flag |= P_JAILED;
    }