/usr/include/sys/jail.h: struct jail { u_int32_t version; char *path; char *hostname; u_int32_t ip_number; };
第 4 章。Jail 子系统
目录
在大多数 UNIX® 系统中,root
拥有至高无上的权力。这会导致安全问题。如果攻击者获得了系统的 root
权限,他将拥有所有功能的控制权。在 FreeBSD 中,存在一些 sysctl,可以削弱 root
的权力,从而最大限度地减少攻击者造成的损害。其中一个功能称为 安全级别
。类似地,从 FreeBSD 4.0 及更高版本开始提供的另一个功能是一个名为 jail(8) 的实用程序。Jail 会对环境进行 chroot 操作,并对在 jail 中 fork 的进程设置某些限制。例如,被 jail 的进程无法影响 jail 外部的进程,使用某些系统调用,或对主机环境造成任何损害。
Jail 正在成为新的安全模型。人们在 jail 中运行可能存在漏洞的服务器,如 Apache、BIND 和 sendmail,这样一来,如果攻击者获得了 jail 中的 root
权限,只会造成一些麻烦,而不是毁灭性的打击。本文主要关注 jail 的内部机制(源代码)。有关如何设置 jail 的信息,请参阅 手册中关于 jail 的条目。
4.1. 架构
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
结构的定义如下:
如您所见,对于传递给 jail(8) 程序的每个参数都存在一个条目,并且在执行程序时会设置这些条目。
/usr/src/usr.sbin/jail/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];
4.1.1.2. 网络
/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) 将 IP 地址放入 in
结构中时,jail
结构中的 ip_number
成员仅在 ntohl(3) 将其转换为主机字节序时才会被设置。
4.1.1.3. 将进程放入 Jail
最后,用户空间程序会将进程放入 Jail。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 在内核中的实现方式。
4.1.2. 内核空间
现在我们将查看文件 /usr/src/sys/kern/kern_jail.c。这个文件是定义 jail(2) 系统调用、适当的 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(_security_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(_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"); 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"); 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"); 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"); 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"); 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");
用户可以通过 sysctl(8) 程序访问这些 sysctl。在整个内核中,这些特定的 sysctl 都是通过其名称识别的。例如,第一个 sysctl 的名称是 security.jail.set_hostname_allowed
。
4.1.2.2. jail(2) 系统调用
与所有系统调用一样,jail(2) 系统调用接受两个参数,struct thread *td
和 struct jail_args *uap
。td
是指向描述调用线程的 thread
结构的指针。在此上下文中,uap
是指向一个结构的指针,该结构包含指向用户空间 jail.c 传递的 jail
结构的指针。在我之前描述用户空间程序时,您看到了 jail(2) 系统调用被传递了一个 jail
结构作为其自己的参数。
/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
结构。接下来,系统调用使用 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),它实现了将进程放入 jail 的功能。
/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 的进程与未被 jail 的进程区分开来。为了理解 jail_attach(2) 为我们做了什么,需要了解一些背景信息。
在 FreeBSD 中,每个内核可见线程由其 `thread` 结构标识,而进程由其 `proc` 结构描述。您可以在 `/usr/include/sys/proc.h` 中找到 `thread` 和 `proc` 结构的定义。例如,任何系统调用中的 `td` 参数实际上是指向调用线程的 `thread` 结构的指针,如前所述。`td` 指向的 `thread` 结构中的 `td_proc` 成员是指向 `proc` 结构的指针,该结构代表包含由 `td` 表示的线程的进程。`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` 结构附加到调用进程。从那时起,调用进程就被识别为被监禁的。当内核例程 `jailed()` 在内核中被调用,其参数是新创建的 `ucred` 结构时,它将返回 1 以告知凭据与监狱连接。所有在监狱内创建的分支进程的公共祖先进程是运行 jail(8) 的进程,因为它调用了 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);
4.2. 限制
在整个内核中,存在与被监禁进程相关的访问限制。通常,这些限制只检查进程是否被监禁,如果是,则返回错误。例如
if (jailed(td->td_ucred)) return (EPERM);
4.2.1. SysV IPC
System V IPC 基于消息。进程可以互相发送这些消息,告诉它们如何行动。处理消息的函数包括:msgctl(3)、msgget(3)、msgsnd(3) 和 msgrcv(3)。之前我提到过,您可以打开或关闭某些 sysctl 来影响监狱的行为。其中一个 sysctl 是 `security.jail.sysvipc_allowed`。默认情况下,此 sysctl 设置为 0。如果设置为 1,它将违背建立监狱的初衷;监狱中的特权用户将能够影响监狱环境之外的进程。消息和信号之间的区别在于消息只包含信号号。
/usr/src/sys/kern/sysv_msg.c:
msgget(key, msgflg)
: `msgget` 返回(并可能创建)一个消息描述符,用于指定其他函数中使用的消息队列。msgctl(msgid, cmd, buf)
: 使用此函数,进程可以查询消息描述符的状态。msgsnd(msgid, msgp, msgsz, msgflg)
: `msgsnd` 向进程发送消息。msgrcv(msgid, msgp, msgsz, msgtyp, msgflg)
: 进程使用此函数接收消息
在与这些函数相对应的每个系统调用中,都有以下条件语句
/usr/src/sys/kern/sysv_msg.c: if (!jail_sysvipc_allowed && jailed(td->td_ucred)) return (ENOSYS);
信号量系统调用允许进程通过对一组信号量进行原子操作来同步执行。基本上,信号量提供了另一种方式让进程锁定资源。但是,等待被使用的信号量的进程将休眠,直到资源被释放。以下信号量系统调用在监狱内被阻止:semget(2)、semctl(2) 和 semop(2)。
/usr/src/sys/kern/sysv_sem.c:
semctl(semid, semnum, cmd, …)
: `semctl` 对由 `semid` 指示的信号量队列执行指定的 `cmd` 操作。semget(key, nsems, flag)
: `semget` 创建一个与 `key` 对应的信号量数组。`key` 和 `flag` 的含义与 `msgget` 中相同。
semop(semid, array, nops)
: `semop` 对由 `semid` 标识的一组信号量执行由 `array` 指示的一组操作。
System V IPC 允许进程共享内存。进程可以通过共享其虚拟地址空间的一部分,然后读写存储在共享内存中的数据,从而直接相互通信。这些系统调用在被监禁的环境中被阻止:shmdt(2)、shmat(2)、shmctl(2) 和 shmget(2)。
/usr/src/sys/kern/sysv_shm.c:
shmctl(shmid, cmd, buf)
: `shmctl` 对由 `shmid` 标识的共享内存区域执行各种控制操作。shmget(key, size, flag)
: `shmget` 访问或创建大小为 `size` 字节的共享内存区域。shmat(shmid, addr, flag)
: `shmat` 将由 `shmid` 标识的共享内存区域附加到进程的地址空间。shmdt(addr)
: `shmdt` 分离之前附加在 `addr` 的共享内存区域。
4.2.2. 套接字
监狱以特殊方式处理 socket(2) 系统调用和相关的底层套接字函数。为了确定是否允许创建某个套接字,它首先检查 sysctl `security.jail.socket_unixiproute_only` 是否已设置。如果已设置,则只有当指定的族为 `PF_LOCAL`、`PF_INET` 或 `PF_ROUTE` 时才允许创建套接字。否则,它将返回错误。
/usr/src/sys/kern/uipc_socket.c: int socreate(int dom, struct socket **aso, int type, int proto, struct ucred *cred, struct thread *td) { struct protosw *prp; ... if (jailed(cred) && jail_socket_unixiproute_only && prp->pr_domain->dom_family != PF_LOCAL && prp->pr_domain->dom_family != PF_INET && prp->pr_domain->dom_family != PF_ROUTE) { return (EPROTONOSUPPORT); } ... }
4.2.3. Berkeley 数据包过滤器
Berkeley 数据包过滤器以独立于协议的方式提供了对数据链路层的原始接口。BPF 现在由 devfs(8) 控制,它是否可以在被监禁的环境中使用。
4.2.4. 协议
有一些非常常见的协议,例如 TCP、UDP、IP 和 ICMP。IP 和 ICMP 处于同一级别:网络层 2。只有在设置了 `nam` 参数的情况下,才会采取某些预防措施来防止被监禁的进程将协议绑定到某个地址。`nam` 是指向 `sockaddr` 结构的指针,该结构描述要绑定服务的地址。更准确的定义是,`sockaddr` “可以作为模板用于引用每个地址的标识标签和长度”。在函数 `in_pcbbind_setup()` 中,`sin` 是指向 `sockaddr_in` 结构的指针,其中包含要绑定的套接字的端口、地址、长度和域族。基本上,这禁止监狱中的任何进程指定不属于调用进程所在的监狱的地址。
/usr/src/sys/netinet/in_pcb.c: int in_pcbbind_setup(struct inpcb *inp, struct sockaddr *nam, in_addr_t *laddrp, u_short *lportp, struct ucred *cred) { ... struct sockaddr_in *sin; ... if (nam) { sin = (struct sockaddr_in *)nam; ... if (sin->sin_addr.s_addr != INADDR_ANY) if (prison_ip(cred, 0, &sin->sin_addr.s_addr)) return(EINVAL); ... if (lport) { ... if (prison && prison_ip(cred, 0, &sin->sin_addr.s_addr)) return (EADDRNOTAVAIL); ... } } if (lport == 0) { ... if (laddr.s_addr != INADDR_ANY) if (prison_ip(cred, 0, &laddr.s_addr)) return (EINVAL); ... } ... if (prison_ip(cred, 0, &laddr.s_addr)) return (EINVAL); ... }
您可能想知道函数 `prison_ip()` 是做什么的。`prison_ip()` 有三个参数:指向凭据的指针(由 `cred` 表示)、任何标志和 IP 地址。如果 IP 地址不属于监狱,则返回 1,否则返回 0。如您从代码中看到的,如果确实是一个不属于监狱的 IP 地址,则不允许协议绑定到该地址。
/usr/src/sys/kern/kern_jail.c: int prison_ip(struct ucred *cred, int flag, u_int32_t *ip) { u_int32_t tmp; if (!jailed(cred)) return (0); if (flag) tmp = *ip; else tmp = ntohl(*ip); if (tmp == INADDR_ANY) { if (flag) *ip = cred->cr_prison->pr_ip; else *ip = htonl(cred->cr_prison->pr_ip); return (0); } if (tmp == INADDR_LOOPBACK) { if (flag) *ip = cred->cr_prison->pr_ip; else *ip = htonl(cred->cr_prison->pr_ip); return (0); } if (cred->cr_prison->pr_ip != tmp) return (1); return (0); }
4.2.5. 文件系统
即使是监狱内的 `root` 用户,如果安全级别大于 0,也不允许取消设置或修改任何文件标志,例如不可变标志、只追加标志和不可删除标志。
/usr/src/sys/ufs/ufs/ufs_vnops.c: static int ufs_setattr(ap) ... { ... if (!priv_check_cred(cred, PRIV_VFS_SYSFLAGS, 0)) { if (ip->i_flags & (SF_NOUNLINK | SF_IMMUTABLE | SF_APPEND)) { error = securelevel_gt(cred, 0); if (error) return (error); } ... } } /usr/src/sys/kern/kern_priv.c int priv_check_cred(struct ucred *cred, int priv, int flags) { ... error = prison_priv_check(cred, priv); if (error) return (error); ... } /usr/src/sys/kern/kern_jail.c int prison_priv_check(struct ucred *cred, int priv) { ... switch (priv) { ... case PRIV_VFS_SYSFLAGS: if (jail_chflags_allowed) return (0); else return (EPERM); ... } ... }
最后修改时间:2024 年 3 月 9 日,由 Danilo G. Baio 修改