第4章  Jail子系统

目录
4.1 Jail的系统结构
4.2 系统对被囚禁程序的限制
Evan Sarmiento版权 © 2001 Evan Sarmiento翻译:[email protected].

  在大多数UNIX®系统中,用户root是万能的。这也就增加了许多危险。 如果一个攻击者获得了一个系统中的root,就可以在他的指尖掌握系统中所有的功能。 在FreeBSD里,有一些sysctl项削弱了root的权限, 这样就可以将攻击者造成的损害减小到最低限度。这些安全功能中,有一种叫安全级。 另一种在FreeBSD 4.0及以后版本中提供的安全功能,就是jail(8)Jail将一个运行环境的文件树根切换到某一特定位置, 并且对这样环境中叉分生成的进程做出限制。例如, 一个被jail控制的进程不能影响这个jail之外的进程、不能使用一些特定的系统调用, 也就不能对主计算机造成破坏。

译者注: 英文单词“jail”的中文意思是“囚禁、监禁”。



  Jail已经成为一种新型的安全模型。 人们可以在jail中运行各种可能很脆弱的服务器程序,如Apache、BIND和sendmail。 这样一来,即使有攻击者取得了Jail中的root, 这最多让人们皱皱眉头,而不会使人们惊慌失措。 本文聚焦Jail的内部原理(源代码), 同时对于改进现役的jail代码提出建议。如果你正在寻找设置 Jail的指南性文档,我建议你阅读我的另一篇文章, 发表在Sys Admin Magazine, May 2001, 《Securing FreeBSD using Jail》。

4.1 Jail的系统结构

  Jail由两部分组成:用户级程序, 也就是jail(8);还有在内核中Jail的实现代码:jail(2) 系统调用和相关的约束。我将讨论用户级程序和Jail在内核中的实现原理。

4.1.1 用户级代码

  jail的用户级源代码在/usr/src/usr.sbin/jail, 由一个文件jail.c组成。这个程序有这些参数:jail的路径, 主机名,IP地址,还有需要执行的命令。

4.1.1.1 数据结构

  在jail.c中,我将最先关注的是一个重要结构体 struct jail j的申明;结构类型的申明包含在 /usr/include/sys/jail.h之中。

  jail结构的定义是:

/usr/include/sys/jail.h: 

struct jail {
        u_int32_t       version;
        char            *path;
        char            *hostname;
        u_int32_t       ip_number;
};

  正如你能看见的,传送给命令jail(8)的每个参数都在这里有对应的一项。 事实上,当命令jail(8)被执行时,这些参数才由命令行真正传入:

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

4.1.1.2 网络

  传给jail(8)的参数中有一个是IP地址。这是在网络上访问jail时的地址。 jail(8)将IP地址翻译成网络字节顺序,并存入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)“将指定的字符串当成一个Internet地址, 并将其转存到指定的结构体中”。inet_aton设定了结构体in, 之后in中的内容再用ntohl()翻译成主机字节顺序。

4.1.1.3 囚禁进程

  最后,用户级程序囚禁进程,执行指定的命令。现在Jail自身变成了一个被囚禁的进程, 叉分生成一个子进程。这个子进程用execv(3)执行用户指定的命令。

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

  正如你能看见的,函数jail被调用,参数是结构体jail中被填入数据项, 而如前所述,这些数据项又来自jail(8)的命令行参数。 最后,执行了用户指定的命令。下面我将开始讨论Jail在内核中的实现。

4.1.2 相关的内核源代码

  现在我们来看文件/usr/src/sys/kern/kern_jail.c。 在这里定义了jail的系统调用、相关的sysctl项,还有网络函数。

4.1.2.1 sysctl项

  在kern_jail.c里定义了如下sysctl项:

/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");
    /* Jail中的进程可设定自身的主机名 */

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
");
    /* Jail中的进程被限制只能建立UNIX套接字、IPv4套接字、路由套接字 */

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");
    /* Jail中的进程可以使用System V进程间通讯原语 */

  这些sysctl项中的每一个都可以用命令sysctl访问。在整个内核中, 这些sysctl项按名称标识。例如,上述第一个sysctl项的名字是 jail.set.hostname.allowed.

4.1.2.2 jail(2)系统调用

  像所有的系统调用一样,系统调用jail(2)带有两个参数, struct proc *pstruct jail_args *uapp是一个指向proc结构体的指针,描述调用这个系统调用的进程。 此时,uap指向一个结构体,这个结构体指定了从用户级程序 jail.c要传送给jail(2)的参数。 在前面我讲述用户级程序时,你已经看见一个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结构体。 然后,系统调用使用copyin()将jail结构体复制到内核内存空间中。 copyin()有三个参数:要复制进内核内存空间的数据 uap->jail,在内核内存空间存放数据的j, 以及数据的大小。Jail结构体uap->jail被复制进内核内存空间, 并被存放在另一个jail结构体j里。

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

  在jail.h中定义了另一个重要的结构体型prison(pr)。 结构体prison只被用在内核空间中。系统调用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)至指定路径。 函数chroot()有两个参数。第一个是p, 表示调用它的进程, 第二个是指向结构体chroot的指针。结构体chroot包含了新的文件系统逻辑根。 正如你看见的,结构体jail中指定的路径被复制到结构体chroot中, 并在后续操作中被使用。

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

  这随后的三行在源代码中非常重要,因为他们指定了内核如何将一个 进程判别为被囚禁的进程。在UNIX系统中,每一个进程都由它自己的proc结构体描述。 你可以在/usr/include/sys/proc.h中看见整个proc结构体。 例如,在任何系统调用中,参数p实际上是个指向进程的proc结构体的指针, 正如前面所说的那样。结构体proc包含的成员可以描述所有者的身份 (p_cred),进程资源限制(p_limit), 等等。在进程结构体的定义中,还有一个指向prison结构体的指针 (p_prison)。

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

  在kern_jail.c中,函数然后复制pr结构体到 p->p_prison中。pr结构体里填充了来自原始jail 结构体中的所有信息。随后,将p->p_flag与恒量 P_JAILED进行按位或运算, 这指明调用进程现在被认为是被囚禁的。每个进程的父进程, 都曾在Jail中进行了叉分(fork)。这父进程正是程序jail本身, 它调用了jail(2)系统调用。当其它程序通过execve()执行时, 就从父进程那里继承proc结构体,因而其p->p_flag 中Jail的标志位被置位,并且p->p_prison 结构体中被填有内容。

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

  当一个进程被从其父进程叉分来的时候,系统调用fork(2) 将用不同的方式处理被囚禁的进程。在系统调用fork中用到两个指向 proc结构体的指针p1p2p1指向父进程的 proc结构体,p2 指向子进程的尚未被填充的proc结构体。 在结构体间复制完所有相关数据之后,fork(2) 检查p2指向的结构体成员 p_prison是否已被填充。如果已被填充, 就将pr.ref的值增加1, 并给子进程的p_flag设上Jail标记。

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

本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.

如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<[email protected]>.
关于本文档的问题请发信联系 <[email protected]>.