2 沙箱模型
2.1 原理
一般而言,对于网络上的网页中的JavaScript代码和插件是不受信的(除非是经过认证的网站),特别是一些故意设计侵入浏览器运行的主机代码更是非常危险,通过一些手段或者浏览器中的漏洞,这些代码可能获取了主机的管理权限,这对主机系统来说是非常危险的。所以,除了保证网页本身之外,还需要保证浏览器和浏览器所在的系统不存在危险。
对于网络上的网页,浏览器认为它们是不安全的,因为网页总是存在各种可能性,也许是无意的或有意的攻击。如果有一种机制,将网页的运行限制在一个特定的环境中,也就是一个沙箱中,使它只能访问有限的功能。那么,即使网页工作的渲染引擎被攻击,它也不能够获取渲染引擎工作的主机系统中的任何权限,这一思想就是沙箱模型。
WebKit中并没有提供沙箱机制的支持,所以后面的介绍主要以Chromium为基础来介绍在多进程架构中,沙箱模型的实现方式。
Chromium是以多进程为基础的,网页的渲染在一个独立的Renderer进程中进行,这为实现沙箱模型提供了基础,因为可以相对容易地使用一些技术将整个网页的渲染过程放在一个受限的进程中来完成,如图12-7所示,受限环境只能被某些或者很少的系统调用而且不能直接访问用户数据。而沙箱模型工作的基本单位就是进程。
图12-7 使用沙箱模型的渲染引擎的示意图
Chromium的沙箱模型是利用系统提供的安全技术,让网页在执行过程中不会修改操作系统或者是访问系统中的隐私数据,而需要访问系统资源或者说是系统调用的时候,通过一个代理机制来完成。下面详细介绍沙箱模型的实现方式和工作原理。
2.2 实现机制
因为沙箱模型严重依赖操作系统提供的技术,而不同操作系统提供的安全技术是不一样的,这样也就意味着不同操作系统上的实现是不一致的,需要分别针对不同平台展开来讨论。不过,不管是Linux还是Windows,或者是其他平台,Chromium都是在进程的粒度下来实现沙箱模型,也就是说需要运行在沙箱下的操作都在一个单独的进程中。所以,对于使用沙箱模型至少需要两个进程,如图12-8所示。
图12-8 应用沙箱模型的进程模型
图中右侧的是目标进程,也就是需要在沙箱中运行的代码,左侧的是代理进程,它需要负责创建目标进程并为目标进程设置各种安全策略,同时建立IPC连接,接受目标进程的各种请求,因为目标进程是不能访问过多资源的。下面主要讨论Linux和Windows平台上沙箱模型的实现和涉及的技术。
2.2.1 Linux
在Linux上,沙箱模型分成两个层,第一层是阻止某个或者某些进程通常能够访问的资源,Chromium中称为“语义层”,这里使用的系统技术主要是“setuid”,详情稍后介绍。第二层是防止进程访问能够攻击内核的接口或者攻击面(Attack Surface,这里主要是内核可能会被未授权的用户调用),这里使用的系统技术主要是“Seccomp”(具体到这里是Seccomp-BPF,它是Seccomp的一个扩展)。
在讨论具体的两层实现和相关技术之前,笔者这里先介绍一下如何在Linux系统中编译和启动沙箱机制。在Linux系统上,读者如果想尝试启用Chromium的沙箱机制,需要按照以下三个步骤来进行:第一,先单独编译目标“chrome_sandbox”,它是独立于编译目标“chrome”的,所以在编译“chrome”目标的时候并不会编译“chrome_sandbox”;第二,运行脚本“build/update-linux-sandbox.sh”,它会将编译完的文件安装到合适的位置;第三,在“.bashrc”中假如“export CHROME_DEVEL_SANDBOX=/usr/local/sbin/chrome-devel-sandbox”即可。这样,Chromium浏览器就能够使用沙箱机制了。
启动沙箱机制之后,如果运行Chromium程序,读者就可以看出如图12-9所给出的进程层次结构树。这是一棵树结构,树的根节点就是“browser”进程,该进程启动后,会孵化出多个子进程,包括图中的“GPU”、“chrome_sandbox”等进程。而对于沙箱模型来说,这里主要关注“chrome_sandbox”进程,它的目的主要是使用“setuid”技术来实现模型的第一层,生成了“zygote”子进程。其中“chrome_sandbox”进程使用的是上面介绍的目标“chrome_sandbox”生成的二进制可执行文件,而其他的是目标“chrome”生成的结果,二者是不一样的。
图12-9 使用沙箱机制后的进程和进程层次树
生成第一个“zygote”之后,该进程会生成两个子进程,第一个是“nacl-helper”进程,是为了支持NaCl插件进程服务的。第二个又是一个“zygote”,这个不同于它的父亲,它主要是为了生成各种“Renderer”进程服务。这样,每个“Renderer”进程经过上面的过程后都会使用沙箱机制进行处理,网页在“Renderer”中的运行就受到了严格地限制。
读者可能疑惑,既然“Renderer”进程根本不能访问各种资源,也不能调用各种系统调用,那么需要使用一些功能怎么办呢(如访问文件系统)?答案是使用一个代理来完成,代理进程和这些“Renderer”进程之间通过进程间通信机制来交互,所有的请求都发送给代理进程,代理进程将结果返回给“Renderer”进程,这里的代理进程就是“Browser”进程,“Browser”进程拥有访问这些资源和系统调用的权限。
首先讨论一下第一层是如何支持的,其中主要使用两种技术。
- 使用“setuid”来为新进程设置新用户ID和组ID,根据Linux的规定,两个不同用户ID之间的数据是隔离开的,这自然将“Renderer”进程同“Browser”进程分开,后者拥有访问很多关键数据的能力,如各个网页的Cookie。在现在的Chromium实现中,不再使用“setuid”来实现,而是使用“CLONE_NEWPID”标记,该标记是在Linux系统调用“clone”时设置,从而为新进程创建一个新的名空间,也就是上面描述的文件系统等。
- 限制网络访问,Chromium使用标记“CLONE_NEWNET”设置在调用“clone”系统调用的参数中。使用了这些技术之后,克隆出来的进程就同父进程分离开来,包括新文件系统(类似于chroot)等和限制网络的访问等。
接下来是第二层的讨论,这里使用的主要技术是“Seccomp”和“Seccomp-BPF”。Seccomp是Linux内核提供的一种简单的沙箱机制,它能够允许进程进入一种不可逆的安全状态,进入该状态的进程只能够调用4个系统调用,包括“exit”、“sigreturn”、“read”和“write”,而且最后两个系统调用只能操作已经打开的文件描述符。如果该进程尝试调用其他的系统调用,那么内核会通过“SIGKILL”信号来杀死该进程。进入该安全状态的方法就是使用系统调用prctl设置标记位PR_SET_SECCOMP就可以了,前提是系统的内核在编译的时候就加入了对“Seccomp”的支持,这一点很重要,因为不是所有使用Linux内核的操作系统都能打开该机制。
“Seccomp-BPF”是“Seccomp”技术的一个扩展,它允许使用BPF所定义的方法来将系统调用转变成BPF格式的小程序。BPF(Berkeley Packet Filter)原是Berkeley开发的一种用来过滤网络包的技术,现在被应用在“Seccomp”。“Seccomp-BPF”将系统调用转变成BPF格式的小程序,这些小程序能够被内核所解释,这样每个系统调用的次数和参数都能够被重新评估或者被限制。
对于开发者来说,如果想要关闭第二层的沙箱技术也很简单,在命令行中加入参数“--disable-seccomp-filter-sandbox”就可以了。
以上的技术不仅应用在Linux系统之上,而且也被应用在ChromeOS中。过去还有些技术用来实现第一层和第二层,如SELinux,Seccomp-legacy,因为上面介绍的技术更加合适,所以现在它们已经被丢弃了。
对于Android系统来讲,虽然Android是基于Linux内核开发出来的,但还是有些区别的。目前沙箱机制的第二层在Android上并没有得到支持,只是第一层得到了支持。但是,在Android上,系统支持的安全机制都已经在Chrome的Android版上得到了启用,主要体现在两个方面,第一是SUID,Android系统上稍微有些不同,它是UID isolation(UID隔离技术),Android可以为每一个进程设置一个新的UID,这样每个进程之间就不能随意修改和访问数据,这是有Linux内核机制来保证的,其实是上面讨论的沙箱机制的第一层。第二是Android的权限机制,每个进程只能访问授权的权限列表中的数据,如地理位置信息、通讯录等,这个是用户数据的隐私管理,不在Chromium的沙箱机制范围内,这里不再讨论。
2.2.2 Windows
Windows的沙箱模型也是基于图12-8的多进程结构,不同于Linux的是它们依赖的操作系统的安全机制不同,在Windows系统中,沙箱模型依赖于三个方面的技术:令牌(Token)、Windows Job对象和Windows Desktop对象。
在Windows中,令牌是进程访问资源的证件,每个进程都有一个令牌,令牌里面包含了一个SID和多个组的SID。而对于资源来说,每个资源都包含一个安全描述符,里面包含了一个列表称为ACL(Access control list),表中的每个项ACE标记了一个访问规则,描述了SID是否允许访问、读写、执行等操作。Chromium为Renderer进程设置了极为严格的令牌,如下面所示。
Regular Groups Logon SID : mandatory All other SIDs : deny only, mandatory Restricted Groups S-1-0-0 : mandatory Privileges None
使用了上面设置的令牌,读者基本上找不到一个Windows上的资源可以访问,这一令牌就是沙箱模型在Windows上使用的令牌。但是,由于Windows认为网络和系统的磁盘卷不是一个安全问题,这也就是意味着Renderer进程或者其他目标进程仍然能够发送和接收网络消息,读写磁盘卷信息。
但是,令牌还是不能够对某些方面做出限制的,所以接下来介绍的是Windows的Job对象。当一个进程运行在Job对象中的时候,更多方面受到了限制。
禁止进程通过系统调用“SystemParametersInfo”修改系统设置,如设置屏保时间和鼠标左右键设置等,禁止进程创建更多桌面或在不同桌面间来回切换等,禁止读写剪切板,禁止修改屏幕分辨率等相关设置。
还有非常多的功能可以通过Job对象被禁止,更多的详情请读者查阅“http://www.chromium.org/developers/design-documents/sandbox”来了解。不仅如此,Job对象还能够防止对CPU、内存和IO等资源的无限制使用。
最后是使用Windows的“Desktop”对象来为所有Renderer进程(或者其他进程如NaCl)构建一个新桌面。因为在桌面内发送或者接收消息是允许的,而且没有受到任何安全策略的限制。Chromium为了阻止这种事情的发生,为所有的目标进程创建一个新桌面,这也意味这这些目标进程没有办法向其他桌面的进程任意发送消息。
在Windows Vista上,还需要使用完整性级别(Integrity Levels),它规定了5种资源访问的级别,包括untrusted、low、medium、high和system,这个级别依次从低到高。如果一个资源的访问级别高于令牌访问的级别,那也会被禁止。
从上面的讨论可以看出,Chromium引入的沙箱机制极大地降低了网页中各种破坏操作系统的潜在风险,将网页的执行置于一个孤立(Isolated)和受限制(Strict)的环境中。安全问题始终是一个重要议题,笔者认为,这必将是浏览器或者Web运行环境中的一个发展方向。