网络技术是从1990年代中期发展起来的新技术,它把互联网上分散的资源融为有机整体,实现资源的全面共享和有机协作,使人们能够透明地使用资源的整体能力并按需获取信息。资源包括高性能计算机、存储资源、数据资源、信息资源、知识资源、专家资源、大型数据库、网络、传感器等。 当前的互联网只限于信息共享,网络则被认为是互联网发展的第三阶段。
检查代码中的安全性缺陷,是软件创建过程中的一个关键要素,它与计划、设计和测试同等重要。作者经过数年的代码安全性检查总结出了标识模式和最佳做法,开发人员可以按照该总结来捕获潜在的安全漏洞。首先,要检查代码运行的环境,考虑运行代码的用户的角色,以及研究代码可能存在的任何历史安全性问题。在理解这些背景问题之后,就可以搜寻特定的安全漏洞了,包括
SQL 注入式攻击、交叉站点
脚本和缓冲区溢出。此外,可以搜寻并修改某些红色标记(如变量名“password”、“secret”)和其他明显但常见的安全性错误。
我的大部分工作就是检查别人的代码,寻找安全性错误。不可否认,这并不是我的首要任务(我的首要任务是设计检查和威胁建模),但是,我的确因此而接触到了大量的代码。
希望您明白,检查其他人的代码是一件好事的同时,却并不是创建安全软件的方式。通过设计、编写、测试以及编写有关安全系统的文档,然后安排时间进行安全性检查、培训和工具使用,才可以生产出安全的软件。仅仅依靠设计、编写、测试以及编写有关项目的文档,然后寻找安全性错误并不能创建安全的软件。代码检查只是该过程的一部分,其自身不能创建安全的代码。
在本文中,我不对代码安全漏洞(例如,整数溢出攻击、SQL 注入式攻击以及缓冲区溢出)加以论述;您可以通过阅读书籍(如我撰写的 Writing Secure Code,Microsoft Press®,2002)来了解这些问题。但是,我将从一个较高的角度来审视代码检查过程中要注意的问题。在正式开始之前,尽管我希望指出这就是我检查代码中安全性错误的方式,但是您没有必要以这种方式来检查代码,而且我也不能保证这就是检查某种缺陷的最完美形式。在查看代码时,我会记录下自己头脑中的想法,希望这些想法对您会有所帮助。
我认为有三种方法可用于检查代码:深入分析、快速分析以及混合方法。我倾向于将重点放在混合方法上,因为它的优势在于可快速覆盖大部分范畴;如果我认为某些地方需要进行更深入的分析,我会做个标记以便稍后对它进行代码检查,这可能会牵涉到相关领域其他的专家提示。但是现在,让我来介绍一下最初的快速代码检查,我喜欢称之为 Sweep“n”Tag 方法(扫描与标记方法),该方法快速扫描代码并标记要求进一步检查的代码。以下是我的这一过程的概要。
分配时间和精力
我使用一个优先级系统来确定检查代码需要花费的相关时间。该系统根据潜在的损坏程度建立,这种潜在的损坏程度体现为安全漏洞是否被利用并且是否存在可能的攻击。配额系统基于下列特征:
默认情况下代码是否运行?
是否利用提升的权限运行代码?
是否对网络接口进行代码侦听?
网络接口是否未经身份验证?
代码是否以 C/C++ 编写?
代码是否具有安全漏洞的历史?
安全性研究人员仔细是否审查过该组件?
代码是否处理敏感或者专有数据?
代码是否可重复使用(例如,DLL、C++ 类标头、库或者程序集)?
根据威胁模型,该组件是处于高风险环境中,还是易于遭受很多高风险威胁?
如果我从该列表得到多于三个或四个肯定的回答,那么我将对代码进行更进一步的检查。事实上,如果代码对传输控制协议 (TCP) 或用户数据报协议 (UDP) 套接字进行侦听,并且在默认情况下处于运行状态,那么请准备花费大量时间来检查代码。
在寻找安全性错误的过程中,我倾向于检查三种主要代码类别:C/C++ 代码、Web 服务器应用程序代码(例如,ASP、ASP.NET、CGI 和 Perl )以及托管代码(主要是C#,还有若干 Visual Basic®.NET)。
应该注意,每种语言之间都有一些细微的差异。首先,关于 C 和 C++ 的首要问题是缓冲区溢出。不可否认,还有其他问题存在,但是在同一句话中听到“缓冲区”和“溢出”这些单词时,您几乎就可以确认这是涉及到 C 或 C++ 了。高级语言(例如,C#、Visual Basic .NET 和 Perl)应该没有缓冲区溢出问题。如果有,错误可能存在于运行时环境,而不在检查的代码中。然而,这些语言通常会用于 Web 服务器应用程序代码中,而且会面临其他类型的缺陷。缓冲区溢出最令人讨厌,因为攻击者可以将代码注入到运行进程中并对它进行袭击。因此,让我们首先看看缓冲区溢出。
C 和 C++ 中的缓冲区溢出
缓冲区溢出是软件行业中的棘手问题,您应该尽最大的努力将它们从您的代码中根除。然而,最好是首先确保它们没有进入到代码中。
我有两种检查缓冲区溢出的方法。第一种,标识所有应用程序的入口点,尤其是网络入口点,同时跟踪在代码中移动的数据并审查数据的处理方式。我假定所有的数据格式都不正确。当查看涉及数据(读取数据或写入数据)的任何代码时,我会问“是否存在可导致该代码失败的数据版本呢?”这种方法检查得很彻底,但是很费时。另一种方法是寻找已知的和潜在危险的构造并跟踪数据回到入口点。例如,请看下面的代码:
void fuction(char *p) {
char buff[16];
•••
strcpy(buff,p);
•••
}
如果我看到这样的代码,我将跟踪变量 p 返回到它的源,如果它来自于我不信任的位置,或者它所复制的来源的有效性未经检查,那么我就知道我发现了一个安全性错误。请注意,strcpy 本身并不危险或不安全。而危险的是使函数变得如此可怕的数据。如果检查数据格式正确,则 strcpy 可以是安全的。当然,如果您判断出错,就会有安全性错误了。我也检查了“n”字符串操纵函数(例如 strncpy),因为您还需要检查缓冲区大小计算是否正确。
我对处理标记文件格式的代码比较提防。我指的标记文件由几个块区组成,每个块区都具有说明下一块区数据的标头。MIDI 音乐格式就是一个很好的例子。在名为 quartz.dll(处理 MIDI 文件)的 Windows 组件中发现并修改了一个严重的安全性错误。格式错误的 MIDI 构造将导致处理文件的代码失败,甚至更糟糕。您可以在 Unchecked Buffer in DirectX Could Enable System Compromise 中阅读有关该错误的详细信息。
我要注意的另一个构造如下所示:
while (*s != '\\')
*d++ = *s++;
这个循环受到源中字符的限制,但不受目标大小的限制。基本上,我使用下面的正则表达式来扫描 *x++ = *y++:
\*\w+\+\+\s?=\s?\*\w+\+\+
当然,也可以使用 *++x = *++y,因此您也需要对它进行扫描。我想再一次强调,该构造没有危险(除非源不受信任),因此您需要确定源数据的可信性。
接下来应该注意的是另一个与缓冲区溢出相关的问题:整数溢出漏洞。
C 和 C++ 中的整数溢出
如果您不知道整数溢出攻击是什么以及如何修复它们,那么您应该首先阅读我的文章“Development Impacts of Security Changes in Windows Server 2003”。当执行算法以计算缓冲区大小并且计算导致上溢或下溢时,真正的安全性漏洞会随同这些缺陷一起出现。请看下面的示例:
void func(char *b1, size_t c1, char *b2, size_t c2) {
const size_t MAX = 48;
if (c1 + c2 > MAX) return;
char *pBuff = new char[MAX];
memcpy(pBuff,b1,c1);
memcpy(pBuff+c1,b2,c2);
}
上面的代码看起来没有问题,但如果将 c1 和 c2 相加,结果超过 232-1,您就会意识到有问题了。例如,0xFFFFFFF0 和 0x40 相加的结果为 0x30(十进制为 48)。当这些值用于 c1 和 c2 时,加起来的和可以通过大小检查,然后代码会将大约 4GB 复制到 48 个字节的缓冲区。这样就会出现缓冲区溢出!类似于这样的许多错误都可以被利用,使攻击者将代码注入您的进程中。
当检查 C 和 C++ 代码的整数溢出问题时,我将查找运算符 new 和动态内存分配函数(alloca、malloc、calloc、HeapAlloc 等等)的所有实例,然后确定如何计算缓冲区大小。然后,我会询问自己下列问题:
这些值可以超过特定的最大值吗?
这些值可以小于零吗?
数据是否被截断(将 32 位值复制到 16 位值中,然后复制 32 位大小)?
[page_break] 我的一位 Microsoft 同事的经验是:如果在用于比较的表达式中执行数学运算,那么就有可能上溢或下溢数据。如果将计算用于确定缓冲区大小,情况会更加糟糕,尤其是,如果一个或多个缓冲区大小计算元素已经被攻击者破坏。
任何语言中的数据库访问代码
作为一般规则,开发人员以高级语言(如 C#、scripting 语言以及类似的语言)编写数据库应用程序。相对而言,很少以 C 和 C++ 编写数据库代码,但是有些人使用各种 C/C++ 类库,例如 MFC 中的 CDatabase 类。
其中可以检测到两个问题。第一个是连接字符串,包括硬编码的密码或者使用管理员帐户的连接。可以检测到的第二个问题是 SQL 注入式攻击漏洞。
当我查看托管代码时,首要的事情就是搜索 System.Data 命名空间的所有代码,尤其是System.Data.SqlClient。当我看到这些时,就要格外谨慎了!接下来,我在代码中查找如“connect”这样的单词(它通常出现在连接字符串旁)。该连接字符串有两个需要关注的属性:连接 id(通常写为 uid)和密码(通常写为 pwd)。这些都是潜在的安全漏洞:
DRIVER={SQL Server};SERVER=hrserver;UID=sa;PWD=$esame
事实上,在上面的示例中有两个错误。第一,以系统管理员帐户 sa 进行连接;这违背了授予最低权限这一必要原则。代码永远不应该以系统管理员帐户连接到数据库,因为当该帐户被恶意用户使用时,它会给数据库带来灾难性后果。第二,密码是硬编码的。有两个理由可以说明这是错误的:第一个理由,密码会被发现;其次,如果更改密码,又该如何处理?(您将必须更新所有客户端。)
接下来的主题是 SQL 注入式攻击。SQL 注入的症结在于使用字符串连接来构建 SQL 语句。当扫描代码时,我将查看 SQL 语句的创建位置。一般而言,这涉及搜索诸如“update”、“select”、“insert”、“exec”以及任意我知道使用的表名或数据库名之类的单词。为了帮助解决问题,我使用下面的 ildasm.exe 来审查托管程序集:
ildasm /adv /metadata /out:file test.exe
然后,在生成的输出中分析“User Strings”部分。如果发现任何使用字符串连接的数据库查询,那么这就是一个潜在的安全缺陷,必须使用参数化查询来对其进行修复。
使用字符串连接构建存储过程也不能防止 SQL 注入。简而言之,字符串连接加上 SQL 语句会使情况变糟,而字符串连接加上 SQL 语句再加上系统管理员帐户就无异于一场灾难。
任意语言中的 Web 页代码
基于 Web 页的应用程序中最常见的错误是跨站点脚本 (XSS) 问题。尽管我也会查找其他问题(例如 SQL 注入和拙劣的加密),但是 XSS 错误相当普遍。核心 XSS 漏洞可能会在受害者的浏览器中显示不受信任的用户输入,所以我首先搜索所有将数据发送给用户的代码构造。例如,在 ASP 中查找 Response.Write 和 <%= %> 标记。接下来,分析被写入的数据从而查看它的来源。如果该数据来自 HTTP 实体(例如,窗体或查询字符串),并且没有检查有效性就将它发送到用户的浏览器,那么就会存在 XSS 错误。下面是一个非常简单,但又最为常见的 XSS 示例:
Hello,
<% Response.Write(Request.QueryString("Name")) %>
正如您看到的那样,“Name”参数被发送回用户,而没有首先检查它是否有效及格式规范。
任意一种语言中的机密与加密
一些开发人员喜欢在代码中存储机密数据(例如,密码和加密密钥),并创建自己的不可思议的密码算法。请不要这么做!
我首先寻找变量名和名称中包含“key”、“password”、“pwd”、“secret”、“cipher”以及“crypt”的函数。任何内容都需要加以分析。您可能经常获得貌似正确但实际错误的“密钥”,但是要注意其他几项,它们也许会产生嵌入式的机密数据或“不可思议的”加码系统。搜索密码算法的同时,我也寻找 XOR 运算符,因为它们经常用于加密。最糟糕的代码是使用嵌入式密钥来 XOR 数据流的代码!
Visual Basic 和 C++ 中的 ActiveX 控件
当我检查新的 ActiveX® 控件时,我始终想问一个问题:为什么不使用托管代码来编写?我问这个问题的原因在于,托管代码允许部分信任方案,而 ActiveX 却不是。
接下来,我分析控件的所有方法和属性(.IDL 文件是进行该操作最好的切入点),并且将自己设想为一个进行恶意攻击的人。我能利用这些方法或者属性来做一些什么样的危险事情呢?通常,很多方法以“VerbNoun”格式(动词 + 名词)进行命名,例如 ReadRegistry、WriteFile、GetUserName 和NukeKey,所以我寻找发音复杂的动词和属于敏感资源的名词(资源)。
例如,如果攻击者可以访问用户硬盘上的任何文件,并且可以将它发送到任意位置(例如,攻击者控制下的 Web 站点),那么 SendFile 方法就存在潜在的危险!任何访问用户计算机上的资源的操作都需要进一步的审查。
如果该控件被标记为可安全编写脚本 (SFS),我会进行额外的检查工作,因为该控件可能会在 Web 浏览器中在不警告用户的情况下被调用。如果该控件在安装时执行 ATL IobjectSafetyImpl 接口或设置下面的“可安全编写脚本”或“可安全激活”实现类别,则您可以确定它是否为 SFS:
[HKEY_CLASSES_ROOT\CLSID\<GUID>\Implemented Categories\{7DD95801-9882-11CF-9FA9-00AA006C42C4}][HKEY_CLASSES_ROOT\CLSID\<GUID>\Implemented Categories\{7DD95802-9882-11CF-9FA9-00AA006C42C4}]
我之前曾提到,通过 SendFile 方法访问和发送用户文件不是好的做法。实际上,如果我能够访问 SendFile 方法,并可以基于该方法返回的错误代码来确定用户硬盘驱动器中是否存在文件,那么它就是一个隐私错误。
小结
这是我检查代码时通过的第一个非常高级别的审查。这些错误中的大多数都非常简单,有人可能会争辩说开发人员不应该犯这样的错误,但他们确实会犯这样的错误。然而,了解到有人会对您代码的进行安全性检查这一点,通常会使您将编写更为安全的代码放在第一位。
您可能还注意到,在包含某些缺陷类型的常见问题中,多数是由不受信任的输入造成的。当在检查代码时,您应该始终询问数据从何而来、是否值得信任。
网络的神奇作用吸引着越来越多的用户加入其中,正因如此,网络的承受能力也面临着越来越严峻的考验―从硬件上、软件上、所用标准上......,各项技术都需要适时应势,对应发展,这正是网络迅速走向进步的催化剂。
……