【2021-07-31 更新】【梳理】简明操作系统原理 第十九章 身份认证和访问控制(docx)

配套教材:
Operating Systems: Three Easy Pieces Remzi H. Arpaci-Dusseau Andrea C. Arpaci-Dusseau Peter Reiher
参考书目:
1、计算机操作系统(第4版) 汤小丹 梁红兵 哲凤屏 汤子瀛 编著 西安电子科技大学出版社

在线阅读:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 认为课本应该是免费的
————————————————————————————————————————
这是专业必修课《操作系统原理》的复习指引。
需要掌握的概念在文档中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
文档下载地址:
链接:https://pan.baidu.com/s/1VeDysG_z0ApB2wtOFgQTxA
提取码:0000

十九 身份认证和访问控制

在计算机安全领域,我们通常将询问某些事物的实体称为主体(principal)。主体具有安全意义的能够请求对资源的访问权的实体,例如,用户、用户组,以及复杂的软件系统。进程或其它活动的计算实体,代表主体执行请求的,通常被称为代理(agent)。如果请求是为了访问特定的资源,通常将资源称为访问请求的对象(object)。由操作系统创建和管理的用于追踪访问的任何形式的数据称为凭证(credential,凭据)。
如果操作系统还没有产生凭证,表明允许一个代理进程访问特定的对象,操作系统就需要进程主体的身份信息,来确定它的请求是否应被授权。不同的操作系统使用不同类型的主体身份。许多操作系统都记录用户身份,用户一般是指人类(近年来,用户这个概念已经大大扩展)。这意味着:特定人员运行的所有进程,可能都具有相同的身份。另一种常见的身份类型是用户组。还有一种身份类型是进程运行的程序(program)。同一个程序可以被运行多次,每次都会打开一个新的进程。在Android等系统中,可以为特定的程序进行授权。
无论采用何种身份类型,都应当能够将它们与进程关联起来。除了操作系统自己的活动,一切都由特定的进程执行。所以,为进程的每个重要动作核对安全策略,是一定有机会的。但操作系统通常不这样做:一旦一个进程通过身份认证,那么在它剩余的生命周期内,基本都依据这次身份认证的判定了(换句话说,基本不会再重新认证了)。

进程一般是由其它进程来创建的。如果子进程总是继承父进程的身份,那么将身份与进程结合起来固然很简单。但是,操作系统启动时,为了能够正常加载所有组件,必须令最初的进程获得最高权限。这之后创建其它进程时,都继承了这个最高权限的身份,那自然就无法应用已有的安全策略了。
如果进程以用户ID之类的作为安全身份,必须为每个新进程都设置正确的用户ID。在许多系统中,用户总是要通过特定的进程来完成工作,例如shell或窗口管理器。如果你在shell中输入一个命令,或者在窗口系统中双击一个图标,就代表你要求操作系统以你的身份创建一个新进程。
Shell或窗口管理器如何确定自己的身份?这就需要一点点操作系统特权。当用户首次与系统进行交互时,操作系统为该用户创建一个进程。操作系统可以在自己的数据结构,例如进程控制块(PCB),设置新进程属于新加入系统的用户。
那么,如何确定用户的身份?自然是需要在用户登录时提供身份信息了。到此为止,我们已经发现了操作系统的一个新需求:它必须能够向人类用户请求身份,并验证他们声称自己是谁,以便为进程分配正确的身份,从而实现预设的安全策略。

首先,如果一个人根本不是这个系统的授权用户,那么必须要拒绝他的登录请求;其次,如果他是授权用户,那么,他具体是哪一种或者哪一个?
对人类进行身份验证,有三种古典的方法:
·基于他知道的进行身份验证。
·基于他拥有的进行身份验证。
·基于他是谁进行身份验证。
我们说它们是古典的,因为这些手段能够追溯到古希腊和古罗马时期。公元前2世纪,坡利比阿斯(Polybius)记载了古罗马军队如何使用口令(watchwords)区别友军与敌军。在公元2世纪,一个名为Celer的古罗马建筑师为他的一个被交给帝国检察官的奴隶写了一封推荐信(目前仍被保留)。这是基于奴隶拥有什么来进行身份验证。在更远古的圣经的时代,基列人(Gileadite)要求战后的难民说出单词“shibboleth”,因为他们觉得他们的敌人以法莲人(Ephraimite)不能正确发音这个词。这是基于被验证者是谁的身份验证:他的母语是基列方言还是以法莲方言。

你所知道的身份验证一般是使用密码(password)完成的。在计算机安全中,密码拥有一段长而不光彩的历史,起源不晚于1960年代早期的MIT的CTSS系统。密码是仅由被授权的一方知晓的秘密。在尝试登录时,将其输入到计算机,证明自己的身份。这种授权方式的效率取决于几点。首先,假设其他人都不知道密码。并且,假设其他人也不能猜出密码来。当然,输入密码的人本身也必须知道密码。

先不管密码被猜出来的情况。如果其他人知道密码,他们是怎样知道的呢?已经知道密码的人可能不小心泄露出来。所以,知道密码的人越少,这种担心自然就越少。我们真不希望其他人获得授权并进入系统,因此希望不要有任何第三方知道密码。这也蕴含着:用户不能将密码写在纸上,否则,偷到这张纸的人就会得知密码。此外,系统本身也必须知道密码,才能验证身份。这也带来了另一点脆弱性:系统中保存的密码可能泄露出来。第一次已知的存储密码泄露事件发生于1962年。这种泄露直到今天还在不断发生,并且发生的范围大得多。
有趣的是,系统实际上并不知道密码具体是什么。检查密码时,只需要确定用户确实知道密码,而不是密码本身是什么内容。系统可以通过存储密码的哈希(hash,散列)值来实现这一点。当用户输入密码后,将输入做一次哈希,与存储的哈希值比对。如果相同,就意味着用户知道密码。如果没有留心存储的授权信息,导致它们泄露了,那么泄露的也只是哈希。如果将泄露的哈希作为密码输入,那么产生的哈希值几乎不可能与原密码的哈希值一致。
但是,仅仅存储与密码不同的内容是不够的。我们还希望,攻击者不能通过分析存储的哈希,得出密码的一些线索。有一类特殊的哈希算法叫做加密哈希(cryptographic hash),它们使得通过哈希值猜出密码是不可行的。然而很不幸,设计它们非常困难,所以聪明人也不应该随意尝试,而是使用由专家创建的加密哈希算法。现代系统在密码哈希方面需要做的是:使用一种被彻底研究的、没有已知缺陷的加密哈希算法。在编写本文时,SHA-3是美国标准的加密哈希算法,并且是一个好选择。

然后,考虑猜出密码的情形。显然,密码的组合种数随着密码长度指数级增长。一些早期的密码只能使用字母,虽然这让密码更好记也更容易输入,但是也令密码的组合数极大地减少了。现在,设置密码时,一般至少都能接受大小写字母、数字和特殊符号。
虽然能够使用多种字符组合,但是攻击者也清楚,人们一般不会使用这些字符的随机字符串作为密码,而是选择一些名字和熟悉的单词等等,因为它们更容易被记住。攻击者猜测密码时,会在开始猜测随机字符之前,先尝试常用的名字和单词。这种密码猜测称为字典攻击(dictionary attack),而且可以很高效。这里的字典并不是指Webster或者Oxford之类的字典,而是关于单词、名称、有意义的字符串(如“123456”)等等的特殊列表,按概率进行排列。一次有效的字典攻击,可以猜出一个典型网站中90%的密码。
如果你在设计系统时足够醒目,攻击者应当无法通过远程登录过程来运行字典攻击。只要稍加小心,攻击者就无法在五六次左右就能猜出一个用户的密码。并且,更没有理由允许一个远程用户猜测密码15000次还猜不对。所以,在多次输入错误密码后关闭一个账户的访问权。或者在数次密码错误后延长检查密码的时间,就可以防护字典攻击。
如果攻击者获得了密码文件,并已知哈希算法,准备好一个字典和一些算力,他就有可能撞出密码来。攻击者只需要将每种可能的密码都试一次,将它们的哈希存入字典中,就可以根据获得的密码文件中的哈希值找到密码。
有一个很简单的修复方案。在对新密码哈希并存储之前,生成一个大的随机数,将其与密码进行连接,将连接结果哈希并存储。同样也需要存储随机数。当用户提供正确密码时,就需要将用户输入的密码与存储的随机数连接起来,再运行哈希算法。这个随机数称为盐(salt)。这是Robert Morris和Ken Thompson在早期的密码安全论文中引入的概念。
加盐密码极大挫败了字典攻击。如果随机数是32位的,就意味着一种密码生成的哈希最多有232种。虽然说,如果选择糟糕的密码,攻击依然是有可能的,但是攻击的成本已经非常高。任何使用密码的良好系统,都应当注意存储加密哈希并加盐的密码,否则,用户就被置于风险中。

关于密码的使用,还有许多引起麻烦的其它问题,但很多都与操作系统无关,本书不予讨论。在计算机安全社区,当前普遍持有的一种观点是:密码是过去的技术,在当今的环境下已经不再足够安全。但不论如何,密码至少可以作为身份验证的机制之一。这种思想成为多因子身份认证(multi-factor authentication)。最常用的是双因子身份认证(two-factor authentication)。你对这个概念可能已经熟悉:在ATM取钱时,不但需要知道自己的个人识别号码(personal identification number,PIN),实际上就是密码,还需要提供更多的证据(比如,你持有你本人的银行卡)证明你的身份。

存储明文密码或密钥。风险是非常大的。因为它们通常会被泄露出去。因此在不需要时,不要存储它们,以保护你的系统。如果确实需要,应使用强加密哈希(或其它强加密方式)存储。存储的位置数量要尽可能少。不要忘记临时的编辑器文件、备份、日志,等等,因为在这些地方也可能存储机密。任何嵌入到交给他人的可执行文件中的内容,都将不再保持机密,因此在可执行文件中存储机密是非常危险的。有些情况中,仅存在可执行程序的堆中的机密也会泄露。因此,避免在运行的程序中存储机密。

一种避免选择密码问题的方式是:使用密码保管库(password vault)或密钥链(key chain)。这些是存储在计算机上的加密后的存储密码的文件。它由自己的密码加密。为了从保管库中取出密码,必须提供保管库的密码,就无需为每个站点记忆单独的密码。这也确保了:攻击者若要使用这些密码,不但要知道保管库的密码,还需要拥有保管库的访问权限。当然,保管库中的密码依然会受到猜测攻击和字典攻击。一些密码保管库会自动生成强密码,不需要保管库的所有人去记忆。你也会发现一些密码保管库将密码存储在云端。如果你提供明文版本的密码。就意味着你向并不需要知道密码的实体分享了密码,也就令你处于不必要的风险中。如果云端只存储加密后的密码,风险就低很多。

有些地方可能要求持有身份证或者门票进入,这就是根据拥有物进行身份验证的典型例子。将某些硬件连接到计算机的端口,例如使用USB的硬件令牌。然后,在软件支持的情况下,操作系统就能够告知尝试登录的用户是否拥有正确的设备。一些安全令牌(也称dongle,软件狗或软件保护器)是以这样的方式工作的。
无论如何,因为我们是尝试对人类用户进行身份验证,所以我们可以用到人的信息转换能力。例如,一些智能令牌在它们的屏幕上显示一串数字或字符串。用户将这个串从键盘输入计算机。操作系统并不得到用户拥有这个设备的直接证明,但是只有能够访问设备的人才知道需要输入这些信息,因此这样的证据也就接近一样的效力。
这些设备依赖于频繁改变(直接或间接)传递给操作系统的信息,比如每几秒或者每次尝试授权时都更改一次。如果不这样做,所有能够获得静态信息的用户都可以通过授权。这种授权机制将“你持有的”转换成了“你知道的”,其安全性取决于攻击者获得设备上的信息有多么困难。

所有基于你持有的物品进行身份验证的共同弱点是:如果你没有,应当怎么办?比如说,手机落在了梳妆台;软件狗掉在了去上班的路上;一个扒手在咖啡馆偷了你的认证设备。现在你面临两个问题。首先,你无法被授权进入操作系统。你可以对着计算机不停BB你想要的,但是它不会鸟你,只是坚持声称你没有需要的授权设备。其次,其他人持有了你的认证设备,他们可以伪装成你,获得授权并进入系统。多因子认证可以预防这个问题:窃贼盗取了你的安全令牌,但是不知道你的密码(当然,如果你在安全令牌背面把密码写上了……)。
如果你学习了系统安全很长时间,你会发现,学者告诉你的安全和真实世界发生的事情之间有着巨大的鸿沟。这种代沟一部分是因为在真实世界中需要应对实际问题,比如用户便利性。一部分是因为安全学者倾向于诋毁任何可能推翻他们的理论的东西,即便这些东西不太实际。基于持有物品进行身份认证的一个例子,是基于发送短信到用户的手机,用户正确输入短信包含的验证码等内容以后,授权用户进入系统。这听起来很脆弱,不但没有考虑到丢失手机的可能性,安全专家还会考虑将短信导向攻击者的手机。
实际上,人们通常会花费足够的心思盯着手机,确保不丢失。如果手机真的丢了,他们很快会发现,并很快采取措施。此外,重定向信息是可能的,但难以实现。在绝大多数情况下,需要花费的努力很可能远远大于侵入系统获得的利益。因此,一个能让安全纯粹主义者避免他们的恐怖凝视的机制,会提供非常可观的安全性。虽然这些话未必在考试中会遇到,但是它们对你以后的职业生涯也会有帮助的。

如果不喜欢密码之类的方法,也不喜欢将智能卡或安全令牌这些东西交给用户,那么还有其它的选择。人类是独特的生物,不同个体身上总会有一些不同的东西,从DNA到外貌。如果操作系统能够精确测量这些特性,就可以判明身份,解决身份认证问题。这个方法吸引了很多人。
然而,事情并不这么简单。设想我们需要使用智能手机上的摄像头进行面部识别来完成身份验证。如果恶意用户恰好与机主本人长得很像呢?如果用户戴上了面罩,如何完成识别?如果尝试解锁的用户持有机主的照片呢?如果光线很暗,或者相机并没有完全拍到人脸呢?如果机主的发型变了,或者整容了呢?
如果使用密码验证,确定密码是否输入正确很容易。不过,对于人脸则不是这样。我们能不能也采用同样的方法,将正确的人脸图像与照相机采集的图像进行精确对比?显然是不可以的。举例:如果录入的图片是在亮光下拍摄的,而当前是在暗光下进行验证,很可能就会识别失败。我们需要在照片上提取出高级的信息来,比如说,尝试计算鼻子的长度,或者获得眼的颜色,又或者对嘴的形状建模。经过一系列这样的处理以后,再进行比较。光照会影响对双眼颜色的判断。总的来说,需要允许判断具有一定的粗略性。如果两幅图片足够接近,则通过授权;否则,不通过。如果不容忍最接近匹配以外的匹配,在有些时候,应当被授权的用户就无法获得授权。这属于假阴性(false negative)。如果容忍了较多的不同,使得不应当被授权的用户获得了授权,则属于假阳性(false positive)。

生物特征识别天然性质,就是其任何实现都会同时存在假阴性和假阳性。两个指标一般是不可以同时最小化的,假阳性率降低后,假阴性率也将增加,反之亦然。典型地,两条曲线有一个交点。在交点处,假阴性率和假阳性率都较低或不太高。但有时候,我们可能更在意假阴性率或假阳性率的其中一种。例如,如果一台智能手机频繁将机主的指纹识别失败,从而拒绝解锁,那么它不会流行起来,毕竟机主的指纹和偷手机的相似的概率是很低的。在这里,假阴性率的要求就更严格。如果正在通过视网膜扫描来打开一个银行保险柜,需要多扫描几次并不是坏事,毕竟如果一个抢劫犯使用义眼成功打开了保险柜,那将是灾难性的后果。在这里,假阳性率的要求就更严格。
进行生物特征识别,需要使用专门的硬件,多数机器可能都不具有。例如,许多计算机,包括智能手机、平板计算机和便携式计算机,都安装了摄像头,但是很多嵌入式设备和服务器则不然。相对来说,只有很少的机器具有指纹识别传感器,能识别其它生物特征的就更少了。一些生物识别技术需要使用的硬件倒是比较常见的,但是这样的激素不多。而且,即便需要的硬件可用了,使用起来的方便性也受到限制。
使用生物识别进行身份认证时,一个更深远的问题是:录入和检查生物信息的位置的物理距离。具体而言,检查由网络上的一台不可信机器提供的生物信息是存在风险的。如果远端的敌人截获了指纹信息,他们不需要你的手指,甚至不需要指纹识别设备,就可以创建你的指纹信息,并获得授权访问你的计算机。如果扫描设备直接物理连接到你的计算机,伪造生物信息的机会就很少。如果扫描设备连接到位于世界的另一端的一台你不能控制的机器上,伪造的机会就多得多了。

之前的讲解,可能使你觉得每一种身份认证方式都很可怕。当然,它们都不是完美的;但作为系统的设计者,并不是要去寻找一个完美的身份验证机制,而是根据系统本身和所处的环境,选择最合适的机制。指纹识别传感器放在手机上就是比较适合的,能够很好的发挥它的作用。一个难以猜测的长密码,则可以带来相当高的安全性。设计良好的智能卡,也可以使得没有持卡的用户几乎不可能获得授权。无论哪种机制失效了,都可以使用其它不会在相同场景下失败的机制来代替。

除了人类用户,还需要考虑非人类用户的授权问题,比如一台Web服务器或者一个智能灯泡。
可以简单地为它们各添加一个帐户。但是,如何确保只有服务器进程关联到帐户webserver,只有灯泡的控制程序关联到帐户lightbulb呢?总不能让服务器上的任意用户都能随意创建属于服务器而不是用户自己的进程吧?
一种方法是:为这些非人类用户也设置密码。为Web服务器用户分配密码后,当需要时,比如要求创建属于服务器的进程,系统管理员作为服务器用户登录,创建一个命令shell,使用它来产生服务器需要的实际进程,来完成工作。通常,shell创建的进程会继承父进程的身份,在这里就是webserver。但更常用的手段是:跳过中间步骤(这里是登录),提供一些机制,使得特权用户被允许创建不属于自己的而属于其它用户的进程。此外,还可以提供允许进程改变自己所属的机制,于是服务器进程可以以某个用户(例如系统管理员)的身份启动,然后将自己改为属于webserver。还有另一个方案,就是允许进程临时改变身份,但记忆之前的身份。显然,这些方法都要求强控制,因为它们允许用户创建不属于自己的进程。
有些时候,对于这些非人类用户,不需要提供授权。相反,特定的其它用户(可信的系统管理员之类)有权为创建的进程分配新身份,而不需要提供除了用户自己的授权以外的其它授权。在Linux和其它Unix系统中,可以使用sudo命令实现这个方案。例如

sudo -u webserver apache2
这代表apache2程序要以webserver身份运行,而不是使用sudo命令的用户身份运行。sudo命令要求提供授权凭证(比如root密码),但不需要webserver本身的任何授权信息。创建apache2后,apache2创建的子进程自然就继承webserver的身份了。

有时我们希望按组来识别用户。例如,四五个系统管理员,允许启动服务器的任何用户。创建一个具有特定权限的系统意义的组很有优势。我们把这四五个超级管理员放在一组,然后把一些安全主体放在另一个组。这样,就可以按组来进行决策,而无需按单个用户来。如果一个系统管理员要求以组的身份进行动作,就检查它是否为组成员。我们同样可以为每个进程关联一个组身份,或者使用进程的个人身份信息作为组列表的索引。后者更灵活,因为这允许我们将用户放进多个组。
非常多的现代操作系统,包括Linux和Windows,都允许用户组,因为它们为应用程序的安全策略带来了方便与灵活性。它们处理组身份与处理个人用户身份十分类似。譬如,子进程通常具有与父进程相同的组权限。使用这类系统时要记住,组身份提供了另一种访问资源的途径,虽具有优势,但也会带来额外的危险。

 Linux遵照早期Unix系统的传统,通过密码对用户进行授权,并将新登录的用户与初始进程绑定。这里讲解一下登录的具体过程。
1、运行在具备特权的系统身份的特殊登录进程显示提示符,要求用户输入其身份,输入形式一般是短用户名。用户输入用户名后按下回车。名称将回显在终端。
2、登录进程提示用户输入密码。用户输入密码,但密码不会回显。
3、登录进程在密码文件中查找用户输入的名称。如果没有找到,拒绝登录。如果找到,确定用户属于的内部用户标识符,初始命令shell在登录完毕后需要提供给用户,并且初始目录为用户的主目录。并且,登录进程也会查找加盐并哈希后的密码,密码存储于系统上的一个安全的位置。
4、登录进程将用户输入的密码与盐混合,并将混合结果进行哈希。哈希结果与上一步查找出来的密码比对。如果不匹配,则拒绝登录尝试。
5、如果匹配,则fork出一个新进程。将创建的子进程的用户和组设置为之前确定好的值,这是具备特权级身份的登录进程被允许做的。将目录改为用户的主目录,然后运行用户的shell进程。目录名和shell类型都是在第3步确定的。
还有一些其它的细节,这些细节能够保证我们能够在相同的终端登录另一个用户,但不进入。
第3步和第4步告诉我们,无论是用户名不存在还是密码错误,都可以登录失败。但是Linux和其它许多系统都不会指出具体是哪个原因导致的。这使得攻击者无法得知自己是猜错了用户名还是猜错了密码。

除了上面说的三种身份验证的大方向,还可以有其它的选择。设想你去机动车辆管理局(DMV)申请驾驶执照。你来到一个柜台,与柜台后的雇员进行交谈,提供一些个人信息,还有相关的费用。为何你相信他就是DMV的人,而且可以给你驾驶证呢?你不知道这个人是谁,他也不给你官方的ID卡,也不背出来点什么机密的东西,证明他是DMV的创建人之一。你相信他是如假包换的,是因为他就站在特定的柜台后面。也就是说,你基于地点对他进行了身份认证。
有时,你还可以根据一个实体做什么来对他进行身份认证。如果你留意一些个人特征,比如打字形式和命令之间的延迟,这也属于生物信息。 Google将这类方法作为身份认证因素引入了Android智能手机。相比实体是否属于行为良好的用户,你对实体到底是什么可能没有那么多的兴趣。比如,许多网站并不关心访客的身份,而更关心他们有没有正确使用网站。这时候,可能就会通过与系统的交互行为来验证身份。

访问控制(access control),就是判断一个请求是否符合安全策略。我们将确定哪些系统资源或服务可以被访问、可以被谁访问、可以在什么环境下以什么方式访问。
本质上,访问控制的算法接受一定的输入,产生一个二值输出,也就是允许或拒绝访问。主体(subject),指的是想要执行访问的实体,比如用户或进程。对象(object),指的是主体需要访问的东西,比如文件或设备。访问(access),则是特定的处理对象的动作,比如读或写。有时候,访问控制也称为授权(authorization)。
什么时候会做出访问控制决策呢?系统必须运行特定的算法来进行这个决策。运行这种算法的代码叫做引用监视器(reference monitor)。我们要求引用监视器不但要正确,还要高效。
之前讲过的完全仲裁(complete mediation)原则告诉我们,无论谁请求什么,都应当进行安全检查。但是这种级别的安全检查会带来较大的开销,所以,很多时候,必须在性能与安全性之间做权衡。但好在,对于一些特殊情况,既可以做到低开销,又可以不影响安全性。
一种方式是:仅将属于主体的对象提供给主体。例如说,一个进程允许访问自己的虚拟内存。只要使用属于自己的那部分内存,操作系统一般就不进行检查。页表中有一些权限位,决定每一页是否可以读写或执行。但这种检查不是由操作系统完成的,而是交由相应的硬件。类似的技巧也可以应用于外围设备。如果一个进程需要访问某种虚拟设备,而没有其它进程被允许,那么当该进程需要使用该设备时,操作系统也不需要检查。比如,一个进程在最初的访问控制决策中已经被允许使用GPU,那么该进程接下来需要写入显存或发起指令时,操作系统也不会再干涉。
以前讨论过,虚拟化基本上是操作系统提供的幻象。进程们共享内存、设备和其它计算资源。看起来,每个进程都可以独占资源。实际上。操作系统运行在背后,有时还与硬件配合,营造这样的幻觉。换句话说,操作系统依然需要确保正确的访问。只依赖虚拟化机制来确保正确访问,仅仅是把问题推给了保护虚拟化功能。对一些其它情况,可能不适用。

计算机科学家们开发出了两种基本方案,它们使用不同的数据结构与不同的方法来进行决策,分别称为访问控制列表(access control list,ACL)和能力(capability)。事实上,说它们是计算机科学家提出来的,有些不精确。它们已经被使用在非计算机领域超过千年了。

假设我们要开一家独家的夜店,就叫,比如说,Chez Andrea。(原书注:Arpaci-Dusseau夫妇让Reiher在讲安全的章节使用这些名字,但并没有使用暴力相逼迫。)夜店只允许最顶尖的那批操作系统研究开发人员进入,而不想让那些比如说做数据库或者编程语言的溜进去。这就需要确保只有指定的顾客才能进门。怎么做呢?一种方法是,雇佣一个大块头的、具有恐吓性的保镖,他持有所有允许进入的成员列表。如果有人想进来,就需要证明自己的身份给保镖。如果是Linus Torvalds(Linux内核创始人) 或者Barbara Liskov(小型低开销交互式分时操作系统Venus的带头人),保镖就让他们进来;干操作系统没干出来什么大名堂的那一大堆搞网络的就会被拦在外面。
另一种方法就是:在门上放一把超大的锁,把钥匙交给搞操作系统的伙伴们。如果Jerome Saltzer(Multics操作系统项目的带头人之一,虽然这个系统并没有在商业上特别成功,但是对后来的操作系统都带来了巨大影响,包括启发了Ken Thompson开发Unix)想进入Chez Andrea,他仅需要拿出钥匙开门。如果做体系结构而不做操作系统的想进来,就没有钥匙,只能待在外面。与第1种方法相比,我们省下了请保镖的钱。如果操作系统领域的新星们想申请进入,就需要为他们准备新钥匙。但也要考虑错误地把钥匙交给了没有资格获得钥匙的人员,或者有成员丢失的钥匙的情况。具体而言,需要确保这些钥匙不能再打开门。无论是使用哪种方法,都需要注意,比如说,某人带着Linus Torvalds或Barbara Liskov的面具,保镖没有发现;又或者,没有小心确认进来的人就是Jerome Saltzer,而把钥匙交给了一个陌生人。这就没有办法再确保流氓地痞都待在外面了。

相同的思想可以应用到计算机系统中。早期的计算机科学家们决定:把锁和钥匙的方法,称为基于能力的系统(capability-based system);基于保镖和准许清单的,叫做访问控制列表系统(access control list system)。能力(capability)类似钥匙、电影票或搭乘地铁的代币。访问控制列表自然类似于列表。如果使用能力,当属于用户X的进程需要读写某个文件时,它需要交出文件的能力给系统。如果使用ACL,系统在与该文件关联的访问控制列表中查找用户X。只有用户在列表中时,才允许其访问。
在高级层面,这两种方法看起来没有太大的不同。但考虑实现需要的数据结构,很快就会发现主要的差异了。

Chez Andrea遵照老式英国夜店的传统,给每个成员提供一个私人的房间,以及图书馆、进餐室、台球室和其它共享区域的访问权限。在这样的情况下,需要确保的不只是成员能进入夜店,还包括例如Ken Thompson不能溜进Whitfield Diffie(著名密码学家)的房间,然后在床单上恶作剧。我们可以使用一个大的ACL,著名允许对每个房间的访问,但这样会变得无法管理。
在典型的使用ACL的操作系统中,每个文件具有自己的ACL,使得在访问控制检查中涉及的列表更简单、更短。例如,在一个进程打开文件/tmp/foo时,系统调用open()会陷入操作系统,操作系统查找进程的进程控制块,确定谁拥有该进程,例如用户X。然后,系统必须持有/tmp/foo的ACL。ACL属于文件的元数据。如果X不在ACL上,X就无法访问该文件。否则,继续判定X的ACL项是否允许请求的访问类型。假如X请求打开/tmp/foo进行读写,但ACL仅允许X读取文件,于是系统拒绝访问,并返回错误给进程。
在哪里存储ACL呢?ACL与实现安全策略高度相关,也不经常改变。为了加快读取,我们希望ACL最好存储在文件的目录项(dirent / dentry)、索引节点(inode)或文件的第一个数据块内,或者这些位置附近。
ACL一般会有多大?很明显,ACL里至少要存储用户ID和访问模式。这么说来,条目数最多可以达到系统中的总用户数。对于一些系统,用户多达上千个。但是一般地,属于一个用户的文件一般只对该用户和其它少数用户可用。所以,为ACL保留能存储总用户数个条目的空间是不必要的,因为绝大多数用户都不会出现在绝大多数的ACL里。一些文件可能应当对所有用户都允许部分操作(读取或执行等),比如常用的可执行文件ls、mv等,以及字体文件、网络配置文件,等等。如果不这样允许,用户们就无法使用操作系统做多少事情。为了节省空间,可以将ACL做成可变大小的。

很幸运,在许多场景中,都可以得益于Bell实验室的Unix系统的遗产。在那些远古的日子里,计算机科学巨匠们漫步地球(或者,至少是New Jersey的某些地方);当时的存储容量非常少,且相当昂贵。那时候,根本没有办法为每个文件存储相当大的ACL。事实上,他们发现,最多只有9个bit的空间充当每个文件的ACL。早期的UNIX设计者们具有足够的智慧,在如此稀缺的存储条件下,也能充分利用好它们。他们认为,需要注意的访问模式主要有3种:读、写和执行。只要管好这3种模式,就能应付好绝大多数安全策略了。他们把这9个bit实现的ACL分成3组:文件所有者、特定的用户组,以及其它任何用户。文件所有者和指定的组ID都存储在inode里。这9个bit中,没有任何bit用来表示具体的用户。
这9个bit就实现了ACL的功能,不但解决了ACL将会吃掉大量存储的问题,还解决了ACL检查开销大的问题。因为访问一个文件必须读取其inode,所以,只要将这9个bit放到inode里,获取ACL就不需要进行任何额外的寻道了。而且,这种形式的ACL在检查时不需要进行任何的全表搜索,只需要通过的少量的逻辑运算来读取其中的任何几个bit。这样的方法为如今绝大多数遵循POSIX的文件系统提供了优异的答案。当然,它也有它的局限性:无法表示复杂的访问模式和共享关系。一些现代系统,比如Windows,允许对这种ACL进行扩展,但很多都基于已有的UNIX风格的9-bit ACL。需要指出的是,UNIX的ACL也吸收了多个早期的操作系统,比如CTSS和Multics,的一些已有的事物。

先来说说ACL的优点。首先,要想得知谁可以访问资源,只需要查询ACL就可以了。其次,假如改变访问权限,只需要改变ACL,因为没有其它东西能给出访问权限。此外,ACL一般与文件本身存储在一起,或存储于文件附近,只要能访问到文件,就能顺便读取到访问控制信息。这在分布式系统中尤为重要,因为这带来了高性能。
然后是缺点。要实现ACL,就要解决好搜索的耗时问题。虽然9-bit ACL很实用,但也限制了ACL的功能。有一种情况是,如果要求获得一个主体(进程或用户)可以访问的全部资源,就需要查找系统中的全部ACL。还有,在分布式环境中,需要对分布在全部机器上的ACL形成整体的视图。如果cs.ucla.edu上的用户需要访问存储在cs.wisconsin.edu上的文件,那么Wisconsin的机器就需要借由自己存储的访问控制列表检查UCLA提供的身份。UCLA和Wisconsin上的用户remzi是指的同一个主体吗?如果不是,就代表可能允许了一个远程用户访问他不应该访问的东西。但是,为分布在多个计算域中的用户维护一致的命名空间是一项具有挑战性的任务。

在这里,我们遇到了一个有趣而困难的分布式系统问题:在不同的机器上,名称具有什么意义?在单台计算机上,这个命名空间(namespace)问题相对容易解决。如果新事物想使用已有的命名,拒绝即可。所以,一个特定的名字只要由任何用户或进程在系统中提出,就总是代表相同的事物。/etc/password对该计算机上的当前用户与其它用户都是同一个文件。
但对于分布式系统呢?如果要保证唯一名称,就需要确保诸如计算机UCLA上不能创建已经存在于University of Wisconsin上的名称,等等。有不同的解决方法。一种是:不干扰,而是使用不同的命名空间。PID便是如此,它只在单一的计算机上有效。另一种是:要求一个权威机构来选择名称。这或多或少是AFS处理文件名创建的方式。还有一种是:将命名空间的一部分分给每个参与者,允许他们在这样的范围内指派任何名称。这是WWW和IPv4使用的方法。这些方法并没有对错之分,应当根据实际需要来选择。

Chez Andrea给每个成员分发钥匙,不同房间使用不同的钥匙,阻止了淘气的成员们给其它成员的房间留下小惊喜。每个成员都携带一组钥匙,允许他进入特定的区域。在能力系统中,运行的进程具有若干组能力,指明了访问许可。如果使用纯的能力系统,则不存在ACL,这些能力代表了该进程的全部访问许可。Hydra等系统使用此方法处理访问控制。Linux内核2.2版本开始,也引入了能力机制。
例如,当打开一个文件时,要么应用程序通过参数提供允许访问的能力,要么操作系统找到相应的能力。如果能力允许进行读或写等操作,则打开文件;否则,拒绝并报错。
能力的本质也只是一串二进制位。既然如此,就没有哪种位组合是进程们构造不出来的。并且,如果一个进程具有这样一串的位,它就可以不断复制它们,并存储在任何想要的位置,乃至其它计算机。
从安全的角度,这听起来不是很好。如果进程想访问一个文件,似乎只需要生成特定的位,然后就可以获得访问权。并且,由于位可以被复制,这意味着我们无法收回进程已经获得的许可,因为进程可能已经将这些位的副本藏匿到别处。进程还可以将能力的副本传送给其它进程,使得其它进程能够提权。
因此,操作系统会控制和维护能力,将它们保存在受保护的内存空间中。进程可以对能力进行许多操作,但必须由操作系统介入。例如,进程A想允许进程B读写/tmp/foo,则不能直接将能力对应的位模式串发送给B,而必须经由系统调用,请求操作系统将正确的能力授予B。这就给了操作系统机会,判定安全策略是否允许B访问/tmp/foo。
操作系统已经具备每个进程的受保护数据结构,即进程控制块(PCB)。因此,只需要将能力列表(位于内核内存)的指针存入PCB里就可以了。当进程尝试特定操作时,调用会陷入操作系统,操作系统会查询能力列表,查看该进程是否具备相应操作的能力。
在一般的系统中,为一个主体的所有被允许访问的任何东西保留在线的能力列表会带来高开销。如果为基于文件的访问控制使用能力,一个用户可能具有上千种能力,每个允许访问的文件对应一个。通常来讲,如果使用能力,系统会把它们存储在安全的区域,并按需导入。附加给进程的能力列表不必要非常长,但决定巨大数量的用户可以为他们运行的每个进程提供哪些能力是个问题。
另一个选项是:操作系统不用存储能力,而是对它们加密保护。如果能力相对长,且进行了强加密,则它们难以被实际手段猜出来。在分布式系统中,加密能力更加合适。

下面是能力的优点。使用能力,可以很容易确定一个特定的主体能够访问哪些系统资源,只需要查询主体的能力列表即可。若要收回访问权限,只需要将对应的能力从能力列表中移除。在操作系统独占对能力的访问权时,这很简单。如果能力列表位于内存中,检查它的成本就相当低,尤其是因为能力可以包含一个指向其保护的资源关联的数据或软件的指针。这样的指针很可能就是系统的能力机制的核心实现。
另一方面,确定一个主体能访问的全部资源的集合的消耗也很大。任何主体都有可能可以访问某种资源,因此同样也必须检查全部主体的能力列表。能让能力列表变得不太长且可管理的方法还没有发展得很好,这并不像UNIX中的短ACL那般。系统必须在确保不能伪造的前提下创建、存储和检索能力,这也是具有挑战性的。
能力的一个巧妙的方面是:提供了一种良好的以受限的特权创建进程的方式。使用访问控制列表时,进程继承了父进程的身份,同样继承了全部特权。如果只想授予父进程的部分特权,是困难的。要么,创建一个具有受限特权的新主体,改变一堆访问控制列表,设置新进程的身份为该主体;要么,需要将访问控制模型进行扩展,使得它不遵循一般的访问控制列表的行为。使用能力时,就容易了:如果父进程具有能力X、Y和Z,但只想子进程具有能力X和Y,那么在子进程创建后,父进程只需要传送X和Y,不传送Z。

实践中,用户可见的访问控制机制倾向于使用访问控制列表,而不是能力,理由有许多。然而,在掩护之下,操作系统也大量使用能力机制。例如,在典型的Linux系统中,open()调用成功后,只要进程保持文件打开,那么ACL就不会再被检测。相反,Linux创建一个数据结构,表明该进程对指定文件具有相应的权限。这个数据结构附加在进程控制块(PCB)里。每次读写时,操作系统查询该数据结构,确定是否允许读写,而不查找访问控制列表。如果文件被关闭了,这个类似能力的数据结构也从PCB中删除,进程就不能再访问该文件,除非再进行一次open()调用,去查询访问控制列表。相似的技术也可以用于控制对硬件与IPC(进程间通信)通道的访问。UNIX把这些资源也视作未文件,因此更为合适。将ACL与能力混合使用,避免了它们各自的缺点。检查能力比检查访问控制列表更容易,因为只需要检查一个指针是否存在于操作系统的数据结构中。为所有可访问的对象管理能力的开销也被避免,因为能力只在ACL检查通过后,才会设定能力。如果一个对象从不被进程访问,ACL也就从不会被检查,也就不需要任何能力。由于进程一般只打开文件的一小部分,因此缩放性问题(scaling issue)通常也不会发生。

谁能决定计算机资源的访问权限?对许多人而言,答案很明显:拥有资源的人。对于用户文件,用户进行访问控制设置。对于系统资源,系统管理员或计算机所有者来决定访问权限。但是,对一些系统和安全策略,这未必是正解。尤其是,一些方面(party)最注重信息安全,要求更紧密的控制。
军事是最明显的例子。我们都听过绝密(top secret)信息这个说法:如果你允许查看到绝密信息,你不能让其他人也能查看。如果你创建的文件涉及绝密,比如一份包含绝密文档的统计数据或引用的报告,那么,你作为文件创建者,甚至也不具备控制访问权限的资格。对信息安全具有最高主管责任的任何实体,来完成相应的决定。换句话说:这种条件下,特定的主体具有为一些属于其他用户的信息设定访问权限的能力,并且其他用户不能覆写设定。更常见的情况是自由决定的访问控制(discretionary access control)。其他人是否恶意访问资源,由拥有的用户进行自由裁量。更受限制的情形是强制访问控制(mandatory access control,MAC)。系统中的至少一部分访问控制决策是由某个权威组织管理,他们能覆写用户自己的决定。这两种访问控制方式不影响选择使用ACL或能力,也经常与访问控制机制的其它方面,比如访问信息是如何存储和处理的,无关。强制访问控制也可以包括自由决定访问控制的元素,它们允许受到强制控制的进一步限制。
很多人不会有机会使用具备强制访问控制的操作系统,因此强制访问控制在这里也不予讨论。如果你确实在强制访问控制很重要的环境中工作,你一定会听说它。因为在这个时候,主管的既然很在意一些东西,以至于需要强制访问控制,他们肯定也会严厉惩罚不遵守规则的用户。Loscocco有一篇论文描述了Linux的一个引入了强制访问控制的特殊版本。如果想学习这种系统,这篇论文是一个不错的入门。

很多系统都将访问控制机制暴露给用户,而且多采用自由决定的访问控制。然而,现代计算机可以具有超过几百万个文件,人类用户单独设定访问控制许可是不可行的。一般地,系统允许每个用户为自己创建的文件建立默认的访问许可。例如,当使用Linux的open()调用创建文件时,用户可以指定为文件分配哪些权限。使用umask()调用可以进一步控制访问许可。
如果需要,所有者可以替换初始的ACL。但经验表明,用户一般不这样做。因此,选择正确的默认值很重要。系统中的许多设置,几乎所有人都从不会去改变它们。
当然,为了达成安全目标,这些控制对一些用户和系统具有必不可少的重要性。比如,一些软件安装包会为它们创建的可执行文件与配置文件设置访问控制。如果随意更改访问控制,就可能暴露敏感信息,或允许攻击者改变关键的系统设置。如果收紧访问控制,也可能令一些后台的守护进程突然停止运行。
众多大机构使用标准的访问控制方法实现安全策略时发现的一个实际问题是:作为不同角色的人们需求不同的权限。例如,在医院里,医生与药剂师都互相具有对方不具有的特权。如果按角色组织访问控制,为角色指派用户,很多安全策略就容易实现。如果一些用户允许根据当前执行的任务切换不同的角色,这个方法尤其有用,因为他不用去想怎么改变自己个人的访问许可,而只需要切换角色。通常,他们只在作为某种角色时,持有其权限。如果退出该角色(变为其它角色),就失去了原角色的权限。
这便是基于角色的访问控制(role-based access control,RBAC)。在研究论文提出了更正式的概念之前,其核心思想已经很普遍。现在,RBAC已经应用于不计其数的组织,尤其是庞大的组织机构。大组织会面对更加严峻的管理挑战,因此,类似RBAC的允许在单次操作中按组处理方法,将会极大降低管理任务的困难程度。举例:公司决定允许开发人员访问某个库,但会计人员均不可以,使用RBAC时,仅需要一次操作就可以完成:将必要的特权授予程序员角色就可以了。如果一个程序员晋升到管理层,不应再访问这个库了,只需要将其从程序员角色中移除就可以。
这样的决策并不代表你怀疑雇佣的会计师们不诚实,容易把库里的机密出售给竞争对手(当然,最好还是在招聘的时候就确保只招到诚信的会计人员吧)。这只是为了履行最少特权(least privilege)原则。很多时候,更应当担心他们可能的不小心。如果会计师干脆不能访问这些库,他们自然就不会因为粗心而把库里的代码泄露了。
RBAC听着很像ACL里的分组,但它的管理能力更强。RBAC允许一个用户担任多个角色。例如,一个程序员可以在提拔为管理层后,依然继续进行开发,或需要把关测试库里的其他成员的代码,因此需要访问这些库。在这个场景下,经理切换角色后,虽然能访问开发的库,但无法访问其它信息,比如成员的业绩评估情况。因此,如果一个鬼鬼祟祟的程序员暗中注入了恶意代码到库里(例如,尝试读取其他成员的业绩,了解他们的工资),由于此时的经理无权访问这些数据,恶意代码也就读不出这些信息。这些系统在用户担任新角色时,通常要求身份验证。
RBAC也可以提供比是否能读写一个文件更细粒度的管理。例如,销售人员可以在某个文件里添加特定产品的购买记录,但无法在该文件中为同一个产品添加补充进货的记录,因为销售人员没有补充货源的职责。这种控制有时也叫类型强制(type enforcement)。它将详细的访问规则与特定对象通过该对象的安全上下文(security context)关联。实现的精确程度将会影响性能、安全上下文占用的存储空间和身份验证。

在Linux和相似的操作系统下,使用访问控制和组,可以建立一个简单的RBAC系统。这些系统的访问控制机制具有一个特性,称为权限提升(privilege escalation)。它允许对特权进行小心的扩展,典型地,允许某个程序用超过调用用户的特权允许。在Unix和Linux系统中,该特性称为setuid,它允许程序使用不同用户的特权运行。但是,这些权限只在程序运行期间被授权,程序退出后即失效。谨慎编写的使用setuid的程序,只用获得的特权执行有限的操作,确保特权不被滥用。不幸的是,并不是所有程序都这样的,这导致了近年来的很多安全问题。可以为每个角色定义一个任意的用户,并关联期望的权限,以此创建一个简易的RBAC系统。
Linux的sudo命令,提供了这类功能,允许指定的用户使用其它身份运行程序。例如,假使该用户位于系统维护的允许使用Programmer身份的用户列表里,则

sudo -u Programmer install new-program
将使用用户Programmer的身份运行安装命令,而不是运行此命令的用户身份。此方法的安全使用要求控制允许以何种身份执行此程序的系统文件的谨慎配置。通常,运行sudo要求额外一步身份验证(例如,输入超级用户密码),这与其它RBAC系统一致。
为了更高级的目的,RBAC系统通常会支持比setuid和sudo允许的更细的粒度以及更慎重的角色指派追踪。这种RBAC系统可能是操作系统的一部分或者扩展插件,或者可能是一种编程环境。经常地,当使用RBAC时,也会运行一定程度的强制访问控制。否则,在sudo的例子中,以Programmer身份运行的用户可以运行命令,去改变文件的访问权限,使得install命令也能被非Programmer身份的使用。有了强制访问控制,用户能够使用Programmer身份进行安装,但不能使用这个角色令诸如销售人员和会计师等角色可以进行安装。

通常,进入了系统的攻击者只能运行在具备受限权限的身份下,他只能进行读取一些信息文件并发送给远程用户之类的操作,或者为这些文件运行标准的实用工具。他甚至连写入的权限都没有。你可能会觉得,这几乎不会对系统造成损害,因为攻击者也无法访问太多东西。
在系统中成功立足后,攻击者会查看哪些方式可以提升权限。他们会寻找代码中的缺陷,或者允许应用程序访问的配置。这些提权尝试通常是攻击者实现成功攻击的第一要务。
在许多系统中,都有一个特殊的用户,常称为superuser或者root用户。这个用户比其它用户具有多得多的特权,因为它的目的是允许最必要、最深远的管理。已经在系统中立足的攻击者的终极目标是,使用提权的手段,成为root用户。一旦能做到,他就具有了系统的全部控制权。他将能够查阅任何文件、修改任何程序、改动任何配置,乃至安装不同的操作系统。所以,留意任何允许提权至超级用户的途径,是极其关键的。

Android作为当今移动设备(尤其是智能手机)上最流行的系统之一,运行它的设备相比传统的服务器计算机,面临不同的访问控制挑战。它们的功能基于许多相对小而独立的应用程序,通常称为APP。它们被下载并安装,运行在仅属于一个用户的设备上。因此,就没有了为多个用户防护来自其它用户的攻击的问题。如果使用标准的访问控制模型,这些APP就会在指定用户的身份下运行。但是,APP由许多实体开发,有一些实体可能是不怀好意的。长远地说,许多APP不应当拥有设备上的绝大多数资源的合法使用权。如果它们被赋予太多权限,恶意程序就可以访问机主的联系人、拨打电话、在网络上乱买东西,以及做出其它不期望的行为。最少特权(least privilege)原则告诉我们,不能给予APP完全的权限,但它们应当具备确实需要的那些权限。
Android运行于Linux的一个版本上,应用程序的访问限制通过为每个安装的APP生成新的用户ID来达成。APP运行在分配的ID下,其访问将会受到基本的控制。然而,Android中间件提供访问控制的额外设施。应用程序开发者定义他们的APP需要的访问。当用户考虑安装该APP到设备上时,他们会被告知APP需要的权限。用户可以选择授予这些权限,或不安装APP,或限制其权限(该做法可能限制APP的功能)。开发者也知名其它APP与该APP通信的方式。编码访问信息的数据结构称为许可标签(permission label)。APP的许可标签(同时包含它能访问的和它提供给其它APP的)在APP的设计期间就设定好,并在安装时编码到特定的Android系统中。
因此,许可标签更像能力,因为拥有它们允许APP做一些事情,缺少相应的标签就使得APP无法做出对应的事。APP的许可标签集APP的许可标签集在安装时静态设定。用户也可以在之后改变权限,即便限制权限可能影响应用程序的功能。许可标签是一种强制访问控制。Enck等详细讨论了Android的安全模型。
Android安全方法有趣但不完美。尤其是,用户有时会无意识地授予一些权限,以及在授权或无法高效使用APP之间做出选择——他们通常选择前者。如果APP是恶意的,这个行为就会带来问题。

猜你喜欢

转载自blog.csdn.net/COFACTOR/article/details/119276314