在大多数UNIX®系统中,用户root是万能的。这也就增加了许多危险。 如果一个攻击者获得了一个系统中的root,就可以在他的指尖掌握系统中所有的功能。 在FreeBSD里,有一些sysctl项削弱了root的权限, 这样就可以将攻击者造成的损害减小到最低限度。这些安全功能中,有一种叫安全级别。 另一种在FreeBSD 4.0及以后版本中提供的安全功能,就是jail(8)。 Jail将一个运行环境的文件树根切换到某一特定位置, 并且对这样环境中叉分生成的进程做出限制。例如, 一个被监禁的进程不能影响这个jail之外的进程、不能使用一些特定的系统调用, 也就不能对主计算机造成破坏。
译者注: 英文单词“jail”的中文意思是“囚禁、监禁”。
Jail已经成为一种新型的安全模型。 人们可以在jail中运行各种可能很脆弱的服务器程序,如Apache、 BIND和sendmail。 这样一来,即使有攻击者取得了jail中的root, 这最多让人们皱皱眉头,而不会使人们惊慌失措。 本文主要关注jail的内部原理(源代码)。 如果你正在寻找设置Jail的指南性文档, 我建议你阅读我的另一篇文章,发表在Sys Admin Magazine, May 2001, 《Securing FreeBSD using Jail》。
Jail由两部分组成:用户级程序, 也就是jail(8);还有在内核中Jail的实现代码:jail(2) 系统调用和相关的约束。我将讨论用户级程序和jail在内核中的实现原理。
Jail的用户级源代码在/usr/src/usr.sbin/jail, 由一个文件jail.c组成。这个程序有这些参数:jail的路径, 主机名,IP地址,还有需要执行的命令。
在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 char path[PATH_MAX]; ... if(realpath(argv[0], path) == NULL) err(1, "realpath: %s", argv[0]); if (chdir(path) != 0) err(1, "chdir: %s", path); memset(&j, 0, sizeof(j)); j.version = 0; j.path = path; j.hostname = argv[1];
传给jail(8)的参数中有一个是IP地址。这是在网络上访问jail时的地址。 jail(8)将IP地址翻译成网络字节顺序,并存入j(jail类型的结构体)。
/usr/src/usr.sbin/jail/jail.c: struct in_addr in; ... if (inet_aton(argv[2], &in) == 0) errx(1, "Could not make sense of ip-number: %s", argv[2]); j.ip_number = ntohl(in.s_addr);
函数inet_aton(3)“将指定的字符串解释为一个Internet地址, 并将其转存到指定的结构体中”。inet_aton(3)设定了结构体in, 之后in中的内容再用ntohl(3)转换成主机字节顺序, 并置入jail结构体的ip_number成员。
最后,用户级程序囚禁进程。现在Jail自身变成了一个被囚禁的进程, 并使用execv(3)执行用户指定的命令。
/usr/src/usr.sbin/jail/jail.c i = jail(&j); ... if (execv(argv[3], argv + 3) != 0) err(1, "execv: %s", argv[3]);
正如你所见,函数jail()被调用,参数是结构体jail中被填入数据项, 而如前所述,这些数据项又来自jail(8)的命令行参数。 最后,执行了用户指定的命令。下面我将开始讨论jail在内核中的实现。
现在我们来看文件/usr/src/sys/kern/kern_jail.c。 在这里定义了jail(2)的系统调用、相关的sysctl项,还有网络函数。
在kern_jail.c里定义了如下sysctl项:
/usr/src/sys/kern/kern_jail.c: int jail_set_hostname_allowed = 1; SYSCTL_INT(_security_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(_security_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(_security_jail, OID_AUTO, sysvipc_allowed, CTLFLAG_RW, &jail_sysvipc_allowed, 0, "Processes in jail can use System V IPC primitives"); /* Jail中的进程可以使用System V进程间通讯原语 */ static int jail_enforce_statfs = 2; SYSCTL_INT(_security_jail, OID_AUTO, enforce_statfs, CTLFLAG_RW, &jail_enforce_statfs, 0, "Processes in jail cannot see all mounted file systems"); /* jail 中的进程查看系统中挂接的文件系统时受到何种限制 */ int jail_allow_raw_sockets = 0; SYSCTL_INT(_security_jail, OID_AUTO, allow_raw_sockets, CTLFLAG_RW, &jail_allow_raw_sockets, 0, "Prison root can create raw sockets"); /* jail 中的 root 用户是否可以创建 raw socket */ int jail_chflags_allowed = 0; SYSCTL_INT(_security_jail, OID_AUTO, chflags_allowed, CTLFLAG_RW, &jail_chflags_allowed, 0, "Processes in jail can alter system file flags"); /* jail 中的进程是否可以修改系统级文件标记 */ int jail_mount_allowed = 0; SYSCTL_INT(_security_jail, OID_AUTO, mount_allowed, CTLFLAG_RW, &jail_mount_allowed, 0, "Processes in jail can mount/unmount jail-friendly file systems"); /* jail 中的进程是否可以挂载或卸载对jail友好的文件系统 */
这些sysctl项中的每一个都可以用命令sysctl(8)访问。在整个内核中, 这些sysctl项按名称标识。例如,上述第一个sysctl项的名字是 security.jail.set_hostname_allowed。
像所有的系统调用一样,系统调用jail(2)带有两个参数, struct thread *td和struct jail_args *uap。 td是一个指向thread结构体的指针,该指针用于描述调用jail(2)的线程。 在这个上下文中,uap指向一个结构体,这个结构体中包含了一个指向从用户级 jail.c传送过来的jail结构体的指针。 在前面我讲述用户级程序时,你已经看到过一个jail结构体被作为参数传送给系统调用 jail(2)。
/usr/src/sys/kern/kern_jail.c: /* * struct jail_args { * struct jail *jail; * }; */ int jail(struct thread *td, struct jail_args *uap)
于是uap->jail可以用于访问被传递给jail(2)的jail结构体。 然后,jail(2)使用copyin(9)将jail结构体复制到内核内存空间中。 copyin(9)需要三个参数:要复制进内核内存空间的数据的地址 uap->jail,在内核内存空间存放数据的j, 以及数据的大小。uap->jail指向的Jail结构体被复制进内核内存空间, 并被存放在另一个jail结构体j里。
/usr/src/sys/kern/kern_jail.c: error = copyin(uap->jail, &j, sizeof(j));
在jail.h中定义了另一个重要的结构体型prison。 结构体prison只被用在内核空间中。 下面是prison结构体的定义。
/usr/include/sys/jail.h: struct prison { LIST_ENTRY(prison) pr_list; /* (a) all prisons */ int pr_id; /* (c) prison id */ int pr_ref; /* (p) refcount */ char pr_path[MAXPATHLEN]; /* (c) chroot path */ struct vnode *pr_root; /* (c) vnode to rdir */ char pr_host[MAXHOSTNAMELEN]; /* (p) jail hostname */ u_int32_t pr_ip; /* (c) ip addr host */ void *pr_linux; /* (p) linux abi */ int pr_securelevel; /* (p) securelevel */ struct task pr_task; /* (d) destroy task */ struct mtx pr_mtx; void **pr_slots; /* (p) additional data */ };
然后,系统调用jail(2)为一个prison结构体分配一块内存, 并在jail和prison结构体之间复制数据。
/usr/src/sys/kern/kern_jail.c: MALLOC(pr, struct prison *, sizeof(*pr), M_PRISON, M_WAITOK | M_ZERO); ... error = copyinstr(j.path, &pr->pr_path, sizeof(pr->pr_path), 0); if (error) goto e_killmtx; ... error = copyinstr(j.hostname, &pr->pr_host, sizeof(pr->pr_host), 0); if (error) goto e_dropvnref; pr->pr_ip = j.ip_number;
下面,我们将讨论另外一个重要的系统调用jail_attach(2),它实现了将进程监禁的功能。
/usr/src/sys/kern/kern_jail.c /* * struct jail_attach_args { * int jid; * }; */ int jail_attach(struct thread *td, struct jail_attach_args *uap)
这个系统调用做出一些可以用于区分被监禁和未被监禁的进程的改变。 要理解jail_attach(2)为我们做了什么,我们首先要理解一些背景信息。
在FreeBSD中,每个对内核可见的线程是通过其thread结构体来识别的, 同时,进程都由它们自己的proc结构体描述。 你可以在/usr/include/sys/proc.h中找到thread和proc结构体的定义。 例如,在任何系统调用中,参数td实际上是个指向调用线程的thread结构体的指针, 正如前面所说的那样。td所指向的thread结构体中的td_proc成员是一个指针, 这个指针指向td所表示的线程所属进程的proc结构体。 结构体proc包含的成员可以描述所有者的身份 (p_ucred),进程资源限制(p_limit), 等等。在由proc结构体的p_ucred成员所指向的ucred结构体的定义中, 还有一个指向prison结构体的指针(cr_prison)。
/usr/include/sys/proc.h: struct thread { ... struct proc *td_proc; ... }; struct proc { ... struct ucred *p_ucred; ... }; /usr/include/sys/ucred.h struct ucred { ... struct prison *cr_prison; ... };
在kern_jail.c中,函数jail()以给定的jid 调用函数jail_attach()。随后jail_attach()调用函数change_root()以改变 调用进程的根目录。接下来,jail_attach()创建一个新的ucred结构体,并在 成功地将prison结构体连接到这个ucred结构体后,将这个ucred结构体连接 到调用进程上。从此时起,这个调用进程就会被识别为被监禁的。 当我们以新创建的这个ucred结构体为参数调用内核路径jailed()时, 它将返回1来说明这个用户身份是和一个jail相连的。 在jail中叉分出来的所有进程的的公共祖先进程就是这个执行了jail(2)的进程, 因为正是它调用了jail(2)系统调用。当一个程序通过execve(2)而被执行时, 它将从其父进程的ucred结构体继承被监禁的属性, 因而它也会拥有一个被监禁的ucred结构体。
/usr/src/sys/kern/kern_jail.c int jail(struct thread *td, struct jail_args *uap) { ... struct jail_attach_args jaa; ... error = jail_attach(td, &jaa); if (error) goto e_dropprref; ... } int jail_attach(struct thread *td, struct jail_attach_args *uap) { struct proc *p; struct ucred *newcred, *oldcred; struct prison *pr; ... p = td->td_proc; ... pr = prison_find(uap->jid); ... change_root(pr->pr_root, td); ... newcred->cr_prison = pr; p->p_ucred = newcred; ... }
当一个进程被从其父进程叉分来的时候, 系统调用fork(2)将用crhold()来维护其身份凭证。 这样,很自然的就保持了子进程的身份凭证于其父进程一致,所以子进程也是被监禁的。
/usr/src/sys/kern/kern_fork.c: p2->p_ucred = crhold(td->td_ucred); ... td2->td_ucred = crhold(p2->p_ucred);
本文档和其它文档可从这里下载:ftp://ftp.FreeBSD.org/pub/FreeBSD/doc/.
如果对于FreeBSD有问题,请先阅读文档,如不能解决再联系<questions@FreeBSD.org>.
关于本文档的问题请发信联系 <doc@FreeBSD.org>.