diff --git a/zh_CN.GB2312/books/arch-handbook/jail/chapter.sgml b/zh_CN.GB2312/books/arch-handbook/jail/chapter.sgml index 3085eefb3a..44cdb791a7 100644 --- a/zh_CN.GB2312/books/arch-handbook/jail/chapter.sgml +++ b/zh_CN.GB2312/books/arch-handbook/jail/chapter.sgml @@ -1,540 +1,557 @@ Evan Sarmiento
evms@cs.bu.edu
2001 Evan Sarmiento &author.cn.intron; &cnproj.translated.by;
Jail子系统 security(安全) Jail(囚禁) root(根用户,管理员用户) 在大多数&unix;系统中,用户root是万能的。这也就增加了许多危险。 如果一个攻击者获得了一个系统中的root,就可以在他的指尖掌握系统中所有的功能。 在FreeBSD里,有一些sysctl项削弱了root的权限, 这样就可以将攻击者造成的损害减小到最低限度。这些安全功能中,有一种叫安全级。 另一种在FreeBSD 4.0及以后版本中提供的安全功能,就是&man.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》。 Jail的系统结构 Jail由两部分组成:用户级程序, 也就是&man.jail.8;;还有在内核中Jail的实现代码:&man.jail.2; 系统调用和相关的约束。我将讨论用户级程序和Jail在内核中的实现原理。 用户级代码 Jail(囚禁) userland program(用户级程序) 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; }; 正如你能看见的,传送给命令&man.jail.8;的每个参数都在这里有对应的一项。 事实上,当命令&man.jail.8;被执行时,这些参数才由命令行真正传入: /usr/src/usr.sbin/jail.c j.version = 0; j.path = argv[1]; j.hostname = argv[2]; 网络 传给&man.jail.8;的参数中有一个是IP地址。这是在网络上访问jail时的地址。 &man.jail.8;将IP地址翻译成网络字节顺序,并存入j (jail类型的结构体)。 /usr/src/usr.sbin/jail/jail.c: struct in.addr in; ... i = inet_aton(argv[3], ); ... j.ip_number = ntohl(in.s_addr); 函数inet_aton 3“将指定的字符串当成一个Internet地址, 并将其转存到指定的结构体中”。inet_aton设定了结构体in, 之后in中的内容再用ntohl()翻译成主机字节顺序。 囚禁进程 最后,用户级程序囚禁进程,执行指定的命令。现在Jail自身变成了一个被囚禁的进程, 叉分生成一个子进程。这个子进程用&man.execv.3;执行用户指定的命令。 /usr/src/sys/usr.sbin/jail/jail.c i = jail(); ... i = execv(argv[4], argv + 4); 正如你能看见的,函数jail被调用,参数是结构体jail中被填入数据项, 而如前所述,这些数据项又来自&man.jail.8;的命令行参数。 最后,执行了用户指定的命令。下面我将开始讨论Jail在内核中的实现。 相关的内核源代码 Jail(囚禁) kernel architecture(内核系统结构) 现在我们来看文件/usr/src/sys/kern/kern_jail.c。 在这里定义了jail的系统调用、相关的sysctl项,还有网络函数。 sysctl项 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, +SYSCTL_INT(_security_jail, OID_AUTO, set_hostname_allowed, CTLFLAG_RW, _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, +SYSCTL_INT(_security_jail, OID_AUTO, socket_unixiproute_only, CTLFLAG_RW, _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, +SYSCTL_INT(_security_jail, OID_AUTO, sysvipc_allowed, CTLFLAG_RW, _sysvipc_allowed, 0, "Processes in jail can use System V IPC primitives"); - /* Jail中的进程可以使用System V进程间通讯原语 */ - + /* Jail中的进程可以使用System V进程间通讯原语 */ + +static int jail_enforce_statfs = 2; +SYSCTL_INT(_security_jail, OID_AUTO, enforce_statfs, CTLFLAG_RW, + _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, + _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, + _chflags_allowed, 0, + "Processes in jail can alter system file flags"); + /* jail 中的进程是否可以修改系统级文件标记 */ 这些sysctl项中的每一个都可以用命令sysctl访问。在整个内核中, 这些sysctl项按名称标识。例如,上述第一个sysctl项的名字是 - jail.set.hostname.allowed. + security.jail.set_hostname_allowed &man.jail.2;系统调用 像所有的系统调用一样,系统调用&man.jail.2;带有两个参数, struct proc *pstruct jail_args *uapp是一个指向proc结构体的指针,描述调用这个系统调用的进程。 此时,uap指向一个结构体,这个结构体指定了从用户级程序 jail.c要传送给&man.jail.2;的参数。 在前面我讲述用户级程序时,你已经看见一个jail结构体被作为参数传送给系统调用 &man.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, , sizeof j); 在jail.h中定义了另一个重要的结构体型prison(pr)。 结构体prison只被用在内核空间中。系统调用&man.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, , sizeof pr->pr_host, 0); if (error) goto bail; chroot(切换逻辑根) 最后,系统调用jail将切换文件系统逻辑根(chroot)至指定路径。 函数chroot()有两个参数。第一个是p, 表示调用它的进程, 第二个是指向结构体chroot的指针。结构体chroot包含了新的文件系统逻辑根。 正如你看见的,结构体jail中指定的路径被复制到结构体chroot中, 并在后续操作中被使用。 /usr/src/sys/kern/kern_jail.c: ca.path = j.path; error = chroot(p, ); 这随后的三行在源代码中非常重要,因为他们指定了内核如何将一个 进程判别为被囚禁的进程。在&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本身, 它调用了&man.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; 当一个进程被从其父进程叉分来的时候,系统调用&man.fork.2; 将用不同的方式处理被囚禁的进程。在系统调用fork中用到两个指向 proc结构体的指针p1p2p1指向父进程的 proc结构体,p2 指向子进程的尚未被填充的proc结构体。 在结构体间复制完所有相关数据之后,&man.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; } 系统对被囚禁程序的限制 在整个内核中,有一系列对被囚禁程序的约束措施。 通常,这些约束只对被囚禁的程序有效。如果这些程序试图突破这些约束, 相关的函数将出错返回。例如: if (p->p_prison) return EPERM; SysV进程间通信(IPC) System V IPC(系统V进程间通信) System V 进程间通信 (IPC) 是通过消息实现的。 每个进程都可以向其它进程发送消息, 告诉对方该做什么。 处理消息的函数是: msgsysmsgctlmsggetmsgsendmsgrcv。 前面已经提到, 一些 sysctl 项开关可以影响 Jail 的行为, 其中有一个是 jail_sysvipc_allowed。 在大多数系统上, 这个 sysctl 项会设成0。 如果将它设为1, 则会完全失去 jail 的意义: 因为那样在 jail 中有权限的进程就可以影响 jail 环境外的进程了。 消息与信号的区别是:消息仅由一个信号编号组成。 /usr/src/sys/kern/sysv_msg.c: &man.msgget.3;: msgget返回(也可能创建)一个消息描述符, 以指派一个在其它系统调用中使用的消息队列。 &man.msgctl.3;: 通过这个函数, 一个进程可以查询一个消息描述符的状态。 &man.msgsnd.3;: msgsnd向一个进程发送一条消息。 &man.msgrcv.3;: 进程用这个函数接收消息。 在这些系统调用的代码中,都有这样一个条件判断: /usr/src/sys/kern/sysv msg.c: if (!jail.sysvipc.allowed && p->p_prison != NULL) return (ENOSYS); semaphores(信号量) 信号量系统调用使得进程可以通过一系列操作实现同步。 信号量为进程锁定资源提供了又一种途径。 然而,进程将为正在被使用的信号量进入等待状态,一直休眠到资源被释放。 在Jail中如下的信号量系统调用将会失效: semsys, semget, semctlsemop /usr/src/sys/kern/sysv_sem.c: &man.semctl.2;(id, num, cmd, arg): Semctl对在信号量队列中用id标识的信号量执行cmd指定的命令。 &man.semget.2;(key, nsems, flag): Semget建立一个对应于key的信号量数组 参数Key和flag与msgget()的意义相同。 &man.semop.2;(id, ops, num): Semop在结构体数组ops中对id标识的信号量完成一系列操作。 shared memory(共享内存) System V IPC使进程间可以共享内存。进程之间可以通过它们虚拟地址空间 的共享部分以及相关数据读写操作直接通讯。这些系统调用在Jail环境中将会失效: shmdt, shmat, oshmctl, shmctl, shmget, 和shmsys /usr/src/sys/kern/sysv shm.c: &man.shmctl.2;(id, cmd, buf): shmctl对id标识的共享内存区域做各种各样的控制。 &man.shmget.2;(key, size, flag): shmget建立/打开size字节的共享内存区域。 &man.shmat.2;(id, addr, flag): shmat将id标识的共享内存区域指派到进程的地址空间里。 &man.shmdt.2;(addr): shmdt取消共享内存区域的地址指派。 套接字 sockets(套接字) Jail以一种特殊的方式处理&man.socket.2;系统调用和相关的低级套接字函数。 为了决定一个套接字是否允许被创建,它先检查sysctl项 jail.socket.unixiproute.only是否被设置为1。 如果被设为1,套接字建立时将只能指定这些协议族: PF_LOCAL, PF_INET, PF_ROUTE。否则,&man.socket.2;将会返回出错。 /usr/src/sys/kern/uipc_socket.c: int socreate(dom, aso, type, proto, p) ... register struct protosw *prp; ... { if (p->p_prison && jail_socket_unixiproute_only && prp->pr_domain->dom_family != PR_LOCAL && prp->pr_domain->dom_family != PF_INET && prp->pr_domain->dom_family != PF_ROUTE) return (EPROTONOSUPPORT); ... } Berkeley包过滤器 Berkeley Packet Filter(伯克利包过滤器) data link layer(数据链路层) Berkeley包过滤器提供了一个与协议无关的,直接通向数据链路层的低级接口。 函数bpfopen()打开一个以太网设备。 代码中有一个条件判断禁止所有被囚禁的进程打开Berkeley包过滤器设备。 /usr/src/sys/net/bpf.c: static int bpfopen(dev, flags, fmt, p) ... { if (p->p_prison) return (EPERM); ... } 网络协议 protocols(协议) 网络协议TCP, UDP, IP和ICMP很常见。IP和ICMP处于同一协议层次:第二层, 网络层。当参数nam被设置时, 有一些限制措施会防止被囚禁的程序绑定到一些网络接口上。 nam是一个指向sockaddr结构体的指针,描述可以绑定服务的地址。 一个更确切的定义:sockaddr“是一个模板,包含了地址的标识符和地址的长度”[2]。 在函数中,pcbbind, sin 里有一个指向sockaddr的指针。结构体包含了套接字可以绑定的端口、地址、 长度、协议族。这就禁止了在Jail中的进程指定协议族。 /usr/src/sys/kern/netinet/in_pcb.c: int in.pcbbind(int, nam, p) ... struct sockaddr *nam; struct proc *p; { ... struct sockaddr.in *sin; ... if (nam) { sin = (struct sockaddr.in *)nam; ... if (sin->sin_addr.s_addr != INADDR_ANY) if (prison.ip(p, 0, ->sin.addr.s_addr)) return (EINVAL); .... } ... } 你也许想知道函数prison_ip()做什么。 prison.ip有三个参数,当前进程(用p表示), 一些标志(flag)和一个IP地址。当这个IP地址属于一个Jail时,返回1; 否则返回0。正如你从代码中看见的,如果,那个IP地址真的属于一个Jail, 就不再允许向一个网络接口绑定协议。 /usr/src/sys/kern/kern_jail.c: int prison_ip(struct proc *p, int flag, u_int32_t *ip) { u_int32_t tmp; if (!p->p_prison) return (0); if (flag) tmp = *ip; else tmp = ntohl (*ip); if (tmp == INADDR_ANY) { if (flag) *ip = p->p_prison->pr_ip; else *ip = htonl(p->p_prison->pr_ip); return (0); } if (p->p_prison->pr_ip != tmp) return (1); return (0); } 被囚禁的用户不能对一个不属于这个Jail的IP地址绑定服务。 这个限制在函数in_pcbbind中也有所体现: /usr/src/sys/net inet/in_pcb.c if (nam) { ... lport = sin->sin.port; ... if (lport) { ... if (p && p->p_prison) prison = 1; if (prison && prison_ip(p, 0, ->sin_addr.s_addr)) return (EADDRNOTAVAIL); 文件系统 filesystem(文件系统) 如此完全级大于0,即便是root,也不允许在Jail中设置文件标志, 如“不可修改”、“添加”、“不可删除”标志。 /usr/src/sys/ufs/ufs/ufs_vnops.c: int ufs.setattr(ap) ... { if ((cred->cr.uid == 0) && (p->prison == NULL)) { if ((ip->i_flags & (SF_NOUNLINK | SF_IMMUTABLE | SF_APPEND)) && securelevel > 0) return (EPERM); }