Linux就这个范儿 第14章 身在江湖
“有人的地方就有江湖”,如今的计算机世界就像一个“江湖”。且不说冠希哥有多么无奈,把微博当QQ的局长有多么失败,就说如此平凡的你我什么时候就成了任人摆布的羔羊也一点都不奇怪。计算机的安全问题一直让我们深受其害。可以说,使用过计算机的人就没有不被病毒、蠕虫甚至木马侵害过的。轻一点的少几个文件或做个勤快的“肉鸡”,严重的可能别人知道你银行密码的速度比你老婆还快。这只能感叹由于利益的驱动让黑客们的技术进步得实在是太快。
还好,有江湖的地方同样也有“仗剑的侠士”,能够“遇见不平一声吼,该出手时就出手”,比如各种各样的杀毒软件、网络安全软件等。其实每一种操作系统自身也是“仗剑的侠士”,只是“武功”的高低不同罢了。如果这么比喻的话,可能马上会有人想到Windows,认为它的“武功”很挫。因为在计算机世界的这个“江湖”中,受害最多的就是Windows所“庇护”的那些用户们。但是这是一个非常错误的认识,Windows在“江湖”上应该属于一等一的高手,人家可是通过了美国C2级安全认证的。Windows的安全问题之所以被人们广为诟病,就是因为受它“庇护”的人太多了。有点忙不过来不是?而且由子可攻击的“肉”多,自然攻击它的狼也多,即便Windows是藏獒也对付不了。毕竟好狗斗不过群狼啊!
安全等级 从高到低:A->B->C2(Windows\SQL Server)->C1->D
14.1 C2级安全认证
既然提到了美国C2级安全认证,那我们就来看看它到底处在一个什么位置上。
话说美国国防部为计算机安全的不同级别制订了4个准则,从高到低的顺序是A、B、C、D,每一个级别还细分了若干子级。显然C2是处于中等偏下的水准,看来Windows真不咋的。但是别着急,咱们接着往下看。
作为A、B这两个最高安全等级是应用于国防的,我们一般人玩不起它,也不是操作系统自己能够搞定的。比如A级设置中,一个磁盘驱动器从生产厂房直至其安装到计算机上都要被严格跟踪。作为我们小老百姓去实施这样的安全防护措施不累么?
那么再看C级,本身被划分为C1和C2两个子级。
作为C1级的系统也要求硬件有一定的安全措施,不过只是要求给机箱加个锁。作为普通人,只要你能给你家大门安上一把锁就算满足这个标准了(应该都能满足)。对于软件的要求是在使用之前必须登录,并且具有完全的访问控制能力,允许系统管理员为一些程序或数据设立访问权限。但是C1级不要求控制进入系统的用户的访问级别,这就允许用户可以把系统的数据任意移走。
C2则对C1进行了加强,引入了受控访问。这一特性不仅以用户权限为基础,还进一步限制了用户执行某些系统指令。授权分级使系统管理员能够给用户分组,授予它们访问某些程序的权限或访问分级目录。另一方面,用户权限以个人为单位来授权用户对某一程序所在目录的访问。如果其他程序和数据也在同一目录下,那么用户也将自动得到访问这些信息的权限。C2级别系统还采用了系统审计。审计特性跟踪所有的“安全事件”,如登录(成功和失败的)、系统管理员所作的操作等。可以说C2安全级别是普通用户能够负担得起且不会给使用上造成太大麻烦的最高安全等级。Windows能够达到这个高度,作为一个面向个人电脑的操作系统已经是最高的了,因为更高的就不是软件能做到的了。其实被人们普遍认为“安全”的Linux也不过如此了,有些地方可能还不如Windows。
可以说我们现在使用的系统几乎都是运行在C1级别这个样子,最主要的原因就是我们从来不想受到系统的束缚,直接拿管理员账号来用,这才导致要借助第三方软件来维持你计算机的安全。
研究Windows显然不是本书的目的所在,只是顺便帮它清洗了一下冤屈。我们还是回归正题,研究一下Linux的安全问题。Linux也是身在计算机这个“江湖”的,但是到目前为止有关Linux安全问题的报道却寥寥无几。这并不是说Linux真的很安全,毕竟针对Linux的病毒不是没有。只能说给Linux写病毒不赚钱,所以干这种事情的人少。但这也并不是说Linux就一点“武功”都不会,要不然也没法在这个“江湖”上立足。本章的内容就跟大家分享一下Linux所依仗着的那些“武功”路数,是如何让它能够在这充满了“血雨腥风”、“你死我活”的“江湖”中抢到了一个不小的山头的。
14.2 Linux的安全问题
计算机安全领域的先驱——Robert Morris曾经说过:“确保计算机安全的三条黄金定律是:不要拥有计算机,不要打开计算机,不要使用计算机。”按照这个说法,世界上就没有安全的计算机。即使是普遍被认为“安全”的Linux系统,在这方面也存在严重的不足之处。事实就是这样的,不管有多么残酷,您都得照单收着,还得享受着,要不您能咋办?
下面我准备从三个方面来阐述一下Linux系统所面临的安全问题,还都挺恐怖的!Linux可能会说自己很冤,可是没有办法,毕竟这种事儿就发生在你身上了。
14.2.1 罴客入侵
对于个人使用的Linux系统其实是不必太在意这个问题的。因为你就是那么一小撮儿,而且很可能是被黑客们不太“看得起”的苦逼程序猿。这等于在你身上没啥油水可捞,所以他们是不会打你的主意的。但是作为企业用户就不同,这背后会涉及巨大的商业利益在里面,没有不心动的黑客。而企业又是Linux系统应用的大户,所以就成了重灾区。
那么黑客们是如何入侵企业的Linux主机呢?一些类似于密码暴力破解的方法就没必要说了,因为即便现在最傻的系统管理员也知道把密码设置得长一点,所以是基本无用的。当然,那些五花八门的黑客秘籍一类的书上花费90%篇幅所介绍的什么监听啊、端口扫描啊、等等的一些伎俩,都是骗你银子的,也基本无用。因为现在大家都在使用ssh -类的远程控制工具,监听、扫描有什么用呢?我要说的是基于“漏洞”的入侵方式。这种方式一直都是黑客们行之有效,屡试不爽的高招。
要说这些“漏洞”是怎么被黑客们发现的呢?三个字——试,分析。经过不断地尝试和分析,在目前任何一台运行着的Linux系统主机上都能找出安全漏洞来。我完全没有夸大的意思,这是事实情况。原因就在于不管什么系统都是人设计和开发出来的,你见过没犯过错误的人吗?“漏洞”就是开发人员所犯下的错误,而且是非常难于被察觉的错误。你还千万别不服气,认为自己十数年的开发工作所积累下来的优良经验,不可能犯下能够产生严重安全问题的错误。
一个最典型的故事就发生在Jon Bently的身边,并在他的《编程珠玑》一书中描述过。他说:在多年的时间里先后让上百位专业程序员实现二分查找法,而且每次都是在给出算法的基础描述后,很慷慨地让他们用两个小时的时间去实现它,而且允许他们使用自己所擅长的高级语言(包括伪代码)。令人惊讶地是,大约只有10%的专业程序员正确地实现了二分查找法。Donald Knuth也说过:尽管第一次二分查找算法早在1946年就被发表,但第一个没有Bug的二分查找算法却是十二年后才被发表出来。
可以说,如果一个简单的二分查找算法没有被正确实现的话,就很有可能产生一个严重危及安全的漏洞。不信,那就先看一下代码1,我用C来实现。
代码1:
static int binary_search( int target, int array[], int size )
{
Int l=, r=size -;
Int h=;
if(size<=)
return -;
while(<=r) {
h=(+r) /;
if (target>array[h] )
=h+:
else if(target<array[h] )
r=h - ;
else
return target;
}
return -;
}
这段代码看起来非常严谨,但是却有着非常严重的问题,你看出来了吗?最关键的是:
h=(+r) /;
因为h是int类型,在32位或64位的机器上,它最大能表示的数是2147483647,如果l与r的和大于这个数会出现什么问题呢?加法溢出,会得到一个负数。如果用一个负数作为数组的下标会出现啥情况?有人说:访问越界,系统崩溃。要我说这还是很优美的方式呢。如果传递给它的数组是在堆中分配①的,系统就会崩溃;但是如果是在栈中分配②的,你的堆栈中的内容就可能泄露了,虽然大多是情况下是无关紧要的,但谁敢保证“有心人”的努力会白费呢?正确的写法应该是:
h=+((r-)/);
如果使用Java语言,可以是:
h=(+r) >>>;
当然,很多人会认为这种错误即便发生了,也不会造成太大损失,毕竟黑客要想从这个漏洞中获取点什么有用的信息甚至入侵到系统中,还是需要付出很多时间代价去剖析或者拥有极大的运气。但是不要忘记,大多数能够被黑客有效利用并成功入侵系统的漏洞,基本上都是这种看似“无关紧要”的错误引发的,比如缓冲区溢出、SQL代码注入等。而且,你不能认为黑客们就没有中500万的运气,也不能低估他们因为利益的驱动而严生耐心的程度。
①使用malloc等库函数分配的。 TCmalloc库 谷歌
②局部变量。
优化内存碎片率是世界性难题,tcmalloc/jemalloc 都不能做到完美,可能要优化确实很困难)。
https://mongoing.com/archives/34751
我给出这个例子要说明的是,很多严重的安全漏洞并不是因为你的粗心大意带来的。因为像上述代码这样简单的例子都要全体计算机工作者们经过12年的实践才能发现。而作为Linux这样一个复杂的软件系统,又有哪些隐患至今还是不为人知的呢?而且一个很实际的问题就是,随着Linux系统的广泛应用,其所暴露出来的安全漏洞也越来越多了。
14.2.2 “病毒泛滥”
如果严格说Linux病毒泛滥是很冤枉的,只是我们将病毒、木马、流氓软件等统统称为了病毒罢了。在我看来,它们不但是计算机病毒,而且还是这个社会的毒瘤。因为现在的病毒制造者根本就不是什么喜欢恶作剧的电脑神童,其真实的本质是趋炎附势的财奴。不但危害着企业的利益,还侵害着我们这些兢兢业业、艰苦奋斗的房奴。
就Linux本身而言,从科学层面定义的计算机病毒是很难在Linux系统中长期生存下去的。从历史上看,1996年秋诞生了Linux上的第一个病毒Staog,后来在1997年初又诞生了
第二个病毒Bliss,它们两者之所以能够成为病毒是因为当时Linux内核存在某种缺陷,而当缺陷被修正了之后,它们就随之消亡了。直到现在,十几年过去了,没有发生任何实质性的Linux病毒大流行的情况出现,更谈不上“泛滥成灾”。可以说Linux的病毒基本就是人们茶余饭后的谈资罢了,因为一个成功的Linux病毒必须是手法高超且具有非凡创意的软件作品,这留给真正的天才们去创造吧,那些财奴们才懒得做这样的事情呢。
但是Linux上的恶意软件却有愈发疯狂的趋势。虽然Linux是以开放源代码著称,但是普通用户有几个能有那么大把的时间和能力,每个软件都从源代码安装,且能够对每一个常用软件的代码都了如指掌呢?这就给某些财奴们创造了机会,而且有时候还能冒充正义的化身,高喊着支持开源软件运动,而在背后却实行着他们不可告人的勾当。使得很多无辜的开发者成为他们替罪的羔羊,很多善良的人们沦为他们赚取大把钞票的工具。随着Android这
样的基于Linux平台的移动操作系统的普及,以及Google等这样极度崇尚所谓“美式*”的公司允许人们任意开设应用商店的境况出现,使得这种现象开始有愈演愈烈的趋势。
可是,很多问题并不是财奴们单方面就能掀起这么大风浪的。毕竟中国的一句古话是:“一个巴掌拍不响”。很多Linux用户也有自身的问题。总是不爱受到限制,喜欢使用root账号,这使得病毒、木马、流氓软件有了在Linux下生根的更广阔的空间和养分。我仅能在这本书中卑微地呼吁一下:亲们,善待Linux,慎用root账号,保护你自身的权益!
14.2.3拒绝服务攻击
拒绝服务攻击也叫DoS (Denial Of Service)攻击,这跟曾经十分流行的操作系统没有半毛钱关系。这是一种非常缺德地侵犯计算机系统安全的行为。虽然遭到这种攻击的用户不会爆出什么“艳照门”来引爆大众的眼球,但损失依然会很惨重。之所以说DoS很缺德,是因为它如果针对你个人,你将无法正常使用你的计算机,除非断开网络,但是没有网络我们还怎么活啊?要是它攻击某些商业性服务器,那么不单提供服务的系统会瘫痪,所有需要得到服务的用户也将失去服务。然而实行这种攻击的“人”却得不到什么实际的好处。我之所以将人字加上引号,就是因为这种“损人不利己”的行为应该已经超出人类的价值观了,虽然有些情况是出于某种所谓“商业”目的。
其实以现在个人计算机的运算能力,加之Linux系统优秀的TCP/IP协议栈实现,普通的DoS攻击并不会带来什么特别大的威胁。面对强大的商业服务器系统,DoS自然也不在话下了。这是Linux的一种优势。但是,当前“黑客”(依然要用引号)们已经变换了策略,采取了更为恐怖的DDoS( Distributed Denial of Service),翻译过来是分布式拒绝服务攻击。这种攻击的危害相当可怕,让很多DoS攻击源同时向某个计算机发起攻击,犹如洪水猛兽一般势不可挡,而且甚至与被攻击计算机处于同一网络的其他无辜计算机也会受累。国内最为著名的发生于2009年5月19日的“暴风门”事件就是一起规模空前的DDoS攻击,直接导致了中国电信的大面积网络瘫痪,面积覆盖大半个中国。足可见DDoS的可怕之处。
面对这种类型的攻击,可以说任何操作系统都是无能为力的。因为这涉及了一个计算机科学历史性的“巨大Bug”- TCP协议的机制问题。从前面的章节中你可以了解到,TCP协议是一种面向连接的协议。两台计算机通过TCP协议通信时,这两台主机要负责维护这个连接。这必然会消耗一定的计算资源和操作系统资源,比如文件描述符。作为服务端的计算机往往需要同时维护多个TCP连接,但是这个数量是有限制的,因为文件描述符会占用内存,纵使可以放开描述符的限制也会遇到内存上的限制,这个你没法放开。DoS或DDoS攻击就利用了这个“漏洞”,把计算机系统的全部资源都消耗干净而让它无法继续工作。当然,这还是很普通的手法。更厉害的还有TCP Sync Flood、UDP Flood等,本书就不一一介绍了。总之是防不胜防。
虽然DoS或DDoS从原理上讲是目前互联网技术无法解决的,但是Linux的开发者们,包括其他操作系统的开发者们,依然在试图改进一些TCP/IP协议的实现算法,来提高系统本身对抗这种攻击的能力。根据我的经验,Linux在这方面不如Windows做得好。在我的从业经验中,Windows 2003的服努器在遇到这类攻击时的存活时间往往比Linux服务器要长。而且有些版本的Linux内核在遇到这种攻击后,会出现“Kernel panic”,整个系统彻底崩溃。Windows 2003却从来没遇到过。换句话说,在攻击结束之后,Linux服务器需要重启才能继
续工作,而Windows 2003服务器能够立即开始工作。这就相当于Linux系统在遇到类似问题时,往往需要更长的系统修复时间,使得整套系统的可靠性下降。虽然Linux有免费的优
势,但是在遇到这种问题的时候,对于某些用户来说,Windows 2003会更划算。
虽然这是一本介绍Linux的书,但是在这里却一直赞扬Windows很强。并不是我有什么
叛逆的心态,只是不爱护短罢了。同时也是想让大家了解Linux在安全方面相较于其他系统还是有不足的地方的,不要去迷信。
14.2.4小结
“聪明莫过帝王,伶俐莫过江湖。养家不可治气,治气不必养家。心有波涛面含春色,此为江湖。江湖子弟,拿得起来放得下,更应守江湖道义。奈何,今日江湖已面目全非。尤其我行英才,掰块馒头扔脸上让干嘛都行,饶块酱豆腐*都能商量。给十块钱能把他父亲小名儿写电视塔上。寂寞风前独悲立,江湖几入还。”这是“非著名相声演员”郭德纲先生对其所从事行业的感叹,我们IT这个行当又何尝不是如此呢?
就像我在开头所说的,虽然本节所列举的这些安全问题并不完全是Linux自身不足造成的,但是它就发生在Linux身上了,不能完全说不管我事儿。同时我也想通过这一节的介绍让大家能够了解到:计算机安全问题依然是任重而道远的,诸君仍需努力。
好了,目前Linux系统上非常棘手的安全问题已经呈现给大家了,而且是一直需要去解决的问题。但是Linux系统依然被看作是安全的,这是因为它已经解决了很多问题。那么接下来的内容,就是要介绍那些Linux已经实现了的且被广泛应用的安全机制。Linux也正是因为有了这些安全机制,才被人们认可了它的安全性。当然,讨论这些机制是否存在漏洞则超出了本书的范围,我也不是这方面的“砖家”,真拍不动它。如果你能够通过本书的介绍发现这些现有的安全机制存在某些漏洞,那就发扬一下开源精神告诉大家吧!
14.3 PAM用户认证机制
日新月异的计算机技术发展了这么多年,用户认证方式也发生了翻天复地的变化。比如:密码验证、指纹认证、RFID认证、虹膜认证等,踊跃而至,层出不穷。可是不管采用什么样的认证方法,都有一个不可回避的问题,那就是你得实现它!
14.3.1 什么是PAM(可拔插认证模块)
Linux作为一个能够同时提供多种服务的操作系统,是不能只提供一个绕不过去的login命令就能保障其不被非法登录的。
类似sshd、ftpd这样的应用也能让用户登录系统并做一些有实质性的工作,也需认证用户身份。
所有能够让用户登录系统的应用都要单独去实现一套用户认证方法很有重复造*的嫌疑。而且人在编写软件时想不出错是很难的,重复地去编写这么多的用户认证代码想保证每一个都不出错更是难上加难。即便一个非常简单的软件错误,都有可能被黑客们拿来利用并入侵到系统中。更何况在遮么重要的用户认证上出现点什么差错呢?而且一旦发现某种方法或算法有问题,要修改起来可就麻烦大了。因为好多应用可能都使用了这种方法或算法,
而它们又不是一个人或一个公司做的。但凡遗漏了谁或谁懒惰了一点,对整个系统的安全性
都可能会带来致命的威胁。
为了解决多个应用的用户认证问题,Linux引入了一套名为PAM的用户认证机制,它的全称是Pluggable Authentication Modules,翻译过来是就是“可拔插认证模块”。它将用户认证功能从应用中独立出来,单独进行模块化设计,统一实现和维护,并提供了一套标准API,以便各应用程序能够方便地使用它们所提供的各种功能。特别的,这种用户认证机制,对于其上层的用户(包括应用程序和最终用户)是透明的。
PAM诞生自1995年,最先由SUN提出并应用于Solaris 2.3上。在这之后,经过广大开发人员的不懈努力,各版本的UNIX系统陆续提供了对PAM的支持,包括FreeBSD和Linux。其中专门针对Linux实现的PAM,通常被称为Linux-PAM。这些不同的PAM,除了具体的实现不同外,框架和标准API都是相同的。所以本书并没有特别指明要介绍的足Linux-PAM,因为这些知识具有普适性。
14.3.2 PAM的体系结构
PAM为了提供足够高的通用性、灵活性和可配置性,采用了插件机制,这样就可以针对不同的应用灵活组织不同的插件来提供“随需应变”的用户认证机制。这也是其“可拔插”之名的由来。
为了实现“可拔插”性,又要兼顾易用性,PAM采用了分层的体系结构:让各认证模块从应用中独立出来,然后通过PAM API作为两者联系的纽带,应用程序可根据实际功能需要,灵活地在其中“插入”所需类别的认证功能模块。所以,PAM可以被划分为三层:应用层、接口层和认证模块层。这种设计思想还有一个特别时髦的名字,叫“高内聚,低耦合”。如果你还搞不清楚啥是“高内聚,低耦合”,PAM就是一个非常典型的例子。具体可
见图14.1所示。
从图14.1中可以看出,PAM API处在中间位置,负责应用与各模块之间的通信。当应用程序调用PAM API时,相关的API实现代码会按照“配置文件”的规定,加载并调用相应的模块所提供的功能来执行具体的认证操作。这样,只要修改“配置文件”的相关项就可以改变具体应用的认证方式。而且,也可以根据实际需要,任意添加新昀认证模块来实现特殊的认证方式。
PAM为了提供更为细粒度的认证控制,给模块划分了四种类型,分别代表四种不同的认证任务。它们是:auth、account、session和password。auth类型的模块用来执行实际的认证工作,比如:提示用户输入密码或判断用户是否为root等;account类型的模块负责对用户的各种属性进行检查,比如:某个用户的密码是否到期、root用户是否允许在这个终端登录等;session类型的模块用于执行用户登录前、退出后的一些操作,以及对每个会话进行跟踪和记录,比如:给新用户初始化home目录、记录用户登录的时间等。password类型的模块实现了用户密码的细粒度管理,比如:设定密码的有效期、允许重复输入的次数等。
这四类模块可以堆叠使用。也就是说,一个认证动作可以同时使用多个相同类型的模块共同去完成。这就像“陪审团”去判断一个用户是否“有罪”一样,只要达到相应的“比例”就可以作出结论。而且每个具体的模块也不会仅限于一种类型,它们的“身份”也会随时变化。具体应该怎样,一切由配置文件说了算。
作为开发人员,不管是要编写需要认证功能的应用,还是准备给PAM编写某种类型的模块,都需要与PAM API打交道。
PAM API所提供的一些接口会与PAM的四类模块有一定的对应关系。例如:用于认证用户的pam_authenticate()接口和用于管理认证凭证的pam_setcred()接口对应着auth类型的模块;用于确认用户是否有权登录系统的pam_acct_mgmt()按口对应着account类型的模块;用于打开和关闭用户对话的pam_open_session()和pam_close_session()接口对应着session类型的模块;用于修改用户口令的pam_chauthtok()接口对应着password类型的模块。
PAM API所提供的另外一些接口就不再与具体的模块相对应了,它们的作用是提供一些管理性功能或实现应用与模块之间的通信。管理性的接口有:pam_start()、pam_end()、pam_get_item()和pam_set_item()等。实现应用与模块之间的通信接口有:pam_putenv()、pam_getenv()和pam_getenvlist()等。
至于PAM API该如何使用,我们稍后再做介绍。
如果是做为Linux系统管理员,要针对某些特殊的应用制定特别的用户认证方案或添加新的PAM模块,就需要与配置文件打交道了。
PAM的配置文件通常是pam.conf文件或pam.d目录,它们都会保存在/etc目录下。具体是使用pam.conf还是pam.d目录,不同的发行版可能不同,但是它们一般不会同时出现。毋庸置疑,在使用pam.conf文件的时候,所有配置信息都会保存在这个文件中;使用pam.d文件的时候,每个应用会有一个与它对应的配置文件,对应方式就是具体的配置文件名会与对应的应用名相同。如果pam.conf有机会与pam.d目录同时出现的话,pam.conf是会被无情地抛弃的。
14.3.3 配置PAM
不管采用何种形式,配置文件的格式都差不多,由很多条记录组成。一般地,一条记录就是一行,并且使用空白字符(包括Tab)将其划分成多个字段。各字段的定义按照从左至右顺序为:应用名、模块类型、控制标记、模块路径和模块参数。但是由于pam.d目录的形式会给每个应用提供一个单独配置文件,这就使得“应用名”这个字段没有存在的必要了,所以你会发现这些配置文件的内容会较pam.conf少一列。此外,如果觉得一条记录占一行会影响可读性,也可以使用斜杠“\”续行(我反倒认为这样更不易读)。代码2是一个典型
的pam.conf文件的内容片段,代码3是pam.d目录下针对sshd的配置文件内容。通过这两个例子,你可以比较一下它们的差别。
代码2:
#%PAM- .
login auth [user_unknown=ignore success=ok ignore deault=bad]\
pam_securetty . so
login auth include system-auth
login account required pam_nologin . so /lib64/security 或/lib/security
login account include system-auth
login password include system-auth
login session required pam_loginuid . so
login session optional pam_console . so
login session required pam_namespace . so
login session optational pam_keyinit . so force revoke
login session include sys tem-auth
login -session optaional pam_ck_connector . so
应用 模块类型 控制标记 模块路径 模块参数
......
other auth required pam_deny . so
other account required pam_deny . so
other password required pam_deny . so
other session required pam_deny . so
......
代码3:
#%PAM- .
auth required pam_sepermit . so
auth include password-auth
account required pam_nologin . so
account include password-auth
password include password-auth
# pam_selinux . os close should be the first session rule
session required pam_selinux . so close
session required pam_loginuid . so
# pam_selinux.os open should only be followed by
# sessions to be executed in the user context
session required pam_selinux. so open evn_params
Session optional pam_keyinit . so force revoke
Session include password-auth
从这两个例子中你能容易辨别的就是模块类型字段了,具体内容与我之前介绍的模块类型是一致的。你会发现,每个应用都会配置多个相同类型的模块,这就是模块的堆叠使用。具体到执行阶段,同等类型的模块会被按照其出现的顺序来执行。
代码2的应用名字段中有一个other应用。实际上并没有这个名称的应用,也不要把你的应用这样命名。因为other代表了所有未明确指定配置项的应用,在这个例子中要求对这些应用都要拒绝认证。即便使用pam.d目录的形式也支持这样的设定,只要文件名是other就行。
那么“控制标记”字段该如何解释呢?你们会发现,我所提供的这两个例子,有的复杂、有的简单,但是不管怎样,都是不知所云。从其内容上看,有些是在描述具体模块的重要程度的,有些则不是。事实上也差不多是这样。
对于规定模块重要程度的控制标记一共有四个,他们是:
● required
表示该模块的认证成功是用尸通过认证的必要条件。也就是说,只要有一个被
标明为required的模块认证失败,用户就一定不会通过认证。但是,即便这类模块
认证失败,PAM也不会立即将错误消息返回给应用,而是继续将其他模块都调用
完毕之后才返回。这样做的目的就是为了麻痹“敌人”,让他们搞不清楚到底是哪
里认证失败的。所以,required是最常用的控制标记。
● requisite
与required差不多,但是只要用它标记的模块认证失败,就会立即返回给应用。
具体的返回值与第一个认证失败的模块有关。一般被requisite标记的模块多数用来
判定当前用户所处的环境,如果环境不够安全,即便是合法的用户也不会通过验证。
这是最严苛的要求,一般很少使用。但是requisite能够降低黑客利用不安全媒介获
得输入密码的机会。
● sufficient
表示该模块验证成功是用户通过认证的充分条件。只要这个模块验证成功了,
就代表没有必要继续去认证这个用户了。那么相应的行为就是只要被sufficient标
记的模块一旦认证成功,就会立即返回给应用,报告成功;但是需要注意的是
sufficient的优先级低于required,那么如果有required失败,刚最终的结果也是失
败的;当sufficient认证失败时,相当于optional。
● optional
这表示即便该模块认证失败,用户也可能通过认证,除非别无它选。换句话说,
它仅供参考。而实际应用中,optional所标记的模块只是显示些信息,根本不去做
什么认证工作。
为了更进一步地说明这四个“控制标记”,可以参考一下图14.2所示的PAM用户认证的基本流程。
这四个是说明模块重要性的,那么其他的呢?include是包含的意思吗?猜对了,就是包含的意思。include要求当前配置文件包含另外一个配置文件,所以它所描述的模块并非真正的模块,而是与当前配置文件具有相同路径的另外一个配置文件。在我们的例子中是system-auth和password-auth这两个配置文件。它们的内容与pam.d目录下的配置文件很像,也是没有“应用名”这个字段,即便实际上使用的是pam.conf。因为在被具体应用的记录所引用时,它们就是要修饰这个应用,显然“应用名”字段不但多余还碍事儿。需要注意的是,include属于控制标记,只会对“模块类型”字段所标记的一类模块起作用。即便system-auth可能包含了全部四种类型的模块,也只有与当前记录所对应的那一类模块的那些记录被包含了进来。我们举了例子,就是代码3的第三行,内容是:
auth include password-auth
假设password-path醌置文件的内容是:
#%PAM- .
# This file is auto-generated.
# User changes will be destroyed the next time authconfig is run.
auth required pam_env . so
auth sufficient pam_Unix. so nullok try_first_pass
auth requisite pam_succeed_if .so uid >= quiet
auth required pam_deny . so
account required pam_Unix . so
account suf ficient pam_localuser . So
......
Password requisite pam_cracklib . so try_first_pass retry= type=
password sufficient pam_Unix.so sha512 shadow nullok try_first_pass
use_authok
......
那么它实际包含的内容就是:
auth required pam_env . so
auth sufficient pam_Unix.so nullok try_first_pass
auth requisite pam_succeed_if.so uid >= quiet
auth required pam_deny . s o
目前所介绍的这些“控制标记”都还是很简单的。但是还有一个非常复杂的,那就是在代码2中所见到的:
[user_unknown=ignore success=ok ignore=ignore default=bad]
这是什么含义呢?好像是对一些返回状态的处理方式!没错,这次你又猜对了,就是描述了对模块具体的返状态的处理方式。类似user unknown、success、ignored等这些内容就是某个模块的返回状态。那么相应的ignore、ok、bad等就是对这些返回状态的处理方式标记,或者说是“动作”。PAM所支持的所有动作见表14-1所示:
表14-1 PAM所支持的动作列表
标 记 |
说 明 |
ignore |
忽略这个返回状态,不会对验证结果产生影响 |
bad |
表示这个返回状态代表验证失败,余下的模块调用也永远失败 |
die |
与bad相同,但是应该立即返回到应用,不再继续调用余下模块 |
ok |
表示这个返回状态应该被看作是验证成功,继续调用余下模块 |
done |
与ok相同,但是应该立即返回到应用,不再继续调用余下模块 |
N(一个整数) |
与ok相同,但是要跳过N个模块才能继续 |
reset |
清除所有的返回状态,继续调用余下模块 |
显而易见的是,利用这些标记可以更加细腻地控制各模块验证结果所占有的权重。而且完全可以用它们描述出之前所介绍的那四种有关模块重要性的“控制标记”。重新描述一下它们就是:
required [ success =ok new_authtok_reqd=ok ignore=ignore def ault=bad ]
requisite [success=ok new_authtok_reqd=ok ignore=ignore defalut=die]
sufficient [success=done new_authtok_reqd=done default=ignore]
optional [ success=ok new_authtok_reqd=ok def ault=ignore ]
对于模块能有哪些返回状态是没有严格要求的。比较常见的可参考表14-2中所列的那些。当然,这些也只是最常见的。如果你想了解更多的返回状态,或者需要开发PAM模块,可以参考/usr/include/security/_pam_types.h文件中所定义的内容。这个文件中定义了很多宏,几乎每一个都代表返回状态。只是它们的命名会与应用在配置文件中的有一些差别,但还是
有规律推断出来的。不相信的话你就试试嘛!
表14-2带见的模块返回状态
状 态 |
说 明 |
success |
认证成功 |
auth_ err |
认证失败 |
new_authtok_reqd |
需要新的认证令牌。有三种情况会返回这个状态:一是根据安全策略需要更改密码;二是密码为空:三是密码已经过期 |
ignore |
忽略底层account类型模块的返回状态,而且无论是否被required、sufficient和optional修饰。出于安全性考虑,这个返回状态通常要被忽略掉,这样其他模块的返回状态就会被参考 |
user unknown |
未知用户 |
try_again |
已经通过了密码服务的初步检测 |
default |
所有没有注明的返回状态 |
可能到这里你会猛然发现,控制标记的“语法”构成需要很多空白字符来进行分割,难
道不会被当作“字段”给弄错吗?答案自然是不会,因为使用了中括号“[]”。要是你忘记
了中括号,肯定就会出错!
哦,还有一个控制标记需要说明一下,那就是substack。这个标记跟include有些类似,但是done和die这两个动作不会立即返回,而是要继续执行完substack中的所有剩余模块。相应地,reset动作也只能影响到substack中模块的返回状态。你完全可以将substack看作是用户以证阶段的“子过程”。
配置文件中余下的两个字段是:“模块路径”和“模块参数”,应该不难理解的。对于模
块路径,可以有两种方式表达:一是使用绝对路径,也就是从根“/”开始的路径;二是只给出一个文件名,PAM会在模块的默认路径中去搜索。PAM模块的默认搜索路径是/lib/security,如果是64位系统就是/lib64/security。需要注意的是,本书所给出的这个默认搜索路径只是最常见的,因为PAM在编译的时候可以特别指定。这就意味着如果你遇到了一个比较蛋疼的Linux发行版,可能就不是这样了。至于模块参数就与具体模块有关了。而且模块参数字段处于最后面,这使得它不会因为拥有空白字符而被划分成若干独立的字段。换句话说就是随意使用空白,用不用中括号“[]”就看您心情了。
某些特殊的模块和其参数配合起来可以获得非常神奇的效果。比如pam_succeed_ if.so
这个模块。它可以是满足全部四种类型模块的功能需求。它的参数很多时候就是一个条件表达式,比如:user!=root、user_ id >=500等。如果“表达式”的计算结果为真,那么pam_succeed_ ifso模块会返回success状态,表示通过认证。如果有效利用,完全可以使得PAM的配置文件像一个拥有选择分支结构的微型语言。实际上也正是如此。
至于PAM所提供的那些模块,本书就不做详细的介绍了。刍需要其配置PAM的时候,可以查看Linux的联机帮助文档。每一个PAM模块,都有详细的文档资料。
似乎到了这里,如何配置PAM算是说明白了,PAM的整个工作机制也开始清晰起来。但是你可能还会有一些疑问,那就是为什么一个简单的确认用户名和密码是否正确的过程,要搞得这么复杂。这里面的个中原因显然是几句话说不清楚的。其中最主要的原因恐怕就是PAM并不是提供一种方法,而是提供了一种机制。它允许用户利用这种机制,实现多种多样的用户认证方法。
就比如:我有一台在互联网上提供HTTP服务的Linux主机,HTTP服务是提供给所有人的,但是管理这台主机的就只能是我本人。虽然利用ssh可以很有效地保证我与主机之间的通信安全,但是我所使用的用户名和密码可能早就被买卖了好多次。那么一个比较安全的
做法是,只允许通过我自己的电脑才能使用ssh连接这台主机。这时只需要改变一下PAM的配置,只允许我这台电脑才能通过用户认证就可以了。当我的HTTP服务开始盈利了,并且足够养活一个团队了,我也忙得不可开交,急需要雇佣一个专门的人来帮我维护。那么也只需要改变一下PAM的配置,就可以让另外一台电脑通过认证了。当然,我是不希望粑root账号给我的雇员的,那么就限制他的电脑不能用root账号吧。
显然在实际的应用过程中,会有更为复杂的需求。如果PAM只是提供了一种方法,可能所有Linux管理员会很高兴,但是更高兴的恐怕是黑客们了。只要在自己的环境中找到那么一个漏洞,可就变成了放之四海而皆准的“真理”了。显然这不是任何人期望看到的结果。
最后,你可能还是存在着那么一点点疑虑,或在暗自抱怨着我或出版社的不细心,总是
有一些笔误或印刷错误。为什么呢?因为在代码2有这样的一行内容:
login -session optaional pam_ck_connector.so
哈哈,欧耶!看到这一点说明你非常细心,但是我也真没有粗心。因为模块类型字段里的那个横线“一”既不是笔误也不是印刷错误,它是真实存在的。它要求这个模块的认证过程不记入日志。与此相对应地那些不带有横线“一”的模块,就都要记入日志。不过具体有没有日志可记就要取决于具体的模块是否有日志要写。事实情况是大多数模块默认情况下不写日志,但是使用debug选项基本都能强迫它们去写。PAM的用户认证日志记录在/var/log/secure(权限600)文件中。只有root用户能够查看。也可以通过监控这个文件的变化,来研究PAM的工作过程。
14.3.4密码映射
既然PAM提供了一套机制,允许使用多种方法的组合进行用户身份确认,那么就不可避免地遇到一个问题——不同的方法有不同的密码体系。为这个问题开脱的最好理由就是“安全”。因为黑客要攻破这样的系统,就必须攻破所有的密码体系。但是一旦采用了多种密码体系,就避免不了在进行一次认证的过程中要求用户多次输入不同的密码。显然如果记性不太好将会遇到很大的麻烦。但是统一密码体系统一密码,就会削弱系统来之不易的安全性。PAM的密码映射机制提供了两全其美的解决方案。
密码映射机制引入了“主密码”和“副密码”这两个概念。主密码加密其他密码体系的密码而形成副密码,并且将这些经过加密的副密码存放在一个用户能访问的地方。主密码一旦通过认证,就可以利用它来解密副密码而获得相应的密码继续认证。这个过程就是“密码映射”。如果密码映射出现错误,或者映射不存在,那么需要密码的相关模块就会提示用户输入密码。主密码一般被保存在PAM API层(P508密码的保存),并在需要时提供给堆叠的各个认证模块。出于安全考虑,密码要在pam_authenticate函数返回之前清除,而且主密码必须足够强壮。一般地,主密码应该比较长,使用大小写字母、数字和特殊符号混合在一起构成。密码如何加密和如何存储则完全取决于PAM的具体实现。
为了实现密码映射,所有类型的认证模块应该支持以下四个映射选项:
● user_first_pass
该选项表示模块执行时不提示用户输入密码,而将该模块之前提示用户输入的
主密码作为它们的公共密码进行验证。如果用户没能通过主密码的认证,则该模块
不提示用户输入密码。这个选项一般用于希望强制用同一个密码通过多个模块认证
的时候。
● try_first_pass
该选项与user_first_pass类似,但是当用户的主密码没能通过认证时,则会提
示用户输入密码。
● use_mapped_pass
该选项要求使用密码映射机制得到模块的密码。也就是说,该模块被定叉后不
会提示用户输入密码,而是用映射密码,也就是使用经过主密码解密副密码得到的
密码来进行认证。即便在此之前用户没能通过主密码的认证,该模块也不会提示用户输
入密码。
● try_mapped_pass
该选项与use_mapped_pass的差别就如同try_first_pass与user_frst_pass的差别
一样。如果主密码不正确,模块就会提示用户输入密码。
需要说明的是,当某个模块的密码被改变之后,PAM会保存所有新旧密码,并且能够让某些相关模块访问到它们。这使得其他模块能够利用此信息更新加密的口令而不必强制用户再次输入密码。
密码映射机制是PAM比较复杂的机制,可以显著提高系统的安全性同时又保障了系统的易用性。当前大多数企业级的Linux发行版并不默认提供利用这种机制(PAM)的用户认证方案,唯一的应用可能就是我在前面给出的那个password-auth文件的例子了。主要是因为“安全”这个问题对于不同的企业是有不同的要求和实施策略,服务器供应商很难做到面面俱到。为了能够让一个Linux系统达到足够强健的安全性而又不失去易用性,作为系统的管理员,可能需要付出更多的汗水和经验值去保障。
14.4 应用PAM API
通过前面的介绍,你应该对PAM的整套运作机制有所了解了。如果你是一名程序员,或许会很急切地想知道如何在自己的程序中集成PAM未进行必要的用户认证,抑或是想了解一下诸如login、sshd这样的程序是如何利用PAM来完成用户认证的,乃至如何向PAM中添加自己的模块。这一节将为您揭晓答案。
14.4.1 开发概述
PAM所能提供的功能在前面已经说过了,就是四类模块的功能。与Linux系统其他的API 一样,PAM API也使用一个共享库(libpam.so文件)来提供所有接口,同时也会提供几个与之相对应的C头文件。
当你要开发一个使用PAM的应用时,应该包含这样的代码:
#include<security/pam_appl.h>
在编译你的程序时,还应该使用类似这样的命令:
gcc—o application… -lpam
当你要给PAM开发一个模块时,应该包含这样的代码:
#include<S ecurity/pam_modules. h>
在编译你的模块时,应该使用类似这样的命令:
gcc - fPIC—C pam_module.c
gcc - shared—o pam_module. so pam_module.o- lpam
虽然开发应用和开发模块的时候所使用的头文件不同,但是都要求连接同一个库:
libpam.so。就是它提供了PAM API的全部接口,也是整个PAM系统的核心。
14.4.2 PAM事务
应用程序认证用户身份的过程和用户认证通过之后对其行为进行跟踪的一整套流程被
称之为PAM事务,以下我会简称为“事务”。
事务拥有一个名称,一般与应用程序名称相同。当然,如果你想别出心裁的话,可以起个花哨的名字,但是你得想办法告诉系统管理员你起的是什么名字。因为在配置文件中“应用名”字段实际上标明的是事务名。
一个事务只能认证一个用户。也就是说,如果你的程序需要认证多个用户,就必须创建多个事务。多个事务可以并行执行,只要你的程序是那样设计的就行。就比如类似sshd、ftpd这样的服务程序,很多时候要处理多个用户的同时登录,那么它们就一定要开启多个事务。具体为什么是这样,慢慢地就能理解。
创建事务的接口是pam_start(),它的完整定义是:
int pam_start(const char*servlce_name. 事务名/应用名
const char *user.
const struct pam—conv *pam_conversation,
pam_handle_t **pamh)
第一个参数service_ name就是事务名,第二个参数user是要认证的用户名。第一个参数必须给定,而第二个参数则是可选的(可以为NULL)。应用程序只要调用了pam_start()接口,PAM一API就会读取配置文件,如果事务名没有给定,这个时候就无法找到合适的配置项了。而第二个参数用户名为什么不需要给定,后面再说。
第三个参数则很重要,这是一个结构体,有如下定义:
struct pam_message {
int msg_style ;
char *msg;
};
struct pam_response t
char *resp;
int resp_retcode;
};
struct pam_conv {
int(*conv) (int, const struct pam_message**, struct pam_response**,
void*)
void *appdata_ptr;
};
结构体pam_conv的conv字段是一个函数指针,一般在C中就是用来进行回调的,在这里也不例外。按照PAM的术语,conv指向的函数叫conversation function,翻译过来就是“对话函数”。
既然叫对话函数,那么是谁跟谁对话呢?模块与应用对话。模块和应用对什么话呢?最简单的就是问用户要密码。因为密码这东西是肯定不能明文保存的,只能等到用的时候叫用户自己去输入。当然,模块和应用还有一些别的对话内容,比如模块的一些警告信息、提示消息等。所以,对话函数由谁提供,由谁来调用,应该不用我明示了吧?
对话函数的前三个参数很重要。第一个是消息的数量,第二个是具体的消息数组(也可以叫“问题数组”),第三个是对具体消息的响应(也可以叫“答案数组”)。有多少个“问题”,就得有多少个“答案”,否则就会发生错误。需要注意的是,“问题数组”的内存由PAMAPI来分配,而“答案数组”的内存则需要应用程序来分配,但是PAM API会负责回收这些内存。更需要注意的是,“问题数组”是值传递,而“答案数组”是引用传递。换句话说,“问题数组”是pam_message揩针的数组,而“答案数组”则是pam_response的数组。如果还是不理解,就看我在后面给出的代码吧。
对话函数的第四个参数属于辅助数据,它实际上就是pam_conv结构体的appdata_ptr字段的内容。在使用C++编程的时候,可以将this指针传递给它。根据实际需要,传递什么都可以,只要你认为是合理的,能用得着的就行。
对于代表“问题”的pam_message结构体,第一个字段msg_style用来标明这是一个什么类型的“问题”,而msg字段则包含具体的问题。一般有四种问题,分别用PAM.PROMPT_ECHO_OFF、PAM_PROMPT_ECHO_ ON、PAM_ERROR_ MSG和PAM TEXT_ INFO这四个宏来代表。PAM PROMPT_ ECHO OFF类型的“问题”要求用户回答问题的时候不能有回显,最典型的就是要求用户输入密码,那么这类问题的msg字段的内容可能就是“Password:”;PAM—PROMPT_ ECHO_ ON类型的“问题”要求用户回答问题的时候可以回显,比如要求输入用户名的时候:PAM ERROR MSG和PAM TEXT- INFO类型的“问题”可以不用回笞,只需要简单地显示msg字段的内容即可,但是“答案数组”也得给这类问题留出位置。作为应用程序,很多时候可以将msg的内容掉包,也就是不直接显示给用户,而是采用另外一些更明确的消息来提示用户。具体怎么用,应用程序的作者说了算。
对于代表“答案”的pam_response结构体只有resp字段有用,resp_retcode字段给个0就行了。resp字段的内容就是具体的答案了,比如需要输入密码,那么这个字段的内容就得是密码。而且一定不能给它赋值一个常量,因为PAM API会试图去回收它的内存。如果你赋值了常量,就会报错。
pam_start()接口的前三个参数就介绍完了。第三个参数一定不能是NULL,而且必须提供“对话函数”,因为PAM API并没有默认提供。至于第四个参数,实际上是一个返回参数,会返回事务句柄。事务句柄能够唯一标识一个事务,不要直接打它的注意,否则PAM会很生气,后果很严重。
当pam_start()接口返回了PAM SUCCESS,就代表成功地创建了一个PAM事务。如果是别的值,那就检查一下是否有参数传递错了,如果不是,那就悲哀吧,您没有权利执行用户认证操作。
用户退出系统之后,应该关闭事务。具体的是执行pam_end()接口。完整定义如下:
int pam_end (pam_handle_t *pamh, int pam_status);
第一个参数不用多说,就是事务句柄。第二个参数则是调用pam_end()之前的那个PAM API接口的返回值。这个参数很重要,它要根据这个具体的返回值做一些清理操作,所以给定不正确的结果就不好说了,很有可能是灾难性的。
注意,pam_end()最好是在用户退出后调用。如果在这之前调用,会导致用户丧失所有已经获得的权限,并被系统拒之门外。当然,对于涉及系统安全的应用程序,一旦发现用户有不轨行为,那也就不用客气了。这也是一种十分必要的安全策略,
14.4.3事务属性
前面说了,pam_start()接口的第二个参数,也就是用户名不需要给定。而且十分重要的第三个参数也不需要给定。这是为什么呢?这就要引出事务属性的概念了。
一个事务拥很多属性。事务名称、用户名、对话函数等这些都是事务的属性。除此之外,还有远程用户名、远程主机名等。设置和获取事务属性值,可以通过pam_set_item()和pam_get_item()这两个接口完成。它们的完整定义是:
intpam_se t_item(pam_handle_t *pamh, int item_type, const void *item);
intpam_get_item(const pam_handle_t *pamh,int item_type,const void **item);
一共三个参数。第一个参数是事务句柄,如果不给定就不知道该处理谁了;第二个参数是具体要设置或获取的属性标志,第三个参数就是具体的属性值了。属性值的类型会根据属性的不同而不同,表14-3列出了一些常用的属性,没有列出的,可以通过联机帮助man来查看。
表14-3常用的事务属性表
属性标识 |
说 明 |
PAM_SERVICE |
事务名称 |
PAM_USER |
要认诳的用户名 |
PAM_TTY |
当前用户所使用的终端设备名,前缀肯定是/dev。如果是图形用户界面,那么这个值应该是环境变量DISPLAY的值 |
|
|
PAM_RHOST |
如果是远程登录认证,那么这个属性就是远程主机名 |
PAM_RUSER |
如果是远程登录认证,那么这个属性一般是登录客户端的名称。与PAM_RHOST属性构成PAM_RUSER@PAM_RHOST来标识认证用户的来源。也可以根据这两个属性来判断用户是否合法 |
PAM_CONV |
模块与应用的对话函数 |
PAM_AUTHTOK |
这个属性就是用户密码,只有在更改密码的时候有效,被看作是新的密码 |
PAM_OLDAUTHTOK |
这个属性是用户过期的密码,只有在更改密码的时候有效,被看作旧密码 |
可以这样说,创建完事务之后,就应该设置相应的属性,否则就有可能通不过认证。设置和获取事务属性的操作很少会发生错误,除非传错了参数,但是这在很多时候编译都是通不过的。
14.4.4用户认证
当必要的事务属性都设置好之后,就开始了具体的身份认证了。这需要调用pam_authenticate0接口来完成。它的完整定义如下:
intpam_authenticate (pam_handle_t *pamh, int flags 位掩码);
这个接口相当简单,就是事务句柄和认证标志两个参数。而认证标志也只有两个,分别是PAM SILENT相PAM DISALLOW_ NULL_AUTHTOK。前者是要求“不声不响”地认证,而后者是要求密码不得为空。实际上这个认证标志是个位掩码,也就是说可以通过C语言的位或“l”操作联合使用。如果这两种行为都不是你期望的,那么直接给个0就行了。
pam_authenticate()接口被调用后,PAM API就会按照配置文件的规定去调用那些auth类型的模块来认证用户。只有返回PAM SUCCESS才表明用户通过了认证。认证失败分为两种情况:一种是因为用户认证信息不正确导致的失败,这是最普遍的情况;二是由于程序本身bug导致事务有问题或认证模块由于某些原因不能工作引起。这两种情况需要区分对待。
pam_authenticate()接口只是执行初步的认证。换句话说,即便这一步的认证通过了,也不代表用户就有权利使用系统。
首先要明确的一个事情是,PAM对于处理远程登录的用户提供一种被称之为“临时用户”的概念。这是一个什么概念昵?对于一个远程登录的用户,当通过pam_authenticate()接口认证之后,可能会获得一个新的用户名,而这个用户名会与系统中现有的某个用户相对应。或者反过来说,远程登录的用卢名可能系统中并不存在,但是PAM会通过某种机制将它映射到系统中的一个用户上面。这个被映射到的系统中的真实用户,就是临时用户。如果采用了这种机制,那么pam_authenticate()接口会修改事务的PAM_ USER属性,使它的值是当前系统中的一个真实用户名。所以当发现登录用户名与真实用户名不相同时,就需要通过getpwnam()系统调用来获得这个真实用户的信息,来给这个登录用户进行授权。有关getpwam()系统调用的详细信息,可以通过联机帮助man获得。
另外一个事情是,用户的密码可能已经过期,或者是根据某些策略被禁止登录。这些事
情将由account类型的模块负责处理,具体到接口是pam_acc_mgmt()。它的完整定义如下: getpasswordname-》/etc/passwd-》系统调用
int pam_acc_mgmt (pam_handle_t *pamh, int flags);
它的两个参数与pam_authenticate()接口相同,就不再复述。需要注意的是它的返回值。如果返回了PAM_SUCCESS,则表明这个用户没有任何问题,可以放行了;如果返回了PAM _NEW_AUTHTOK_REQD,就标明这个用户的密码已经过期,需要修改该密码:其他的返回值都表示这个用户有问题,绝对不能放行。
当发现一个用户的密码已经过期,那么可以有两种策略:一是禁止登景;二是提示修改密码。具体选择何种策略,应该根据具体应用的需求决定。因为禁止登录这个策略还可以通过修改配置文件做到,所以较为常见的是默认策略是提示修改密码。修改密码的操作由password类型的模块负责处理,具体到接口是pam_chauthtok(),完整定义如下:
int pam_chauthtok(pam_handle_t *pamh. int flags);
同样地,这个接口有两个参数,flags也有两个标志。PAM_SILENT标志的行为与前面两个接口是一致的。另外一个标志是PAM_CHANGE_ EXPIRED_AUTHTOK,声明只是修改一下已经过期的密码,如果不给定这个标志,就会要求用户输入旧密码和新密码。这两个标志同样可以使用C语言的位或“l”操作联合使用。
出于安全方面考虑,在进行用户认证的时候,适当调整一下当前进程的优先级,这样可
以获得更高的安全保证和更快的认证速度。至于如何调整当前进程的优先级,可以查看联机
帮助中有关setpriority()系统调用的详细介绍。
14.4.5认证凭证
当一个用户通过了认证之后,就应该给他/她创建一个认证凭证了。所谓认证凭证,就像身份证一样可以确认用户的身份。当用户在执行某些操作需要再次进行身份认证时,可以
直接拿这个认证凭证给系统,说我是合法公民,我要行使我的权利。如果这个凭证具有绝对
的说服力,那么Linux系统肯定是会放行的。
实际上,在Linux系统中用户的UID和GID就是一种认证凭证。而且这种凭证是要取决于运行这个程序的用户本身所具备的认证凭证。换句话说,如果一个较低级别的用户执行了具有用户认证功能的程序,那么这个程序能够认证的用户级别只能更低。为了能够让级别较高的用户通过认证,往往这类程序需要root账号来执行。比如login命令、su命令等,它们都拥有root权限。
/bin/su
/bin/login
创建、管理和删除用户凭证的PAM API接口只有一个,就是pam_setcred()。它的完整
定义如下:
int pam_setcred(pam_handle_t *pamh, int flags);
这个接口的flags参数有5个标志。虽然可以使用位或“l”操作联合使用,但是除了PAM_SILENT与其他四个能这样做之外,其他的是会产生冲突的。至于为什么,表14-4说
明了这个问题。
表14-4常用的事务属性表
标 志 |
说 明 |
PAM_ESTABLISH_CRED credit信用 |
为用户初始化认证凭证 |
PAM_DELETE_CRED |
删除用户的认证凭证 |
PAM_REINITIALIZE_CRED |
完全重新初始化用户认证凭证 |
PAM_REFRESH_CRED |
延长现有用户凭证的有效期 |
在已经给用户创建了认证凭证之后,如果要对用户的权限进行修改,可是使用PAM_REINITIALIZE_CRED标志来重新初始化用户凭证;当需要延长用户凭证的有效期时,可以使用PAM_REFRESH_CRED 标志,最典型的sudo命令就是这样做的;如果用户退出,在关闭事务之前,应该删除认证凭证。
认证凭证管理功能由auth类型的模块提供。执行成功会返回PAM_SUCCESS,
你可能会有一些疑问,那就是在Linux中完全可以使用UID和GID作为认证凭证来行使用户权利,似乎单独去创建一个新的用户凭证有些多此一举。这可以通过PAM的体系结构来理解。因为PAM是完全模块化的高度可配置的,而且目前所介绍的这些接口很多都与具体类型的模块相对应。也就是说,作为应用程序的开发者,根本不知道调用某个接口之后PAM会去做什么,包括pam_setcred()。这样的话,在开发程序时就不能做任何假定。那么
最合理的解决方案就是不要去问为什么,按照规则来就行了。虽然在Linux下使用pam_setcred()来创建一个认证凭证看起来有些多余,但是谁能保证某些auth类型的模块不
会拒绝一些特殊的用户呢?
14.4.6 PAM会话
当创建成功认证凭证之后,就要去创建一个PAM会话来初始化用户环境和跟踪记录用户之后的行为。PAM会话属于事务的一部分,在用户退出之后,要在删除认证凭证和关闭事务之前关闭会话。
创建和关闭PAM会话的接口是pam_open_session()和pam_close_session(),它们的完整定义如下:
int pam_open_session (pam_handle_t *pamh, int flags);
int pam_close_session (pam_handle_t *pamh, int flags);
它们的flags参数只有一个标志可以选择,就是PAM_SILENT。
从表面上看,PAM会话很多时候也是没什么用的。按照PAM的设计,PAM会话用于初始化用户环境和记录用户行为。由于创建PAM会话是在一个用户已经通过认证之后要做的事情,即便不创建这个会话,应用程序自己也可以做到PAM所规定的那些事情。其实PAM会话与前面介绍的认证凭证是类似。作为应用程序的开发者是不能做任何假定的,那就按照标准执行好了。
14.4.7用户认证流程
应用程序用来进行用户认证的PAM API接口基本上算是介绍得差不多了,如果你想了解更为详细内容,可以去查询Linux的联机帮助。
现在可以总结一下用户认证的基本流程,大体上可以划分为如下这几个步骤:
1. 创建PAM事务;
2. 设定事务属性;
3. 与用户交互提示输入用户名和密码;
4. 确认用户的有效性和密码是否过期;
5. 如果密码过期,提示用户修改密码;
6. 创建认证凭证;
7. 创建PAM会话;
8. 执行认证后操作:
9. 关闭PAM会话;
10. 删除认证凭证;
11. 关闭PAM事务。
各接口调用的基本流程图见图14.3所示:
根据这个流程图,我们可以编写一段简单的使用PAM进行用户认证的代码,见代码4所示。代码可能有点长,但还是比较容易读懂的。
代码4:
#include<security/pam_appl. h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<pwd.h>
#include<unistd.h>
#define MAX_UN_SIZE 256
pam_handle_t *pamh=NULL;
char tusername =NULL;
struct passwd *pwd=NULL.
static int
conv_func(int n, const struct pam_message **msg, struct pam_response
**resp, void *data)
{
/*
*这就是对话函数,与用户进行认证交互。
*/
struct pam_response *aresp=calloc (n, sizeof (struct pam_response)};
for (int i=; i<n; ++i) {
switch (msg [i]- >msg_style) {
case PAM_PROMPT_ECHO_OFF:
/*要求用户输入密码,getpass系统调用可以做到。*/
aresp [i]. resp=strdup (getpass (msg [i] ->msg));
aresp [i]. resp_retcode=;
break;
default:
/*其他情况我们忽略掉*/
break;
)
)
*resp=aresp;
return PAM_SUCCESS;
}
static char*
get_username()
{
/*
*这个函数用于获取用户名,模拟login命令。
*/
static char un [MAX_UN_SIZE];
char *p=NULL;
int ch=;
do{
printf("Please input user name:”);
for (p=un; (ch=getchar() ) != ‘\n’; ) {
if (p<un+sizeof (un) - ) {
*p++=Ch;
}
)
*p=’\’;
) while (p==un);
return un;
}
static int
auth(pam_handle_t *pamh)
(
/*
*用户的认证过程在这里完成,直到认证通过才会退出。
*/
const char *tmpl_user=NULL;
const void *item=NULL;
int pam_err=;
while (){
pam_err=pam_authenticate (pamh, PAM_SILENT);
switch (pam_err) {
case PAM_SUCCESS:
/*判断是否有临时用户*/
pam_err=pam_get_item (pamh. PAM_USER, &item);
if (pam_err==PAM_SUCCESS) {
tmpl_user= (const char*)item;
if (strcmp (username, tmpl_user) !=)
pwd=getpwnam(tmpl_user);
}
pam_err=pam_acct_mgmt (pamh, PAM_SILENT);
if (pam_err==PAM_ NEW_AUTHTOK_REQD) {
/*提示用户修改密码*/
pam_err=pam_chauthtok (pamh,
PAM_SILENT | PAM_CHANGE_EXPIRED_AUTHTOK);
}
break;
case PAM_MAXTRIES:
/*达到认证次教的上限,宣告失败。*/
break;
efault:
continue:
}
break;
}
return pam_err;
}
int main (int argc, char *argv[ ] )
{
struct pam_conv pamc={conv_func, NULL);
int pam_err=;
/*创建一个事务*/
pam_err = pam_start ( "login" . NULL, NULL, &pamh ) ;
if (pam_err != PAM_SUCCESS ) {
printf ( " pam_start ( ) fail\n " ) ;
return -;
}
/*设置事务属性*/
username=get_username();
pam_err=pam_set item(pamh, PAM_USER, username);
pam_err=pam_set_item(pamh, PAM_CONV. &pamc );
/*获取用户的基本权限信息,用于通过认证后的授权。*/
pwd=getpwnam (username);
pam_err=auth (pamh);
if (pam_err!=PAM_SUCCESS){
printf( "auth() fail\n");
pam_end (parnh, pam_err);
return -;
}
/*判断用户是否存在*/
if (NULL==pwd){
printf(*Invalid user\n");
pam_end (pamh, pam_err);
return -;
}
/*初始化用户组的权限,用于创建认证凭证*/
initgroups( username, pwd- >pw_gid);
pam_err=pam_setcred (pamh, PAM_ESTABLISH_CRED | PAM_SILENT);
if (pam_err !=PAM_SUCCESS){
printf( "pam_setcred fail\n*);
pam_end( pamh, pam_err);
return -;
}
pam_err=pam_open_session (pamh, PAM_SILENT);
if (pam_err !=PAM—SUCCESS){
printf(*pam_open_session()fail \n*);
pam_err=pam_setcred(pamh, PAM_DELETE_CRED|PAM_SILENT);
pam_end( pamh. pam_err);
return -;
}
setuid (pwd->pw_uid);
/*
这个地方可以添加一些通过认证的操作
*/
pam_err=pam_close_session (pamh, PAM_SILENT);
pam_err = pam_setcred(pamh, PAM_DELETE_CREDI PAM_SILENT) ;
pam_end ( pamh , pam_err ) ;
return ;
}
14.4.8模块开发
PAM模块的开发从表面上看要比应用开发容易得多。但是那也只是表面上的。先不说PAM机制本身是否存在漏洞,作为它的模块本身必须拥有更高的安全标准。因为PAM在执行认证的过程中要调用很多个模块,只要其中一个有问题,其他模块的努力都等于白费。所以要把一个安全可靠的认证模块写好,是一件非常不容易的事情。遗憾的是,本书只能教会您那些表面的东西,就是一个PAM模块是怎么构成的。至于它内部的那些具体实现和注意事项,还需要大家去努力学习,很多是需要不断地积累的。
那么PAM模块是如何构成的呢?非常简单,就是普通的共享库。只要能够导出固定名称的儿个接口就行了。之前就说过,PAM API中有几个接口是与模块相对应的,就是:pam_setcred()、pam_authenticate()、pam_acct_mgmt()、pam_chauthtok()、pam_open_session()和pam_close_session()。那么只要有一个共享库实现了与这些接口相对应的一些接口就能够成为PAM的模块。注意,只要实现一些就行,不用全部,一个、两个都可。也正是因为这样,虽然PAM规定了模块有四种类型,但是实现一个模块的时候却不用去严格区分它的类型。也就是说,你实现了属于什么类型的接口这个模块就“属于”什么类型,你实现了全部四种类型的接口,你的模块就是四种类型都支持的模块。从这点上看,说一个模块是支持什么类型的应该比较准确。
这些接口的标准定义如下:
int pam_sm_authentica te(pam_handle_t*pamh,int flags,int argc,char **argv);
int pam_sm_setcred (pam_handle_t*pamh, int flags, int argc, char **argv);
int pam_sm_acct_mgmt (pam_handle_t *pamh, int flags, int argc. char **argv);
int pam_sm_chauthtok(pam_handle_t *pamh, int flags, int argc. char **argv);
int paIn—sm_open_session (pam_handle_t *pamh,int flags,int argc,char **argv);
int pam_sm_close一session (pam_handle_t *pamh, int flags, int argc, char **argv);
从这些标准定义中,你可能有了惊奇的发现,就是模块要实现的接口名称与对应PAM API的接口名称只是在中间增加了“sm”,而且前面的两个参数也都一样。至于后面的那两个参数也应该比较熟悉,与C语言的main()函数参数相同,就是用来处理命令行选项的。因为模块也是支持选项的。
具体的代码本书就不给了。对这部分内容有兴趣的大家可以去参考Linux PAM源代码包中那些模块的实现代码。任何一个都非常有代表性。
14.4.9密码的保存
通过前面这些文字的介绍,你应该对PAM的整套机制和如何使用它有了较为深入的了解。但是作为一个用户认证机制,与用户交互最多的内容恐怕就是密码了。但是PAM如何知道用户输入的密码是有效的呢?答案是很明显的,就是PAM事先知道用户的正确密码。但是密码可是一个非常敏感的东西,谁都不希望自己的密码被别人知道,即便是系统管理员也不行。要解决这个问题,PAM就只能加密密码,而且这个加密过程必须是不可逆的。换句话说,PAM不能从加密的密码获得密码本身的内容,而且包括其他任何程序或人。这样的算法显然很多,MD5、SHAI等都能做到。那么这个密码是怎么保存的呢?
对于密码是如何保存的这个问题,在PAM机制中我们无法找到一个准确的答案。为什么呢?这个取决于auth类型的模块。不同的auth类型模块对于密码上的处理是不尽相同的。拿pam_unix.so这个模块举例,它采用了UNIX原始的影子(/etc/shadow)文件来保存密码。
Apr :: localhost su: pam_unix(su-l:session): session opened for user root by steven(uid=)
Apr :: localhost su: pam_unix(su-l:session): session closed for user root
Apr :: localhost sshd[]: pam_unix(sshd:session): session closed for user steven
Apr :: localhost sshd[]: pam_unix(sshd:session): session opened for user steven by (uid=)
Apr :: localhost sshd[]: pam_unix(sshd:session): session closed for user steven
Apr :: localhost sshd[]: pam_unix(sshd:session): session opened for user steven by (uid=)
Apr :: localhost su: pam_unix(su-l:session): session opened for user root by steven(uid=)
[root@_centos ~]# grep -i pam_unix /var/log/secure
Apr :: localhost sshd[] 31311是指进程ID
ps aux|grep
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 0.0 0.3 ? Ss : : sshd: steven [priv]
既然说到了影子文件,又由于Linux的默认密码保存机制也是使用影子文件,我就借这
个机会细说一下什么是影子文件。
话说在早期的Unix实现申(那个时候还没有Linux啥事儿),用户密码保存在/etc/passwd(看它的名字就是干这个的)文件中。由于多种原因,/etc/passwd必须让所有用户读取。虽然密码是采用不可逆加密算法加密的,但是对于这种算法只要肯花时间依然是可以破解的(MD5现在都被破解了,还是个中国人干的)。这就给密码的安全性带来了威胁。为了进一步加强密码的安全性,后来的Unix System V系统中开始使用/etc/shadow来保存密码,这就是影子文件。而且这个文件只允许root账号读取。
存储在影子文件中的密码依然使用不可逆加密算法加密,开始的时候使用的是DES(加密算法标准)的各种变体。但是随着计算机处理速度的显著提高,暴力破解DES加密密码带来的威胁也与日剧增,不得不改变策略。于是又引入了一些新的算法,比如MD5和Blowfish。
这个时候问题就来啦,到底应该使用何种加密算法呢?如果统一升级采用一种公认的可靠算法,那么老的影子文件怎么办?毕竟不能让所有用户都去重新更改密码(UNIX系统可以连续运行很多年),这个成本很大。这个时候就要采用一种这样的办法,老用户使用老的加密算法,新用户使用新的加密算法。这孰需要改变影子文件的格式。下面的内容就是我的一台计算中影子文件的一段内容:
jagen: $$nd8XAZBf $azSIbwUFtvQz....:::::::
这个影子文件使用冒号“:”将一行文本分割成多个字段,我们只关心开头的两个字段就行了。第一个字段是用户名,第二个字段就是加密后的密码。如果口令的第一个字符不是“$”,那么此口令使用的是基于DES算法加密的;如果第一个字符是“$”,那么直到下一个“$”间的字符表明所使用的加密算法,而处于第二和第三个“$”之间的字符为加扰( salt)字符,随机生成的,用于改变算法的加密方式。
可以看得出,第一个字符是“$”,表明使用的肯定不是DES加密算法。在这个例子中,算法标识符是“6”,通过查看crypt的联机帮助得知使用的是SHA-512算法。要查看当前系统默认采用何种密码加密算法,可以查看/etc/login.defs文件。
使用影子文件只是Linux系统保存用户密码的一种策略,通过修改PAM的配置文件可以改变这种策略,比如使用NIS、LDAP等将用户密码集中保存起来。
最后需要注意的一个问题是,影子文件只有root账号才能访问。而pam_Unix.so模块正
是使用影子文件来读取密码的。那么,如果在PAM中给某个应用配置需要使用它来进行密码校验,那么这个应用程序必须以root权限来运行,否则将永远会认证失败。
14.5安全增强系统
在大多数用户的眼里,每次登录Linux系统都必须输入密码,而且除了root之外,一个普通用户不能随意读写其他用户的文件,也不能更改系统设置,应该足够安全了。当了解到PAM那样细致入微的用户认证机制之后,更加坚定了对Linux系统安全性的信心,似乎从此以后就可以高枕无忧了。且慢,有关安全性酌讨论我们才刚刚开始,危机依旧四伏,而且问题很多时候就恰恰出现在PAM那看似细致入微的机制中。
什么?PAM机制不安全?并不是,只是因为PAM是用户认证机制,而不是用户授权机制。真正的用户授权机制在Linux的内核,而且这套机制存在问题。
14.5.1 主动访问控制DAC
Linux内核的用户授权机制也叫访问控制机制,在第2章中我们已经讨论过用户身份问题和文件管理方式就是受控于这种机制。Linux系统中的用户主要分为root(系统管理员)和普通用户两种(root和非root),并将他/她们划分到不同的群组中。而这两种用户是否能够访问系统中的某个文件则与该文件的rwx权限属性有关。如果某个程序想要访问这个文件,Linux内核会根据该程序的拥有者的UID和所属群组的GID与该文件UID和GID的rwx权限属性进行对比来决定是否允许操作。
Linux这种控制文件访问的机制有一个非常好听的名字——主动访问控制,英文的叫法是Discretionary(*的) Access Control,简称DAC。名字虽然好听,但是缺陷可一点都不少:
● root拥有特权
特权这种事儿,在任何时候都是十分可怕的。root在DAC中不受任何限制,如果
某个拥有root权限的程序被一小撮别有用心的人所控制,那么他就可以利用这个程序
在你的系统上千任何事情,访问任何路径和文件,而且你还不知道。 chown xx:xx 程序路径 两个目的 DAC和限制普通程序的资源使用 root不受ulimit约束
● SUID程序的权限升级
Linux为了实现类似让用户能够修改自己密码的操作(影子文件只能由root存取),
只能给某些程序开启一个后门,这个后门就是SUID。这可是特权的直接产物,因为有
特权就会有人通过特权走捷径。虽然设计良好的带有SUID的程序都会集成PAM,但是
万一在实现上有什么闪失的话,依然会被一小撮别有用心的人所利用,并且很容易就能
将权限提升到root,那么他就可以利用这样的程序在你的系统上千任何事情,而且你还
是什么都不知道。
● 用户可以利用程序来更改文件的存取权限
如果某个年轻的Linux用户为了自己的方便,将某个目录的权限设定为777,那么
该目录就可以被所有人任意访问。这可是一件非常危险的事情,别人想在这个目录下干
什么就干什么,你依然是什么都不知道。
其实缺陷还有很多,但是已经无需多举了,这已经足够让所有人倒吸一口凉气了。而且对于这些缺陷更为要命的是,防火墙、入侵监测系统等完全无能为力,是不是该擦擦背后的冷汗了呢?
那么Linux是否在面对安全问题时就完全缴械投降了呢?没有,因为还有MAC。千万不要以为这是“乔帮主”的发明,也跟网卡的物理地址无关,它是……
14.5.2 强制访问控制MAC
与主动访问控制相对应的是强制访问控制,英文的叫法是Mandatory Access Control,简称MAC。
MAC可以规避DAC的所有问题。因为MAC针对特定的程序与特定的文件进行关联性地访问控制。即便你是root,在使用不同的程序的时候,你所能访问的文件也只是那个程序所能访问的文件。换句话说,MAC控制的主体是程序而不是使用者,MAC规定了一个程序能够访问哪些文件,而跟使用这个程序的用户无关。
但是,一个Linux系统中的程序数量比它的用户数量多个几十倍是一点都不奇怪的,那么每个程序都使用MAC来控制似乎不太现实。Linux系统采用了一个折中的方案——DAC与MAC结合使用。
对于大多数与安全无关的程序就使用DAC来控制好了。对于那些安全攸关的程序,比如passwd、httpd等则采用MAC来控制,只能让它们读取/etc/shadow文件或访问/var/www目录,即便它们存在某些问题能够被那些别有用心的人所利用,又能掀起多大的风浪呢?而且即便是使用MAC控制的程序,也首先要进行一次DAC的控制。这样,整个系统的访问控制能力不但细腻得多,安全得多,也容易使用了。
那么Linux是怎么实现MAC的,我们又该怎么使用呢?不用着急,答案马上揭晓。就像DAC那样,MAC也是由Linux内核来实现的,比较经典的有SELinux、AppArmor和Grsecurity,LIDS,其中SELinux已经包含在CentOS/RHEL/Fedora Linux、Debian/Ubuntu、Suse、Slackware等很多发行版中,我们就以SELinux来认识一下访问控制是如何提供强健的安全保证和防御未知攻击的吧。
14.5.3 SELinux-Linux的MAC实现
作为Linux新老用户如果你说不认识它,那我就会怀疑你之前用的是Linux吗?因为这
个东西在Linux下扎根已经有10年(到2013年)的时间了,而且所处的位置也是十分显耀的根目录下。但是也不管你是Linux新用户还是老用户,不知道SELinux是MAC也完全不奇怪,甚至老用户们根据过往的经验还会十分讨厌SELinux。那么从现在起,我就让新用户了解它,老用户喜欢它。
1. SELinux到底是什么?
首先,SELinux是一种基于策略的MAC安全系统,是以Linux内核可加载模块的形式提供的。
其次,SELinux的全称是Security Enhanced Linux,翻译过来就是安全强化Linux,是Linux上最杰出的新型安全子系统。
另外,SELinux的来头也着实不小。是由芙国国家安全局(NSA,The National Security Agency)和SCC(Secure Computing Corporation)开发,所以它的能力是不容小觑的。
最后,SELinux是在2.6版本的Linux内核中开始被引用。对于目前可用的各种Linux安全模块来说,SELinux是功能最全面,而且是测试最充分的,是在超过20年的MAC研究基础上建立起来的。自从Linux拥有了SELinux,大型企业和*部门才真正地开始考虑采用Linux作为其主要的操作系统。可以说SELinux是Linux的一个十分显耀的卖点。这也说明但凡能叫得响的Linux发行版,基本上都会集成SELinux。
2. 开启SELinux
非常遗憾的一件事情是,搜遍Google和百度,看到的都是如何关闭SELinux。或许它真的给你带来了不小的麻烦,但是如果你是一个十分谨慎的人,那么就从现在起,开启SELinux吧。
但是当我准备提笔的时候,却突然发现不知道该如何下手了。因为不同的Linux发行版开启SELinux的方式有很大不同,而且即便是相同的发行版的不同版本也会有很大变化。这的确是一个非常难办的事情。就冲着达一点,我认为大部分用户都迫切地要关闭SELinux并不是盲目的行为,因为它真的很“讨人厌”。但是话分两头说,SELinux在配置的时候虽然有些烦人,但是安全性那可是杠杠地,从日益恶化的安全局面上看,还真的很难找出不开启SELinux的理由。
那么所幸我就找一个比较通用的Linux发行版做为例子吧,如果你选择了其他的发行版,那就问问Google和百度吧,虽然都是告诉你如何关闭SELinux的,但是反向操作不就是开启了吗?哦,差点忘记说了,我选择了CentOS 6.x作为参考例子。选择它的原因是因为这玩意儿本身就是RHEL,多数企业用的都是它。而且在后面所讲解的所有内容,也都是基于它的。
好了,废话不多说了。打开/etc/sysconfig/selinux这个文件,你可能会看到下而的内容:
# This file controls the state of SELinux on the system.
# SEIINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
SELINUX=disabled
# SELINUXTYPE= can take one of these two values:
# targeted - Targeted processes are protected.
# mls - Multi Level Security protection.
SELINUXTYPE= targeted
知果不是这样,可能你并没有关闭SELINUX,看看是不是跟下面的差不多:
# This file controls the state of SELinux on the system.
# SELINUX= can take one of these three values:
# enforcing - SELinux security policy is enforced.
# permissive - SELinux prints warnings instead of enforcing.
# disabled - No SELinux policy is loaded.
SELINUX=enf orcing
# SELINUXTYPE= can take one of these two values:
# targeted - Targeted processes are protected,
# mls - Multi Level Security protection.
SELINUXTYPE= targeted
发现什么地方有变化了吗?就是将SELINUX=disable变更成SELINUX=enforcing了。
disable很好理解,就是关闭嘛!那么enforcing对于SELinux意味着什么呢?它代表了一种工作模式,强制SELinux使用你所定义的所有安全策略。那么与enforcing模式相对应的是permissive模式。如果SELinux工作在permissive模式之下,则并不代表系统已经真正受到SELinux的保护了,因为它只要求显示一些相关的信息状态,余下的就什么都不做了。
开启和关闭SELinux都要重新启动系统,因为这个需要内核本身被重新加载。如果你不确定SELinux是否已经成功开启了,可以使用setatus命令来确认,在我的系统下,会得到这样的结果:
setatus
SELinux status: enabled
SELinuxfs mount /selinux
Current mode : enforcing
Mode from config file: enforcing
Policy version:
Policy from config file: targeted
虽然开启或关闭SELinux要重启系统,但是在enforcing和permlsslve模式之间进行切换就很方便了。只需要使用setenforce命令就可以,给它一个参数0或1。0就代表permlsslve模式,那么1就代表enforcing模式啦。如果要查看当前SELinux工作在什么模式下,可以使用getenforce命令来获得。
需要注意的是,一旦你开启了SELinux,而且设置它工作在enforcing模式之下,很可能会有悲剧发生,你有一堆服务无法顺利启动。最主要的提示信息是/lib目录下的某些文件没有读取权限而导致启动失败。一旦发生这种悲剧,可以将SELinux设置为permissive模式,然后使用“restorecon -Rv/”命令。如果有人很倒霉或者出于好奇将SELINUXTYPE=targeted设置成了SELINUXTPE=mls,那可能会是更大的悲剧,因为你都无法登泵系统也无法关机。如果尝试断电的话,就更加悲剧了,单用户模式你都进不去了。这个时候怎么办呢?动用内核启动参数吧,我们之前介绍过了。在GRUB的交互界面中指定内核参数enforcing=0就行。启动之后就是喜剧了:)
如果你在读完本节的内容之后,还是觉得SELinux有些累赘,不妨将它设置为permissive模式,这样当你的系统受到安全威胁的时候可以使用dmesg命令来查有关信息。当然,这个模式并不是真的让你这么干的,它存在的目的是让你能够调试一下你所设置的策略是否合理。
至于后面那个SELINUXTYPE=targeted或者SELINUXTYPE=mls是怎么回事,我们后面再做详细探讨。
3. 安全上下文
在进一步讲述SELinux之前,我们先来了解一下它的最基本的概念——安全环境,或者说是安全上下文( context)。
安全上下文实际上是一个标签,如果系统中开启了SELinux,那么系统中所有的文件、目录、进程、套接字、乃至用户本身就会带有这个标签。这个标签由:用户、角色、域,类型、敏感度和类别这五个部分构成,并使用冒号“:”分割。这五个部分并不总是全部出现,具体的要根据你选择了何种策略。使周“id—Z”命令可以查看当前用户的安全上下文,在我的系统中是这个样子的:
unconf ined_u: unconfined_r: uncon fined_t:s0 -S0: c0. c1023
id:--context(-Z)works only on an selinux-enabled kernel
要查看进程的安全上下文,可以使用“ps—Z”命令,至于文件或目录的安全上下文,可以使用“Is -Z”命令。
安全上下文中的第一个部分是用户。它与Linux本身的用户是完全不同的概念。它们可以共存,而且很多时候还会被人们弄混。因为一个SELinux的用户会与Linux本身的用户使用相同的文本表达(登录名)。但是不管怎样,都要记住它们两个完全不是一回事儿。使用su命令并不能改变SELinux的用户。
每一个Linux本身的用户可以和一个SELinux的用户相对应,也就是说当他/她通过合途径登录了一个Linux系统,那么同时就会分配给他/她一个SELinux用户。可以通过“semanage login一1”命令来查看这种对应关系,在我的系统中是这个样子:
Login Name SELinux User MLS/MCS Range
_default_ unconf ined_u s0-s0 : c0 . c1023
Root unconf ined_u s0-s0 : c0 . c1023
sys tem_u system_u s0s0 : c0 . c1023
安全上下文中的第二个部分是角色。角色规定了用户能干什么,这跟我们现实社会中的
角色很像,比如:有些人是医生、有些人是教师、有些人是官员……一个人如果有了社会角色,那么他/她就会得到某些别的角色无法获得的资源,比如:医生可以给病人下处方、教师可以授予学生学位,官员可以决定你是不是医生或是教师……
每一个SELinux用户都会有它对应的角色,而且还可以对应多个角色,现实社会是这样的,比如:一个医生也可以是老师,去培养新一代的医生。
4. 身份
在SELinux中,身份的概念与传统的Linux UID是完全不同的,它们可以共存。但是在很多时候会被人们弄混,因为一个SELinux的身份会跟标准的Linux登录名使用相同的文本表达(大多数情况都是这样),但是不管怎样都要记住,它们两个完全不是一回事儿。在SELinux中身份是安全上下文的一部分,它会决定什么可以被执行。运行su命令是不会改变SELinux的身份的。
5. SELinux的策略
一开始我们就说SELinux是一种基于策略的MAC安全系统,那么它都提供了什么策略呢?回答这个问题是很难的,因为SELinux的策略可以有千百种,我却完全不知道你到底喜欢哪一种。
但是对于SELINUXTYPE这个配置项留给我们的选择并不多,因为Linux发行商们已经为我们编制好了一些策略套件,拿来直接使用或做一些小幅度的改动就可以放心地使用了。而这个SELINUXTYPE配置项就是用于选择策略套件的。可是话又说回来,既然已经编制好了,干嘛还要给我们选择呢?只能说安全这玩意儿,真是众口难调啊!
对于CentOS 6.x,它提供了三个策略套件,分别是targeted、mls和mmlmum,也分别代表了不同的侧重点。targeted是最常用的策略套件,也是CentOS 6.x默认的。它侧重的是对网络服务的严格限制,用来在没有严重影响用户体验的情况下尽可能多的保护关键进程,在CentOS 4只有15个已经定义好的targets存在,包括httpd、named、dhcpd、和mysqld,而后来的CentOS版本受保护的对象越来越多,targets数量猛增,现已超过了200个。在保护安全的同时targeted策略也期望能达到这么一种境界,大多数用户没有感觉到SELinux在运行而它却在默默起着保护作用。至于其他没有被SELinux保护的行为仍旧只能要靠古老稳定的UNIX安全系统来保障了。SELinux还允许用户使用多策略,我们可以使用mls套件来实现。它能做到不需要对安全策略进行破坏性地改变就能使MLS生效,我们需要在文件/etc/sysconfig/selinux中把SELINUXTYPE=targeted这行改成SELINUXTYPE=mls,使用chcon或semanage命令重新标签上下文,然后重启系统。如果有的用户只想使用一个“小”的SELinux在内存不多的虚拟机上运行,他可以采用targeted策略慢慢删除不需要的软件包,但是这样白手起家地开始工作迟早有一天会让他泄气的。所以针对这种需求,SELinux推出了和targeted策略一样的mlnlmum软件包,先只要求安装base policy软件包,然后包含所有targeted策略的selinux-policy-minimum rpm包进行后安装再加载到内核里。
14.6结束语
除了通过用户认证机制和访问控制机制提高Linux系统的安全性外,把入侵检测系统(LIDS)集成到Linux内核中,可以进一步加强Linux内核的安全性。
另外,“亡羊补牢,为时不晚”,syslogd被用来处理系统日志。虽然这已经是事后诸葛亮,但是它是系统安全与管理的一个重要方面,帮助我们排查错误原因防止类似的问题再次发生。syslogd记录的日志一般在/var/log/下,当然也有存储在另外的服务器上的。它可以记录who、when、where和what等要素,这样你就可以知道系统什么时候重新引导过、软硬件错误、系统运行的服务信息等。
有时日志还会包含一些敏感数据,例如本地启用了哪些服务,用户账号和配置数据,在这种情况下需要考虑加密存储与传输数据。
3A 授权(内核实现,可加载内核模块,DAC,MAC)->认证(PAM程序 用户态实现 共享库文件 libpam.so)-》审计(syslogd服务 用户态实现)
f