搜索
您的当前位置:首页正文

Linux+和Unix+安全编程HOWTO

来源:六九路网
Linux和Unix安全编程HOWTO

David A. Wheeler

Copyright © 1999, 2000 by David A. Wheeler

本文对在Linux和Unix系统下编写安全程序给出了一组设计和实现的指导原则。这样的程序包括用来浏览远端数据的应用程序、CGI脚本程序、网络服务器和setuid/setgid程序。本文还包含了针对C、C++、Java、Perl、Python和Ada95的特别指导。

Table of Contents 1. 导言

2. 背景知识

Unix、Linux与开放源码软件的历史 Unix

自由软件基金会 Linux

开放源码软件

Linux与Unix的比较 安全准则

安全程序的类型 多疑症是个优点

为什么要编写本文档? 设计和实现原则的来源 文档习语

3. Linux和Unix安全特性概要

进程 进程属性 POSIX能力 进程创建与操作 文件

文件系统对象的属性 创建时的初始值 改变存取控制属性 使用存取控制属性 文件系统分级结构 System V的IPC 套接字和网络连接 信号

配额与限制 动态连接库 审核

PAM

4. 证实所有的输入

命令行 环境变量

有些环境变量是危险的

环境变量的存储格式是危险的 解决方案 -- 提取和清除 文件描述符 文件内容 CGI输入 其它输入 字符编码

限制合法的输入时间和负载水平

5. 避免缓存溢出

C/C++中的危险

C/C++中库的解决方案 标准C库的解决方案 静态和动态分配缓存 strlcpy和strlcat libmib Libsafe 其它库

C/C++的编译解决方案 其它语言

6. 程序内部结构与解决方案

保证接口的安全 特权最小化

最小化授予的特权

最小化可以使用特权的时间 最小化特权有效的时间 >最小化获得特权的模块 考虑用FSUID来限制特权

考虑使用chroot来最小化可用文件 避免创建Setuid/Setgid脚本 安全地配置并使用安全的缺省值 安全地失败 避免竞争状态 次序问题 锁定

只信任值得信任的通道 使用内部一致性检查代码 自我限制资源

7. 小心对其它资源的调用出口

限制调用出口为合法值

检查系统调用的所有返回值

8. 明断地发回信息

最小化反馈

处理完整的/不响应的输出

9. 特定语言的问题

C/C++ Perl Python

Shell脚本语言(sh及csh的变种) Ada Java

10. 专题

密码 随机数

加密算法与协议 PAM 其它事项

11. 结论

12. 参考文献 A. 历史回顾 B. 感谢

C. 关于文档许可

D. GNU自由文档许可证(原文) E. 关于作者 List of Tables

4-1. 非法UTF-8初始序列

Chapter 1. 导言

A wise man attacks the city of the

mighty and pulls down the stronghold in which they trust.

Proverbs 21:22 (NIV)

本文对在Linux和Unix系统下编写安全程序提出了一组设计和实现的指导原则。在本文中,“安全程序”是指一个位于安全区域内的程序,要从不具有与该程序相同访问权限的信息源获取输入数据。这样的程序包括用来浏览远端数据的应用程序、CGI脚本程序、网络服务器和setuid/setgid程序。本文不涉及对操作系统内核本身的修改,虽然这里讨论的许多原则可以应用到操作系统内核上。这些指导原则来源于对许多创建安全程序的“教训”的调查(加上作者的观察),并重新组织成一组更普遍的原则。 本文还包含了针对C、C++、Java、Perl、Python和Ada95等一些编程语言的特别指导。

本文不包括保证测量、软件工程的步骤和质量保证手段,这些确实很重要,但在其它地方有更为广泛的讨论。保证测量包括测试、仔细检阅、配置管理和正规化方法。对安全问题的保证测量开发进行专门阐述的文献包括Common Criteria [CC 1999]和System Security Engineering Capability Maturity Model

[SSE-CMM 1999]。更普遍的软件工程方法或步骤在诸如Software Engineering Institute的Capability Maturity Model for Software (SE-CMM)、ISO 9000(以及ISO 9001和ISO 9001-3)和ISO 12207等文献中有详尽的定义。

本文不讨论在一个给定的环境下如何配置一个安全的系统(或网络)。很明显这对安全使用一个给定程序是必要的,但已经有太多的其它文献讨论了安全配置。 Garfinkel [1996]是一本优秀的有关配置安全的类Unix系统的一般性书籍。其它有关类Unix系统安全的书籍包括Anonymous [1998]。还可以在诸如

http://www.unixtools.com/security.html 的站点上找到配置类Unix系统的信息。配置安全Linux系统的信息可以从包括Fenzi [1999]、Seifried [1999]、Wreski [1998]和Anonymous [1999]在内的各种各样文献中得到。

本文假设读者了解一般性的计算机安全问题、类UNIX系统的一般安全模型和C编程语言。本文还包括了Linux和Unix安全编程模型的一些资料。

尽管本文覆盖了所有类Unix系统,包括Linux和各种Unix分支,但对Linux给予了特别的关注,并提供了有关Linux特性的细节。这是出于以下几个理由。最简单的理由就是Linux的普及:按照一项调查,1999年安装Linux的服务器超过了所有Unix操作系统的总合(Linux占25%,而所有的Unix系统一共占15%)[Shankland 2000]。另外,本文的初始版本只讨论了Linux,所以虽然其范围扩展了,有关Linux的信息还是明显占主要部分的。如果你了解本文没有包含的有关信息,请告诉我。

你可以在 http://www.dwheeler.com/secure-programs 找到本文的主拷贝。本文也是Linux文档计划(LDP) http://www.linuxdoc.org 的一部分。它在其它几处也有镜象。请注意,这些镜象,包括LDP的拷贝和(或)你所用发行版中的拷贝,可能要比主拷贝陈旧。我很欢迎对本文提出意见,但希望在提意见之前你能确定所用版本为最新的文档。

本文版权属于 (C) 1999-2000 David A. Wheeler,受GNU自由文档许可证(GFDL)保护;参见最后一节以了解更多信息。

本文首先讨论了Unix、Linux和安全性的背景知识。下一节描述了一般的Unix和Linux安全模型,概述了进程、文件系统对象等等的安全属性与操作。接下来就是本文的实质内容,在Linux和Unix系统上开发应用程序的一组设计与实现指导原则。本文末尾给出了结论、大量参考文献和附录。

设计与实现的指导原则按照我所认为该强调的程序员角度进行了分类。程序接受输入、处理数据、调用其它资源和产生输出;所有安全性指导原则在概念上都可归入其中某一类。我把处理数据又进一步划分为: 避免缓存溢出(在某些情况下可以认为是输入问题)、程序内部与过程的结构化、特定语言信息和若干专题。实际的章节编排稍微做了些调整以便于阅读。因此,本文的指导原则章节讨论了证实所有输入的合法性、避免缓存溢出、程序内部与过程的结构化、小心地调用其它的资源、明断地发回信息、特定语言信息和若干专题(比如如何获得随机数)。

Chapter 2. 背景知识

I issued an order and a search was made, and it was found that this city has a long history of revolt against kings and has been a place of rebellionand sedition.

Ezra 4:19 (NIV)

Table of Contents

Unix、Linux与开放源码软件的历史 安全准则

安全程序的类型 多疑症是个优点

为什么要编写本文档? 设计和实现原则的来源 文档习语

Unix、Linux与开放源码软件的历史

Unix

1969-1970年,AT&T贝尔实验室的Kenneth Thompson、Dennis Ritchie和其他人开始在一台较老的PDP-7上开发一个小型的操作系统。不久这个操作系统被命名为Unix,是对此前一个叫做MULTICS的操作系统项目的双关语。在1972-1973年,系统被重新用C语言改写了,这是一个很有眼光的不寻常的举动:这个决定使Unix成为第一个可以脱离其原始硬件而存在的被广泛应用的操作系统。其它的新方法也不断被加入Unix,一部分原因是贝尔实验室与学术界的良好协作。1979年,Unix的“第七版”(V7)发布了,它是所有现存Unix系统的鼻祖。

从此,Unix的历史开始变得错综复杂。由Berkeley领导的学术界开发了被称为Berkeley软件发行版(BSD)的变种,而AT&T继续在“系统III”和随后的“系统V”的名义下开发Unix。在八十年代后期到九十年代早期,这两个主要分支间的“战争”激化了。经过多年开发,每个变种都采用了其它变种的许多关键特性。在商业上,系统V赢得了“标准之战”(使其绝大部分接口进入了正式标准),而且绝大多数硬件厂商都切换到AT&T的系统V。但是,系统V包含了许多BSD的革新,使该系统更像是两大分支的融合。BSD分支并未消失,而是广泛应用在研究、PC硬件和目的单一的服务器上(例如许多WEB站点都使用某个BSD变种)。

最终结果是有很多不同版本的Unix,都来源于原先的第七版。绝大多数版本的Unix都是私有的,由其相应的硬件厂商维护,例如,Sun的Solaris就是系统V

的变种。Unix的BSD分支中有三个版本成为开放源码软件:FreeBSD(注重在PC类硬件上安装简便)、NetBSD(注重于很多不同的CPU结构)和NetBSD的一个变种,OpenBSD(注重于安全性)。更多有关的一般信息可以在

http://www.datametrics.com/tech/unix/uxhistry/brf-hist.htm 上找到。更多有关BSD历史的信息可以在[McKusick 1999]和

ftp://ftp.freebsd.org/pub/FreeBSD/FreeBSD-current/src/share/misc/bsd-family-tree 上找到。

有兴趣阅读目前使用类Unix系统的辩护言论的读者可以看看 http://www.unix-vs-nt.org。

自由软件基金会

1984年,Richard Stallman的自由软件基金会(FSF)启动了GNU计划,以创建一个UNIX操作系统的自由版本。Stallman所指自由的含义是软件可以被自由使用、阅读、修改和再次发行。FSF成功地创建了许多有用的软件,包括一个C编译器(gcc)、一个给人留下深刻印象的文本编辑器(emacs)和一大批基本的工具软件。但是在九十年代,FSF在开发操作系统内核上遇到了麻烦[FSF 1998];而没有内核,他们的其它软件就无法应用。

Linux

1991年,Linus Torvalds开始开发一个他称为“Linux”的操作系统内核[Torvalds 1999]。这个内核可以同FSF的资料和其它软件(特别是某些BSD软件和MIT的X-windows软件)结合起来构成一个可以自由修改而且非常有用的操作系统。本文把内核本身称为“Linux内核”,而把该完整的结合体称为“Linux”。注意,很多人使用术语“GNU/Linux”来称呼该结合体。

在Linux社团中,不同的组织以不同的方式结合可用软件。每一种结合被称为一个“发行版”,而开发发行版的组织就被称为“发行商”。普通的发行版包括Red Hat、Mandrake、SuSE、Caldera、Corel和Debian。各种发行版间存在差异,但所有的发行版都基于同一个基础:Linux内核与GNU的glibc库。由于这两者都是受“copyleft”类型许可证保护的,对它们的修改一般也必须提供给所有人,这是BSD和源于AT&T的Unix系统之间所不存在的统一力量,使不同Linux发行版统一在同一个基础之上。本文不针对任何特定的发行版;但在讨论时,假设使用的Linux内核版本是2.2以上,C库为glibc 2.1或更高版本,基本上这一假设对当前所有的主要Linux发行版都成立。

开放源码软件

对“自由软件”不断增长的兴趣使得定义和解释它越来越有必要。一个被广泛使用的术语“开放源码软件”在[OSI 1999]中有进一步的定义。Eric Raymond [1997,

1998]撰写过几篇考察其发展过程的重要文章。另一个被广泛使用的术语是“自由软件”,“自由”通常的解释是“言论自由,而非免费啤酒”。这两个术语都不是完美的。术语“自由软件”经常与可执行文件无偿提供但不允许浏览、修改或再次分发源码的程序相混淆。与之相反,术语“开放源码”有时被误用在那些源码可以浏览,但在使用、修改或再次分发上有限制的软件上。本文采用术语“开放源码”的通常意义,也就是软件的源码可以自由使用、浏览、修改或再次分发。有兴趣阅读开放源码软件的辩护言论的读者可以看看 http://www.opensource.org 以及 http://www.fsf.org。

Linux与Unix的比较

本文使用“类Unix”一词来描述有意类似Unix的系统。特别是“类Unix”一词包含了所有主要的Unix变种和Linux发行版。

Linux不是起源于Unix的源码,但它的接口被有意设计为类似Unix的。因此,从Unix得来的教训在两者上都可以应用,包括安全方面的信息。本文中的大多数信息都可以应用在任何类Unix的系统中,但特意增加了一些Linux特定信息以使Linux用户能够利用Linux性能的优越性。

类Unix的系统共享许多安全机制,虽然其中存在细微的差异,而且不是所有系统都支持全部的机制。这里的全部机制包括每个进程的用户与群组标识(uid与gid)和具有读、写与执行许可(对于用户、群组和其他人)的文件系统。参见Thompson [1974]和Bach [1986]以了解Unix系统的一般信息,包括它们的基本安全机制。第三章总结了Unix和Linux的关键安全机制。

安全准则

你需要熟悉许多普遍的安全准则;请查阅诸如[Pfleeger 1997]一类的有关计算机安全的通用读本。计算机安全目标通常由三个总体目标来描述:

• • •

秘密性(也被称为秘密),意味着计算系统的内容只能被授权对象访问。 完整性,意味着内容只能被授权对象以被授权的方式所修改。

可用性,意味着内容可以被授权对象使用。该目标经常提到的反义词就是

拒绝服务。

有些人还定义了一些附加安全目标,而其他人则把那些附加目标归为这三个目标的特例。例如,有些人独立地把无拒绝定义为一个目标;这样就能够“证明”某个发送者发送或接收者接收到消息,即使该发送者或接收者随后希望拒绝它。隐私有时也被从秘密性中独立出来;有些人把它定义为保护一个用户而非数据的秘密性。大多数目标都要求识别和确认,有时这也被单独列为一个目标,而且审计(也被称为负责)经常也是一个可取的安全目标。有时“访问控制”和“可靠性”也被单独列出来。无论把安全目标如何组织在一起,在任何情况下找出程序总的安全目标都是重要的,只有这样才能知道何时可以达到这些目标。

Saltzer [1974]和Saltzer and Schroeder [1975] 列出了以下依然有效的设计安全保护系统的原则:

最小特权。每个用户和程序在操作时应当使用尽可能少的特权。该原则限

制了事故、错误或攻击带来的危害。它还减少了特权程序之间潜在的相互作用,从而使对特权无意的、没必要的或不适当的使用不太可能发生。这种想法还可以引申到程序内部:只有程序中需要那些特权的最小部分才拥有特权。 机制经济。保护系统的设计应当尽可能地简单和小。用他们的话来说,“逐行审阅软件和对硬件进行物理检查一类的技术是实现保护机制所必需的。这样的技术要想成功,小而简单的设计是必不可少的。”

公开设计。保护机制不应该依赖于攻击者的无知。其机制反而应当是公开的,依赖于象密码或密钥这样比较少的(而且容易改变的)项目的保密。公开设计使广泛的公开详细检查成为可能,而且还可以让用户确信所要使用的系统是可以满足需要的。坦白地说,要保持一个广泛应用系统的秘密是不现实的;反向编译和硬件破解能够很快暴露一个产品的任何“秘密”。Bruce Schneier证明了聪明的设计者应当“对任何有关安全的东西都要求公开源码”,以及确保它受到普遍的审核,而且任何找到的问题都已得到修正[Schneier 1999]。

完全中介。每一个访问企图都应该被检查;把认证机制放在不会被推翻的位置上。例如,在客户--服务器模型中,通常服务器应该进行所有的访问检查,因为用户可以构建或修改他们自己的客户程序。 安全失败的缺省值(例如,基于许可的方案)。缺省反应应当是拒绝服务,而且保护系统能随后辨别哪种情况下访问是允许的。

>特权分离。理想情况下,访问对象应当依赖于多个条件,这样破坏一个保护系统并不能进行完全的访问。

最少的公共机制。使共享机制的数量和使用(例如,对/tmp或/var/tmp目录的使用)最小化。共享对象提供了信息流和无意的相互作用的潜在危险通道。

心理上的接受程度/使用简便。人机界面必须设计得易于使用,这样用户就可以按惯例自动地正确使用保护机制。如果安全机制非常符合用户对自己保护目标的想象,错误就很少发生。

• • •

安全程序的类型

许多不同类型的程序可能都需要成为安全程序(按照本文的定义)。一些通用的类型为:

用来浏览远端数据的应用程序。用做浏览器的程序(如文字处理程序或文件格式浏览器)经常需要浏览一个不可信的用户从远端发来的数据(一个网页浏览器会自动执行这种请求)。显然,不应当允许一个不可信用户的输入使应用程序运行任意的程序。支持预置的宏(在显示数据时运行)通常是不明智的;如果必须这么做,那么应该创建一个安全沙箱(这是一个

• • • •

复杂而且容易出错的工作)。小心处理下面会讨论的缓存溢出一类的问题,它会允许一个不可信用户强迫浏览器运行一个任意程序。

系统管理员(root)使用的应用程序。这类程序不应当信任非系统管理员就可以控制的信息。

本地服务器程序(也被称为daemon)。

可访问网络的服务器程序(有时被称为网络daemon)。 CGI脚本程序。这些是一类特殊的可访问网络的服务器程序,但应用得十分普遍,应当算作一个单独的类别。这样的程序通过一个WEB服务器被间接地执行,虽然WEB服务器可以过滤掉一些攻击,但仍然需要抵抗许多攻击。

setuid/setgid程序。这些程序由一个本地用户执行,但在执行时会立刻拥有程序所有者或其所在群组的特权。从很多方面来说,这些是最难保证安全的程序,因为它们有很多输入是在不可信用户的控制之下,而且有些输入不是很明显的。

本文把这些不同类型的程序都归为一类。这样做的不利之处在于本文指出的某些问题不适用于所有类型的程序。特别是setuid/setgid程序有很多令人惊奇的输入,本文有几条只针对它们的准则。然而事情并非如此简单界定的,一个特定的程序可能无法进行这样的分类(例如,一个CGI脚本程序可以setuid或setgid,或者进行有同样效果的配置),而且有些程序可以分为若干个可执行部件,每个都可以被认为是一个不同“类型”的程序。把这些不同类型的程序都归为一类的好处在于考虑问题时不会把一个不适当的类别应用在某个程序上。在下面你将看到,很多原则都可以应用到所有需要安全的程序上。

本文对C编写的程序有一些偏爱,也有一些关于其它语言如C++、Perl、Python、Ada95和Java的说明。这是因为C是在Linux下实现安全程序最普遍的语言(CGI脚本程序倾向于使用Perl),而且大多数其它语言的实现都调用C库。这并不意味着在实现安全程序时C是“最好”的语言,无论使用的是哪一种编程语言,本文所说明的原则绝大多数都适用。

多疑症是个优点

编写安全程序最主要的困难在于编写时需要一个不同的精神状态,简而言之,就是处于偏执狂的状态。其原因就是错误(也被称为缺陷或Bug)的影响可能会有着极大的差异。

普通的非安全程序有许多错误。虽然这些错误不受欢迎,但一般只在罕见或不太可能的情况下出现,如果用户无意中发现某个错误,他们以后会试图避免用同样的方式使用该软件。

对安全程序,情况恰恰相反。某些用户会有意搜寻并产生这些罕见或不太可能的情况,希望这样的攻击可以使他们获得未经授权的特权。因此,在编写安全程序时,多疑症就是一个优点了。

为什么要编写本文档?

一个曾被问到的问题是“为什么你要编写本文档?”我的回答是:在过去几年中,我注意到很多Linux和Unix的开发者看来不断地重复落入相同的安全陷阱中。审查者缓慢地找到这些问题,但要是能使代码一开始就不出问题则更好。我认为出问题的一部分原因在于没有一个简单明显的地方可以让开发者去获得如何避免已知陷阱的资料。这些资料是可以公开得到的,但经常是不容易找到、过时、不完全或有其它问题。绝大多数这样的资料并非特别针对Linux的,虽然Linux已经开始被广泛应用!这样就引出了答案:我编写本文是希望未来的Linux软件开发者可以不再重复过去的错误,以获得一个更加安全的Linux。我加上Unix,是由于确信程序可以在这些系统中移植通常是个明智的选择。你可以在

http://www.linuxsecurity.com/feature_stories/feature_story-6.html 上读到有关此问题更为广泛的讨论。

另一个可能被问到的相关问题是“为什么要编写自己的文档而不是简单地引用其它文档?”这有好几个答案:

很多这样的资料都很分散;把关键信息放在一个有条理的文档中会便于使用。

• 有些这样的信息不是为程序员而写的,而是写给管理员或用户的。 • 大多可得到的信息都强调可移植的结构(即可以在所有类UNIX的系统中使用的结构),而根本没有谈及Linux。出于可移植性的考虑,一般最好避免使用Linux独有的功能,但有时使用Linux独有的能力确实可以增强安全性。即使期望获得非Linux的可移植性,可能在Linux下还是希望能够支持Linux独有的功能。而且通过强调Linux,可以把对Linux感兴趣的人有所助益但对其它人无意义的信息包括进来。

设计和实现原则的来源

有几篇参考文献有助于说明如何编写安全程序(或者换而言之,如何发现现有程序的安全问题),并构成了本文下面将要强调的指导原则的基础。

对一般目的的服务器程序和setuid/setgid程序,有许多值得一看的文献(虽然有些文献如果不被提及的话很难被发现)。

Matt Bishop [1996, 1997]对此问题发表了几篇极有价值的论文和报告,而且他有个网页 http://olympus.cs.ucdavis.edu/~bishop/secprog.html专门讨论这个问题。根据Garfinkel和Spafford讨论如何编写安全的SUID和网络程序的那本书 [Garfinkel 1996] 第22章的部分内容,AUSCERT发布了一个编程核对清单 [AUSCERT 1996]。 Galvin [1998a]描述了一个开发安全程序的简单步骤和核对清单;后来又在 Galvin [1998b]中对核对清单进行了更新。 Sitaker [1999] 为“Linux安全审计”小组提出了一个需查找的问题列表。 Shostack [1999] 为检阅对安全性敏感的代码定义了另一个核对清单。 NCSA [NCSA] 提供了一组简要

而实用的安全编程指南。 其它有用信息的来源包括 安全Unix编程常见问题 [Al-Herbish 1999], 安全审计的常见问题 [Graham 1999],和 Ranum [1998]。某些建议必须警惕,比如,BSD setuid(7)的man信息 [Unknown] 推荐使用access(3),却没有说明通常与之相伴随的竞争的危险。在Wood [1985]中的“Security for Programmers”一章里有一些有用但过时的忠告。 Bellovin [1994] 包含了一些有用的指导原则以及一些特别的实例,诸如如何重新构建更简单安全的ftpd程序。 FreeBSD [1999] 也包含了一些有用的指导原则。

对于使用CGI作为与WEB的接口的程序,有很多文献给出了安全守则。其中包括 Van Biesbrouck [1996]、 Gundavaram [unknown]、 [Garfinkle 1997] Kim [1996]、 Phillips [1995]、 Stein [1999] 和 Webber [1999]。

还有很多文献是针对某一种语言的,这在本文的专有语言一节会进一步讨论。例如,Perl部分包括 perlsec(1),描述如何使Perl应用更安全。在

http://www.cs.princeton.edu/sip 的安全网络编程站点涉及了普遍的计算机安全问题,但主要是Java、ActiveX和JavaScript一类的移动编码系统;Ed Felten(其骨干人员之一)与人合写了一本安全Java的书([McGraw 1999]),讨论Java的安全问题。Sun的安全编码指南提供了一些主要针对Java和C的指导原则;可以在 http://java.sun.com/security/seccodeguide.html上找到这些资料。 Yoder [1998]收集了一些处理应用程序安全所用的样板。这并非一组专门的指导原则,而是可能会有用的一组编程通用样板。Schmoo小组维护着一个连接如何编写安全代码信息的网页 http://www.shmoo.com/securecode。

还有很多文献从另一个角度(如“如何破译系统”)描述了这个问题。例如McClure [1999],而且Internet上有不计其数的资料。

还有大量已经找出的已有程序漏洞的资料。它们可以作为“不要这么做”的有用范例,不过需要花时间从大量特别事例中提取更普遍的指导原则。有一些讨论安全

从诸多事中发展出一问题的邮件列表;其中一个最著名的邮件列表是 Bugtraq,

个漏洞列表。CERT的协调中心(CERT/CC)是报告漏洞的一个主要Internet安全问题报告中心。CERT/CC偶尔发布一些报告,提供某个严重安全问题的描述及其影响,并给出如何获取补丁的指示或者变通办法的细节;更多的信息可见 http://www.cert.org。注意,CERT开始只是个计算机紧急响应小组,但按官方的说法“CERT”现在不代表任何事。能源部的 Computer Incident Advisory Capability (CIAC)也报告漏洞。这些不同的组织可能会用不同的名称来报告同样的漏洞。为了解决这个问题,MITRE 支持常见漏洞揭露(CVE)列表来对所有公开的已知安全漏洞创建单一的独有标识(“名称”)以相互区别;参见

http://www.cve.mitre.org。 NIST的ICAT是一个计算机漏洞的可查找目录,把每个CVE漏洞分类以便于随后查找和比较;参见 http://csrc.nist.gov/icat。 本文总结了我所认为最有用的准则,这样程序员可以坐下来读这一篇文档,并为实现一个安全程序做好相当的准备。它不是一个所有可能准则的大全。在这里我使用了自己的组织方式(每个列表都有自己与众不同的结构),而且Linux独有

的准则(如关于能力和fsuid值)也是我自己提出的。强烈推荐参考阅读上述的所有参考文献。

文档习语

系统手册页用格式名称(号码)、给出,其中号码为手册的章节号。表示“不指向任何位置”的指针值被称为NULL;在大多数情况下,C编译器把整数0转变为值NULL,但需要注意,在C标准中并未要求NULL由一串全0比特来实现。C和C++处理字符“\\0”(ASCII 0)的方式比较特殊,本文中用此值表示NIL(通常它被称为“NUL”,但“NUL”和“NULL”听起来完全一样)。函数和方法名一般使用其正确格式,即使这意味着在某些语句中需要以一个小写字母打头。我用“类Unix”一词来表示Unix、Linux或其它基本模型与Unix非常相似的系统;不用POSIX的原因是诸如Windows 2000这样的系统实现了POSIX的一部分,但其安全模型完全不同。攻击者被叫做“攻击者”、“黑客”或“对手”。有些新闻工作者用“黑客”来代替“攻击者”;本文避免这样的用法,因为很多Linux和Unix的开发者以“黑客”一词的传统非恶意词意把自己视为“黑客”。也就是说,对于很多Linux和Unix的开发者,“黑客”一词依然简单地意味着专家或爱好者,特别是对于计算机。本文使用“新”的或“逻辑”引文系统,而不是传统的美式引文系统:如果标点不属于引文,则引文中不包括任何尾随的标点。虽然这可能会使版面优美度稍有降低,但传统的美式系统在引文中加入了外在的字符。这些外在的字符对散文没有什么影响,但放在代码或计算机命令中却是灾难性的。

Chapter 3. Linux和Unix安全特性概要

Table of Contents 进程 文件

System V的IPC 套接字和网络连接 信号

配额与限制 动态连接库 审核 PAM

Discretion will protect you, and understanding will guard you.

Proverbs 2:11 (NIV)

在讨论如何应用Linux或Unix安全特性指南前,先从一个程序员的角度了解一下这些特性是很有用的。本节对这些在几乎所有类Unix系统上广泛应用的特性进行简要的描述。尽管如此,还是需要注意类Unix系统不同版本间相当大的差异,以及不是所有系统都具有这里所描述的能力。本节也着重提到了Linux特有的一些扩展或特性;从安全编程的角度来看,不同Linux发行版非常相似,因为它们本质上都使用相同的内核与C库(以及鼓励任何创新迅速传播的基于GPL的许可)。本文不讨论很多类Unix系统都不实现的强制存取控制(MAC)的实现之类的问题。如果你已经知道了这些特性,可以跳过本节继续阅读。 很多编程指南简单地略过了Linux或Unix有关安全的部分,而且忽略了重要的信息。特别是它们经常讨论通常情况下“如何应用”某物,而不考虑影响这些应用的安全属性。与此相反,在单个函数的手册页中有大量的详细信息,但是有时手册页对于如何使用每个单独函数的详细讨论模糊了关键的安全性问题。本节试图弥补此缺憾;只为程序员提供Linux下可能会用到的安全机制的全局概貌,但特别注重有关安全问题的分支。本节比经典的编程指南更为深入,特别是集中在有关安全的问题上,并指出从哪里可以获得进一步的资料。

首先,是基本情况。 Linux和Unix从根本上来说可以分为两部分:内核与“用户空间”。绝大多数程序运行在用户空间(在内核之上)。Linux支持“内核模块”的概念,简单地说就是在内核里动态载入代码的能力,但还是有这样的基本划分。有些其它的系统(如HURD)是基于“微内核”的系统;它们有一个功能很有限的小内核和一组“用户”程序来实现传统上由内核实现的底层函数。

有些类Unix系统进行了大量修改以支持增强的安全性,特别是支持美国国防部对强制存取控制(B1级别以上)。本文的目前版本不涉及这些系统或这些问题;我希望在未来的版本中可以加入这些内容。

当用户登录时,他们的用户名被映射为整数,来标明自己的“UID”(用户ID)和作为其中成员的“GID”(组ID)。UID 0是传统上被称为“root”的具有特权的用户(角色),在绝大多数类Unix系统(包括Unix)中root可以强制变更大多数的安全性检查,被用来管理系统。就安全性而言,进程是唯一的“主题”(也就是说,只有进程是活动的目标)。进程可以访问各种数据对象,特别是文件系统对象(FSO)、系统V进程间通信(IPC)对象和网络端口。进程还可以设置信号。其它有关安全的主题包括配额与限制、库、审核和PAM。以下几节对此进行详细说明。

进程

在类Unix系统上,用户级别的活动由运行的进程来实现。绝大多数Unix系统支持作为独立概念的“线程”;一个进程内的线程共享内存,而且系统的调度器实际上是调度线程。Linux的做法与此不同(依我看是做得更好):线程与进程没有本质的差异。在Linux下,在某个进程创建另一个进程时,它可以选择共享哪些资源(比如内存可以共享)。随后Linux内核会进行优化以获得线程级的速度;参见clone(2)以了解更多信息。值得注意的是Linux内核的开发者倾向于使用“任务”一词,而不是“线程”或“进程”,但外界的文档则倾向于使用进程一词(所以我在文中如此使用)。在多线程应用程序编程时,使用某个标准的线程库来隐藏这些差异通常要好一些。这不仅使线程更易于移植,而且有些库通过把多个应用程序级的线程实现为单个操作系统线程的方法提供一个间接的附加级别;这可以改进某些系统上一些应用程序的性能。

进程属性

在类Unix系统中,每个进程所有的典型属性如下:

• • • • • •

• •

RUID, RGID -- 运行进程的用户的真实用户ID和组ID

EUID, EGID -- 用于权限检查(文件系统除外)的有效用户ID和组ID SUID, SGID -- 保存的用户ID和组ID;用来支持下面要讨论的切换许可“开和关”。不是所有的类Unix系统都支持它。

补充群组 -- 用户有成员资格的群组(GID)列表。

umask -- 在创建一个新的文件系统对象时决定缺省访问控制设置的一组比特位;参见umask(2)。

scheduling parameters -- 每个进程都有一个调度策略,缺省策略为SCHED_OTHER的进程还具有nice、priority和counter的附加属性。参见sched_setscheduler(2)以了解更多信息。 limits -- 每个进程的资源限制(参见下文)。

filesystem root -- 进程角度的根文件系统起始处;参见see chroot(2)。

下面是与进程有关的不太普通的属性:

FSUID, FSGID -- 用于文件系统访问检查的用户ID和组ID;一般等于相应的EUID和EGID;这是一个Linux特有的属性。

• capabilities -- POSIX能力信息;一个进程实际上有三组能力:有效的、可继承的和许可的能力。参见下文中有关POSIX能力的更多信息。版本2.2以上的Linux内核支持这一点;有些其它的类Unix系统也支持,但不够普遍。

在Linux下,如果确实需要了解哪些属性与每个进程相关,最可靠的信息源是Linux源码,特别是/usr/include/linux/sched.h中的task_struct定义。 创建新进程的可移植方式是使用fork(2)调用。BSD作为优化技术引进了一个叫做vfork(2)的变种。vfork(2)的使用原则很简单:如果可以避免就不要使用它。vfork(2)与fork(2)不同,在调用execve(2V)或退出之前,子进程借用父进程的内存和控制线程;在子进程其资源时,父进程被悬挂。其原理是在旧的BSD系统中,fork(2)实际上会导致内存复制,而vfork(2)则不会。Linux则根本不会出现这个问题;因为Linux内部采用写时复制的语义,只有在改变时才复制内存页(实际上Linux还是有些表要复制的;在绝大多数情况下由此带来的负荷不大)。尽管如此,由于有些程序依赖于vfork(2),最近Linux实现了BSD的vfork(2)语义(以前Linux下的vfork(2)只是fork(2)的别名)。vfork(2)的问题在于,进程要想不与其父进程互相干扰需要相当的技巧,特别是使用高级语言。其后果在于:一旦代码改变,甚或编译器版本变化,都会很容易使调用了vfork(2)的程序失效。在绝大多数情况下应该避免vfork(2);它的主要用途在于支持需要vfork语义的老程序。

Linux支持Linux特有的clone(2)调用。该调用与fork(2)类似,但允许明确说明哪些资源可以共享(如内存、文件描述符等等)。可移植程序不应该直接使用此调用;而是应该象前面所说的那样,依赖于使用该调用实现线程的线程库。 本文不是编写程序的完全手册,所以将跳过大量存在的处理进程的信息。可以参见wait(2)、exit(2)一类的文档以了解更多内容。

POSIX能力

POSIX能力是支持把通常由root拥有的特权分割为更多更专门特权的一组比特位组合。POSIX能力是由一个IEEE标准草案定义的;它不是Linux所独有的,但也并非其它类Unix系统普遍支持的。Linux内核2.0不支持POSIX能力,版本2.2增加了对进程的POSIX能力的支持。当Linux文档(包括本文)中提到“要求root权限”时,实际上几乎都是意味着像能力文档中所说的那样“要求某个能力”。如果想知道要求的特定能力,请在能力文档中进行查找。

在Linux中,其最终目的是允许能力与文件系统中的文件联系起来;但到本文档完成时,Linux还不支持这一点。Linux对能力传递有支持,但缺省情况下被禁用。版本2.2.11的Linux增加了一个叫做“能力绑定设置”的特性,使能力的应用更直接更有用。能力绑定设置是一组允许被系统中任意进程所拥有的能力(否则,

只有特殊的初始化进程可以拥有这些能力)。如果某能力不在此绑定设置中,则无论有没有权限,都不可以被任意进程所使用。例如,此特性可用来禁止内核模块加载。利用此特性的一个工具实例是LCAP http://pweb.netcom.com/~spoon/lcap/。

更多有关POSIX能力的资料可以从

ftp://linux.kernel.org/pub/linux/libs/security/linux-privs 获得。

进程创建与操作

进程可以用fork(2)、不推荐使用的vfork(2)或者Linux独有的clone(2)来创建;这些系统调用都复制当前进程,并从中创建两个进程。一个进程可以通过调用execve(2)、或它的各种前端(参见exec(3)、system(3)和popen(3))来执行一个不同的程序。

在程序执行时,其文件设置自己的setuid或setgid比特位,进程的EUID或EGID(分别)被设置为文件的EUID或EGID值。在用来支持setuid或setgid脚本时,由于存在竞争状态,此功能会导致一个老的UNIX安全漏洞。在内核打开文件来查看运行的解释器和(正在设置ID的)解释器回转并重新打开文件以解释文件之间,攻击者可以改变文件(直接或通过符号连接)。

不同的类Unix系统采用不同的方法处理setuid脚本的安全问题。某些系统,如Linux,在执行脚本时完全忽略setuid和setgid比特位,这显然是一个安全的措施。SysVr4和BSD 4.4的大多数现代发行版使用一种不同的方法来避免内核竞争状态。在这些系统中,当内核把要打开的setuid脚本的名称传递给解释器时,不使用路径名(这会允许竞争状态),而是传递文件名/dev/fd/3。这是一个脚本已经打开的特殊文件,所以不会出现攻击者可以利用的竞争状态。即使在这些系统上,我依然建议不要在安全程序中使用setuid/setgid脚本编程语言,下面会进一步讨论这个问题。

在某些情况下,进程会影响各种UID和GID的值;参见setuid(2)、seteuid(2)、setreuid(2)和Linux特有的setfsuid(2)。特别是保存的用户ID(SUID)属性允许可信任的程序临时切换自己的UID。类Unix系统支持按以下规则使用SUID:如果RUID被改变,或者EUID被设置为不等于RUID的值,SUID就被设为新的EUID。非特权用户可以用自己的SUID来设置EUID,把RUID设为EUID,以及把EUID设为RUID。

Linux特有的进程属性FSUID是用来允许NFS服务器一类的程序把自己的文件系统权限限制在某些给定的UID上,而不给这些UID向进程发送信号的许可。一旦EUID被改变,FSUID就被改为新的EUID值;FSUID的值可以用Linux独有的调用setfsuid(2)单独进行设置。注意,非root调用者只能把FSUID设置为当前的RUID、EUID、SEUID或当前的FSUID。

文件

对于所有类Unix系统,最主要的信息存放地点是根为“/”的文件树。文件树是一个目录的分级结构,每个目录都可以保护文件系统对象(FSO)。

在Linux中,文件系统对象(FSO)可以是普通文件、目录、符号连接、命名管道(FIFO)、套接字(参见下文说明)、特殊字符(设备)文件或特殊块(设备)文件(在Linux下可以用find(1)命令显示其列表)。其它类Unix系统有同样或相似的一组FSO类型。

文件系统对象由文件系统收集,可以在文件树的目录下安装和卸载。文件系统的类型(如ext2和FAT)是一组管理磁盘上数据以优化速度、可靠性等等的特殊规范集;很多人用“文件系统”作为文件系统类型的同义词。

文件系统对象的属性

不同的类Unix系统支持不同的文件系统类型。文件系统的访问控制属性可以稍有区别,在安装时选择的选项也会影响访问控制。在Linux下,ext2文件系统是目前最常用的文件系统,但Linux还支持大量的文件系统。绝大多数类Unix系统也都支持多个文件系统。

类Unix系统上的绝大多数文件系统至少都包含以下内容:

拥有UID和GID -- 标识文件系统对象的“所有者”。除非另加说明,只有所有者或root可以改变存取控制属性。

• 许可比特位 -- 每个用户(所有者)、群组及其他人的读、写和执行比特位。对于一般文件,读、写和执行就是相应的典型意义。对于目录,显示目录下内容需要“读”许可,而在实际进入目录以使用其内容时需要的“执行”许可有时被称为“查找”许可。一个目录的“写”许可是允许在目录内对文件进行增加、移动和改名操作;如果只想允许增加文件,设置下面说明的“sticky”比特位。注意,符号连接的许可值没有用;其对应的目录和连接文件的许可值才是真正有效的。

• “sticky”比特位 -- 在目录被设置之后,只有root、文件所有者或目录所有者才能在该目录下解除连接(删除)和改变文件名。这是一个很常用的Unix扩展,在Open Group的单一Unix规格版本2中有其定义。老版本的Unix把它叫做“保存程序正文”比特位,并用它来标明需要保留在内存的可执行文件。系统这样做确保了只有root可以设置此比特位。(否则用户可以强制“所有内容”都保存在内存中来使系统崩溃)。在Linux下该比特位对普通文件没有影响,而且普通用户可以修改所拥有文件的该比特位;Linux的虚拟内存管理使这种老用途不再重要。

• setuid, setgid -- 在对可执行文件设置后,执行该文件会(相应地)把进程的有效UID或有效GID设为文件拥有UID或GID。所有的类Unix系统都支持这一点。在Linux和System V系统中,在对没有执行权限的文

件设置了setgid时,该文件在存取时将被强制锁定(如果安装的文件系统支持强制锁定的话);这种意义重载使很多人诧异,而且在类Unix系统中并不常见。事实上,如果这样的设置没有意义,在Open Group的单一Unix规格版本2中chmod(3)将允许系统忽略打开不可执行文件setgid的请求。在Linux和Solaris中,对目录设置setgid时,该目录下创建的文件会自动把自己的GID设置为目录的GID。这样做的目的是支持“工程目录”:用户可以把文件保存在如此特别设定的目录下,文件的群组所有者就自动改变。但是,对目录设置setgid比特位没有被单一Unix规格[Open Group 1997]之类的标准所定义。

• 时间戳 -- 保存每个文件系统对象存取和修改的时间。尽管如此,文件系统对象的所有者可以任意设置这些值(参见touch(1)),所以在信任该信息时要小心。所有的类Unix系统都支持这一点。 下面是对ext2文件系统inux独有的属性,虽然很多其它文件系统也有类似的功能:

不可改变的比特位 -- 不允许改变文件系统对象;只有root可以设置或清除该比特位。只有ext2支持它,不能在所有Unix系统中移植(甚至不能在所有Linux文件系统中移植)。

• 只附加比特位 -- 只允许对文件系统对象进行附加操作;只有root可以设置或清除该比特位。只有ext2支持它,不能在所有Unix系统中移植(甚至不能在所有Linux文件系统中移植)。

其它的常见扩展包括表明“不可删除此文件”的某种比特位。

很多这样的值在安装时会受影响,所以,诸如特定的比特位可以被当做具有某个特定值(无论在媒介上它们的值如何)来处理。参见mount(1)以了解更多的有关信息。有些文件系统不支持其中的某些存取控制值;也可参见mount(1)以了解这些文件系统是如何进行处理的。比较特别的是,很多类Unix系统支持MS-DOS磁盘,其缺省状态只支持其中的很少属性(而且没有标准方法来定义这些属性)。在这种情况下,类Unix系统仿效标准属性(可能通过特殊的磁盘文件来实现),而且这些属性一般会受mount(1)命令影响。

需要特别注意,除非类Unix系统支持更为复杂的规范(如POSIX的ACL),对于增加和移动文件,只有许可比特位和文件所在目录的拥有者确实有意义。除非系统有其它的扩展,而标准Linux 2.2没有扩展,如果所在目录允许,没有被许可比特位授予许可的文件还是可以被移开的。同样,如果上级目录允许其子目录被某些用户或群组改变,那么该目录的下级目录就可以被那些用户或群组更换。

IEEE有关安全的POSIX标准草案为支持用户和群组许可列表的真实ACL定义了一项技术。不幸的是这并未得到类Unix系统的广泛支持,而且类Unix系统支持的方式也不完全一样。例如,标准的Linux 2.2在文件系统中既没有ACL,也没有POSIX能力值。

Linux中的ext2文件系统缺省为root用户保留少量空间,这在Linux下算不了什么。这可以部分抵御拒绝服务攻击;即使某个用户占完了与root用户共享的磁盘,root用户还剩下少量空间(如用于关键函数)。该缺省值为文件系统空间的5%;参见mke2fs(8),特别是其中的“-m”选项。

创建时的初始值

在创建时,应用以下规则。在绝大多数Unix系统中,通过creat(2)或open(2)来创建一个新的文件系统对象(FSO)时,FSO的UID被设为进程的EUID,而FSO的GID被设为进程的EGID。Linux由于存在FSUID和setgid目录扩展,与此稍有区别;FSO的UID被设为进程的FSUID,而FSO的GID被设为进程的FSGID;如果所在目录的setgid比特位被设置,或者文件系统的“GRPID”标志被设置,FSO的GID实际上被设为所在目录的GID。这一特性支持“工程”目录:生成一个“工程”目录,为该工程创建一个特殊的群组,为该群组所拥有的工程创建一个目录,然后使该目录成为setgid:放入该目录下的文件自动被该工程所拥有。类似的,如果一个新的子目录创建在设置了setgid比特位(而文件系统的GRPID没有设置)的目录下,新的子目录的setgid比特位也被设置(这样工程的子目录也“工作正常”);在其它情况下,新文件的setgid被清除。FSO的基本存取控制值(读、写、执行)是计算得来的(请求值& ~进程的umask)。新文件开始总是具有一个清除了的sticky比特位和一个清除了的setuid比特位。

改变存取控制属性

可以用chmod(2)、fchmod(2)或chmod(1)来设置大多数这样的值,但请参考chown(1)和chgrp(1)。在Linux下,有些Linux独有的属性用chattr(1)来操作。 注意,在Linux下只有root可以改变一个给定文件的所有者。某些类Unix系统允许普通用户把自己文件的所有关系传递给其他人,但这使情况复杂化,在Linux中是禁止的。例如,如果要限制磁盘的使用,允许这样的操作就允许用户宣称大文件实际上属于某些其他的“受害者”。

使用存取控制属性

在Linux和大多数类Unix系统下,只有在打开文件时检查读和写属性的值;在每次读或写操作时不再检查这些值。因为文件系统是类Unix系统的中心,还有很多调用会检查这些属性。检查这些属性的调用包括open(2)、creat(2)、link(2)、unlink(2)、rename(2)、mknod(2)、symlink(2)和socket(2)。

文件系统分级结构

这些年来一直是在“什么文件放在哪里”的基础之上建立规则的。如果可能,请依照这些规则把信息放进分级结构里。例如,把全局配置信息放入/etc。文件系统分级结构标准(FHS)试图以一种合乎逻辑的方式定义这些规则,而且在Linux上得到了广泛应用。FHS是以前Linux的文件系统结构标准(FSSTND)的更新版本,包含了从Linux、BSD和System V等系统上得到的教训与实践。参见 http://www.pathname.com/fhs 以了解有关FHS的更多信息。有关这些规则的概要在Linux的hier(5)和Solaris的hier(7)中。有时,不同的规则相互冲突;如果可能,使这些情况在编译或安装时可以配置。

System V的IPC

很多类Unix系统,包括Linux和System V系统,支持System V的进程间通信(IPC)对象。实际上,System V的IPC是Open Group的单一Unix规格版本2[Open Group 1997]所要求的。 System V的IPC对象可以是以下三种:System V的消息队列、信号量集和共享内存段。每个这样的对象都有以下属性:

每个创建者、创建者群组和其他人的读和写许可。 • 创建者UID和GID -- 对象创建者的UID和GID。

• 所有者UID和GID -- 对象所有者的UID和GID(初始时等于创建者的UID)。

在存取这样的对象时,规则如下:

如果进程有root权限,同意被存取。

• 如果进程的EUID是对象所有者或创建者的UID,那么检查相应的创建者许可比特位,看看是否允许存取。

• 如果进程的EGID对象所有者或创建者的GID,或者进程所属群组中某个群组的GID就是对象所有者或创建者的GID,那么在存取时检查相应的创建者群组许可比特位。

• 否则,在存取时检查相应的“其他人”许可比特位。

注意,root或具有所有者或创建者EUID的进程可以设置所有者UID和所有者GID,并能删除对象。更多的信息参见ipc(5)。

套接字和网络连接

套接字用于通信,特别是在网络上。套接字起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种同样支持套接字,而且支持套接字是Open Group的单一Unix规格[Open Group 1997]所要求的。System V系统传统上使用一个不同(不兼容)的网络通信接口,但这对像Solaris这样包括对套接字支持的系统来说不值一提。Socket(2)创建一个通信端点并返回一个描述符,与open(2)对文件的操作相似。套接字的参数指定协议族和类型,例如Internet域(TCP/IPv4)、Novell的IPX或“Unix

域”。服务器程序一般调用bind(2)、listen(2)和accept(2)或select(2),客户程序一般调用bind(2)(虽然可能被省略)和connect(2)。参见这些例程相应的man帮助页以了解更多信息。通过相应的man帮助页可能很难理解如何使用套接字;也许需要参考Hall的“Beej”[1999]一类的文献来学习如果共同使用这些调用。

“Unix域套接字”实际上并不代表一个网络协议;它们只能与同一台机器上的套接字相连接。(在为标准Linux内核编写本文的目前情况下)。在被作为流使用时,它们与命名管道非常相似,而优越性很明显。特别是Unix域套接字面向连接;每一个到套接字的新连接都产生一个新的通信管道,这与命名管道完全不同。正是由于这一特性,Unix域套接字经常被用来代替命名管道实现很多重要服务中的IPC。就像可以拥有非命名管道一样,可以用socketpair(2)来得到非命名Unix域套接字;与非命名管道类似,非命名Unix域套接字对于IPC也很有用。 Unix域套接字有几个有趣的安全内涵。首先,虽然Unix域套接字可以出现在文件系统中,而且可以对它们使用stat(2),却不能用open(2)打开它们(只能使用socket(2)和友好接口)。其次,Unix域套接字可以用来在进程间传递文件描述符(而不仅仅是文件的内容)。其它IPC机制都不提供的这一奇特能力被用来破解所有规范(描述符基本上可以用作受限制版本的计算机科学意义上的“能力”)。文件描述符用sendmsg(2)发送,其中msg(消息)的msg_control域指向一个控制消息头的数组(msg_controllen域必须指定数组中所包含的字节数目)。每条控制消息都是一个带有数据的cmsghdr结构,为达到此目的需要把cmsg_type设置为SCM_RIGHTS。文件描述符通过recvmsg(2)获得,然后以相似的方式传递下去。坦白地说,该特性风格有些绮靡,但值得了解。 Linux 2.2支持Unix域套接字的一个附加特性:可以获取对端的“可信任证明”(pid、uid和gid)。下面是一段代码示例:

/* fd= file descriptor of Unix domain socket connected to the client you wish to identify */

struct ucred cr; int cl=sizeof(cr);

if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cr, &cl)==0) { printf(\"Peer's pid=%d, uid=%d, gid=%d\\n\ cr.pid, cr.uid, cr.gid);

标准的Unix约定是需要root权限来绑定小于1024的数字作为TCP和UDP本地端口号,而任何进程都可以绑定一个大于或等于1024的不受约束的端口号。Linux遵循了这一约定,更具体地说,要绑定小于1024的端口号,Linux要求进程具有CAP_NET_BIND_SERVICE能力;一般此能力只有euid为0的进程才拥有。想进一步了解的读者可以查看Linux下相应的源码;在Linux 2.2.12中为文件/usr/src/linux/net/ipv4/af_inet.c中的函数inet_bind()。

信号

信号是类Unix操作系统世界里“中断”的一种简单形式,而且是Unix中古老的一部分。进程可以对另一个进程设置“信号”(即使用kill(1)或kill(2)),这样其它的进程就会异步地接收到信号并进行处理。进程要拥有向某些其它进程发送信号的许可必须具有root权限,或者发送进程的真实或有效用户ID等于接收进程的真实或保存的set-user-ID。

虽然信号是Unix中古老的一部分,但在不同的实现下语义有所不同。从根本上说,它们涉及到“在处理一个信号时出现另一个信号会发生什么?”一类的问题。较早的Linux libc 5对某些信号操作使用一组与新的GNU libc库不同的语义。要了解更多信息,请参考glibc的FAQ(在某些系统中可以在/usr/doc/glibc-*/FAQ找到其本地拷贝)。

对于新程序,应该只使用POSIX信号系统(这依然是基于BSD上的工作);它受到广泛的支持,而且不会有某些早期信号系统具有的问题。POSIX信号系统是基于使用数据类型sigset_t,它可以通过一组操作来处理:sigemptyset()、sigfillset()、sigaddset()、sigdelset()和sigismember()。可以从sigsetops(3)中读到这些信息。然后用sigaction(2)、sigaction(2)、sigprocmask(2)、sigpending(2)和sigsuspend(2)来建立一个处理信号的程序(参见相应的man帮助页以了解更多信息)。

一般应该使信号处理程序很小而且简单,同时小心对待竞争状态。由于信号天生就是异步的,所以很容易导致竞争状态。

对于服务器程序有一个通用约定:如果接收到SIGHUP,应该关闭所有日志文件、重新打开并读入配置文件,然后重新打开日志文件。这样就支持服务器不暂停地完成重新配置,以及无数据丢失的日志轮转。如果要编写的服务器程序可以使用该约定,请支持这一约定。

配额与限制

很多类Unix系统都有支持文件系统配额和进程资源限制的机制。这当然也包括Linux。这些机制在抵御拒绝服务攻击时特别有用;通过限制每个用户的可用资源,可以使单个用户难以耗尽整个系统的资源。但在这里要小心这些术语,因为文件系统配额和进程资源限制都存在“硬”和“软”限制,而其意义有少许差别。 在每个安装点上可以定义存储(文件系统)的配额限制,限制可用的存储块数目或单独文件(inodes)的个数,而且可以对某个给定用户或给定群组设置这样的限制。“硬”配额限制是不可被超过的限制,而“软”配额限制是可以临时超过的。参见quota(1)、quotactl(2)和quotaon(8)。

rlimit机制支持大量的进程配额,如文件大小、子进程数、打开文件数,等等。也存在“软”限制(也叫做当前限制)和“硬”限制(又叫做上限)。软限制任何时刻都不可被超过,但通过调用可以把它增加到硬限制的值。参见getrlimit()、setrlimit()和getrusage()。注意,有多种方法设置这些限制,包括PAM模块pam_limits。

动态连接库

实际上所有程序执行都依赖于库。在包括Linux的大多数现代类Unix系统中,程序缺省使用动态连接库(DLL)进行编译。这样就可以更新某个库,所有使用该库的程序如果可能的话,都将使用新的(希望有所改进的)版本。

动态连接库通常被放在若干特殊目录下。通常这些目录包括/lib、/usr/lib、有关PAM模块的/lib/security、有关X-windows的/usr/X11R6/lib和/usr/local/lib。 对于库的命名和进行库的符号连接有些特殊约定,这样就可以更新库,同时继续支持需要使用不具有反向兼容的老版本库的程序。在执行特定程序时可以覆盖某个指定库,甚至只覆盖某个库里的指定函数。这是类Unix系统相对于类Windows系统的一个实际优点;我相信类Unix系统有一个更好的系统来处理库的更新,这也是Unix和Linux系统被认为比基于Windows的系统更稳定的原因。

在包括所有Linux系统的基于GNU glibc的系统中,程序启动时自动寻找的目录列表存储在文件/etc/ld.so.conf中。很多源于Red Hat的发行版一般在文件/etc/ld.so.conf中不包含/usr/local/lib。我认为这是个Bug,要在源于Red Hat的系统里运行很多程序都需要进行一个通用的“修复”,把/usr/local/lib加入/etc/ld.so.conf。如果只是想覆盖某个库里的若干函数,而想保留该库的其它部分,可以在/etc/ld.so.preload中输入要覆盖的库名(.o文件);这些“预载入”的库会优先于标准库使用。通常这种预载入文件是用于紧急补丁的;发行版在发行时一般不会包含这样的文件。在程序启动时寻找所有这些目录太花时间,所以实际上使用了一个cache管理方法。程序ldconfig(8)缺省读入文件

/etc/ld.so.conf,在动态连接目录里建立相应的符号连接(这样就遵循了标准约定),然后把cache写入/etc/ld.so.cache,这样就可以被其它程序使用了。所以一旦增加一个DLL,或删除一个DLL,或者DLL目录集发生改变,ldconfig就要运行一次;在安装库时,运行ldconfig通常是软件包管理程序需要执行的一个步骤。在启动时,程序使用动态加载程序来读入文件/etc/ld.so.cache,然后载入其所需的库。

各种环境变量可以控制这一过程,而且事实上也有允许覆盖此过程的环境变量(所以可以在某次特别的执行过程中临时替换某个不同的库)。在Linux下,环境变量LD_LIBRARY_PATH是一组用逗号隔开的目录,在查找标准目录集之前先查找这些库;这在调试新库或为特殊目的使用非标准库时很有用。变量LD_PRELOAD列出了覆盖标准集的函数所在的目标文件,就像/etc/ld.so.preload一样。

如果不采取特别的措施,允许用户控制动态连接库会对setuid/setgid程序造成灾难性的后果。因此在实现GNU glibc时,如果是setuid或setgid程序,将忽略这些变量(和其它类似的变量),或者严格限制这些变量所起的作用。GNU的glibc库通过检查程序的证明来确定其是否为setuid或setgid程序;如果uid和euid不同,或者gid和egid不同,则库就假设该程序为setuid/setgid程序(或者为其子程序),然后严格限制它控制连接的能力。如果载入GNU的glibc库,就可以看到这种情况;请特别阅读一下文件elf/rtld.c和

sysdeps/generic/dl-sysdep.c。这就意味着如果使uid和gid等于euid和egid,再调用程序,这些变量就具有完全的效力。其它类Unix系统处理这些情况有所不同,但原因相同:一个setuid/setgid程序不应受到环境变量集的过分影响。

审核

不同的类Unix系统处理审核的方式不同。在Linux下最普遍的“审核”机制是syslogd(8),通常与klogd(8)结合使用。可能还可以看看wtmp(5)、utmp(5)、lastlog(8)和acct(2)。某些服务器程序(如Apache的web服务器)还有自己的检查跟踪机制。依照FHS,审核日志文件应该存储在/var/log或其子目录下。

PAM

Sun Solaris和几乎所有Linux系统使用可插入确认模块(PAM)系统进行确认。PAM允许确认方法(例如,密码的使用、智能卡,等等)的运行期间配置。在本文中还会进一步讨论PAM。

Chapter 4. 证实所有的输入

Table of Contents 命令行 环境变量 文件描述符 文件内容 CGI输入 其它输入 字符编码

限制合法的输入时间和负载水平

Wisdom will save you from the ways of wicked men, from men whose words are perverse...

Proverbs 2:12 (NIV)

某些输入来自不可信的用户,所以在使用前那些输入必须被证实(过滤)。你可以决定什么是合法的,并拒绝不符合定义的东西。千万不要反其道而行(标明哪些是非法的并拒绝它们),因为很有可能忘记处理某个重要的情况。限制最大字符长度(如果合适的话,还有最小的长度),确定超过该长度时不会失去控制(参见缓存溢出一节以了解更多内容)。

对于字符串,识别合法字符或合法模式(如正规表达式),并拒绝不匹配的东西。当字符串中包含控制字符(特别是换行符或NIL)或shell转义字符时,会有些特殊问题;最好在接受输入过程中遇到转义字符时直接“忽略”这些字符,这样这些字符被不会被意外地发送出去。CERT在处理这种情况时更保守一些,他们推荐忽略所有不属于非忽略字符列表的字符[CERT 1998, CMU 1998],参见下面“限制呼出为合法值”一节以了解更多信息。

限制数字在最小(通常为0)和最大允许值之间。文件名需要检查;通常不把“..”(上一级目录)作为合法值。对文件名而言,最好禁止改变目录,比如不把“/”包含在合法字符集中。完全的email地址检查实际上非常复杂,因为存在大量已有格式,如果要完全支持它们,合法性检查非常复杂;如果必须进行这样的检查,请参见mailaddr(7)和IETF RFC 822 [RFC 822]以了解更多信息。 这些测试一般应集中放在一起,便于今后检验合法性测试的正确性。

要保证合法性测试确实是正确的;特别是在检查的输入会被另一个程序使用的情况下(如文件名、email地址或URL)。这些测试经常出现细微的错误,产生所谓的“代理问题”(检查程序与实际使用数据的程序所用的假设有差异)。 在解析用户输入时,临时放弃所有特权是个好主意。特别是在解析工作复杂(比如使用类lex或类yacc工具),或者编程语言无法防御缓存溢出(如C和C++)的情况下。参见下面最小化许可一节。

下面的几个小节讨论程序的不同类型的输入;注意,输入包括进程状态,如环境变量、umask值,等等。不是所有的输入都在非可信用户的控制之下,所以只需要关注那些受非可信用户控制的输入。

命令行

很多程序使用命令行作为输入接口,通过传递参数接收输入。setuid/setgid程序的命令行接口是由某个非可信用户提供的,所以必须保护好自己。用户对命令行有很大的控制权(通过调用诸如execve(3)之类的调用)。因此,setuid/setgid程序必须证实命令行的输入,不可相信命令行参数0(用户可以把它设为包括NULL的任意值)报告的程序名称。

环境变量

缺省情况下,环境变量从进程的父进程继承而来。但是,在程序执行另一个程序时,调用程序可以把环境变量设置为任意值。这对setuid/setgid程序而言很危险,因为其入侵者可以完全控制它们得到的环境变量。由于环境变量一般是继承来的,同样可以传递使用;安全程序可能调用某些其它程序,在没有特殊措施的情况下,这会把有潜在危险的环境变量值传递给调用的程序。

有些环境变量是危险的

有些环境变量是危险的,因为很多库和程序被环境变量以某些隐含、模糊或未公开的方式所控制。例如,sh和bash shell使用IFS变量来决定哪个字符被用来分隔命令行参数。由于shell是被若干底层调用(如C中的system(3)和popen(3),或Perl中的back-tick算符)执行的,把IFS设置为不寻常的值就会搅乱那些看起来安全的调用。该行为在bash和sh里有说明,但不引人注目;许多长时间的用户知道IFS,只不过是因为了解IFS可用来破坏安全性,而不是因为有意经常使用的缘故。更糟的是,不是所有的环境变量都有文档说明,而且即使有,其它的程序也可以改变和增加危险的环境变量。所以唯一的真正解决方案(下文有描述)是只选择所需要的环境变量,不理会其余的环境变量。

环境变量的存储格式是危险的

一般来说,程序应该使用标准的访问例程来访问环境变量。例如,在C里应使用getenv(3)获取环境变量的值,使用POSIX标准例程putenv(3)或BSD扩展setenv(3)来设置环境变量的值,使用unsetenv(3)来清除环境变量。需要说明的是,Linux下也实现了setenv(3)。但黑客不会这么善良;黑客可以用execve(2)直接控制传递给程序的环境变量数据区。这就可能进行一些肮脏的攻击,只有那些了解环境变量工作实质的人才能理解这些攻击。在Linux下可以阅读

environ(5)来了解环境变量工作实质的概要。简而言之,环境变量在内部作为一

个指向字符的指针数组的指针来存储;该数组按顺序存储并以NULL指针结尾(这样就可以知道何时数组结束)。指向字符的指针每个都依次指向一个形式为“NAME=value”的以NIL结尾的字符串值。这包含若干意义,例如,环境变量名不能包含等号,而且name或value都不能含有NIL字符。但是,这种格式有一个很危险的含义,就是允许多个入口使用同一个变量名而值不同(如SHELL有多个值)。虽然典型的命令shell禁止这么做,本地操作的黑客可以使用execve(2)制造出这样的情况来。

这种存储格式(以及设置方式)的问题在于程序可能会检查某个值(看看是否合法)而实际上使用的却是另一个不同的值。在Linux下,GNU的glibc库试图保护程序免受此影响:在实现glibc 2.1的getenv时,总是获取第一个匹配的入口,setenv和putenv总是设置第一个匹配的入口,而unsetenv实际上会清除所有匹配入口的设置(应该祝贺GNU的glibc实现者如此实现unsetenv!)。但是,有些程序直接访问环境变量,重复遍历所有环境变量;在这种情况下,它们可能会使用最后一个匹配的入口,而不是第一个。其结果就是如果检查的是第一个匹配的入口,但实际使用的是最后一个匹配的入口,黑客就可以以此来绕过保护例程。

解决方案 -- 提取和清除

对于安全的setuid/setgid程序,应该小心提取需要作为输入(如果需要的话)的简短的环境变量列表。然后应该清除整个环境,再重新设置一小组必需的环境变量作为安全的值。如果调用了下一级的程序,这实际上并不是什么更好的办法;因为没有可行的办法来列出“所有的危险值”。即使对直接或间接调用的每一个程序的源码都进行了仔细检阅,还是有人可以在你编写完代码后加入新的未公开的环境变量,其中就可能有一个可利用的环境变量。

清除环境的简单方式是把全局变量environ设置为NULL。全局变量environ在中定义,C/C++用户需要#include该头文件。在产生线程前需要处理该值,但这几乎不成问题,因为在程序执行的开始阶段就需要进行这些处理。另一个清除环境的方式是使用未公开的函数clearenv()。clearenv()有个奇怪的历史;有人建议在POSIX.1中定义它,但不知什么原因它没有进入标准。尽管如此,POSIX.9(绑定POSIX的Fortran 77)中定义了clearenv(),所以它具有了半官方的地位。clearenv()定义在,但在使用#include包含它之前,必须确定__USE_MISC已经#defined。

一个几乎可以确定会不断添加的值是PATH,一个查找程序的目录列表;PATH应该不包括当前目录,一般应该像“/bin:/usr/bin”那样简单。一般还会设置IFS(其缺省值为“ \\\n”)和TZ(时区)。如果不提供IFS或TZ,Linux也不会死机,但没有TZ值时有些基于System V的系统会出问题,而且据传言某些shell需要IFS值被设置。在Linux下,参见environ(5)以了解可能需要设置的通用环境变量列表。

如果确实需要用户提供的值,首先要检查这些值(以保证这些值与合法值的模式相匹配,而且在某些合理的最大长度之内)。理想情况是在/etc下有些标准的可信赖文件,包含“标准的安全环境变量值”的信息,但是现在没有为此目的定义的标准文件。与此相似,可能需要在那些具有PAM模块的系统里检查PAM模块的pam_env。

如果采用不允许直接重新设置环境的语言编写setuid/setgid程序,一个方法是建立一个“包裹”程序。包裹程序把环境程序设置为安全值,然后调用其它程序。注意:确定包裹程序会实际执行预期的程序;如果它是个解释程序,要确定不会出现可能的竞争状态,使得解释器能够载入另一个与授予了特殊的setuid/setgid权限的程序不同的程序。

文件描述符

一组“打开文件描述符”,即预先打开的文件,会传递给程序。setuid/setgid程序必须处理这样的情况,即用户开始选择哪些文件是打开的,以及打开到哪里(在他们的许可限制内)。setuid/setgid程序不应该假设打开某个新文件会总是打开到某个固定的文件描述符ID上。不应假设标准输入(stdin)、标准输出(stdout)和标准错误(stderr)总是指向某个终端或者总是打开的。

这样做的理由很简单;因为攻击者可以在启动程序之前打开或关闭某个文件的描述符,从而制造出意料之外的情况。如果攻击者关闭了标准输出,在程序打开下一个文件时,该文件会被当做标准输出打开,所有的标准输出都会被传给它。某些C库在stdin、stdout和stderr未被打开时会自动把它们打开(指向/dev/null),但并非对所有类Unix系统都是这样。

文件内容

如果程序从某个给定文件中获取指令,不应给予它特别的信任,除非只有某个可信的用户才能控制其内容。一般这就意味着非可信用户无法修改文件、文件的目录及其上级目录。否则,必须按可疑的情况对待该文件。

如果文件里的指令可能来自于非可信用户,那么要确定从该文件来的输入受到了本文所描述的防护。特别是要检查这些值与合法值的集合匹配,而且缓存没有溢出。

CGI输入

CGI输入其实是指定的一组环境变量和标准输入。这些值必须被证实。 一个额外的复杂情况是很多CGI输入采用所谓的“URL编码”格式提供,即某些值是以%HH的格式编写的,其中HH是相应字节的十六进制代码。你或着你的

CGI库必须正确处理这些输入,先URL解码输入,再检查作为结果的字节值是否可以接受。必须正确处理所有的值,包括可能有问题的值,如%00(NIL)和%0A(换行)。不要对输入多次解码,否则诸如“%2500”的输入会被错误处理(%25会翻译成“%”,结果“%00”又会被错误地翻译成NIL字符)。 CGI脚本一般会受到包含特殊字符输入的攻击;参见上面的说明。

某些HTML表格包括客户端的检查,以去除某些非法值。这种检查对用户可能有些帮助,但对安全性毫无用处,因为攻击者可以直接向WEB服务器发送这样的“非法”字符。如下文说到的那样(在“只信任值得信任的通道”一节),服务器必须对所有输入进行自己的检查。

其它输入

程序必须确保所有输入都受到控制;这对于setuid/setgid程序特别困难,因为它们的此类输入太多。程序必须考虑的其它输入包括当前目录、信号、内存映像(mmaps)、System V的IPC和umask(决定新创建文件的缺省许可)。应该考虑在程序启动时把目录明确改到某个恰当的名称完整的目录(使用chdir(2))。

字符编码

多年来美国人一直使用ASCII对字符编码,可以方便地交换英语文本。但不幸的是,ASCII完全不足以处理大多数其它语言的字符集。多年来不同的国家采用不同的技术交换不同语言的文本。最近,ISO推出了ISO 10646,用于表示世界上所有字符的单一31比特编码方案,被称为通用字符集(UCS)。可用16比特(UCS的前65536个字符)表示的字符被称为“基本多语言平台”(BMP),而且BMP意图覆盖所有口头语言。Unicode论坛推出了Unicode标准,注重于16比特字符集,并为了有助于互操作性增加了一些附加的约定。

尽管如此,大多数软件不是为处理16比特或32比特字符设计的,所以开发出一种叫做“UTF-8”的特殊格式来编码这些潜在的国际字符,现有的程序和库更容易处理这种格式。在IETF RFC 2279和其它一些地方有UTF-8的定义,所以它是一种可以自由阅读和使用的定义完好的标准。UTF-8是可变长度编码;从0到0x7f (127)的字符作为单个字节被编码为自身,更大值的字符则被编码为2到6个字节信息(依赖于具体值)。编码被特别设计以具有以下良好特性(取自RFC和Linux中utf-8的man帮助页):

标准的US ASCII字符(0--0x7f)编码为自身,这样只包含7比特ASCII字符的文件和字符串在ASCII和UTF-8编码下是相同的。这对于许多已有美国程序和数据文件的反向兼容性而言是太好了。

• 所有大于0x7f的UCS字符被编码为仅由0x80到0xfd范围内的字节组成的多字节序列。这就意味着ASCII字节不会变成另一个字符的一部分。许多其它的编码方法允许嵌入NIL的字符,会导致程序失效。

在UTF-8和2字节或4字节固定长度的字符表示(分别被称为UCS-2和UCS-4)之间进行转换很容易。

• UCS-4字符串保持了按字典分类的顺序,可以直接对UTF-8数据应用Boyer-Moore快速查找算法。

• 所有可能的2^31个UCS代码都可以用UTF-8进行编码。

• 表示单个非ASCII的UCS字符的多字节序列的第一个字节总是在0xc0到0xfd范围内,表示该多字节序列有多长。多字节序列的所有其它字节都在0x80到0xbf范围内。这就可以很容易地实行重新同步;如果丢失了某个字节,可以很容易地跳到“下一个”字符,而且总是可以很容易地跳到“前一个”或“后一个”字符。

简而言之,UTF-8转换格式正在成为交换国际文本信息的主要方法,因为它可以支持世界上的所有语言,而且还与美国的ASCII文件反向兼容,并具有其它一些良好特性。出于诸多理由,我推荐使用这种编码,特别是在把数据保存到“文本”文件时。

提及UTF-8的原因在于某些字节序列不是合法的UTF-8,而且可能成为可利用的安全漏洞。RFC提到了以下内容:

UTF-8的实现者需要考虑如何处理非法的UTF-8序列的安全方面的问题。可以想象,在某些情况下黑客可以通过发送一个UTF-8语法不允许的八进制序列,来利用一个不谨慎的UTF-8解释器。

对于执行对UTF-8编码形式的输入进行关乎安全的合法性检查的解释器,可以采用一个方式特别微妙的攻击,使得特定的非法八进制序列被解释为字符。例如,某个解释器可能禁止编码为单个八进制序列00的NUL字符,但允许非法的两个八进制序列C0 80,把它解释为一个NUL字符。另一个例子就是可能会有个解释器禁止八进制序列2F 2E 2E 2F(“/../”),但允许非法的八进制序列2F C0 AE 2E 2F。

有关此问题的较为充分的讨论可以在

http://www.cl.cam.ac.uk/~mgk25/unicode.html 上的Markus Kuhn的 UTF-8 and Unicode FAQ for Unix/Linux 里看到。

UTF-8字符集是可以列出所有非法值(并证明已经全部列出)的一种情况。如果要确定是否得到一个合法的UTF-8序列,需要检查两件事:(1)初始序列是否合法,(2)如果合法,是否第一个字节后面跟随了所要求的合法后续字符数目?进行第一项检查很简单,可以证明下面是所有非法UTF-8初始序列的完全列表: Table 4-1. 非法UTF-8初始序列

UTF-8序列 10xxxxxx 1100000x

非法的原因

作为字符的起始字节非法(80..BF) 非法,过长(C0 80..BF)

UTF-8序列 11100000 100xxxxx 11110000 1000xxxx 11111000 10000xxx 11111100 100000xx 1111111x

非法的原因

非法,过长(E0 80..9F) 非法,过长(F0 80..8F) 非法,过长(F8 80..87) 非法,过长(FC 80..83) 非法;为规格说明所禁止

需要说明的是在某些情况下,可能会要松散地打断(或内部使用)十六进制序列C0 80。这是个可以代表ASCII NUL(NIL)的过长序列。由于C/C++在把NIL字符包含在普通字符串里会出问题,有些人在想要把NIL作为数据流的一部分时就使用该序列;Java甚至把这种方法奉为经典。在处理数据时可以自由地在内部使用C0 80,但从技术角度来说,在保存数据时应该把它转换回00。根据实际需要,可以自行决定是否“马虎”一下,把C0 80作为UTF-8数据流的输入。 第二个步骤是检查正确的后续字符数目是否包含在字符串中。如果第一个字节的头两个比特被置位了,那么数一下头一个比特位后被置位比特的个数,然后再检查是否有那么多以比特“10”开头的后续字节。因此,二进制数11100001就要求两个以上的后续字节。

与此有关的一个问题是在ISO 10646/Unicode中某些语句可以用多种方式表示。例如,有些带着重号的字符可以用单个字符(带着重号)表示,也可以用一组字符(如基本字符加上单独排版的着重号)来表示。这两种形式可能看起来完全一样。也可以在其中插入一个零宽度的空格,使看起来相似的二者有所区别。有些情况下要小心这样的隐藏文本会干扰程序。

限制合法的输入时间和负载水平

设置超时和负载水平限制,特别是对于接收的网络数据。否则,攻击者可能通过不断地请求服务很容易地导致拒绝服务。

Chapter 5. 避免缓存溢出

Table of Contents C/C++中的危险

C/C++中库的解决方案 C/C++的编译解决方案 其它语言

An enemy will overrun the land; he will pull down your strongholds and plunder your fortresses.

Amos 3:11 (NIV)

一个非常普遍的安全性缺陷是“缓存溢出”。从技术上来说,缓存溢出是程序内部实现的问题,但它是个非常普遍而且严重的问题,所以把它放在单独的一节里进行说明。在CERT,1998年13篇报告中的9篇和至少1999年一半的报告都与缓存溢出有关,这大概可以加深你对该问题重要性的认识。Bugtraq的一项非正式调查发现大约2/3的响应认为缓存溢出是安全性薄弱环节的首要因素(其余的响应认为“配置错误”是首要原因)[Cowan 1999]。这是一个古老而且众所周知的问题,但它还是不断地重复出现[McGraw 2000]。

如果把一组值(通常是一个字符串)写入某个固定长度的缓存区并越过缓存边界(一般是越过缓存的结尾)持续写入至少一个值时,缓存溢出就发生了。缓存溢出在从用户那里读入输入放进缓存时会发生,但也会在程序的其它处理过程中发生。

如果某个安全程序允许缓存溢出,它就经常会被对手利用。如果该缓存为局部C变量,溢出就可以被用来强迫函数运行攻击者选择的代码。这种特殊的攻击手段被称为“堆栈冲击”攻击。放置在堆中的缓存也好不到哪里去;攻击者可以用这样的溢出来控制程序里的变量。更多的细节可以从Aleph1 [1996]、Mudge [1995]或 http://destroy.net/machines/security/ 上Nathan P. Smith的“Stack Smashing Security Vulnerabilities”WEB站点上找到。

大多数编程语言从根本上避免了这个问题,或者是因为它们自动地重新设置数组大小(如Perl),或者是因为它们一般检测并防止缓存溢出(如Ada95)。但是,C语言根本没有提供对此问题的保护,C++在使用时也很容易导致此问题。

C/C++中的危险

C用户必须避免使用不检查边界的危险函数,除非它们能确保边界不会被超过。在大多数情况下应避免的函数包括strcpy(3)、strcat(3)、sprintf(3)(以及相近的vsprintf(3))和gets(3)函数。它们应该被相应的诸如strncpy(3)、strncat(3)、snprintf(3)和fgets(3)函数所替代,但请阅读下面的讨论。函数strlen(3)应该被避免,除非能确定可以找到一个作为终止的NIL字符。scanf()函数族(scanf(3)、

fscanf(3)、sscanf(3)、vscanf(3)、vsscanf(3)和vfscanf(3))在应用时经常是危险的;不要在没有控制最大长度(格式%s是个特别普遍的问题)的情况下使用它来向某个字符串发送数据。其它可能会允许缓存溢出(与应用有关)的危险函数包括realpath(3)、getopt(3)、getpass(3)、streadd(3)、strecpy(3)和strtrns(3)。必须小心getwd(3);发送给getwd(3)的缓存必须至少有PATH_MAX个字节长。如果你很重视可移植性问题,那么还有一个额外的问题:某些系统上的snprintf对缓存溢出并无实际的保护;据我所知,Linux下的版本是正确工作的。 [vf]scanf(const char *format, ...) 参数可能溢出。 realpath(char *path, char resolved_path[]) 路径缓存可能溢出。 [v]sprintf(char *str, const char *format, ...) str缓存可能溢出。

C/C++中库的解决方案

C/C++中的一个解决方案是使用没有缓存溢出问题的库函数。第一小节描述了“标准C库”的解决方案,可以解决问题但存在不足之处。接下来描述了缓存的固定长度和动态重新分配方案的一般性安全问题。最后一个小节描述了各种替代库,如strlcpy和libmib。

标准C库的解决方案

C中防止缓存溢出的“标准”解决方案是使用可以防御这些问题的标准C库调用。该方案严重依赖于标准库函数strncpy(3)和strncat(3)。如果选择了该方案,要小心:这些调用的语义有些出人意料,难以正确使用。如果源字符串长度至少等于目标字符串的长度,函数strncpy(3)就不会用NIL来终止目标字符串,所以要在调用strncpy(3)之后要确定目标字符串的最后一个字符被设置为NIL。如果要多次重复使用同一个缓存,一个有效的方法是告诉strncpy()该缓存比其实际长度短一个字符,并在使用之前把最后一个字符设置为NIL。strncpy(3)和

strncat(3)都要求传递可用的剩余空间大小,这是容易出错的一个计算(计算错误会允许缓存溢出攻击)。这二者都没有提供一个简单的机制来确定是否发生了溢出。最后,与要替代的strcpy(3)相比,strncpy(3)还会带来相当大的性能下降,因为strncpy(3)要用NIL填满目标的剩余空间。我收到一些email,对最后这一点表示惊奇,但这在Kernighan和Ritchie第二版[Kernighan 1988, page 249]中明确说明过,而且在Linux、FreeBSD和Solaris的man帮助页上对此行为也有明确说明。这就意味着仅仅把strcpy换成strncpy可以导致严重的性能降低,在大多数情况下没有什么很好的理由。

静态和动态分配缓存

strncpy及其友好函数是静态分配缓存的一个例子,也就是说,一旦缓存被分配,其大小就是固定的。其替代方法就是在需要的时候动态重新分配缓存大小。而结果表明两种方案都有安全性隐患。

在使用固定长度缓存有个一般性的安全问题:缓存长度固定这一事实可能被利用。这是与strncpy(3)和strncat(3)、snprintf(3)、strlcpy(3)、strlcat(3)以及其它此类函数有关的问题。其基本思想是攻击者建立一个确实很长的字符串,这样在字符串被截断时,其最终结果就正好是攻击者所想要的(而不是开发者所预想的)。可能字符串是由若干小片段连接起来的;攻击者可以使第一段就跟整个缓存一样长,这样后面的字符串连接就都没有用了。下面是几个特别的例子: 假设代码调用gethostbyname(3),如果成功,立即用strncpy或snprintf把hostent->h_name复制到一个固定长度的缓存里。用strncpy或

snprintf可以防止超长的完全合格的域名(FQDN)造成的溢出,这样你可能以为成功了。但是这样可能会切断FQDN的尾部。按照下面可能发生的情况,这也许会是件很不情愿发生的事。

• 假设代码使用strncpy、strncat、snprintf等等来把一个文件系统对象的完整路径复制到某些缓存。进一步假设其初始值是由一个非可信用户提供的,而复制是传递某个计算结果到一个函数的过程的一部分。听起来是安全的,对吗?现在假设攻击者用很多“/”填充在路径的开头。这样就使得后面的操作是针对文件“/”了。如果程序是建立在相信结果安全的话,该 程序就可能被利用。或者,攻击者可以设计一个接近缓存长度的长文件名,这样试图对文件名进行附加的操作就会静悄悄地失败(或者只能部分实现,从而可以被利用)。

在使用静态分配的缓存时,确实需要考虑源与目标参数的长度。清醒地检查输入和中间计算结果也可以处理这个问题。

另一个替代方案是动态地重新分配所有字符串内存,而不是使用固定大小的缓存。GNU编程指南推荐该通用方案,因为它允许程序处理任意大小的输入(除非内存耗尽)。当然,动态分配字符串内存的主要问题是可能耗尽内存。内存甚至可能在担心缓存溢出之外的程序其它部分就被耗尽;任何内存分配都可能失败。同样,由于动态重新分配内存会导致内存分配效率不高,即使从技术上来说还有足够的虚拟内存可供程序继续运行,可能内存就被完全耗尽了。此外,在耗尽内存前,程序可能使用了大量的虚拟内存;这很容易产生“thrashing”的结果,即计算机花费所有时间在磁盘与内存之间来回移动信息(而不是做些有用的工作)的情况。这就跟拒绝服务攻击的效果一样了。对输入大小有些合理的限制就可以改善这种情况。一般来说,如果采用重新分配字符串内存的话,程序应该设计成在内存耗尽时可以安全失败。

strlcpy和strlcat

OpenBSD采用的一个替代方案是使用Miller和de Raadt [Miller 1999]编写的strlcpy(3)和strlcat(3)函数。这是使用一个不同(而且不易出错)的接口来提供C字符串拷贝和连接的最低限度静态大小缓存的方案。可以在一个新的BSD风格的公开源码许可证下从

ftp://ftp.openbsd.org/pub/OpenBSD/src/lib/libc/string/strlcpy.3 获得这些函数的源码及文档。

首先,是它们的原型:

size_t strlcpy (char *dst, const char *src, size_t size); size_t strlcat (char *dst, const char *src, size_t size);

strlcpy和strlcat二者都把目标缓存的整个大小作为参数(而不是要复制的字符最大数目),并保证结果以NIL终止(只要其大小大于0)。记住,应该把NIL的一个字节包括在大小里。

strlcpy函数从NUL结尾的字符串src复制size-1个字符到dst,用NIL作为结果的结尾。strlcat函数把NIL结尾的字符串src附加到dst的末尾。最多附加size - strlen(dst) - 1个字符,并用NIL作为结果的结尾。

strlcpy(3)和strlcat(3)的一个小的不足之处在于它们不是大多数类Unix系统缺省安装的。在OpenBSD里,它们是的一部分。这并不是个难题;因为它们只是小函数,甚至可以在自己程序的源码里包含它们(至少作为一个选项),并创建一个单独的小包来载入它们。甚至还可以用autoconf来自动处理这一问题。如果有更多的程序使用这些函数,那么在不远的将来它们就会成为Linux发行版和其它类Unix系统的标准部分。

libmib

自动地动态重新分配字符串内存的一个C工具集是

http://www.mibsoftware.com/libmib/astring 上Forrest J. Cavalier III提供的“libmib分配字符串内存函数”。libmib有两个变种;“libmib-open”采用允许修改和重新发布的自己的类X11许可证,看起来明显是公开源码,但重新发布必须选择一个不同的名称,尽管如此,开发者声称它“可能尚未完全测试。”要不断地获得libmib-mature,必须缴纳订阅费。其文档不是公开源码,但可以免费获取。

Libsafe

Arash Baratloo、Timothy Tsai和Navjot Singh(朗讯技术公司)开发出Libsafe,封装了若干已知的易受堆栈冲击方法攻击的库函数。这一封装(称为一种“中间件”)是包含了诸如strcpy(3)一类C库函数修改版本的一个简单的动态载入库。这些修改后的版本实现了原有功能,但在某种程度上可以确保任一缓存溢出都被控制在现有堆栈帧之内。它们的原始性能分析显示这个库的负载很小。Libsafe的有关文章和源码放在 http://www.bell-labs.com/org/11356/libsafe.html 上。Libsafe的源码可在完全开放源码的LGPL许可证下获得,而且有迹象表明许多Linux发行商有兴趣使用它。

Libsafe的方案看起来有些用。Linux发行商肯定应该考虑包含Libsafe,它的方案也值得其他人考虑。尽管如此,作为软件开发者,Libsafe只是支持深入防御

的有用机制,而不能真正防止缓存溢出。下面列出了几个理由说明为何不应该在代码开发中仅仅依靠Libsafe。

• • •

Libsafe只保护一小组已知有显著缓存溢出问题的函数。在编写本文时,它要比本文列出的已知有缓存溢出问题的函数少得多。同时,它也不能保护自己编写的会导致缓存溢出的代码(例如在一个while循环里)。 即使在某个发行版里安装了libsafe,它安装的方法也会影响其使用。其文档推荐设置LD_PRELOAD来启用libsafe的保护。但是问题在于用户可以不设置该环境变量......使得对于他们所执行的程序该保护被禁用! Libsafe只保护在返回地址时堆栈的缓存溢出,还是可以在过程帧中溢出堆或其它变量。

除非可以确信所有应用平台都使用了libsafe(或类似的东西),否则应该在保护程序时当它不存在。

LibSafe看来是假设保存的帧指针是在每个堆栈帧的开头。这并非一直正确的。编译器(如gcc)可以优化掉一些东西,特别是选项

“-fomit-frame-pointer”删除了libsafe似乎会需要的信息。因此,libsafe可能对某些程序不起作用。

libsafe的开发者自己也承认软件开发者不应该仅仅依靠libsafe。用他们的话来说:

通常公认的解决缓存溢出攻击的最佳方法是修补有缺陷的程序。但是,修补有缺陷的程序需要知道哪个特殊程序是有缺陷的。使用libsafe和其它替代安全措施的真正收益在于保护尚未获知是否易受攻击的程序避免未来的攻击。

其它库

glib库(而非glibc库)是为C程序员提供了许多有用函数的广泛应用的开放源码库。例如GTK+和GNOME都使用了glib。我希望glib v2.0可以包含strlcpy()和strlcat()(我提交了一个补丁来完成这一点),使得这些函数可以容易地移植应用。目前我还没有一个能够显示glib库函数可以防止缓存溢出的决定性分析。尽管如此,许多glib函数自动地分配内存,而且自动地不以正常方式失败来截断失效(例如试图用别的什么来代替)。因此在许多情况下,大多数glib函数不能在大多数安全程序里使用。

C/C++的编译解决方案

有个完全不同的方案是使用执行边界检查的编译方法(参见[Sitaker 1999]中所列的方法)。在我看来,这样的工具在进行多层次防范时很有用,但把该技术作为唯一的防范手段就不是很明智。至少有两个理由这样讲。首先,大多数这样的工具都只对缓存溢出提供部分保护(“完全的”防范一般会慢12-30倍);C和C++根本就不是为防止缓存溢出而设计的。其次,对于开放源代码程序,人们无

法确定将使用什么工具来编译程序;对于某个给定系统使用缺省的“常用”编译器可能会突然打开安全漏洞。

一个更有用的工具是“StackGuard”,标准GNU的C编译器gcc的一个修改版。StackGuard通过在返回地址前插入一个“守卫”值(称作“canary”)起作用;如果缓存溢出改写了返回地址,canary的值(很可能)改变了,系统在使用地址前会察觉出来。这很有价值,但要注意这并没有防止缓存溢出改变其它的值(而这还是可以被用来对系统进行攻击)。有个叫“PointGuard”的工具把StackGuard扩展为可以在其它数据项前增加canary,PointGuard会自动保护特定的值(如函数指针和远程跳转缓存)。但是,使用PointGuard来保护其它变量类型要求程序员明确地介入(程序员必须指定哪一个数据值需要用canary保护)。这可能有价值,但容易意外地忘记保护一个被认为不需要保护的数据值 -- 但它实际上是需要进行保护的。更多有关StackGuard、PointGuard和其它替代方案可参见Cowan [1999]。

与之相关,可以修改Linux内核,使堆栈段不可执行;这样的Linux补丁已经有了(参见包含此部分的Solar Designer的补丁

http://www.openwall.com/linux/)。但是,这种做法不是建立在Linux内核里的。一部分理由是因为这种保护并不像看起来那样完善;攻击者可以简单地迫使系统调用其它已经在程序中的某些“有趣”的位置(如库、堆或静态数据段)。同样,有时Linux确实需要堆栈中的可执行代码,比如用来实现信号和用来实现GCC的“trampolines”。Solar Designer的补丁确实可以处理这些问题,但这使补丁变得复杂。就个人而言,我希望它被结合进主要的Linux发行版中,因为它确实使攻击变得更困难,而且可抵御一部分现有的攻击。尽管如此,我同意Linus Torvalds和其他人的观点,即它并没有增加如显示的那样多的保护,而且可以被相对容易地绕过。可以看一下Linus Torvalds对不包括该支持的解释 http://lwn.net/980806/a/linus-noexec.html。

简而言之,最好是先开发自己能抵御缓存溢出的正确程序。然后,再使用诸如StackGuard等技术和工具来作为额外的安全网络。如果在代码本身下了工夫以消除缓存溢出,那么StackGuard就会更为有效,因为需要调用StackGuard进行保护的“盔甲上的缝隙”会少很多。

其它语言

缓存溢出问题是使用很多其它诸如Perl、Python、Java和Ada95语言的一个很好的理由。说到底,几乎现在使用的所有其它编程语言(除了汇编语言)都可以防范缓存溢出。当然,使用其它语言并不能消除所有问题;特别是考虑到“限制呼出为合法值”一节中有关NIL字符的讨论。还有确保其它语言的基础结构(如运行库)可用而且安全的问题。尽管如此,在开发防范缓存溢出的安全程序时,还是肯定应该考虑使用其它编程语言的。

Chapter 6. 程序内部结构与解决方案

Table of Contents 保证接口的安全 特权最小化

避免创建Setuid/Setgid脚本 安全地配置并使用安全的缺省值 安全地失败 避免竞争状态

只信任值得信任的通道 使用内部一致性检查代码 自我限制资源

Like a city whose walls are broken down is a man who lacks self-control.

Proverbs 25:28 (NIV)

保证接口的安全

接口必须小(尽可能地简单)、窄(只提供必需的函数)而且不可绕过。信任必须最小化。应用程序和数据浏览器可能被用来显示外部开发的文件,所以一般不允许它们接受程序,除非你愿意完成大量的创建安全沙箱所必需的工作。载入应用程序时就运行的一个自动执行的宏是最危险的;从安全的观点来看,除非你对宏可以做什么有极强的控制(一个“沙箱”),这会是一个等待发生的灾难,而且过去的经验表明,真正的沙箱是很难实现的。

特权最小化

正如前文所说的那样,使程序只拥有完成任务所必需的最少量特权(被称为“极小特权”)是一个重要的通用原则。这样,如果程序被破坏,危害也是有限的。最极端的例子是干脆不写安全程序 -- 如果可以的话,通常应该这样做。例如,如果可以,不要让程序setuid或setgid;只让它作为一个普通程序,要求系统管理员在运行该程序前登录执行。

在Linux和Unix下,确定进程特权的主要是一组与之相关的ID:每个进程都有用户和群组的真实、有效和保存ID。Linux还有文件系统uid和gid。处理这些值是使特权最小化的关键,而且有若干种方法使它们最小化(在下面讨论)。还可以使用chroot(2)最小化文件对程序的可视性。

最小化授予的特权

可能最有效的技术就是简单地最小化授予的最高特权。特别是在可能的情况下,避免授予程序root特权。如果某程序只需要存取一小组文件,不要使它setuid root;考虑为不同的函数分别创建用户或群组帐户。

一个常用技术是创建一个特殊的群组,把文件的群组所有权改为该群组,然后使程序setgid到该群组。最好在可能的情况下尽量使程序setgid,而不是setuid,因为群组成员获得的权限较少(特别是它不会具有改变文件许可的权利)。 通常这被用于获取游戏的高分。游戏通常是setgid games,成绩文件属于群组games,而程序本身及其配置文件为其他用户(如root)所有。这样,破坏一个游戏就允许犯罪者改变高分,但没有给予他改变游戏的可执行文件或配置文件的特权。重要的是后一点;如果攻击者可以改变游戏的可执行文件或它的配置文件(可用来控制可执行文件的运行),那么他们就可能获得对运行游戏的用户的控制。

如果创建一个新群组还不够,可以考虑创建一个新的伪用户(一个确实特殊的角色)来管理一组资源。WEB服务器一般就这么做;通常WEB服务器是用一个特殊用户(“nobody”)建立起来的,这样就可以与别的用户隔离起来。确实,WEB服务器在此很有教育意义:WEB服务器通常需要root权限来启动(这样才可以连接到端口80上),但一旦启动之后,它们通常放弃所有的权限,以用户“nobody”身份运行。另外,伪用户通常不拥有其运行的基本程序,所以破解了该帐户并不会允许改变程序本身。其结果就是,闯入一个运行着的WEB服务器一般不会自动破坏整个系统的安全。

如果必须给予某个程序root权限,应该考虑使用Linux 2.2以上版本提供的POSIX能力特性,在程序启动时就立即使这些权限最小化。在启动后立刻调用cap_set_proc(3)或Linux特有的capsetp(3)例程,就可以永久地把程序的能力减小为它实际所需要的那些能力。注意,不是所有类Unix系统都实现了POSIX能力,所以此方案会丧失可移植性;尽管如此,如果只是把它作为仅在可用处应用的防护选项,使用此方案实际上就不会限制可移植性。同样,由于Linux内核2.2以上版本中包含了底层调用,而在某些Linux发行版中没有安装简化这些调用应用的C语言级的库,这使得在应用程序中的应用变得略微有些复杂。要了解更多有关Linux下POSIX能力的实现,参见

http://linux.kernel.org/pub/linux/libs/security/linux-privs。

可用来简化最小化授予特权的一个Linux独有的工具是SuSE开发的

“compartment”工具。该工具设置文件系统的根目录、uid、gid和(或)能力集,然后运行给定的程序。它不用修改程序,运行某些其它程序特别方便。下面是版本0.5的句法:

句法:compartment [options] /full/path/to/program

Options:

--chroot path 把根目录改为path --user user 把uid改为user

--group group --init program --cap capset --verbose --quiet 把gid改为group

在其它操作前先执行程序/脚本program 设置capset名称。可以指定多个capset。 显示所有提示 不产生日志文件

这样就可以用以下命令启动一个更加安全的匿名FTP服务器: compartment --chroot /home/ftp --cap CAP_NET_BIND_SERVICE anon-ftpd

在写作本文的时候,该工具还不成熟,在通常的Linux发行版中也不提供,但情况会很快改变。你可以通过 http://www.suse.de/~marc 下载该程序。

最小化可以使用特权的时间

如果可能的话,就永久性地放弃特权。有些类Unix系统,包括Linux,实现了保存“以前的”值的“保存”ID。最简单的方法就是把某个不可信ID 两次设置为其它ID。在setuid/setgid程序中,需要经常把有效gid和uid设置为真实的gid和uid,特别是在fork(2)之后,除非有不这么做的充分理由。注意,在从root权限改变为其它权限时要先改变gid,否则无效 -- 一旦放弃了root权限,就无法改变很多其它的东西了。

值得注意的是一个众所周知的相关Bug,即使用POSIX能力会干扰这一最小化。这个Bug影响Linux内核版本2.2.0到2.2.15,可能还涉及具有POSIX能力的若干其它类Unix系统。参见http://www.securityfocus.com上Bugtraq id 1322以了解更多信息。下面是其概要:

Linux内核最近实现了POSIX“能力”。这些“能力”是特权控制的一种附加格式,以更为明确地控制具有特权的进程可以做些什么。能力是用三个(相当大)比特位域实现的,每个比特代表特权进程可以执行的特别操作。通过设置特定的比特位就可以控制特权进程的操作 -- 只有需要访问各种函数的程序的特定部分才可以有访问函数的权利。这是个安全措施。问题在于能力是使用fork()进行复制的,也就是说,如果能力被父进程修改,则修改会被传递下去。通过把三个比特位域中每一个的所有能力设为零(即所有比特位都关闭),然后执行一个setuid程序,尝试在执行代码前放弃特权,这在以root身份运行时会是危险的,就象sendmail所做的那样,这样就可以加以利用。当sendmail使用setuid(getuid())试图放弃特权时,由于其比特位域所要求的能力不存在而操作失败,并且不检查返回值。sendmail继续以超级用户的特权运行,可能会以root身份执行某个用户发来的文件从而导致完全的危害。

sendmail使用的一个方案是尝试在setuid(getuid())之后执行setuid(0);一般情况下这会失败。如果成功,程序就停止了。更多的信息可参见http://sendmail.net/?feed=000607linuxbug。在近期这可能是个其它程序中的好主意,虽

然很明显更好的长期解决方案是升级基本系统。

最小化特权有效的时间

使用setuid(2)、seteuid(2)和相关函数以确保程序的特权只在需要的时刻有效。正如上面说明的那样,可能在解析用户输入时你会希望确保这些特权被禁用,但更一般的情况是,只在确实需要的时候才启用这些特权。注意,如果某些缓存溢出攻击成功的话,可以迫使程序运行任意代码,而且那个代码可以重新启用临时放弃了的特权。因此,最好是尽快完全地放弃特权。尽管如此。临时禁用这些许可防范了诸如诱使程序写入某个原来无意写入的文件的一整类技术攻击。由于最小化特权有效时间的技术防范了很多攻击,在程序中无法完全放弃特权的地方就值得这么做。

>最小化获得特权的模块

如果只有几个模块被授予了许可,那么确定它们是否安全就容易得多。这样做的一个方法是:让单个模块使用特权,然后放弃,这样随后调用的其它模块就不会误用该特权。另一个方案是在不同的可执行文件里使用不同的命令;某个命令是可以以某个特权用户(如root)的身份执行大量任务的一个复杂工具,而其它工具是setuid的,而且是只允许使用一个小的命令子集的简单工具。这一简单的小工具检查输入是否符合可以接受的各种标准,如果确定输入是可以接受的,就把输入传递给第一个工具。这甚至可以用多种方法来分层,例如,复杂的用户工具可以调用一个简单的setuid“包裹”程序(来检查输入为安全值),然后把信息传递给另一个复杂的可信工具。该方案对于基于GUI的系统特别有用:让GUI部分以普通用户身份运行,然后把与安全相关的请求传递给另一个对实际运行有特殊权限的程序。

有些操作系统在单个进程中有多级信任的概念,如Multics的环。标准Unix和Linux无法这样在单个进程中用函数区分信任的多个级别;调用内核会增加特权,但一个给定进程只有单一的信任级别。Linux和其它类Unix系统有时可以通过把一个进程复制成多个进程,每个进程有一个不同的特权来模拟这一能力。要做到这一点,可以建立一个安全的通信通道(一般是使用非命名管道或非命名套接字),然后复制出多个进程并使每个进程放弃尽可能多的特权。然后再使用一个简单协议来允许低信任度的进程请求高信任度进程的服务,并确保高信任度的进程只支持有限的一组请求。

Java 2和Fluke之类的技术在此方面有一定的优越性。例如,Java 2可以指定诸如只允许打开某个特定文件的很细的许可。但通用操作系统目前一般没有这样的能力;在不远的将来可能情况会有所改变。

考虑用FSUID来限制特权

每个Linux进程都有两个叫作文件系统用户ID(fsuid)和文件系统群组ID(fsgid)的Linux独有的状态值。在检查文件系统许可时使用这两个值。如果你在构建一个象文件服务器那样供任意用户操作的程序(比如NFS服务器),可能就要考虑使用这些Linux扩展了。使用它们时,在保持root权限的同时,在以普通用户身份存取文件前只需要改变fsuid和fsgid。这个扩展相当有用,它提供了一个无需删除其它(可能必需的)权限就限制了文件系统访问权限的机制。只设置fsuid(而不设置euid),本地用户就无法向进程发送信号。在这种情况下避免竞争情况也容易得多。尽管如此,此方案的一个缺点就是这些调用无法移植到其它类Unix系统上。

考虑使用chroot来最小化可用文件

可以用chroot(2)来限制对程序可见的文件。这要求仔细地建立一个目录(称为“chroot监”)并正确地进入。这可以成为一个增强程序安全性的相当有效的技术 -- 很难去干扰看不见的文件。但是,这是建立在一整套假设之上的,特别是程序必须没有root权限,它必须无法获得root权限,而且chroot监必须恰当地建立。我推荐在适合的地方使用chroot(2),但不要仅仅依赖于它;而是使它作为多层防御的一部分。下面是使用chroot(2)的若干说明:

程序还是可以使用整个机器共享的非文件系统对象(例如System V的IPC对象和网络套接字)。最好也使用不同的伪用户和(或)群组,因为所有类Unix系统都包含了隔离用户的能力;这至少可以限制一个被破解的程序可能对其它程序造成的危害。注意,目前绝大多数类Unix系统(包括Linux)都没有隔离有意进行合作的程序;如果担心恶意的程序合作,就需要获得一个实现了某种强制访问控制和(或)限制隐藏通道的系统。 • 如果不希望以后被使用,务必要关闭对外面文件的文件系统描述符。特别是不要打开到chroot监之外目录的描述符,或者出现这样的描述符可能被获取的情况(如通过Unix套接字或着某个/proc的早期实现)。如果程序得到了chroot 监之外目录的描述符,就可以借此脱离chroot监了。 • chroot监必须安全地建立。不要用某个普通用户的根目录(或子目录)作为chroot监;使用一个独立的位置或为此目的特别设置的“根”目录。把确实最少数目的文件放在那里。一般会有/bin、/etc/、/lib以及一两个其它可能的目录(比如FTP服务器就还有个/pub)。只把进行了chroot()之后需要运行的放入/bin;有时什么都不需要(尽量避免把shell放在那里,虽然有时这没什么作用)。可能还需要一个/etc/passwd和/etc/group,这样在文件列表时可以显示一些正确的名称,但在这样做时,不要包含真实的系统值,而且肯定要把所有密码替换为“*”。在/lib下只放所必需的;用ldd(1)来查询/bin下的每一个程序,看需要些什么,然后只在该目录下包括它们。在Linux下,可能需要ld-linux.so.2一类的很少几个基本库,别的都不需要。一般来说把所有文件都复制下来是个聪明的办法,而不是生成硬连接;虽然这会浪费一些时间和磁盘空间,但它使对chroot监内文件的攻击不会自动蔓延到正常的系统文件。在支持chroot监的系统上安装/proc文件系统一般是不明智的。实际上这在Linux的2.0.x版本上是一个已知的安全漏洞,因为/proc下有些伪目录可以让chroot的程序逃

脱。Linux内核2.2修补了这一已知漏洞,但可能还存在其它问题;所以尽可能地不要这么做。

• 如果程序可以获得root权限,chroot实际上就不起作用。例如,程序可

以使用mknod(2)一类的调用来创建一个可以浏览物理内存的设备文件,然后利用产生的设备文件来修改内核空间,给予自己任意想得到的特权。另一个root程序如何逃出chroot的例子在

http://www.suid.edu/source/breakchroot.c 上有说明。在该例中,程序为当前目录打开一个文件描述符,创建并chroot到一个子目录,把当前目录设为此前打开的当前目录,然后从当前目录重复使用cd回溯(由于在当前chroot之外,就成功地移动到真实文件系统的根目录),再对其结果调用chroot。在阅读本文档的时候,这些漏洞可能已经被填补了,但事实是root特权在传统上意味着“所有特权”,而且很难被去掉。最好只是假设使用chroot()对要求持续root特权的程序略有帮助而已。当然,也可以把程序分成若干部分,这样至少可以有一部分可以放在chroot监里。

避免创建Setuid/Setgid脚本

许多类Unix系统,特别是Linux,简单地忽略脚本的setuid和setgid比特位以避免前面描述过的竞争状态。由于类Unix系统对setuid脚本的支持各不相同,最好在可能的情况下避免在新应用程序中使用setuid脚本。作为特殊情况,Perl包含一个特殊的安排以支持setuid的Perl脚本,所以如果确实需要这种功能,可以接受在Perl中使用setuid和setgid。如果需要在自己的解释器中支持这种功能,可以查看一下Perl的做法。除此之外,一个简单的方法是用一个创建安全环境的小setuid/setgid可执行代码(如清除并设置环境变量)“封装”脚本,然后再调用脚本(使用脚本的完整路径)。要确保脚本不会被攻击者修改!Shell脚本语言还有其它的问题,实际上不应该setuid/setgid;参见下文的特定语言一节。

安全地配置并使用安全的缺省值

配置可以被认为是当前的头号安全问题。因此,应该努力做到以下两点:(1)使初始安装是安全的,(2)在保持安全的情况下便于重新配置系统。

在管理员有机会配置某个程序之前,该程序应该具有最严格的访问策略。请不要创建“样板”工作用户或“允许访问一切”的配置作为启动配置;很多用户只是“全部安装”(安装所有提供的服务),而没有时间去配置许多服务。在有些情况下,程序可能通过依赖已有的认证系统决定采用更宽松的策略,例如,FTP服务器可以合法地认定一个可以登录进某个用户目录的用户应该允许访问该用户的文件。无论如何要小心这样的假定。

使安装程序的脚本尽可能地安全地安装程序。缺省情况下,把所有文件作为root或其他系统用户所有的文件来安装,不允许其他用户改写这些文件;这样可以防

止非root用户安装病毒。实际上最好使这些文件不可被非可信用户读。在可能的情况下也可以允许非root安装,这样没有root权限的用户和不能完全信任安装者的管理员也可以使用程序。

尽可能地使配置简单清晰,这也包括安装后的配置。尽可能地使用“安全”的方案,否则,很多用户会在不了解风险的情况下使用某个不安全的方案。在Linux下,利用linuxconf之类的工具,这样用户可以使用现有基础便捷地配置他们的系统。 如果有配置语言的话,缺省项应该是拒绝访问,直到用户明确同意访问。如果有配置文件样板的话,其中应该包含很多清晰的注释,这样管理员就可以了解配置在做些什么。

安全地失败

安全程序应该总是“安全地失败”,也就是说,它应该设计为一旦失败,出现的是最安全的结果。对于关键性安全程序,这一般意味着在检测到某些错误行为(格式不正确的输入、到达某个“不可能到达”的状态,等等)时,程序应该立刻拒绝服务并停止处理该请求。不要试图“指出用户想做什么”:只是拒绝服务。有时这会降低可靠性或可用性(从用户的角度来看),但它会提高安全性。在若干场合这可能不是所期望的结果(例如拒绝服务比丧失秘密或完整要坏得多的场合),但这样的情况是相当少见的。

注意,我推荐的是“停止处理该请求”,而不是“一起失败”。特别是大多数服务器不应该在得到格式不正确的输入时完全暂停,因为这会给拒绝服务攻击创造一个平常的机会(攻击者只要发送垃圾比特就可以阻止他人使用服务了)。有时让整个系统宕机是必需的,特别是到达某个“不可能到达”的状态可能表明一个问题的严重程度使得继续运行变得不理智。

要仔细考虑检测到失效时发回的错误信息。如果什么都不发回,可能很难诊断问题,但发回太多的信息,可能无意中帮助了攻击者。通常最好的方法是回答“拒绝访问”或“遇到多种错误”,然后把更详细的信息写入一个审计日志文件(在这里就可以更好地控制哪些人可以看到这些信息)。

避免竞争状态

“竞争状态”可以定义为“对事件相对节奏意外的临界依靠引发的异常行

为”[FOLDOC]。竞争状态一般涉及一个或多个进程访问某个共享资源(如某个文件或变量),而这一多重访问没有被适当地控制。

一般来说,进程不是以原子方式运行的,另一个进程甚至可以在任意两条指令之间中断它。如果一个安全程序的进程对这样的中断没有准备,其它进程就能够干扰安全程序的进程。如果其它进程的任意代码在安全程序进程的任意一对操作之间被执行,操作都不应该失败。

竞争状态的问题可以从概念上分为两类:

不可信进程导致的阻碍。有些安全分类学把这个问题称为“次序”或“非原子”状态。这种情况是由于运行其它不同程序的进程在安全程序的步骤之间“失脚滑入”了其它操作引起的。这些其它程序可能是某个攻击者特别执行来造成问题的。本文将把这些称为次序问题。

• 可信进程(从安全程序的角度)导致的阻碍。有些分类学把它称为死锁、活锁或者锁定失效状态。这种情况是由于运行“相同”程序的进程引起的。由于这些不同的进程可能具有“相同”的优先级,如果没有适当地加以控制,可能会以其它程序无法做到的方式互相干扰。有时这种阻碍会被利用。本文将把这些称为锁定问题。

次序问题

一般来说,必须检查代码中如果任意代码在某对操作之间被执行就可能会失败的任一对操作。

注意,载入和保存共享变量一般是由不同的操作实现的,而且也不是原子操作。这就意味着一个“增加变量”的操作通常会转变为载入、增加和保存操作,所以如果变量是与其它进程共享的,就可能会干扰增加操作。

安全程序必须确定是否同意某个请求,如果是的话,对请求作出反应。一个不可信用户应该无法在程序反应之前改变此决定中用到的任何信息。这种竞争状态有时被称为“检查时间/使用时间”(TOCTOU)竞争状态。

这个问题在文件系统中经常出现。程序一般应该避免使用access(2)来确定是否同意某个请求,并随后使用open(2),因为用户可以在这两个调用之间移动文件,可能建立他们自己用来替代的符号连接或文件。安全程序应该设置自己的有效ID或文件系统ID,然后直接进行open调用。安全地使用access(2)也是可能的,但只有在用户无法影响文件或文件系统根目录下文件所在路径上的任一目录时才是安全的。

例如,在对文件维护信息执行一系列操作(比如改变其所有者、复制文件或改变其许可比特)时,先打开文件,然后对打开的文件执行操作。这就意味着应该使用fchown( )、fstat( )或fchmod( )系统调用,而不要使用chown()、chgrp()和chmod()一类的需获取文件名的函数。这样做可以防止在程序运行过程中文件被替换(一个可能的竞争状态)。例如,如果关闭某个文件,然后使用chmod()改变其许可,攻击者就可以在这两个步骤之间删除文件并建立到另一个文件(如/etc/passwd)的符号连接。其它有趣的文件包括/dev/zero,它可以向程序提供无限长的数据流输入。另外,应该避免使用access( )函数来确定访问文件的能力:使用跟随open( )的access( )函数会出现竞争状态,而且差不多总是个Bug。如果不可信进程可以修改其父进程的相关目录,这样做才是迫不得已的。

特别对于所有用户共享的/tmp和/var/tmp目录会出现这个问题。如果可能应避免使用这些目录及其子目录。特别是想象一下如果用户在任意时刻在打算使用的目录里创建文件(包括符号连接)会出现什么情况(例如,在确定文件名的时刻和试图打开文件的时刻之间)。甚至无法检查给定的文件是否为符号连接;如果某个不可信用户拥有该文件,此用户就可以在检查后改变它。

锁定

经常出现某个程序需要确定对某些资源(例如某个文件、某个设备或某个特别的服务器进程的存在)的独占权。任一锁定资源的的系统都必须处理锁定的标准问题,即死锁(“抱死”)、活锁以及在程序无法清除自己的锁时释放“受困”的锁。如果程序受困于互相等待对方释放资源时,死锁就发生了。例如,如果进程1锁定了资源A并等待资源B,而进程2锁定了资源B并等待资源A,死锁就发生了。简单地要求所有进程按相同顺序锁定资源就可以防止许多死锁(例如必须按字母顺序锁定资源)。

在类Unix系统上,传统上通过创建一个标识锁的文件来实现资源锁定,因为这样移植性很好。这也使“修正”受困锁定变得简单,因为系统管理员可以查看文件系统来发现被设置的锁。受困锁定可以因为程序没有在结束后清除(例如崩溃或发生功能障碍)而出现,也可以因为整个系统崩溃而出现。注意,这些是“报告”(而非“强制”)锁定 -- 所有需要资源的进程都必须合作使用这些锁。 尽管如此,还是需要避免几个陷阱。首先,即使在创建文件时把它设置为独占(O_EXCL)模式(O_EXCL模式在文件已存在的情况下通常会失败),具有root权限的程序还是可以打开该文件。所以,如果想用一个文件来表示锁定,不要使用open(2)和独占模式,在进行这一操作时需要root权限。简单的做法是换用link(2)来创建同一目录下某些文件的硬连接; 如果某个硬连接已经存在的话,即使是root也不能创建它。

其次,如果锁文件位于某个安装成NFS的文件系统上,那么可能会遇到NFS版本2不完全支持普通的文件语法的问题。即使假定对于客户程序是“本地”的工作,这也可能成为一个问题,因为某些客户程序没有本地磁盘,而且可能所有的文件都是通过NFS远程安装的。open(2)手册解释了在这种情况下该如何处理(也可以用来处理root程序):

\"......依赖于[open(2)的O_CREAT和O_EXCL标志]来执行锁定任务的程序会包含竞争状态。使用锁文件进行原子文件锁定的解决方案是在同一个文件系统(如包含主机名和pid)上创建一个独特的文件,使用link(2)建立一个到锁文件的连接并用stat(2)检查那个独特文件的连接数是否增加到2。请不要使用link(2)调用的返回值。\"

很明显,该解决方案只有在所有操作锁的程序都合作,而且所有非合作的程序都不允许介入的情况下才起作用。特别是用来进行文件锁定的目录对于创建和移动文件必须没有宽松的文件许可。

NFS版本3增加了对open(2)中O_EXCL模式的支持;参见IETF RFC 1813,特别是“CREATE”的“mode”参数的“EXCLUSIVE”值。可惜的是,在写作本文时不是所有人都切换到了NFS版本3,所以对于可移植程序不能依赖这一点。 如果要在本地机器上锁定某个设备或某个已有进程,请尽量使用标准约定。我推荐使用文件系统分级结构标准(FHS);Linux系统已经广泛地参考了该标准,但它还试图结合其它类Unix系统的想法。FHS描述了这些锁定文件的标准约定,包括这些文件的命名、存放和标准内容[FHS 1997]。如果只是想保证在某个给定机器上服务器程序不会执行一次以上,那么通常应该创建一个内容为pid的/var/run/NAME.pid作为进程标识符。与此相同,应该把设备锁文件一类的锁文件放在/var/lock里。这种方案有一个小的不足,就是在程序突然暂停时会使文件挂起,但这种情况是个标准的实际情况,而且很容易用其它系统工具来处理这个问题。

重要的是合作使用文件来代表锁的程序应该使用“相同”目录,而不仅仅是相同的目录名。 这是与网络系统有关的问题:FHS明确地提到/var/run和/var/lock是不可共享的,而/var/mail是可共享的。因此,如果希望一个在单机上工作的锁不与其它机器相互干扰,就应该使用/var/run之类不可共享的目录(例如希望允许每台机器运行自己的服务器程序)。尽管如此,如果希望一个网络上所有共享文件的机器都受锁的控制,就需要使用一个共享的目录;/var/mail就是这样的一个地方。参见FHS第二章以了解有关该主题的更多信息。

当然,没必要一定使用文件来代表锁。 网络服务器通常无需担心这个问题;纯粹的绑定就可以作为一种锁,因为如果某个现存的服务器程序绑定了一个给定端口,其它的服务器程序就不能绑定该端口。

另一个锁定的方案是使用POSIX的记录锁,通过fcntl(2)作为一个“可任意使用的锁”来实现。这些锁是可任意使用的,也就是说,使用时要求需要锁定的程序间的合作(就象用文件代表锁定的方案所做的那样)。 有许多理由推荐使用POSIX记录锁:几乎所有类Unix平台都支持POSIX记录锁定(这是POSIX.1所要求),它可以锁定文件的一部分(而不是整个文件),而且可以处理读锁定和写锁定之间的区别。更有用的是,如果一个进程死掉了,它的锁自动被删除,就像通常所期望的那样。

还可以使用基于System V强制锁定框架的强制锁定。这只适用于锁定文件的setgid比特位被设置而群组执行比特位没有被设置的文件。同样,另外,文件系统安装时还必须允许强制文件锁。在这种情况下,每一次read(2)和write(2)都要检查锁定,这比报告性的锁更彻底,同时也更慢。此外,强制锁无法广泛移植到其它类Unix系统上(可用于 Linux和基于System V的系统,但在其它系统上不是必需的)。注意,具有root权限的进程也可以被强制锁停止,这使得它可以被用作拒绝服务攻击的基础。

只信任值得信任的通道

一般而言,不要信任靠不住的通道上的结果。

在大多数计算机网络中(当然包括Internet),未经证明的传输都是不可靠的。例如,在Internet上可以伪造任意数据包,包括包头的值,所以不要用它们的值作为安全决定的主要依据,除非可以证实。在某些情况下可以断定一个声称来自“内部”的数据包确实来自内部,因为本地防火墙会阻止外部的欺骗,但是被破坏的防火墙、可选择的路径和活动编码使这样的假设都值得怀疑。与之相似,不要假设低端口号(小于1024)是可靠的;在大多数网络中这样的请求可以被伪造或着某个平台会允许使用低端口号的端口。

如果是在实现一个标准,而且继承了不安全的协议(如FTP和RLOGIN),请提供安全的缺省项并明确说明所做的假设。

域名服务器(DNS)被广泛应用在Internet上来维持计算机名与其IP(数字)地址之间的映射。所谓“反向DNS”的技术排除了一些简单的欺骗攻击,并对确定主机名有所帮助。但是,此技术对于确认的决定是不可信的。其问题在于,DNS请求最终会被送到某些可能被攻击者控制的远端系统上。因此,要把DNS的结果当作需要证实的输入来处理,不要靠它来完成严肃的访问控制。 如果要求密码的话,应该力图建立可靠的路径(如要求在登陆前输入一个不可伪造的键值,或者显示诸如发光LED一类的不可伪造的图样)。不幸的是,标准Linux即使对于普通的登录序列都没有一条可靠的路径,而且由于目前普通用户可以改变LED,使得LED目前还不能被用来确认一条可靠的路径。在处理密码时,在可靠的端点之间要进行加密。

任意的email(包括地址的“from”值)也是可以伪造的。使用数字签名是防止这类攻击的一个方法。一个更简单的防范方法是要求email来回传送特别的随机生成值,这通常只是对于签署公开邮件列表一类的低价值事务是可以接受的。 如果需要在不可靠网络上建立一条可信通道,就需要某些密码学服务(最起码是在密码学上安全的一个散列);参见下面的加密算法和协议一节。

注意,在客户/服务器模型中,包括CGI,服务器都必须假定客户可以修改任何值。例如,所谓的“隐藏域”和cookie值可以在被CGI程序接收到之前被客户改变。除非它们被以客户无法伪造的方法标识而且服务器检查该标识,这些值是不可靠的。

本地用户可以控制例程getlogin(3)和ttyname(3)返回的信息,所以不要在用于安全目的时信任它们。

这个问题也适用于引用其它数据的数据。例如,HTML或XML允许通过引用包含可能存储在远端的其它文件(如DTD和格式表单)。但是这些外部引用可能被修改,所以用户可能会得到一个意料之外的不同文档;格式表单可能在关键位置的词语被修改为“清空”,使其显示被涂改,或插入新的文本。外部的DTD可

能被修改为阻止文档使用(通过增加破坏合法性的声明)或在文档中插入不同的文本[St. Laurent 2000]。

使用内部一致性检查代码

程序应该检查以确保其调用的参数和基本状态假设是合法的。在C语言中,assert(3)一类的宏可用于进行这样的检查。

自我限制资源

在网络守护进程中,应该排除或限制过多的负荷。应该设置限制值(使用setrlimit(2))来限制会被使用到的资源。至少要用setrlimit(2)来禁止创建“core”文件。例如,缺省情况下Linux会在程序异常失败时创建一个core文件来保存所有的程序内存,而这样的文件可能会包含密码或其它敏感数据。

Chapter 7. 小心对其它资源的调用出口

Table of Contents 限制调用出口为合法值 检查系统调用的所有返回值

Do not put your trust in princes, in mortal men, who cannot save.

Psalms 146:3 (NIV)

限制调用出口为合法值

要保证调用其它程序的出口只允许每个参数的合法而且期望的值。听起来不难,但实现起来就难得多了,因为有很多库调用或命令会以潜在的令人惊异的方式调用低级例程。例如,若干popen(3)和system(3)一类的系统调用通过调用命令shell来实现,也就是说,它们会受到shell转义字符的影响。同样,execlp(3)和execvp(3)也可能会调用shell。很多指南建议在产生一个进程时完全避免使用popen(3)、system(3)、execlp(3)和execvp(3),直接用C中的execve(3)[Galvin 1998b]。至少应该避免在可以使用execve(3)时使用system(3);因为system(3)使用shell来扩展字符,对于恶作剧者来说system(3)会提供更多机会。Perl和shell的backtick(`)也以同样的方式调用命令shell;参见Perl一节。 这个问题最令人头疼的例子就是shell转义字符。标准的类Unix命令shell(存储在/bin/sh)对许多字符有特别的解释。如果这些字符被传递给shell,那么除非被忽略,都将使用它们的特殊解释;这一事实可被用来破坏程序。按照WWW安全FAQ[Stein 1999, Q37],这些转义字符是: & ; ` ' \\ \" | * ? ~ < > ^ ( ) [ ] { } $ \\n \\r

不幸的是,这并非实际存在的所有转义字符。下面是可能出问题是一些其它字符: “!” 在表达式里意味着“否”(就象在C语言里一样);如果程序的返回值

被检测,预先考虑“!”会欺骗脚本在某些事情已经成功时以为它失败了,或是相反。在某些shell里,“ !”会访问命令的过去,这会导致真正的问题。在bash里,只有交互模式会出问题,但tcsh(某些Linux发行版里的csh的复制形式)甚至在脚本里都使用“!”。新的bash看来也用“ !”来访问命令的过去 -- 但可能只用于交互模式。 • “#”是注释字符,随后的文本被忽略。

• “-”会被误解为后面是一个选项(或者象“--”一样禁用其它选项)。甚至当它位于文件名的中间,或者前面存在被shell认为是空格的字符,都可能会出问题。

• “ ”(空格)和其它空格字符会把某个“单独的”文件名变成多个参数。

其它控制字符(特别是NIL)会在某些shell的实现上产生问题。

• 按照应用的情况,甚至可以想象“.”(“在当前shell下运行”)和“=”(设置变量)都是令人担心的字符,而且目前找到的例子表明这些问题会带来更严重的安全问题。

忘记其中的某个字符会导致灾难性后果,例如,许多程序把反斜杠作为转义字符加以忽略[rfp 1999]。像在“证实输入”一节所讨论的那样,一个推荐方案是至少在输入时立刻忽略掉所有这些字符。同样,目前最好的解决方案是识别出希望允许的字符,并且只使用这些字符。

许多程序有执行“额外”操作的“逃逸”代码;要确保它们没有被包括在内(除非希望它们出现在消息里)。例如,许多面向行命令的邮件程序(如mail和mailx)使用代字符(~)作为逃逸字符,可以用来发送许多命令。结果,像“mail admin < file-from-user”这样看起来清白的命令就会被用来执行任意程序。vi和emacs一类的交互程序有“逃逸”机制,通常允许用户使用过程中执行任意的shell命令。应该无论如何都检查调用程序的文档来寻找逃逸机制。

避免逃逸代码的问题甚至涉及到底层硬件部件及其仿真器。大多数调制解调器实现了所谓的“Hayes”命令集,用“+++”序列、一个延迟再加上“+++”来强迫调制解调器切换模式(并把后面的文本作为命令解释)。这可以被用来实现拒绝服务攻击,甚或强迫用户与另一个人相连接。许多“终端”接口实现了VT100一类早已过时的老物理终端的逃逸代码。比如这些代码可以用来改变终端接口的字体颜色。尽管如此,不要允许直接向终端屏幕发送任意的不可信数据,因为某些代码会导致严重问题。在某些系统上可以重新映射按键;在若干系统上甚至可以发送代码来清除屏幕、显示一组希望受害者运行的命令以及把这些设置发“回去”(强迫受害者运行攻击者选择的命令)。

与之相关的一个问题是NIL字符(字符0)可能会有些出人意料的后果。大多数C和C++函数假设该字符标识字符串的结束,但其它语言(如Perl和Ada95)中的字符串处理例程可以处理包含NIL的字符串。由于很多库和内核调用使用的是C语言规范,结果就是被检查的内容并非实际所用到的[rfp 1999]。 在调用其它程序或引用某个文件时,应该总是指定其完整路径(如/usr/bin/sort)。对于程序调用,即使PATH值设置不正确,这也会排除调用“错误”的命令可能引起的错误。对于引用其它文件,这会减少“错误”的起始目录所带来的问题。

检查系统调用的所有返回值

每个可以返回错误状态的系统调用都必须检查错误状态。这样做的一个理由是几乎所有的系统调用都会需要有限的系统资源,而用户可以经常以各种方式影响资源。setuid/setgid程序可以通过setrlimit(3)和nice(2)之类的调用来设置这些限制。服务器程序和CGI脚本程序的外部用户可以简单地通过制造大量的同时发

起的请求来耗尽资源。如果不能妥善地处理错误,那么就使用前面讨论过的打开失败吧。

Chapter 8. 明断地发回信息

Table of Contents 最小化反馈

处理完整的/不响应的输出

Do not answer a fool according to his folly, or you will be like him yourself.

Proverbs 26:4 (NIV)

最小化反馈

避免给予不可靠用户过多的信息;简单地成功或失败,如果失败就只说失败,使失败原因的信息最少。把详细的信息保存下来作为检查跟踪的日志文件。例如: 如果程序要求某种用户认证(如编写一个网络服务或登录程序),在认证

前只给用户提供尽可能少的信息。特别是要避免在认证前给出程序的版本号。否则,如果某个特别版本的程序被发现有个漏洞,那么还没有升级该版本的用户就容易招致攻击者的入侵。 • 如果程序接受了密码,不要返回它;这会造成密码被他人看见的另一个机会。

处理完整的/不响应的输出

用户可能会阻塞或使安全程序返回用户的输出通道无法响应。例如,一个网页浏览器可能被有意地暂停或者使它的TCP/IP通道响应变慢。安全程序必须处理这样的情况,特别是应该能够快速解锁(最好在回答之前),这样就不会给拒绝服务攻击提供机会。应该总是对进行中的面向网络的写请求设置超时。

Chapter 9. 特定语言的问题

Table of Contents C/C++ Perl Python

Shell脚本语言(sh及csh的变种) Ada Java

Undoubtedly there are all sorts of languages in the world, yet none of them is without meaning.

1 Corinthians 14:10 (NIV)

有许多特定语言的安全问题。许多可以做如下总结:

• • • •

在实际可行的地方打开所有可用相关的警告和保护机制。对于编译语言,这包括编译机制和运行机制。一般情况下,与安全相关的程序应该在打开所有告警的条件下顺利编译。

应该避免语言中危险和不赞成的操作。所谓“危险”,是指很难正确使用的操作。

应该确保语言的基本结构(如运行库)是可用和安全的。 自动收集垃圾字符串的语言应该特别小心地立刻清除秘密数据(特别是密钥和密码)。

准确地了解所用操作的语法。从文档中查看操作的语法。除非可以确信返回值是不相关的,否则不要忽视它们。对于像C那样不支持异常的语言这一点很难做到,但事情就是这个样子。

C/C++

C和C++程序最大的安全问题就是缓存溢出;参见有关缓存溢出一章以了解更多信息。C还有个额外的弱点就是不支持异常,这就使得在编程时很容易忽略关键的错误状态。

对于使用gcc进行C或C++编译,应该至少使用以下编译标识(打开一大批告警信息)并力图消除所有告警(注意,使用-O2是由于有些告警只能被在更高的优化情况下执行的数据流分析所检测到): gcc -Wall -Wpointer-arith -Wstrict-prototypes -O2

对于发现潜在的安全漏洞可能会找到有些很有用的审计工具。下面就是几个这样的工具:

可靠软件技术(RST)的ITS4静态地检查C/C++代码。ITS4通过对源码执行模式匹配来进行工作,寻找已知可能危险的模式(如特定的函数调用)。ITS4对于非商业用途是免费的,包括其源码和特定的修改与重新发布的权利。警告,该工具的许可声明可能最初被误用。RST宣称ITS4是“开放源码”,但实际上它的许可证并不符合开放源码定义(OSD)。特别是ITS4的许可不符合第六点,即禁止在开放源码许可中存在“只用于非商业用途”的条款。可惜的是RST坚持用“开放源码”一词来描述他们的许可。ITS4是个优秀的工具,发布所用的许可证对于商业软件而言相当慷慨,虽然这样使用“开放源码”一词会给人以试图在没有真正开放源码的情况下赢得开放源码的名声的感觉。RST说他们只是不接受OSD的定义,希望换用一个不同的定义。从法律的角度而言,没人反对他们这么做,只是使用OSD定义的包括超过5000个软件计划(至少那些由位于

http://www.sourceforge.net的SourceForge主持的软件计划是这样的)、Linux发行商、Netscape(现在是AOL)、W3C、记者(如Economist的记者)以及其它组织。大多数程序员不想艰难地通过许可协议,所以使用其它的定义会造成混乱。我不相信RST有意制造误解;他们是一家声誉良好的公司,其人员也是声誉良好而且诚实。不幸的是他们采用的特殊姿态导致了(以我的观点)不必要的混乱。不管怎样,可以从 http://www.rstcorp.com/its4 获得ITS4。 • LCLint是静态检查C程序的一个工具。最低程度LCLint也可以作为更好的lint。如果增加了程序注解,LCLint就可以在标准lint功能之外执行更进一步的检查。该软件使用GPL许可证,可以从 http://lclint.cs.virginia.edu 处获取。

Perl

Perl程序员应该首先阅读perlsec(1)的man帮助页,那里描述了许多用Perl编写安全程序涉及的问题。特别是perlsec(1)描述了“污染”模式,大多数安全Perl程序应该使用该模式。如果真实的用户或群组ID与有效用户或群组ID不同,或者使用了命令行标识-T (在以其他人身份运行程序如CGI脚本时使用后一种方式),则自动启用污染模式。污染模式会打开各种检查,如检查路径目录以确定它们对其他人是不可写的。

尽管如此,污染模式最明显的效果是不可以使用来自程序以外的数据来偶然影响程序之外的事情。在污染模式下,所有外部获得的输入,包括命令行参数、环境变量、本地信息(参见perllocale(1))、特定系统调用(readdir、readlink、getpw*调用的gecos域)的结果和所有文件输入,都被标记为“被污染”。被污染的数据不可以直接或间接地被应用在执行子shell的命令里,或者被用在修改文件、目录或进程的命令里。一个重要的例外:如果向system或exec传递一个参数列表,该列表中的成员不会被检查是否被污染,所以在污染模式下使用system或exec要特别小心。

来自被污染数据的任何数据值也是被污染的。这里有一个例外;从被污染的数据中提取一个子字符串可以使数据不被污染。但也不要盲目地采用“.*”作为子字符

串,因为这样会失去污染机制的保护。作为替代,应该识别出程序允许的“安全”模板,并用它们来提取“好的”值。在提取出值之后,可能还需要对它进行检查(特别是它的长度)。

open、glob和backtick函数调用shell来扩展文件名里的通配符;这可被用来打开安全漏洞。应该尽量完全避免这些函数,或者在perlsec(1)里描述的低权限的“沙箱”里使用它们。

坦白地说,Perl的open()函数对于大多数安全程序而言“有太多的魔法”;如果没有小心地过滤,它在解释文本时会制造很多安全问题。在编写打开或锁定某个文件的代码之前,请参考perlopentut(1)的man帮助页。在大多数情况下,sysopen()提供了打开文件的一个更安全(虽然更为错综复杂)的方法。 新的Perl 5.6增加了有三个参数的open()调用,去掉了有魔力的行为,从而不需要错综复杂的sysopen()。

Perl程序应该打开告警标志(-w),来警告有潜在危险的或过时的语句。 也可以在受限制的环境下运行Perl程序。更多的信息可参见标准Perl发行版中的“安全”模块。我不能确定对它进行的审计情况,所以用户需要注意。也可以调查一下“安全的分布式Internet脚本编写的企鹅模型”,虽然在写作本文时其代码和文档还无法得到。

Python

可以被非授权用户执行的有特权的Python程序(如setuid/setgid程序)应该不包含“用户”模块。用户模块会读入并执行pythonrc.py文件。由于这个文件会处于不可信用户的控制之下,包含用户模块就会允许攻击者强迫可信的程序执行任意代码。

Python通过其RExec类包括了对“受限制运行”的支持。这主要是用来执行applet和移动代码,但也可以用来限制程序的权限,即使代码不是由外界提供的。缺省情况下,一个受限制运行的环境允许读(而不是写)文件,而且不包括网络访问操作或GUI交互操作。这些缺省值可以改变,但要小心在受限制环境中造成漏洞。特别是允许用户不受限制地对某个类增加属性会允许各种彻底改变环境的方法,因为Python的工具调用了许多“隐含”的方法。注意,在缺省情况下,许多Python对象是通过引用传递的;如果把一个对可变值的引用插入受限制的程序环境中,受限制的程序就可以用一种受限制环境外可见的方式改变此对象!因此,如果要访问某个可变值,很多情况下应该复制这个可变值或使用Bastion模块(它支持对另一个对象的受限制访问)。更多的信息可参见Kuchling [2000]。 我不能确定对它进行的审计情况,所以用户需要注意。

Shell脚本语言(sh及csh的变种)

我强烈反对在setuid/setgid安全代码中使用标准命令shell的脚本语言(如csh、sh和bash)。有些系统(如Linux)完全禁用它们,所以会制造不必要的移植性问题。在某些老的系统上,由于竞争状态(像在进程一节讨论的那样)它们基本上是不安全的。即使对于其它系统,它们也确实不是个好想法。标准命令shell由于会受到不明显输入的影响而依旧声名狼籍 -- 通常是由于它们在设计时是为了某个交互式用户“自动”地完成工作,而不是防范某个坚定的攻击者。例如,“隐藏的”环境变量甚至在脚本执行前就可以影响它们如何操作。即使是可执行文件的文件名或目录下的内容都会产生影响。例如,在许多Bourne shell的实现上,如下操作会获得root权限(感谢NCSA描述了这种利用): % ln -s /usr/bin/setuid-shell /tmp/-x % cd /tmp % -x

有些系统可能已经填补了这一漏洞,但问题依然存在:大多数命令shell不是用来编写安全程序的。出于编程目的,应该即使在许可的系统上也避免创建setuid的shell脚本。作为替代,用其它语言编写一个小程序来清除环境,然后调用其它可执行程序(有些可能是shell脚本)。

如果一定要使用shell脚本语言,至少应该把脚本放在一个不可被移动或改变的目录下,而且在脚本中非常明确地把PATH和IFS设为已知值。

Ada

在Ada95中,Unbounded_String类型通常比String类型更灵活,因为在必要时它会自动地重新确定大小。尽管如此,也不要把密码或密钥之类的特殊敏感值存储在Unbounded_String上,因为core dump和页表随后还保持着这些值。作为替代,应该用String类型存储这些值并尽快用“ ”之类的常量来重写数据。

Java

如果要用Java来开发安全程序,坦白地说,第一步(在学习了Java之后)就是要阅读两本有关Java安全的教材,即Gong [1999]和McGraw [1999](后一本特别要看第7.1节)。还应该看一下Sun发布的安全代码指南http://java.sun.com/security/seccodeguide.html。有一组描述Java安全模型的幻灯片可以从 http://www.dwheeler.com/javasec 免费获取。

下面是基于Gong [1999]、McGraw [1999]和Sun的指南的若干关键要点: 1. 不要使用公共域或变量;把它们声明为私有的并提供访问函数以限制对它们的访问。

2. 除非有很好的理由,把方法都设为私有的(如果确实没这样做,说清楚其理由)。这些非私有的方法必须保护自己,因为它们可能会接收到受污染的数据(除非已经用其它方式对它们进行了保护)。

3. 避免使用静态域变量。这样的变量附着在类(而非类的实例)上,而类可以被其它类所定位。其结果就是静态域变量可以被其它类找到,因此很难保证它们的安全。

4. 永远不要把可变对象返回给潜在有恶意的代码(因为代码可能会改变它)。注意,数组是可变的(即使数组的内容不可变),所以不要返回一个含有敏感数据的内部数组的引用。

5. 永远不要直接保存用户给定的可变对象(包括对象的数组)。否则,用户可以把对象交给安全代码,让安全代码“检查”对象,并在安全代码试图使用数据时改变数据。应该在内部存储数组前复制它们,而且要小心(例如,警惕用户编写的复制例程)。

6. 不要依赖于初始化。有好几种方法给未初始化的对象分配内存。

7. 除非有很好的理由,应该使每件事都是确定的。如果某个类或方法不是确定的,攻击者就可以用某种危险而无法预知的方法来扩展它。注意,作为安全性的交换,这会带来可扩展性的丧失。

8. 不要在安全性上依赖包的范围。若干类,如java.lang,缺省是关闭的,而且某些Java虚拟机(JVM)会让你关闭其它包。否则,Java类是没有关闭的。因此,攻击者可以向包中引入一个新类,并用此新类来访问你以为保护了的信息。

9. 不要使用内部类。在内部类转换为字节代码时,内部类会转换为可以访问包中任意类的类。更糟的是,被封装类的私有域静悄悄地变成非私有的,允许内部类访问!

10. 最小化特权。如果可能,完全不要请求任何特殊的许可。McGraw更进一步地推荐不要标记任何代码;我认为可以标记代码(这样用户可以决定“只有列表上的发送者可以运行标记过的代码”),但在编写程序时要使程序不需要沙箱设置之外的权限。如果一定要有更大的权限,审读代码就会特别困难。

11. 如果一定要标记代码,应该把它们都放在一个档案文件里。这里最好引用McGraw [1999]的原文: 此规则的目的是防止攻击者使用混合匹配攻击,构建新applet或库把某些标记类与有恶意的类连接在一起,或者把根本意识不到会被一起使用的标记类连接在一起。通过把一组类标记在一起,就可以使这种攻击更困难。现有的代码标记系统在防止混合匹配攻击上做得还不够,所以这一规则还不能完全防止此类攻击。但使用单个答案没什么坏处。

12. 应该使类不可被复制。Java的类复制机制允许攻击者不运行构建函数就实例化某个类。要使类不可被复制,只要在每个类里定义如下方法:

public final void clone() throws

java.lang.CloneNotSupportedException { throw new java.lang.CloneNotSupportedException();

}

13. 如果确实需要使类可被复制,那么可以采用几个保护措施来防止攻击者重新定义复制方法。如果是定义自己的复制方法,只需要使它是确定的。如果不是定义自己的复制方法,至少可以通过增加如下内容来防止复制方法被恶意地重载: public final void clone() throws

java.lang.CloneNotSupportedException { super.clone(); }

14. 应该使类不可序列化。系列化运行攻击者看到对象的内部状态,甚至私有部分。要防止这一点,需要在类里增加如下方法: private final void writeObject(ObjectOutputStream out) throws java.io.IOException { throw new java.io.IOException(\"Object cannot be serialized\"); }

15. 甚至在序列化没问题的情况下,也应该对包含直接处理系统资源的域和包含与地址空间有关信息的域使用临时关键字。否则,解除类的序列化就会允许不适当的访问。可能还需要把敏感信息标识为临时的。 16. 如果对类定义了自己的序列化方法,就不应该把内部数组传递给需要数组的DataInput/DataOuput方法。其理由在于:所有的

DataInput/DataOuput方法都可以被重载。如果某个可序列化的类向某个DataOutput(write(byte [] b))方法直接传递了一个私有数组,那么攻击者就可以构建子类ObjectOutputStream并重载write(byte [] b)方法,从而可以访问并修改那个私有数组。注意,缺省的序列化并没有把私有字节数组域暴露给DataInput/DataOutput字节数组方法。

17. 应该使类不可被解除序列化。即使类不可被序列化,它依然可以被解除序列化。攻击者可以构建一个字节序列,使它碰巧是被解除序列化的某个类实例,而且具有攻击者选定的值。换句化话说,解除序列化是一种公共的构建函数,允许攻击者选择对象的状态 -- 显然是一个危险的操作! 要防止这一点,需要在类里增加如下方法:

private final void readObject(ObjectInputStream in) throws java.io.IOException {

throw new java.io.IOException(\"Class cannot be deserialized\"); }

18. 不要通过名称来比较类。毕竟攻击者可以用相同的名称定义类,而且一不小心就会授予这些类不恰当的权限。因此,下面是一个判断某个对象是否含有某个给定类的错误方法的例子:

if (obj.getClass().getName().equals(\"Foo\")) {

19. 如果要判断两个对象是否含有完全相同的类,不要对双方使用getClass()并使用“==”操作符进行比较,而应该使用如下形式: if (a.getClass() == b.getClass()) {

20. 如果确实需要判断某个对象是否含有某个给定类名,需要严格按照规范并确保使用当前名称空间(当前类的ClassLoader所在名称空间)。因此,应该使用如下形式: if (obj.getClass() == this.getClassLoader().loadClass(\"Foo\")) { 21. 本原则来自McGraw和Felten,而且确实是个好原则。要补充的是,尽可能地避免比较类值通常是个好注意。通常最好是尽力设计类的方法和接口,从而完全不必要做这些事。尽管如此,实际上无法完全做到,所以知道这些技巧还是很重要的。

22. 不要把秘密(密钥、密码或算法)存储在代码或数据里。有恶意的JVM可以迅速看到这一数据。打乱代码并不能在认真的攻击者面前实际隐藏代码。

Chapter 10. 专题

Table of Contents 密码 随机数

加密算法与协议 PAM 其它事项

Understanding is a fountain of life to those who have it, but folly brings punishment to fools.

Proverbs 16:22 (NIV)

密码

尽可能地不要编写处理密码的代码。特别是对于本地应用程序,尽量依靠普通的用户登录认证。如果应用程序为CGI脚本,则尽量依靠web服务器来提供保护。如果应用程序是在网络上的,避免用明文发送密码(如果可能的话),因为它很容易被网络窥探者俘获并在此后加以利用。使用某些算法的固定密码或使用某种掩护算法来“加密”密码从本质上来说跟用明文发送密码是一样的。

对于网络,至少要考虑使用摘要密码。摘要密码是从散列密码发展来的;一般服务器程序向客户程序发送一些数据(如日期、时间、服务器程序名称),客户程序把该数据与用户密码结合,再用散列处理该值(所谓的“摘要密码”)并把散列处理后的结果回答给服务器程序;服务器程序核实此散列值。这会起作用,因为实际上没有以任何形式发送密码;密码只是用来导出散列值。摘要密码不能被认为是通常意义上的“加密”,甚至在法律限制为了保密进行加密的国家一般也可以接受。摘要密码容易受到主动攻击的威胁,但可以抵御被动的网络窥探者。摘要密码的一个弱点在于服务器必须拥有所有未经散列处理的密码,这使得服务器成为攻击的一个非常诱人的目标。

如果应用程序必须处理密码,应该在使用后立刻重写它们以减少暴露的危险。在Java中,不要用String类型来存储密码,因为String是不可改变的(在收集内存垃圾和重用之前无法重写它们,可能会存在很长时间)。作为替代,在Java中使用char[]来保存密码,这样就可以立刻被重写。 I

如果应用程序允许用户设置密码,则检查密码并只允许“好的”密码(如不在字典中、有特定的最小长度等等)。可以参考一下如下信息

http://consult.cern.ch/writeup/security/security_3.html 以了解如何选择一个好的密码。

随机数

很多库例程产生的“随机”数是准备用于仿真、游戏等等;它们在被用于密钥生成一类的安全函数时是不够随机的。其问题在于这些库例程使用的算法的未来值可以被攻击者轻易地推导出来(虽然看起来它们可能是随机的)。对于安全函数,需要的随机值应该是基于量子效应之类的确实无法预测的值。

Linux内核(1.3.30以上)包括了一个随机数发生器,对于很多安全目的是足够的。该随机数发生器收集来自设备驱动程序和其它来源的环境噪音放入一个熵池。在作为/dev/random被访问时,只返回熵池中预计的噪音比特数之内的随机字节(如果熵池为空,则调用被阻塞,直到收集了附加的环境噪音)。在作为/dev/urandom被访问时,即使熵池为空也返回所要求的那么多字节。如果用随机值作加密用途(如产生一个密钥),则应使用/dev/random。更多的信息可以在系统文档random(4)中找到。

加密算法与协议

在保证系统安全时,加密算法与协议通常是必需的,特别是在通过Internet一类不可靠网络进行通信时。如果可能,使用过程加密来防止过程拦截,并隐藏认证信息,这样还同时支持了对隐私的保护。

要使加密算法与协议变得正确是困难的,所以不要创建自己的加密算法与协议。作为替代,应当使用现有的遵从标准的协议,如SSL、SSH、IPSec、GnuPG/PGP和Kerberos。只使用公开发表并经受了多年攻击考验的加密算法(比如三重DES,它还不受专利的困扰)。特别是不要创建你自己的加密算法,除非你是密码学专家而且知道自己在做什么;创建这样的算法是专家才能胜任的工作。 与之相关的一个注意事项是,如果必须创建自己的通信协议,应该检查一下此前出现过什么问题。Bellovin [1989]对TCP/IP协议族安全问题的评论一类的经典文献可能会有所帮助,同样的文献还有Bruce Schneier [1998]和Mudge对Microsoft的PPTP实现的破坏以及随后的工作。当然,要确保任何新的协议都接受了广泛的审阅,并尽量重用已有的工作。

对于背景信息和代码,可能应该看一下经典文献“应用密码学”[Schneier 1996]。Linux特有的资源包括Linux加密HOWTO

http://marc.mutz.com/Encryption-HOWTO/。

PAM

可插入认证模块(PAM)是一个用于认证用户的灵活机制。很多类Unix系统都支持PAM,包括Solaris、几乎所有Linux发行版(如Red Hat Linux、Caldera和2.2版本以上的Debian)和3.1版本以上的FreeBSD。通过使用PAM,你所编写的程序可以独立于确认计划(如密码、智能卡等)。从根本上来说,程序调用PAM,在运行时刻通过检查本地系统管理员设定的配置来决定需要哪一个“认证模块”。如果你所编写的程序要求认证(如输入密码),则应当包含对PAM的

支持。可以在 http://www.kernel.org/pub/linux/libs/pam/index.html 找到更多有关Linux-PAM项目的资料。

其它事项

程序在使用自己的某些假设之前至少应对它们进行检查(例如在程序的开头)。例如程序依赖于某个给定目录设置的“sticky”位,对它进行一下测试;这样的测试只需要很少的时间,就可以防止出现严重问题。如果担心对每个调用都进行测试会增加执行时间,至少在安装时需要执行测试,或者至少在应用程序启动时执行测试更好一些。

对程序启动、过程启动和可疑的活动编写审计日志。有关值的可能信息包括日期、时间、uid、euid、gid、egid、终端信息、进程ID和命令行的值等。函数syslog(3)对实现审计日志很有帮助。这里存在着一个危险,即用户通过执行大量产生审计记录的事件直到系统耗尽存储记录的资源,可以制造出拒绝服务攻击(或至少停止审计)。对付此威胁的一个方法是对审计记录的记录频率加以限制;如果有“太多”的审计记录产生则有意放慢响应的速度。放慢响应速度可能只应该适用于有攻击者嫌疑的用户,但在很多情况下,单个攻击者可以伪装成潜在的很多用户。 如果存在内建的脚本语言,可能该语言会设置某个对执行脚本的程序有不友好影响的环境变量。要防止出现这样的情况。

如果需要某种复杂的配置语言,要确保该语言有注解字符,并且包含许多有注解的安全范例。通常“#”被用作注解,意味着“本行的剩余部分为注解”。 如果可能的话,不要创建setuid或setgid为root的程序;而让用户作为root登录。

为代码加上签名。这样,其他人可以检查得到的内容是否做过改动。

考虑静态连接安全程序。这样就可以确定安全程序不使用它,从而防止对动态连接库机制的攻击。

在审读代码时,考虑匹配不成立的所有情况。例如,如果有switch语句,如果一个情况都不匹配时会发生什么?如果有“if”语句,如果条件为假时会发生什么?

Chapter 11. 结论

The end of a matter is better than its beginning, and patience is better than pride.

Ecclesiastes 7:8 (NIV)

在Linux和Unix这样的类Unix系统上设计并实现一个真正安全的程序实际上是一项困难的工作。其困难在于,一个真正安全的程序必须对所有可能的输入以及被某个潜在怀有敌意的用户控制的环境变量做出恰当的响应。安全程序的开发者必须对他们所用的平台具有深刻的理解,寻找并使用手册指南(例如本文),并采用保证过程(例如仔细检阅)来减少程序中的薄弱环节。 最后,下面是本文中的一些关键指导原则:

• • •

• •

证实所有的输入,包括命令行输入、环境变量、CGI输入,等等。不要仅仅拒绝“坏的”输入;定义“可接受”的输入并拒绝不符合的任意输入。 避免缓存溢出。这是当前主要的编程错误。

设计程序内部结构。确保接口的安全、最小化特权、使初始配置和缺省值安全以及安全失败。避免竞争状态,只信任可信赖的通道(例如绝大多数服务器程序都不应该信任其客户程序的安全检查)。

小心对其它资源的调用输出。限制调用输出值为合法值(特别注意转义字符),并检查系统调用的所有返回值。 明断地发回信息。特别是最小化反馈并处理对某个不可信用户的完整的或不响应的输出。

Chapter 12. 参考文献

The words of the wise are like goads, their collected sayings like firmly embedded nails--given by one Shepherd. Be warned, my son, of anything in addition to them. Of making many books there is no end, and much study wearies the body.

Ecclesiastes 12:11-12 (NIV)

注意,此处对WEB上可得到的技术文章给予了特别的重视,因为可获得的大多数此类技术信息都是以这种方式存在的。

[Al-Herbish 1999] Al-Herbish, Thamer. 1999. Secure Unix Programming FAQ. http://www.whitefang.com/sup.

[Aleph1 1996] Aleph1. November 8, 1996. ``Smashing The Stack For Fun And Profit''. Phrack Magazine. Issue 49, Article 14.

http://www.phrack.com/search.phtml?view&article=p49-14 or alternatively http://www.2600.net/phrack/p49-14.html.

[Anonymous 1999] Anonymous. October 1999. Maximum Linux Security: A Hacker's Guide to Protecting Your Linux Server and Workstation Sams. ISBN: 0672316706.

[Anonymous 1998] Anonymous. September 1998. Maximum Security : A Hacker's Guide to Protecting Your Internet Site and Network. Sams. Second Edition. ISBN: 0672313413.

[AUSCERT 1996] Australian Computer Emergency Response Team

(AUSCERT) and O'Reilly. May 23, 1996 (rev 3C). A Lab Engineers Check List for Writing Secure Unix Code.

ftp://ftp.auscert.org.au/pub/auscert/papers/secure_programming_checklist [Bach 1986] Bach, Maurice J. 1986. The Design of the Unix Operating System. Englewood Cliffs, NJ: Prentice-Hall, Inc. ISBN 0-13-201799-7 025. [Bellovin 1989] Bellovin, Steven M. April 1989. \"Security Problems in the TCP/IP Protocol Suite\" Computer Communications Review 2:19, pp. 32-48. http://www.research.att.com/~smb/papers/ipext.pdf

[Bellovin 1994] Bellovin, Steven M. December 1994. Shifting the Odds -- Writing (More) Secure Software. Murray Hill, NJ: AT&T Research. http://www.research.att.com/~smb/talks

[Bishop 1996] Bishop, Matt. May 1996. ``UNIX Security: Security in Programming''. SANS '96. Washington DC (May 1996). http://olympus.cs.ucdavis.edu/~bishop/secprog.html

[Bishop 1997] Bishop, Matt. October 1997. ``Writing Safe Privileged Programs''. Network Security 1997 New Orleans, LA. http://olympus.cs.ucdavis.edu/~bishop/secprog.html

[CC 1999] The Common Criteria for Information Technology Security Evaluation (CC). August 1999. Version 2.1. Technically identical to International Standard ISO/IEC 15408:1999. http://csrc.nist.gov/cc/ccv20/ccv2list.htm

[CERT 1998] Computer Emergency Response Team (CERT) Coordination Center (CERT/CC). February 13, 1998. Sanitizing User-Supplied Data in CGI Scripts. CERT Advisory CA-97.25.CGI_metachar.

http://www.cert.org/advisories/CA-97.25.CGI_metachar.html.

[CMU 1998] Carnegie Mellon University (CMU). February 13, 1998 Version 1.4. ``How To Remove Meta-characters From User-Supplied Data In CGI Scripts''. ftp://ftp.cert.org/pub/tech_tips/cgi_metacharacters.

[Cowan 1999] Cowan, Crispin, Perry Wagle, Calton Pu, Steve Beattie, and Jonathan Walpole. ``Buffer Overflows: Attacks and Defenses for the

Vulnerability of the Decade''. Proceedings of DARPA Information Survivability Conference and Expo (DISCEX), http://schafercorp-ballston.com/discex To appear at SANS 2000, http://www.sans.org/newlook/events/sans2000.htm. For a copy, see http://immunix.org/documentation.html.

[Fenzi 1999] Fenzi, Kevin, and Dave Wrenski. April 25, 1999. Linux Security HOWTO. Version 1.0.2.

http://www.linuxdoc.org/HOWTO/Security-HOWTO.html

[FHS 1997] Filesystem Hierarchy Standard (FHS 2.0). October 26, 1997.

Filesystem Hierarchy Standard Group, edited by Daniel Quinlan. Version 2.0. http://www.pathname.com/fhs.

[FOLDOC] Free On-Line Dictionary of Computing. http://foldoc.doc.ic.ac.uk/foldoc/index.html.

[FreeBSD 1999] FreeBSD, Inc. 1999. ``Secure Programming Guidelines''. FreeBSD Security Information.

http://www.freebsd.org/security/security.html

[FSF 1998] Free Software Foundation. December 17, 1999. Overview of the GNU Project. http://www.gnu.ai.mit.edu/gnu/gnu-history.html

[Galvin 1998a] Galvin, Peter. April 1998. ``Designing Secure Software''.

Sunworld. http://www.sunworld.com/swol-04-1998/swol-04-security.html. [Galvin 1998b] Galvin, Peter. August 1998. ``The Unix Secure Programming FAQ''. Sunworld.

http://www.sunworld.com/sunworldonline/swol-08-1998/swol-08-security.html

[Garfinkel 1996] Garfinkel, Simson and Gene Spafford. April 1996. Practical UNIX & Internet Security, 2nd Edition. ISBN 1-56592-148-8. Sebastopol, CA: O'Reilly & Associates, Inc. http://www.oreilly.com/catalog/puis

[Garfinkle 1997] Garfinkle, Simson. August 8, 1997. 21 Rules for Writing Secure CGI Programs. http://webreview.com/wr/pub/97/08/08/bookshelf [Graham 1999] Graham, Jeff. May 4, 1999. Security-Audit's Frequently Asked Questions (FAQ). http://lsap.org/faq.txt

[Gong 1999] Gong, Li. June 1999. Inside Java 2 Platform Security. Reading, MA: Addison Wesley Longman, Inc. ISBN 0-201-31000-7.

[Gundavaram Unknown] Gundavaram, Shishir, and Tom Christiansen. Date Unknown. Perl CGI Programming FAQ.

http://language.perl.com/CPAN/doc/FAQs/cgi/perl-cgi-faq.html

[Hall \"Beej\" 1999] Hall, Brian \"Beej\". Beej's Guide to Network Programming Using Internet Sockets. 13-Jan-1999. Version 1.5.5. http://www.ecst.csuchico.edu/~beej/guide/net

[Kernighan 1988] Kernighan, Brian W., and Dennis M. Ritchie. 1988. The C Programming Language. Second Edition. Englewood Cliffs, NJ: Prentice-Hall. ISBN 0-13-110362-8.

[Kim 1996] Kim, Eugene Eric. 1996. CGI Developer's Guide. SAMS.net Publishing. ISBN: 1-57521-087-8 http://www.eekim.com/pubs/cgibook Kuchling [2000]. Kuchling, A.M. 2000. Restricted Execution HOWTO. http://www.python.org/doc/howto/rexec/rexec.html

[McClure 1999] McClure, Stuart, Joel Scambray, and George Kurtz. 1999. Hacking Exposed: Network Security Secrets and Solutions. Berkeley, CA: Osbourne/McGraw-Hill. ISBN 0-07-212127-0.

[McKusick 1999] McKusick, Marshall Kirk. January 1999. ``Twenty Years of Berkeley Unix: From AT&T-Owned to Freely Redistributable.'' Open Sources: Voices from the Open Source Revolution.

http://www.oreilly.com/catalog/opensources/book/kirkmck.html.

[McGraw 1999] McGraw, Gary, and Edward W. Felten. January 25, 1999.

Securing Java: Getting Down to Business with Mobile Code, 2nd Edition John Wiley & Sons. ISBN 047131952X. http://www.securingjava.com.

[McGraw 2000] McGraw, Gary and John Viega. March 1, 2000. Make Your Software Behave: Learning the Basics of Buffer Overflows.

http://www-4.ibm.com/software/developer/library/overflows/index.html. [Miller 1999] Miller, Todd C. and Theo de Raadt. ``strlcpy and strlcat --

Consistent, Safe, String Copy and Concatenation'' Proceedings of Usenix '99. http://www.usenix.org/events/usenix99/millert.html and

http://www.usenix.org/events/usenix99/full_papers/millert/PACKING_LIST

[Mudge 1995] Mudge. October 20, 1995. How to write Buffer Overflows. l0pht advisories. http://www.l0pht.com/advisories/bufero.html. [NCSA] NCSA Secure Programming Guidelines.

http://www.ncsa.uiuc.edu/General/Grid/ACES/security/programming. [Open Group 1997] The Open Group. 1997. Single UNIX Specification, Version 2 (UNIX 98).

http://www.opengroup.org/online-pubs?DOC=007908799.

[OSI 1999]. Open Source Initiative. 1999. The Open Source Definition. http://www.opensource.org/osd.html.

[Pfleeger 1997] Pfleeger, Charles P. 1997. Security in Computing. Upper Saddle River, NJ: Prentice-Hall PTR. ISBN 0-13-337486-6.

[Phillips 1995] Phillips, Paul. September 3, 1995. Safe CGI Programming. http://www.go2net.com/people/paulp/cgi-security/safe-cgi.txt [Raymond 1997] Raymond, Eric. 1997. The Cathedral and the Bazaar. http://www.tuxedo.org/~esr/writings/cathedral-bazaar

[Raymond 1998] Raymond, Eric. April 1998. Homesteading the Noosphere. http://www.tuxedo.org/~esr/writings/homesteading/homesteading.html [Ranum 1998] Ranum, Marcus J. 1998. Security-critical coding for programmers - a C and UNIX-centric full-day tutorial. http://www.clark.net/pub/mjr/pubs/pdf/.

[RFC 822] August 13, 1982 Standard for the Format of ARPA Internet Text Messages. IETF RFC 822. http://www.ietf.org/rfc/rfc0822.txt.

[rfp 1999]. rain.forest.puppy. ``Perl CGI problems''. Phrack Magazine. Issue 55, Article 07. http://www.phrack.com/search.phtml?view&article=p55-7 or http://www.insecure.org/news/P55-07.txt.

[St. Laurent 2000] St. Laurent, Simon. February 2000. XTech 2000 Conference Reports. ``When XML Gets Ugly''.

http://www.xml.com/pub/2000/02/xtech/megginson.html.

[Saltzer 1974] Saltzer, J. July 1974. ``Protection and the Control of

Information Sharing in MULTICS''. Communications of the ACM. v17 n7. pp. 388-402.

[Saltzer 1975] Saltzer, J., and M. Schroeder. September 1975. ``The Protection of Information in Computing Systems''. Proceedings of the IEEE. v63 n9. pp. 1278-1308. http://www.mediacity.com/~norm/CapTheory/ProtInf. Summarized in [Pfleeger 1997, 286].

[Schneier 1996] Schneier, Bruce. 1996. Applied Cryptography, Second

Edition: Protocols, Algorithms, and Source Code in C. New York: John Wiley and Sons. ISBN 0-471-12845-7.

[Schneier 1998] Schneier, Bruce and Mudge. November 1998. Cryptanalysis of Microsoft's Point-to-Point Tunneling Protocol (PPTP) Proceedings of the 5th ACM Conference on Communications and Computer Security, ACM Press. http://www.counterpane.com/pptp.html.

[Schneier 1999] Schneier, Bruce. September 15, 1999. ``Open Source and Security''. Crypto-Gram. Counterpane Internet Security, Inc. http://www.counterpane.com/crypto-gram-9909.html

[Seifried 1999] Seifried, Kurt. October 9, 1999. Linux Administrator's Security Guide. http://www.securityportal.com/lasg.

[Shankland 2000] Shankland, Stephen. ``Linux poses increasing threat to Windows 2000''. CNET.

http://news.cnet.com/news/0-1003-200-1549312.html

[Shostack 1999] Shostack, Adam. June 1, 1999. Security Code Review Guidelines. http://www.homeport.org/~adam/review.html.

[Sitaker 1999] Sitaker, Kragen. Feb 26, 1999. How to Find Security Holes http://www.pobox.com/~kragen/security-holes.html and http://www.dnaco.net/~kragen/security-holes.html

[SSE-CMM 1999] SSE-CMM Project. April 1999. System Security

Engineering Capability Maturity Model (SSE CMM) Model Description Document. Version 2.0. http://www.sse-cmm.org

[Stein 1999]. Stein, Lincoln D. September 13, 1999. The World Wide Web Security FAQ. Version 2.0.1

http://www.w3.org/Security/Faq/www-security-faq.html

[Thompson 1974] Thompson, K. and D.M. Richie. July 1974. ``The UNIX Time-Sharing System''. Communications of the ACM Vol. 17, No. 7. pp. 365-375.

[Torvalds 1999] Torvalds, Linus. February 1999. ``The Story of the Linux Kernel''. Open Sources: Voices from the Open Source Revolution. Edited by Chris Dibona, Mark Stone, and Sam Ockman. O'Reilly and Associates. ISBN 1565925823. http://www.oreilly.com/catalog/opensources/book/linus.html [Unknown] SETUID(7) http://www.homeport.org/~adam/setuid.7.html. [Van Biesbrouck 1996] Van Biesbrouck, Michael. April 19, 1996. http://www.csclub.uwaterloo.ca/u/mlvanbie/cgisec.

[Webber 1999] Webber Technical Services. February 26, 1999. Writing Secure Web Applications. http://www.webbertech.com/tips/web-security.html. [Wood 1985] Wood, Patrick H. and Stephen G. Kochan. 1985. Unix System Security. Indianapolis, Indiana: Hayden Books. ISBN 0-8104-6267-2. [Wreski 1998] Wreski, Dave. August 22, 1998. Linux Security Administrator's Guide. Version 0.98.

http://www.nic.com/~dave/SecurityAdminGuide/index.html

[Yoder 1998] Yoder, Joseph and Jeffrey Barcalow. 1998. Architectural Patterns for Enabling Application Security. PLoP '97

http://st-www.cs.uiuc.edu/~hanmer/PLoP-97/Proceedings/yoder.pdf

因篇幅问题不能全部显示,请点此查看更多更全内容

Top