环境变量

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

有些环境变量是危险的

有些环境变量是危险的,因为很多库和程序被环境变量以某些隐含、模糊或未公开的方式所控制。例如,shbash 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在<unistd.h>中定义,C/C++用户需要#include该头文件。在产生线程前需要处理该值,但这几乎不成问题,因为在程序执行的开始阶段就需要进行这些处理。另一个清除环境的方式是使用未公开的函数clearenv()。clearenv()有个奇怪的历史;有人建议在POSIX.1中定义它,但不知什么原因它没有进入标准。尽管如此,POSIX.9(绑定POSIX的Fortran 77)中定义了clearenv(),所以它具有了半官方的地位。clearenv()定义在<stdlib.h>,但在使用#include包含它之前,必须确定__USE_MISC已经#defined。

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

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

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