摘要
因为安全缺陷而复审代码是软件制作过程中的一个关键部分,贯穿于计划编制、设计和测试。本文是作者反省其多年来的代码安全复审工作而总结出的所有开发人员在追踪潜在安全漏洞时可以遵循的一些识别模式和准则。该过程开始于检查代码的运行环境,考虑到将要运行代码的用户的角色,并研究该代码可能存在安全问题的历史。在对这些
背景问题有一个了解之后,特定弱点就可以被找到,包括SQL注入攻击,跨站点脚本,和缓冲区溢出。另外,某些危险信号,例如象 "password"、
"secret" 之类的变量名以及其它一些明显而普遍的安全失误都可以被查出并纠正。
我的大部分工作涉及到复审别人的代码,寻找安全方面的错误。诚然,这并不是我的首要任务——往往是设计复审和威胁模型分析[编者:threat
modeling 即威胁模型分析]—但是我确实要看到大量的代码。
但愿你能理解对别人代码的复审工作,虽然是做了件好事,它确不是创造一个安全的软件的方法 。 你要通过设计、编写、测试并证明安全系统的过程,并通过在进度表中考虑安全复审、培训和使用工具所用的时间来产生安全的软件。只简单地设计、编写、测试和
编制项目文档,然后再寻找安全错误并不能创造出安全的软件。代码复审仅仅只是这个过程的一部分,但是它本身并不能创造出安全代码。
在本文中我将不会讨论代码弱点的本性,比如整数溢出攻击、SQL注入和缓冲区溢出;你可以在一些书本中进一步了解这些问题(比如我的书
《Writing Secure Code》,Microsoft Press®,2002)。而是采用在一个高层次的观点来考虑复审代码过程中的问题。然而,在开始之前我想指出的是这仅仅是我复审代码
查找安全错误的方法;它不一定是你应该使用的复审代码方法,我也不能保证它的形式完全适合于某些特定种类的漏洞。我想证明的是我在看代码时脑中想到的同样对你
也的确是有帮助的。
在我看来,复审代码有三种方法:详细分析,快速分析和混合方法。我倾向于采用混合方法,因为它有迅速覆盖很大范围的优点;如果我觉得某些东西需要更
深入的分析我会将它标出来以便将来进行代码复审,可能涉及到其他专家关注的领域。但是现在,我只讨论最初的快速代码复审,正如我喜欢这样称呼它,这种扫描''n''标记方法—迅速地扫描代码,并标出需要进一步复审的代码。
下面是我实现这个过程的要点。
分配时间和努力
我有一套等级评定体系,我用它来确定复审代码所需的相对时间。这个体系基于如果弱点被利用后存在的潜在危害以及可能受到的潜在攻击。
具体针对的范围基于以下几个特点:
- 代码以缺省值运行吗?
- 代码以高优先级运行吗?
- 代码是否侦听某个网络接口?
- 网络接口是不可靠的吗?
- 代码是用C/C++写的吗?
- 该代码以前是否有历史弱点?
- 该组件是否由安全研究员做过最终详细审查?
- 该代码处理敏感或隐私数据吗?
- 代码是可复用的吗(例如,某个 DLL、C++ 类头文件、库、或程序集)?
- 根据威胁模型分析,该组件处于高风险环境或遭受高风险威胁吗?
如果该列表中同时有三或四个以上项目被言中,这时我将在更深的层次上复审代码。事实上,如果代码是侦听传输控制协议(TCP)或用户数据报协议(UDP)的
socket 并以缺省值运行,那么就要准备大量时间来复审该代码。
在查找安全错误时,我倾向于复审三个主要类别的代码:C/C++代码,网络服务器应用代码,(比如 ASP、ASP.NET、CGI、和
Perl) 以及托管代码(主要是C#,和一些 Visual Basic .NET)。
你应该意识到每一种语言都存在一些细微的差别。首先,C 和 C++ 的首要问题是缓冲区溢出。诚然,还有一些其它问题,但是当你在同一个句子里听到单词“缓冲区”和“超限”时
,你几乎可以确信这肯定涉及到了C 或 C++。更高级的语言如 C#、Visual Basic.NET 和 Perl
应该没有缓冲区溢出问题。如果有,那么这个缺陷可能出在运行时环境而非正在被复审的代码中。然而,这类语言常被用于 编写网络服务器应用软件代码,并遭遇其它类型的缺陷。缓冲区超限是令人讨厌的,因为攻击者可以将代码注入正在运行的进程中并取得控制权。所以让我们首先看看缓冲区溢出。
C和C++的缓冲区溢出
缓冲区超限是软件行业的祸根,你应该竭尽全力将它们从代码里清除出去。但是,最好是首先不要让它们进入代码。在我复审缓冲区超限的代码有两个办法。第一是识别出这个应用软件
的所有进入点,尤其是网络进入点,跟踪数据在代码中的移动并质问数据是如何被处理的。我假定所有数据都是畸形的。当看到接触(读出或写入)该数据的任何代码时,我
便问,“有没有导致该代码失败的数据版本?”。这种方法虽然彻底但非常耗时。另一个技术是寻找已知的和潜在的危险结构并跟踪数据回到进入点。
以下面的代码为例:
void fuction(char *p) {
char buff[16];
…
strcpy(buff,p);
…
}
如果我看到像这样的代码,我将跟踪变量 p 到它的源头,并且如果它来源于某个我并不信任的地方,或在接近它被拷贝的地方没有进行合法性检查,这时我知道已经找到了一个安全缺陷。值得注意的是,并不是说
strcpy 本身是危险的或者说是不安全的。应该说,恰恰是这个数据使得这类函数惊慌失措。如果你检查的数据具有良好的格式,那么
strcpy 可能就是安全的。当然,如果你犯错,那么你的代码就有一个安全错误。我也检查“n”版本字符串处理函数,比如
strncpy,因为你也要检查那些缓冲区大小的计算是正确的。
我谨慎对待那些处理标注文件格式的代码。通过标注那些由块组成的文件,这里每个块都有一个头描述下一个数据块。MIDI音乐格式就是一个很好的例子。一个严重的安全缺陷被发现后,在一个处理
MIDI 文件的名为 quartz.dll 的 Windows 组件中被修复。有个畸形的 MIDI 结构导致了处理文件的代码失败,或更糟。你可以在
Unchecked Buffer in DirectX Could Enable System Compromise
得到更多关于这个缺陷的内容。
另一个我留心的结构是:
while (*s != ''\\'')
*d++ = *s++;
这个循环囿与源中的某个字符;它不受目的地大小的限制。我主要用下面的正则表达式扫描 *x++ = *y++。
\*\w+\+\+\s?=\s?\*\w+\+\+
当然,人们也可能用 *++x = *++y,因此你也需要对此进行扫描。我想在这里再次强调的是这个结构并不危险,除非数据源是不可信的,因此你需要确定数据源是否可信赖。
下面是另一类你应该注意的与缓冲区超限有关的问题:整数溢出弱点。
C和C++的整数溢出
真正的安全缺口发生 于计算某个缓冲区大小的算法以及计算导致的上溢或下溢。看下面的例子:
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 位大小)?
这里有一个我在微软的同事常用的规则:如果你某个被用于比较的表达式中执行一个数学运算,此时你将有一个潜在的上溢和下溢数据。如果该计算被用于确定一个缓冲区
的大小,那么事情会变得加倍地糟糕,尤其是如果一个或更多的缓冲区大小的计算方法被攻击者利用时。
任何语言中的数据库存取代码
作为一般原则,数据库应用软件的开发者使用更高级的语言如C#,脚本语言和类似的语言。相对而言,很少有数据库代码是用
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
然后我 在输出结果中查看“用户串”部分。如果我发现任何使用串连接的数据库查询,这是一个潜在的安全缺陷,并且必须用参数化查询赖取代它以修复该缺陷。
用字符串连接建立存储过程对SQL入侵也于事无补。总之,字符串连接加SQL语句是很槽的,但是字符串连接加SQL语句加系统管理员帐户加简直就是灾难。
任何语言编写的
WEB 页面代码
在基于 WEB 的应用程序中最为普通的错误是跨站点脚本(XSS)问题。虽然还有我要找的其它问题,比如 SQL 入侵和弱的加密系统,XSS
缺陷相当普遍。XSS 的核心弱点是可能将不可 信的用户输入显示在受害者的浏览器上,因此我首先要搜索任何发送数据到用户代码构成。举个例子,我在ASP中寻找
Response.Write 和 <%= %> 标签。然后,我查看所写的数据来源于什么地方。如果数据来源于一个HTTP实体,比如一个表单或一个查询串,并且没有经过合法性检查就被送到用户的浏览器上。那么便存在一个
XSS 缺陷 。这里有一个非常简单但是并不常见的 XSS 例子: Hello,
<% Response.Write(Request.QueryString("Name")) %>r>
正如你所看到的,“Name”参数并没有首先经过合法性检查以及格式是否良好便被送回给用户。
保密和任何语言编写的密码系统
一些开发者有个癖好就是将密码数据存储在代码中,比如口令和密钥,并且创造他们自己的不可思议的加密算法。千万不要做这两件傻事!
我首先查找包含“key” “password” “pwd”“secret” “cipher”和“crypt”的变量名和函数名,只要发现其中
任何一个便进行分析。你常常能获得“key”的假结论,但是其它的结果很有趣,可能产生出嵌入的密码数据,或者是那“不可思议的”加密系统。当搜索加密算法时,我也寻找异或
(XOR)操作,因为它们常被用于加密操作。最槽糕的代码就是那些使用一个嵌入密钥异或一个数据流!
Visual Basic 和 C++ 中的 ActiveX 控件
我在复审一个新的 ActiveX® 控件时总在问一个问题:为什么不能用托管代码来写它们?我之所以问这个问题是因为托管代码允许局部信任情
形,但是 ActiveX 不会。
接着,我着眼于控件上的所有方法和属性(.IDL文件是最好的地方),并且我把自己放在一个坏家伙的角度。我能用这些方法和属性做些什么坏事呢?一般来说,一些方法用动词加名词的格式来命名,比如说ReadRegistry,WriteFile,GetUserName,
以及 NukeKey 之类,因此我寻找与敏感资源有关的动词和名词。
举个例子,如果攻击者能获取任何在用户硬盘上的文件,并发送到任何位置,比如攻击者控制的网站。那么 SendFile
方法便有潜在的危险,任何访问用户机器上资源的事情都应受到进一步的详细审查。
如果控件涉及脚本安全(SFS)问题,我会做额外的复查工作,因为它可能会在没有警示用户的情况下,在浏览器中在被调用。如果它实现了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 方法并根据该方法返回 的出错代码确定某个文件存在于用户硬盘上。
结束语
这是我在复审代码时首先要考虑的重中之重。一些缺陷并不复杂,有人会说开发人员不应该犯这种错,但他们确实犯了。然而意识到为了安全而复审代码,通常会
促使你将编写更加安全的代码放在首位。
你可能已经注意到某些缺陷类型的一个共同点,那就是大多数缺陷都是由于不可靠的输入所导致的。在复审代码时,你应该总是问这些数据从哪里来以及你是否相信它们。
|