内容


安全编程

警惕输入

找出并保护程序的入口

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 安全编程

敬请期待该系列的后续内容。

此内容是该系列的一部分:安全编程

敬请期待该系列的后续内容。

早在 2001 年,许多大公司安装了应用程序“SAP R/3 Web Application Server demo”,却没有发现它有一个致命的漏洞。这个应用程序中有一个名为 saposcol 的程序,没有能保护自己免受恶意输入值的攻击。攻击者可以通过设置 PATH 环境变量来改变 saposcol 寻找其他程序的位置,然后为 saposcol 创建一个恶意的“扩展”程序来运行。由于 saposcol 具有 setuid root 权限,这就意味着因为这一个编程上的失误,本地用户可以 (作为 root)很快地获得整个计算机系统的控制权(参阅 参考资料 中的链接,以深入了解这一点及本文中提到的其他相关内容)。

本专栏的前一期文章 指出了一些常见的输入数据类型以及检查它们的方法。但是如果您不知道所有的数据自何处而来,那么,只是知道如何检查数据类型是不够的。本文讨论了数据进入您的程序的各种不同途径 —— 有一些并不是显而易见的 —— 并着重讨论了如何适当地去处理它们。

如果您不控制,攻击者就会来控制

安全的程序中第一道防线是检查每一个不可信的输入。但是这是什么意思呢?可以归结为以下三点:

  1. 限制程序暴露的部分。如果您的程序分为若干块 —— 这通常是一个好主意 —— 那么尽量设计得让攻击者根本不能与大多数块通信。这包括不能让他们利用各块之间的通信路径。如果攻击者不能查看、修改或者插入他们自己的数据到那些通信路径中(包括作为块间的中间人潜入),那是最好了。如果那不可能 —— 比如当块之间使用网络通信时 —— 那么使用加密等机制来防范攻击者。后续的文章将更深入地讨论这一问题。
  2. 限制暴露部分所允许的输入类型。有时您可以修改设计以使只有少数的输入可以接受 —— 如果可以,那么就这样做吧。
  3. 严格检查不可信的输入。真正“安全”的程序应该没有任何输入,但那种程序是没有用处的。因而,您需要对来自于不可信源的输入路径的数据进行严格的检查。 前一期文章 论述了如何检查各种不同类型的数据;本文将帮助您确定这些数据的来源。这并不是说您 需检查进入您的程序的数据。通常明智的做法是检查多个位置的数据,但是您必须至少检查所有的数据一次,并且明智的做法是至少在数据第一次进入时进行一次检查。

程序的类型决定一切

您必须检查所有不可信的输入 —— 但是什么是不可信的输入呢?其中一些取决于您的程序要做什么。如果您的程序是数据的浏览器或者编辑器(比如文字处理器或者图像显示器),而这些数据有可能来自攻击者,所以那是不可信的输入。如果您的程序响应网络上的请求,那些请求可能正是来自攻击者 —— 所以网络连接是不可信的输入。

另一个重要的因素是您的程序是如何设计的。如果您的程序运行时身份是“root”或者其他一些特权用户,或者有对数据(比如数据库中的数据)的访问特权,那么从程序中没有特权的部分到那些有特权的部分的输入是不可信的。

尤其重要的情形是所有的“setuid”或者“setgid”的程序。只是运行一个 setuid/setgid 程序就会获得特权,这些程序特别难以保证安全。为什么呢?因为 setuid/setgid 程序有特别多的输入 —— 它们中很多输入多得惊人 —— 可以被攻击者控制。

常见的输入源

下面的章节将讨论一些常见的输入以及如何处理这些输入。当您编写程序的时候这些输入每一个都应该考虑,如果它们不可信,一定要谨慎对它们进行过滤。

环境变量

环境变量可能令人难以置信地危险,尤其是对那些 setuid/setgid 程序及它们调用的程序。危险的原因在于以下三个方面:

  1. 许多库和程序由环境变量以非常含糊的方式控制着 —— 实际上,很多都完全没有文档化。命令 shell /bin/sh 使用 PATH 和 IFS 等环境变量,程序加载器 ld.so (/lib/ld-linux.so.2) 使用 LD_LIBRARY_PATHLD_PRELOAD 等环境变量,很多程序使用 TERMHOMESHELL 环境变量 —— 所有这些环境变量都可用于开发程序。这样的环境变量数不胜数;对调试来说它们很多都是晦涩的变量,并且将它们全部列出也是无济于事的。实际上,您不可能了解全部环境变量,因为有一些并没有文档化。
  2. 环境变量是继承而来的。如果程序 A 调用 B,而 B 调用 C,C 调用 D,那么程序 D 将获得的环境变量就是程序 A 所获得的环境变量,除非有一些程序在这个过程中对其进行了改动。这就意味着,如果 A 是一个安全的程序,而 D 的开发者为了调试方便而增加了一个没有文档化的环境变量,那么程序 D 的这个附加的环境变量就会成为程序 A 的一个漏洞!这种继承不是偶然的 —— 这是为了使环境变量有用 —— 但是这也使之成为一个严重的安全问题。
  3. 环境变量可以被本地运行的攻击者 完全控制,而且攻击者可以用不同寻常的方式来利用这一点。如 environ(5) 手册页(参阅 参考资料)中所描述,环境变量在内部作为字符指针数组来存储(数组以一个 NULL 指针结束),每一个字符指针指向一个形式为 NAME=value (这里 NAME 是环境变量名) 的以零字符结尾的(NIL-terminated)字符串。这一细节的重要性何在?这是因为攻击者可能会做一些不合常理的事情,例如为同一个环境变量名创建多个值(比如两个不同的 LD_LIBRARY_PATH 值)。这可以很容易地导致库使用环境变量去做意想不到的事情,有可能被利用。GNU glibc 库对此有防范的例程,但是使用环境变量的其他库和任何例程可能很快陷入困境。

有一些情形下,程序经过了修改,以使得难以利用它们来使用环境变量。历史上,很多攻击利用的是命令 shell 处理 IFS 环境变量的方法,但是当今大部分的 shell(包括 GNU bash)已经经过了修改,从而使 IFS 难以利用。

不幸的是,尽管这一加固措施是一个好主意,但它还是不够 —— 您还是需要谨慎地去处理环境变量。在 Unix 类系统上所有的程序如何运行,这是一个特别重要的(虽然难以理解)例子。Unix 类系统(包括 GNU/Linux)首先通过系统加载器来运行程序(在大部分 GNU/Linux 系统中这个加载器是 /lib/ld-linux.so.2),它可以定位并加载所需要的共享库。这个加载器通常由环境变量来控制。

在大部分 Unix 类系统中,加载器通常在环境变量 LD_LIBRARY_PATH 中列出的目录中开始搜索库。我应该说明一下, LD_LIBRARY_PATH 被很多 Unix 类系统使用,但不是全部都用;HP-UX 用的是环境变量 SHLIB_PATH ,AIX 用的是 LIBPATH 。而且,在 GNU-based 系统(包括 GNU/Linux)中,环境变量 LD_PRELOAD 所列出的库首先加载,并且优先于所有其他的库。

问题是,如果攻击者可以控制程序用到的底层库,那么攻击者就可以控制整个程序。例如,假设攻击者可以运行 /usr/bin/passwd(一个可以改变您的口令的特权程序),但却用环境变量去改变这个程序用到的库。攻击者可以编写自己的口令加密函数 crypt(3), 然后当特权程序尝试调用这个库时,攻击者可以让这个程序来做任何事情 —— 包括允许永久地、无限制地控制整个系统。当前,加载器通过检测程序是否设置了 setuid/setgid 来防范这一问题,如果设置了,它们就会忽略 LD_PRELOADLD_LIBRARY_PATH 环境变量。

那么,我们安全了吗?没有。如果恶意的 LD_PRELOAD 或者 LD_LIBRARY_PATH 值没有被 setuid/setgid 程序清除,它将被传递到其他程序,并导致出现加载器试图去防范的问题。因而,虽然加载器让编写安全的程序成为 可能,但您还不得不去防范恶意的环境变量。而且,这还不能处理那些没有文档化的环境变量的问题。

对于安全的 setudi/setgid 程序来说,惟一可靠的办法是,始终在程序开始时“提取并清除”环境变量:

  • 提取出您确实需要的环境变量(如果有)。
  • 清除所有的环境变量。在 C/C++ 中,通过包含 <unistd.h> 并将 environ 变量设为 NULL 可以清除环境变量(尽早做这些事情,特别是在创建线程之前)。
  • 只将您所需要的环境变量设置为可靠的值。您几乎肯定要重新添加的一个环境值是 PATH ,它是搜索程序的目录列表。典型的 PATH 应该只是设置为 /bin:/usr/bin,或者一些类似的值。不要向 PATH 中添加当前路径“.”,或者甚至一个空条目(这样在开始和结束的冒号可被利用)。典型的,您还需要设置 IFS (设置为它默认的“ \t\n” —— 空格、制表符和新行)和 TZ(时区)。其他您可能需要设置的是 HOMESHELL 。您的应用程序可能还需要更多,但是要限制它们 —— 除非是特别需要,否则不要接受潜在的攻击者的数据。

文件

正如我在前一期文章中提到的,不要信任可以被攻击者设置的文件名。Linux 和 Unix 允许用任意的字符序列来作为文件名,所以,如果您正在使用一个来自攻击者的目录或者接受他的一个文件名,一定要有所准备。攻击者可以创建以“-”开头的文件名,或者含有“&”等特殊字符的文件名,等等。

不要信任可以被不可信用户控制的文件内容。这包括那些被程序浏览或编辑的可能是由攻击者寄来的文件。例如,著名的文本编辑器 vim 版本 5.7,当要编辑一个文件时,将查找一个内置的 statusline 命令来在它的状态行上设置信息,而那个命令又可以执行任意的 shell 程序。攻击者可以用电子邮件给受害者发送特别处理过的文件,如果受害者用 vim 来阅读或者编辑它,受害者就可能会去运行攻击者想要运行的任何程序!!!

避免从当前目录中获得配置信息,因为用户可能会浏览一个由攻击者控制的目录,攻击者在那里创建了一个恶意的配置文件(例如,攻击者可能已经发送了一个包括数据和恶意配置文件的压缩目录)。而应该从 /etc、用户的主目录和/或桌面环境的库中获得配置信息。通常,将配置信息以及其他信息存储在“~/. program-name”文件中是很方便的;文件名最前的句点是为了让它不影响正常的显示。如果您真正必须要从当前目录下得到配置信息,那么要非常严格地检查其中的所有数据。

不要让攻击者控制任何临时文件。我建议,如果一个用户是可信的,那么将临时目录放在那个用户的主目录下。如果这不可接受,那么要用安全的方法来创建和使用临时文件(我将在后一期文章中讨论如何安全地创建临时文件)。

文件描述符

不怀好意的攻击者可能启动一个程序而只是对它的标准输入、标准输出或者标准错误做一些奇怪的事情。例如,攻击者可能会关闭它们中的一个或多个,以使得您打开的下一个文件同时也是正常的输出位置。这对 setuid/setgid 程序来说尤其是一个问题。当前的一些 Unix 类系统已经可以防范这一问题,但不是所有的系统都可以。

setuid/setgid 程序防范这一攻击的一种方法是,使用 open() 反复打开 /dev/null 直到文件描述符的值大于 2(您必须在打开文件前做这件事情,最好是在程序初始化时)。然后,如果对 open() 的第一次调用返回的是 2 或者更小,那么不输出任何消息并退出。通过先反复打开 /dev/null,您自己保护了自己 —— 如果您偶尔试图去打开文件并输出错误消息时,不会再发生坏的事情。在这种情况下不需要输出错误消息,因为文件描述符 0 到 2 只是在攻击者试图去搅乱您的程序的时候才会被关闭。

命令行

程序启动时可以接受来自命令行的数据 —— 但是您可以相信这些数据吗?setuid/setgid 程序尤其不能。如果您不能相信这些数据,那就要自己做好一切准备,包括大的参数、大量的参数、不可知的字符,等等。注意,程序的名字只是命令行值的第 0 个参数 —— 不要相信程序名,因为攻击者会改变它。

不但如此,还要尽力去设计您的命令行语法,以使它更容易安全地使用。例如,支持用标准的“--”(双破折号)选项来表示“不再有选项”,这样脚本就可以使用这个选项来防止攻击者通过创建以破折号开头的文件名 (如“-fr”)来攻击。否则,攻击者可以创建“-fr”文件,并让用户运行“yourcommand *”;这时您的程序可能会将文件名(“-fr”)曲解为一个选项。

图形用户界面(GUI)

这里是应对灾难的一个方案:一个进程拥有特别的特权(例如,如果它设置了 setuid/setgid),它使用操作系统的图形用户界面(GUI)库,而且 GUI 用户不是完全可信任的。问题是,GUI 库(包括 Unix 的、Linux 的和 Windows 的)并不是设计这样使用的。这样做也不现实 —— GUI 库很大,而且依赖于庞大的基础结构,所以很难为了安全性而去分析所有的代码。GTK+ GUI 库当发现它是在 setuid 程序中运行时甚至会停止,因为它没有假定会这样使用(感谢 GTK+ 的开发者主动预防了这一安全问题)。

难道这意味着您只能使用命令行?不是。将您的程序分为小一些的部分,用没有特权的部分去实现 GUI,用单独的部分去实现需要特权的操作。下面是一些常见的做法:

  • 通常,简单的方法是将有特权的操作作为命令行程序来实现,由 GUI 调用 —— 那样您可以“无偿地”获得 GUI 和命令行界面(CLI),从而简化了脚本编写和调试。典型地,CLI 特权程序是一个 setuid/setgid 程序。当然,有特权的程序必须保护自己不受攻击,但是这个方法通常意味着这一部分必须是安全的程序,这些程序应更小并且更容易保护。
  • 如果您需要高速通信,将这个程序作为有特权的程序启动,把它分为可以安全通信的独立的进程,然后将一个进程永久释放特权并去运行 GUI。
  • 另一种方法是实现有特权的服务器来响应请求,并创建 GUI 作为客户机。
  • 使用 Web 界面;创建一个有特权的服务器,然后使用 Web 浏览器作为客户机。这实际上是先前方法的一个特例,但它很灵活,因此通常值得考虑。如任何其他为我们带来网络数据问题的 Web 应用程序一样,您将需要使它安全。

网络数据

如果数据来自于网络,您应该认为它是高度不可信的。不要相信“源 IP”地址、HTTP“Referrer”头的值或者类似的数据所告诉您的数据来自何方;那些来自发送者的值可以被伪造。当心来自域名系统(DNS)的值;DNS 实现的是一个分布式数据库,那些值中有一些可能是攻击者提供的。

如果您有一个客户机/服务器系统,服务器应该永远不要相信客户机。客户机数据在到达服务器前可以被操纵,客户机程序可能已经被修改,或者攻击者可能创建了他们自己的客户机(很多这种情况!)。如果您正在从 Web 浏览器获得数据,不要忘记 Web cookie、HTML 表单数据、URL 等可以被用户设置为任意的值。这是网络购物车应用程序中常见的问题;许多这样的应用程序使用隐藏的 HTML 表单域来存储产品信息(比如价格)和相关信息(比如运费),当用户发送这些值时就盲目地接收。用户不仅能将产品价格设置为更低的值或者零,有时他们还可以设置负值的价格,以得到商品和附加的现金反款。记住,必须检查 所有的数据;有一些网络购物车检查了产品数据却忘记去检查运费。

如果您正在编写一个 Web 应用程序,查询数据时要限制使用 GET 请求。不要让 GET 请求实际上去改变数据(比如传输的钱数)或者进行其他行为。用户很容易被欺骗去点击他们 Web 浏览器中恶意的超链接,这样就会发送 GET 请求。相反,如果您得到的 GET 请求有查询以外的行为,那么返回一个“you asked me to do X,is that okay? (Ok,Cancel)”格式的确认消息。注意,限制 GET 查询不能帮您解决错误的客户机数据的问题(如先前段落所讨论的) —— 服务器还是需要检查来自它们的客户机的数据!

其他来源

程序有很多其他的输入,比如当前目录、信号、内存映射、System V IPC、umask、文件系统状态。有了在这里获得的信息,重要的是不要忽略这些也可以作为输入,即使它们有时看起来不像是输入。

结束语

安全的程序必须检查每一个不可信的输入通道,这样做可以避免很多问题。但是那也还不够。有时,即使只是读入数据也可以是安全漏洞 —— 甚至在数据被检查之前!处理数据可以导致程序以可怕的方式失败。我们将要讨论的是当前第 1 号安全漏洞 —— 缓冲区溢出。我的下一期文章将论述这个漏洞是什么样的,如何进行防范,以及为什么可以期望它未来将不再是问题。


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux
ArticleID=21920
ArticleTitle=安全编程: 警惕输入
publish-date=02102004