暴露在互联网上的系统要将坏人隔离在外是一个很大的挑战,而且一直更新最新的安全补丁也不太容易。因此,一些聪明的管理员们尝试采用系统的方法来限制可能发生的入侵,这其中一个绝佳的方法就是使用chroot监狱(jail)。
chroot监狱极大地限制了应用程序可查看的文件系统范围,拥有更少的系统权限,这些都是为了限制应用程序误操作或者被坏人利用从而对系统造成损害。
本文简述一下chroot是如何工作的,并着重讨论一下开发者和管理员能够用到的一些最佳实践来让系统更加安全。
chroot背景
chroot系统调用将当前进程及其子进程的root目录修改到一个特定的路径,通常是在文件系统真正的root目录下的一些受限的子目录中。进程认为新的路径就是系统的“/”,因此我们将这个受限制的环境称为“监狱”。除非一些特殊手段,要从监狱里面逃出来是根本不可能的。
chroot系统调用存在于所有已知的UNIX版本中,它能够为运行的进程创建一个临时根目录,这种方法将一个受限制的文件系统(比如,/chroot/named)作为进程可见的最上层目录。
如何从监狱中逃脱
有一些非常有名的越狱手段,最常用的是在监狱中获取root权限。这种方法用于将程序chroot到了一个子目录,但是却将当前目录留在监狱外面。
我们会为各种逃脱手段添加更多地注释--也就意味着会更多地介绍什么是必须被保护的,而不是教你如何越狱--但是
这里有一篇整体介绍chroot的好文章。
- 使用mknod来创建原始磁盘设备,从而可以让你更加随心所欲地操作系统
- 使用mknod来创建/dev/mem,修改核心内存
- 寻找一个无意留下的通向监狱外面的硬链接(尽管软链接不能从监狱逃脱,但硬链接可以)
- 使用ptrace来跟踪在监狱外面的进程。我们能够修改这个程序让它为我们做一些坏事情。
几乎所有的越狱手段都需要root权限。
chroot通用原则
下面的介绍没有先后顺序,也没有站点会用到所有的原则。特别是有一些是应用在源代码层面的,另外一些是给系统管理员用来监禁现有系统的。
很多原则可能用起来非常琐碎,因为一个可用的系统有那么多层面可以来加固,但是我们仍然会列举所有能想到的来供你选择。最根本的一个原则是:“有的放矢,对症下药”。
我们通常最可能担心远程缓存溢出(remote buffer overflows), 这会让坏人完全地控制CPU:我们所有的步骤都是为了万一发生这种情况如何限制破坏。
在监狱中使用非root用户来运行程序
chroot监狱并非牢不可破的,但并不容易,这需要在监狱中获取root权限,因此我们必须采取措施来降低这种可能性。在监狱中使用非root用户来运行程序,这是我们所知最安全的方法。可能使用root用户启动某些守护进程从而进行一些需要相应权限的操作是必要的(比如说绑定某些序号较小的端口),但是这些程序在完成工作之后必须“放弃”root权限。
我们相信这是正确设置监狱最重要的一个因素。
正确地“放弃”权限
我们看到有时候在一些操作系统上,一个程序通过使用“saved"uid能够在非root用户和root用户间来回切换,这会被获取root权限的坏人所利用。
如果考虑到操作系统的差别,如何正确地设置非常棘手:变数在于setresuid() seteuid(), setreuid(), and setuid()--而且这好像还不算完。如何正确选择取决于你的操作系统。
在这一点上目前最好的资源是Usenix 2002的一篇杰出论文Setuid Demystified, 作者是Hao Chen, David Wagner和Drew Dean: 它切中要害,读者可以参考5.2节“Comparison among Uid-setting System Calls”。
明确地chdir到监狱中
chroot命令本身并不会改变工作目录,因此如果新的根目录在当前目录下的话,应用程序仍然可以访问到外面的资源。
在运行chroot之前应用程序应该明确的切换到监狱里的一个目录中:
...
chdir(dir);
chroot(dir);
setXXuid(nonroot); // give up root permissions correctly.
...
这样可以截断一条越狱的小路(但是要提醒的是你必须使用前面提到的一系列setXuid中的正确命令)。
另一个chdir和chroot的可选顺序是:
...
chroot(dir);
chdir("/");
...
二者看起来是等价的。
监狱越小越好
这增强了一些不易发现的弱点的抵抗力。通常这要求开发者在进行chroot之前做一下未被监禁文件的预加载(后面我们会更详细地介绍)。但是可能的话我们要非常无情地从监狱中删除内容。
限制未监禁程序运行被监禁程序
对于有些没有chroot命令行的系统来说,唯一的选择是包装一个程序来进行关键的chroot操作,放弃root权限,然后执行被监禁的可执行文件。
这个包装程序必须以root用户执行,但是包装程序自身一定不能放在监狱中。否则入侵者能够破解这个包装程序,在下次系统启动时,入侵者的程序就能够在非监禁的环境中以root用户来运行了。这就是被完全攻陷了。
让root拥有尽可能多的监禁文件
这限制了当被入侵时,侵入者修改系统的可能。我们的感觉被侵入最有可能的原因是入侵者在环境中执行任意代码从而导致缓存溢出,对于那些监禁系统中从来不需要写的文件来说,让它们只读而且属主是root,这意味着侵入后不可能chmod文件属性,也就不能修改文件。这条规则同样适用于目录。
彻底限制文件和目录的所有权限
我们认为如果一个权限位是不必要的,那就不应该被设置。例如,监禁环境下“/dev/”目录应该是d--x--x--x,属主是root,即使这个目录中唯一的文件是/dev/null。禁止搜索目录在所有已知可能方案中是一个非常精明的实践。
建立一个文件权限设置脚本
当第一次设置监狱的时候,很多权限相关的环节是手工调整的,因为我们在逐渐收紧约束,寻找可能的突破点。这种研究很不容易,得来的知识很有必要用源代码来表示出来。
我们通常建立一个小的shell脚本--在监狱之外--来设置监禁环境中每一个文件的属主,属组和权限模式。经常从一些递归的改变开始-先将所有都硬编码为非常严格的权限,然后将一些可以允许的文件权限放宽。很重要的一点是在脚本中包含文档来说明为什么某些特定的权限被放宽,同时还要描述起初为什么要将某些文件放到监狱中。
一旦文件创建好了,我们通常将所有权限相关的改变放到这里,然后重新运行脚本来使权限生效。这是确保脚本符合运行环境的唯一的办法。脚本带来的另一个优势是它为下一位设置类似环境的人提供了参考文档。
我们在一个项目(在chroot监狱中运行BIND)中用到的设置权限的样例脚本。个别细节并不重要,但它提供了一个思路
cd /chroot/named
# by default, root owns /everything/ and only root can write
# but directories have to be executable too.
chown -R root.named .
find . -print | xargs chmod u=rw,og=r # *all* files
find . -type d -print | xargs chmod u=rwx,og=rx # directories
# the "secondaries" directory is where we park files from
# master nameservers, and named needs to be able to update
# these files and create new ones.
find conf/secondaries -type f -print | xargs chown named.named
find conf/secondaries -type f -print | xargs chmod ug=r,o=
chown root.named conf/secondaries
chmod ug=rwx,o= conf/secondaries
# the var/run business is for the PID file
chown root.root var
chmod u=rwx,og=x var
chown root.named var/run
chmod ug=rwx,o=rx var/run
尽量在守护进程里做chroot操作
而不是显示地调用chroot命令(这种方式需要修改代码)。内部实现chroot的守护进程通常能够将可执行文件隔离在监狱之外。这种方式一个很大的优势在于入侵者无法直接感染二进制文件。
但是更立竿见影的好处在于共享库和其他启动文件可以从宿主系统中直接加载,而不必放到监狱中去。这不仅使系统更加安全--更少的对外暴露--而且让设置更加容易。
很多情况下,即使配置文件能够在监狱外被加载,但是如果守护进程包含了任何形式的“重读配置”选项的话,这种方式通常就不可行了。
预加载动态加载对象
对于在程序中使用了chroot的开发者来说,要考虑到需要访问宿主系统资源的操作并在关闭监狱大门之前完成操作。这些步骤最初并不太明显,需要一个试错的过程,但是我们已经找到几个这样的例子。
很多系统在运行时加载命名解析程序,这些程序并不在可执行程序绑定的共享对象中。我们发现在关闭监狱大门前一个简单的gethostbyname调用能够加载所有需要的库文件,因此后面的命名服务请求能够被正确处理:
(void) gethostbyname("localhost");
我们相信syslog也在此之列, 因为许多系统在此时要使用UNIX的域名套接字,需要访问syslogd正在监听的端口。我们没有尝试syslog的修改,因此也就不能提供一些特殊建议。我们相信Solaris--就它使用各种“门”的情况看--会更加复杂。
对于允许通过命令行选择运行时用户和组的守护程序来说,用户名到UID和GID的映射必须在chroot之前完成,这样才会用到宿主系统的/etc/syswd和相关文件,而不是监狱里面的那些。可以查看下一节来找到原理性解释。
下面这段C代码演示了如何将用户查找跟修改用户ID分开来执行:
if ( geteuid() == 0 )
{
struct passwd *userent = 0;
if ( (run_as_user != 0) && (userent = getpwnam(run_as_user)) == 0 )
{
/* ERROR */
}
chroot( working_dir );
if ( userent )
setXXuid(userent); // use the proper call!
...
避免使用监禁环境中的/etc/passwd文件
特别是用来决定守护进程的运行时用户ID的用户名到UID的映射。这个映射包括扫描passwd文件中特定的名字(比如,named)然后找到关联的用户ID。如果有人想方设法侵入了监禁环境的passwd文件,就有可能将当前用户的UID改为0,也就是root。这在守护进程下次重启后生效。
坏人最初并不能侵入这个文件,因为当前用户不能进行写操作,但是如果这个守护进程不管如何得到了一个可写的文件描述符,缓存溢出就可能被用来修改这个文件:我们相信已经多次遇到这种情况了。这很常见,在系统某处的一个bug能够对整个系统的安全造成难以置信的影响。
在chroot之前切记关闭文件描述符
我们不想给打开非监禁资源留下句柄,因为这些都能够被监狱内的资源所利用。一些文件描述符是必须的(比如,syslog daemon),但是开发者应该牢记关闭任何非必需的资源。
从外面链接配置文件
一些系统(比如BIND)在监禁的守护进程和其他在用户模式运行的工具共享配置文件。这种情况下,因为守护进程要访问配置文件,当然要将它放到监狱内,但是在用户模式下的其他工具也需要访问这个文件。不要重新构建这些工具来从特殊路径(比如/chroot/named/etc/named.conf)读取配置,而是应该在正常的地方建立一个从外面到里面的符号链接文件:
# ln -s /chroot/named/etc/named.conf /etc/named.conf
这能够让大多数工具正常运行,但是要小心的是记住修改/etc/named.conf也会影响到监狱中的文件。
这没有什么副作用,尽管开始看起来不太明显。从内向外的符号链接对管理员有用,但是对监狱内的系统却没有用。
更新环境变量来体现新的根目录
在监狱中需要更新一些环境变量来反应新的监狱内观。特别的,很多shell使用$PWD变量来表示当前工作目录,使用getcwd(3)库函数查询这个变量。
在成功chroot之后调用putenv("PWD=/")用新目录来来同步这个变量。
其他环境变量可能需要被修改(或删除),这取决于非监禁环境的环境泄露会对监禁环境造成多大的影响。
其他资源
How to break out of a chroot jail — 很棒的演示,告诉我们为什么要小心
Setuid Demystified, — 这个主题绝佳的参考
原文链接:
http://www.unixwiz.net/techtips/chroot-practices.html