文章来源: http://www.360doc.com/content/11/0610/16/6295074_126040631.shtml
C 中大多数缓冲区溢出问题可以直接追溯到标准 C 库。最有害的罪魁祸首是不进行自变量检查的、有问题的字符串操作(strcpy、strcat、sprintf 和 gets)。一般来讲,象“避免使用 strcpy()”和“永远不使用 gets()”这样严格的规则接近于这个要求。
今天,编写的程序仍然利用这些调用,因为从来没有人教开发人员避免使用它们。某些人从各处获得某个提示,但即使是优秀的开发人员也会被这弄糟。他们也许在危险函数的自变量上使用自己总结编写的检查,或者错误地推论出使用潜在危险的函数在某些特殊情况下是“安全”的。
第 一位公共敌人是 gets()。永远不要使用 gets()。该函数从标准输入读入用户输入的一行文本,它在遇到 EOF 字符或换行字符之前,不会停止读入文本。也就是:gets() 根本不执行边界检查。因此,使用 gets() 总是有可能使任何缓冲区溢出。作为一个替代方法,可以使用方法 fgets()。它可以做与 gets() 所做的同样的事情,但它接受用来限制读入字符数目的大小参数,因此,提供了一种防止缓冲区溢出的方法。例如,不要使用以下代码:
void main() { char buf[1024]; gets(buf); } |
而使用以下代码:
#define BUFSIZE 1024 void main() { char buf[BUFSIZE]; fgets(buf, BUFSIZE, stdin); } |
C 编程中的主要陷阱
C 语言中一些标准函数很有可能使您陷入困境。但不是所有函数使用都不好。通常,利用这些函数之一需要任意输入传递给该函数。这个列表包括:
坏消息是我们推荐,如果有任何可能,避免使用这些函数。好消息是,在大多数情况下,都有合理的替代方法。我们将仔细检查它们中的每一个,所以可以看到什么构成了它们的误用,以及如何避免它。
strcpy()函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目。复制字符的数目直接取决于源字符串中的数目。如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会陷入大的麻烦中!
如果知道目的地缓冲区的大小,则可以添加明确的检查:
if(strlen(src) >= dst_size) { /* Do something appropriate, such as throw an error. */ } else { strcpy(dst, src); |
完成同样目的的更容易方式是使用 strncpy() 库例程:
strncpy(dst, src, dst_size-1); dst[dst_size-1] = '\0'; /* Always do this to be safe! */ |
如果 src 比 dst 大,则该函数不会抛出一个错误;当达到最大尺寸时,它只是停止复制字符。注意上面调用 strncpy() 中的 -1。如果 src 比 dst 长,则那给我们留有空间,将一个空字符放在 dst 数组的末尾。
当然,可能使用 strcpy() 不会带来任何潜在的安全性问题,正如在以下示例中所见:
strcpy(buf, "Hello!"); |
即使这个操作造成 buf 的溢出,但它只是对几个字符这样而已。由于我们静态地知道那些字符是什么,并且很明显,由于没有危害,所以这里无须担心 ― 当然,除非可以用其它方式覆盖字符串“Hello”所在的静态存储器。
确保 strcpy() 不会溢出的另一种方式是,在需要它时就分配空间,确保通过在源字符串上调用 strlen() 来分配足够的空间。例如:
dst = (char *)malloc(strlen(src)); strcpy(dst, src); |
strcat()函数非常类似于 strcpy(),除了它可以将一个字符串合并到缓冲区末尾。它也有一个类似的、更安全的替代方法 strncat()。如果可能,使用 strncat() 而不要使用 strcat()。
函数 sprintf()和 vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿 strcpy() 行为。换句话说,使用 sprintf() 和 vsprintf() 与使用 strcpy() 一样,都很容易对程序造成缓冲区溢出。例如,考虑以下代码:
void main(int argc, char **argv) { char usage[1024]; sprintf(usage, "USAGE: %s -f flag [arg1]\n", argv[0]); } |
我们经常会看到类似上面的代码。它看起来没有什 么危害。它创建一个知道如何调用该程序字符串。那样,可以更改二进制的名称,该程序的输出将自动反映这个更改。 虽然如此, 该代码有严重的问题。文件系统倾向于将任何文件的名称限制于特定数目的字符。那么,您应该认为如果您的缓冲区足够大,可以处理可能的最长名称,您的程序会 安全,对吗?只要将 1024 改为对我们的操作系统适合的任何数目,就好了吗?但是,不是这样的。通过编写我们自己的小程序来推翻上面所说的,可能容易地推翻这个限制:
void main() { execl("/path/to/above/program", < |
函数 execl() 启动第一个参数中命名的程序。第二个参数作为 argv[0] 传递给被调用的程序。我们可以使那个字符串要多长有多长!
那么如何解决 {v}sprintf() 带来得问题呢?遗憾的是,没有完全可移植的方法。某些体系结构提供了 snprintf() 方法,即允许程序员指定将多少字符从每个源复制到缓冲区中。例如,如果我们的系统上有 snprintf,则可以修正一个示例成为:
void main(int argc, char **argv) { char usage[1024]; char format_string = "USAGE: %s -f flag [arg1]\n"; snprintf(usage, format_string, argv[0], 1024-strlen(format_string) + 1); } |
注意,在第四个变量之前,snprintf() 与 sprintf() 是一样的。第四个变量指定了从第三个变量中应被复制到缓冲区的字符最大数目。注意,1024 是错误的数目!我们必须确保要复制到缓冲区使用的字符串总长不超过缓冲区的大小。所以,必须考虑一个空字符,加上所有格式字符串中的这些字符,再减去格式 说明符 %s。该数字结果为 1000, 但上面的代码是更具有可维护性,因为如果格式字符串偶然发生变化,它不会出错。
{v}sprintf() 的许多(但不是全部)版本带有使用这两个函数的更安全的方法。可以指定格式字符串本身每个自变量的精度。例如,另一种修正上面有问题的 sprintf() 的方法是:
void main(int argc, char **argv) { char usage[1024]; sprintf(usage, "USAGE: %.1000s -f flag [arg1]\n", argv[0]); } |
注意,百分号后与 s 前的 .1000。该语法表明,从相关变量(本例中是 argv[0])复制的字符不超过 1000 个。
如果任一解决方案在您的程序必须运行的系统上行不通,则最佳的解决方案是将 snprintf() 的工作版本与您的代码放置在一个包中。可以找到以 sh 归档格式的、自由使用的版本;请参阅 参考资料。
继续, scanf系列的函数也设计得很差。在这种情况下,目的地缓冲区会发生溢出。考虑以下代码:
void main(int argc, char **argv) { char buf[256]; sscanf(argv[0], "%s", &buf); } |
如果输入的字大于 buf 的大小,则有溢出的情况。幸运的是,有一种简便的方法可以解决这个问题。考虑以下代码,它没有安全性方面的薄弱环节:
void main(int argc, char **argv) { char buf[256]; sscanf(argv[0], "%255s", &buf); } |
百分号和 s 之间的 255 指定了实际存储在变量 buf 中来自 argv[0] 的字符不会超过 255 个。其余匹配的字符将不会被复制。
接下来,我们讨论 streadd()和 strecpy()。由于,不是每台机器开始就有这些调用,那些有这些函数的程序员,在使用它们时,应该小心。这些函数可以将那些含有不可读字符的字符串转换成可打印的表示。例如,考虑以下程序:
#include |
该程序打印:
\t\n |
而不是打印所有空白。如果程序员没有预料到需要 多大的输出缓冲区来处理输入缓冲区(不发生缓冲区溢出),则 streadd() 和 strecpy() 函数可能有问题。如果输入缓冲区包含单一字符 ― 假设是 ASCII 001(control-A)― 则它将打印成四个字符“\001”。这是字符串增长的最坏情况。如果没有分配足够的空间,以至于输出缓冲区的大小总是输入缓冲区大小的四倍,则可能发生缓 冲区溢出。
另一个较少使用的函数是 strtrns(),因为许多机器上没有该函 数。函数 strtrns() 取三个字符串和结果字符串应该放在其内的一个缓冲区,作为其自变量。第一个字符串必须复制到该缓冲区。一个字符被从第一个字符串中复制到缓冲区,除非那个 字符出现在第二个字符串中。如果出现的话,那么会替换掉第三个字符串中同一索引中的字符。这听上去有点令人迷惑。让我们看一下,将所有小写字符转换成大写 字符的示例:
#include |
以上代码实际上不包含缓冲区溢出。但如果我们使用了固定大小的静态缓冲区,而不是用 malloc() 分配足够空间来复制 argv[1],则可能会引起缓冲区溢出情况。
|
避免内部缓冲区溢出
realpath() 函数接受可能包含相对路径的字符串,并将它转换成指同一文件的字符串,但是通过绝对路径。在做这件事时,它展开了所有符号链接。
该 函数取两个自变量,第一个作为要规范化的字符串,第二个作为将存储结果的缓冲区。当然,需要确保结果缓冲区足够大,以处理任何大小的路径。分配的 MAXPATHLEN 缓冲区应该足够大。然而,使用 realpath() 有另一个问题。如果传递给它的、要规范化的路径大小大于 MAXPATHLEN,则 realpath() 实现内部的静态缓冲区会溢出!虽然实际上没有访问溢出的缓冲区,但无论如何它会伤害您的。结果是,应该明确不使用 realpath(),除非确保检查您试图规范化的路径长度不超过 MAXPATHLEN。
其它广泛可用的调用也有类 似的问题。经常使用的 syslog() 调用也有类似的问题,直到不久前,才注意到这个问题并修正了它。大多数机器上已经纠正了这个问题,但您不应该依赖正确的行为。最好总是假定代码正运行在可 能最不友好的环境中,只是万一在哪天它真的这样。getopt() 系列调用的各种实现,以及 getpass() 函数,都可能产生内部静态缓冲区溢出问题。如果您不得不使用这些函数,最佳解决方案是设置传递给这些函数的输入长度的阈值。
自己模拟 gets() 的安全性问题以及所有问题是非常容易的。 例如,下面这段代码:
char buf[1024]; int i = 0; char ch; while((ch = getchar()) != '\n') { if(ch == -1) break; buf[i++] = ch; ------------------------------
在前面的安全编码实践的文章里,我们讨论了GS编译选项,数据执行保护DEP功能,以及静态代码分析工具Prefast。这里,我们讨论在C/C++代码中禁用危险的API,其主要目的是为了减少代码中引入安全漏洞的可能性。
在微软产品的安全漏洞中,有很大一部分是由于不正确的使用C动态库(C Runtime Library) 的函数,特别是有关字符串处理的函数导致的。表一给出了微软若干由于不当使用C动态库函数而导致的安全漏洞【1,p242】。
表1:不当使用C动态库函数而导致的安全漏洞 不当使用C动态库函数容易引入安全漏洞,这一点并不奇怪。C动态库函数的设计大约是30年前的事情了。当时,安全方面的考虑并不是设计上需要太多注意的地方。
有关完整的危险API的禁用列表,大家可以参见http://msdn.microsoft.com/en-us/library/bb288454.aspx. 在这里我们列出其中的一部分,以便大家对那些API被禁用有所体会。
表2:禁用API的列表(部分) 其它被禁用的API还有scanf, strtok, gets, itoa等等。 ”n”系列的字符串处理函数,例如strncpy等,也在被禁用之列。
从上面的介绍可以看出绝大多数C动态库中的字符串处理函数都被禁用。那么,如何在代码中替代这些危险的API呢?在表2里,我们看到有两种替代方案:
后面我们会讨论这两种方案的不同之处。这里我们先说它们的共同点:提供更安全的字符串处理功能。特别在以下几个方面:
以StringCchCopy举例。它的定义如下: HRESULT StringCchCopy( LPTSTR pszDest, size_t cchDest, LPCTSTR pszSrc ); cchDest指明目标缓存区pszDest最多能容纳字符的数目,其值必须在1和STRSAFE_MAX_CCH之间。StringCchCopy总是确保pszDest被拷贝的字符串是以NULL结尾。并且提供以下的返回代码: S_OK,STRSAFE_E_INVALID_PARAMETER,和STRSAFE_E_INSUFFICIENT_BUFFER。这样,采用StringCchCopy来替代被禁用的strcpy的话,就可以有效降低由于误用字符串拷贝而导致缓存溢出的可能。
使用StrSafe非常简单。在C/C++代码中加入以下的头文件即可。 #include "strsafe.h" StrSafe.h包含在Windows Platform SDK中。用户可以通过在微软的网站直接下载。 下面给出一个使用StrSafe的代码示例【2】。 不安全的代码: void UnsafeFunc(LPTSTR szPath,DWORD cchPath) { TCHAR szCWD[MAX_PATH]; GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD); strncpy(szPath, szCWD, cchPath); strncat(szPath, TEXT("\\"), cchPath); strncat(szPath, TEXT("desktop.ini"),cchPath); } 在以上代码里存在着几个问题:首先,没有错误代码的校验。更严重的是,在strncat中,cchPath是目标缓存区可以存放字符的最大数目,而正确传递的参数应该是目标缓存区剩余的字符数目。 使用StrSafe后的代码是 bool SaferFunc(LPTSTR szPath,DWORD cchPath) { TCHAR szCWD[MAX_PATH]; if (GetCurrentDirectory(ARRAYSIZE(szCWD), szCWD) && SUCCEEDED(StringCchCopy(szPath, cchPath, szCWD)) && SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("\\"))) && SUCCEEDED(StringCchCat(szPath, cchPath, TEXT("desktop.ini")))) { return true; } return false; }
SafeCRT自Visual Studio 2005起开始支持。当代码中使用了禁用的危险的CRT函数,Visual Studio 2005编译时会报告相应警告信息,以提醒开发人员考虑将其替代为Safe CRT中更为安全的函数。 下面给出一个使用Safe CRT的代码示例【3】。 不安全的代码: void UnsafeFunc (const wchar_t * src) { // Original wchar_t dest[20]; wcscpy(dest, src); // 编译警告 wcscat(dest, L"..."); // 编译警告 } 以上这段代码里存在着明显缓存溢出的问题。 使用Safe CRT后的代码是 errno_t SaferFunc(const wchar_t * src) { wchar_t dest[20];
errno_t err = wcscpy_s(dest, _countof(dest), src); if (!err) return err; return wcscat_s(dest, _countof(dest), L"..."); }
我们看到,StrSafe和Safe CRT存在功能重叠的地方。那么什么时候使用StrSafe,什么时候使用SafeCRT呢? 下面的表格【1,p246】里列出了两者之间的差异。采用何种方式应该根据具体情况而定。有时候也许只能采取其中一种方式:例如如果你的开发系统是Visual Studio 2003的话,就只能使用StrSafe。或者你的代码中有许多itoa的话,就考虑使用Safe CRT,因为StrSafe中没有提供简单的替代方式。有时候也许两者都可以。这种情况下,我个人是更喜欢采用StrSafe这种方式,因为它不依赖具体的动态库支持。如果是编写Win32上的程序的话,StrSafe的HRESULT的返回代码,也和Win32 API的代码类似,这样代码的整体风格可能会更加一致。
表3:StrSafe和Safe CRT对比
在开发过程中,代码中全面禁用危险的API的编码实践,存在着一定的争议性。其中最具有代表性的观点可以参见Danny Kalev的Visual C++ 8.0 Hijacks the C++ Standard一文【4】。争论主要集中在以下几点。
以StrSafe举例,由于增加了更多的动态校验,其速度较C动态库的函数相比,是有所下降的。在【2】一文中,给出了对StrSafe速度方面的测试数据如下: 测试例子:1千万次字符串合并调用。结果: C动态库:7.3秒 StrSafe:8.3秒 我们看到,如果开发的系统不是完全以字符串处理为工作核心的话,使用StrSafe对系统性能的影响是可以控制的。
首先,同意如果代码中正确使用危险API的话,也是可以避免安全漏洞的引入。但是,在具体的开发实践中,存在着以下问题:
第二点尤其关键。大家看到这里可能会有疑问,使用危险的API有这么容易出问题吗?即便代码复查(code review)也没能看出来?【5】中给出了一个微软安全漏洞的具体实例。 微软 05-047 Plug-n-Play RPC:即插即用中的漏洞,允许远程执行代码和特权提升。经过身份验证的攻击者可以通过创建特制的网络消息并将该消息发送到受影响的系统来尝试利用此漏洞。导致这个严重的安全漏洞的代码如下: #define MAX_CM_PATH 360 GetInstanceList( IN LPCWSTR pszDevice, IN OUT LPWSTR *pBuffer, IN OUT PULONG pulLength) { WCHAR RegStr[MAX_CM_PATH], szInstance[MAX_DEVICE_ID_LEN]; ... // Validate that passed in pszDevice is an actual registry entry // If lookup for the key fails, reject call and cleanup. // ghEnumKey points to HKLM\System\CurrentControlSet\Enum if (RegOpenKeyEx(ghEnumKey, pszDevice, 0, KEY_ENUMERATE_SUB_KEYS, &hKey) != ERROR_SUCCESS) { Status = CR_REGISTRY_ERROR; goto Clean0; } ... ulLen = MAX_DEVICE_ID_LEN; // size in chars ... // Query szInstance from registry RegStatus = RegEnumKeyEx(hKey, ulIndex, szInstance, &ulLen, ...); if (RegStatus == ERROR_SUCCESS) { // Build lookup string given a valid registry root key and valid instance ID wsprintf(RegStr, TEXT("%s\\%s"), pszDevice, szInstance);} 复查这段代码时,我们看到,虽然使用了危险的API:wsprintf,但应该是不会发生缓存溢出的问题。这是因为根据MSDN,
图1:注册表字符数目的限制 于是:
但实际上,wspringf还是导致了缓存溢出的安全漏洞。到底是怎么回事?我们来看一下攻击代码: errno_t SaferFunc(const wchar_t * src) int main() { PWCHAR pszFilter = (PWCHAR)malloc(sizeof(WCHAR)*1000); PWCHAR Buffer = (PWCHAR)malloc(86); wsprintf(pszFilter,L"ISAPNP\\ReadDataPort\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\0"); CM_Get_Device_ID_List((PCWSTR)pszFilter,Buffer,86,1); return 0; } 攻击代码之所以有效,是因为:
通过这个例子我们看出,在开发复杂的系统中,即便是有经验的开发人员,加上严格的代码复查过程,还是有可能由于使用危险的API而导致安全漏洞的引入。这也是微软在Windows Vista的开发过程中全面禁用危险API的原因。
这一点的考虑是非常值得重视的。不管是StrSafe,还是Safe CRT,都不是工业界标准。因此,如果开发的系统需要移植到其它平台的话,采用Safe CRT是肯定不合适的。StrSafe的Inline方式,因为不依赖特定库,对可移植性的影响相对较小。
在C/C++程序中禁用危险的API,可以有效降低在代码中引入安全漏洞的可能。在考虑了性能和可移植性的因素下,强烈建议在开发过程中,使用StrSafe或Safe CRT中对应的安全函数来替代被禁用的危险的API调用。 } |
哎呀!可以用来读入字符的任何函数都存在这个问题,包括 getchar()、fgetc()、getc() 和 read()。
缓冲区溢出问题的准则是:总是确保做边界检查。
C 和 C++ 不能够自动地做边界检查,这实在不好,但确实有很好的原因,来解释不这样做的理由。边界检查的代价是效率。一般来讲,C 在大多数情况下注重效率。然而,获得效率的代价是,C 程序员必须十分警觉,并且有极强的安全意识,才能防止他们的程序出现问题,而且即使这些,使代码不出问题也不容易。
在 现在,变量检查不会严重影响程序的效率。大多数应用程序不会注意到这点差异。所以,应该总是进行边界检查。在将数据复制到您自己的缓冲区之前,检查数据长 度。同样,检查以确保不要将过大的数据传递给另一个库,因为您也不能相信其他人的代码!(回忆一下前面所讨论的内部缓冲区溢出。)
|
其它危险是什么?
遗憾的是,即使是系统调用的“安全”版本 ― 譬如,相对于 strcpy() 的 strncpy() ― 也不完全安全。也有可能把事情搞糟。即使“安全”的调用有时会留下未终止的字符串,或者会发生微妙的相差一位错误。当然,如果您偶然使用比源缓冲区小的结 果缓冲区,则您可能发现自己处于非常困难的境地。
与 我们目前所讨论的相比,往往很难犯这些错误,但您应该仍然意识到它们。当使用这类调用时,要仔细考虑。如果不仔细留意缓冲区大小,包括 bcopy()、fgets()、memcpy()、snprintf()、strccpy()、strcadd()、strncpy() 和 vsnprintf(),许多函数会行为失常。
另一个要避免的系统调用是 getenv()。使用 getenv() 的最大问题是您从来不能假定特殊环境变量是任何特定长度的。我们将在后续的专栏文章中讨论环境变量带来的种种问题。
到 目前为止,我们已经给出了一大堆常见 C 函数,这些函数容易引起缓冲区溢出问题。当然,还有许多函数有相同的问题。特别是,注意第三方 COTS 软件。不要设想关于其他人软件行为的任何事情。还要意识到我们没有仔细检查每个平台上的每个常见库(我们不想做那一工作),并且还可能存在其它有问题的调 用。
即使我们检查了每个常见库的各个地方,如果我们试图声称已经列出了将在任何时候遇到的所有问题,则您应该持非常非常怀疑的态度。我们只是想给您起一个头。其余全靠您了。
|
静态和动态测试工具
我们将在以后的专栏文章中更加详细地介绍一些脆弱性检测的工具,但现在值得一提的是两种已被证明能有效帮助找到和去除缓冲区溢出问题的扫描工具。 这两个主要类别的分析工具是静态工具(考虑代码但永不运行)和动态工具(执行代码以确定行为)。
可 以使用一些静态工具来查找潜在的缓冲区溢出问题。很糟糕的是,没有一个工具对一般公众是可用的!许多工具做得一点也不比自动化 grep 命令多,可以运行它以找到源代码中每个有问题函数的实例。由于存在更好的技术,这仍然是高效的方式将几万行或几十万行的大程序缩减到只有数百个“潜在的问 题”。(在以后的专栏文章中,将演示一个基于这种方法的、草草了事的扫描工具,并告诉您有关如何构建它的想法。)
较好 的静态工具利用以某些方式表示的数据流信息来断定哪个变量会影响到其它哪个变量。用这种方法,可以丢弃来自基于 grep 的分析的某些“假肯定”。David Wagner 在他的工作中已经实现了这样的方法(在“Learning the basics of buffer overflows”中描述;请参阅 参考资料),在 Reliable Software Technologies 的研究人员也已实现。当前,数据流相关方法的问题是它当前引入了假否定(即,它没有标志可能是真正问题的某些调用)。
第二类方法涉及动态分析的使用。动态工具通常把注意力放在代码运行时的情况,查找潜在的问题。一种已在实验室使用的方法是故障注入。这个想法是以这样一种方式来检测程序:对它进行实验,运行“假设”游戏,看它会发生什么。有一种故障注入工具 ― FIST(请参阅 参考资料)已被用来查找可能的缓冲区溢出脆弱性。
最终,动态和静态方法的某些组合将会给您的投资带来回报。但在确定最佳组合方面,仍然有许多工作要做。
|
Java 和堆栈保护可以提供帮助
如上一篇专栏文章中所提到的(请参阅 参考资料), 堆栈捣毁是最恶劣的一种缓冲区溢出攻击,特别是,当在特权模式下捣毁了堆栈。这种问题的优秀解决方案是非可执行堆栈。 通常,利用代码是在程序堆栈上编写,并在那里执行的。(我们将在下一篇专栏文章中解释这是如何做到的。)获取许多操作系统(包括 Linux 和 Solaris)的非可执行堆栈补丁是可能的。(某些操作系统甚至不需要这样的补丁;它们本身就带有。)
非可执行堆栈涉及到一些性能问题。(没有免费的午餐。)此外,在既有堆栈溢出又有堆溢出的程序中,它 们易出问题。可以利用堆栈溢出使程序跳转至利用代码,该代码被放置在堆上。 没有实际执行堆栈中的代码,只有堆中的代码。这些基本问题非常重要,我们将在下一篇专栏文章中专门刊载。
当然,另一种 选项是使用类型安全的语言,譬如 Java。较温和的措施是获取对 C 程序中进行数组边界检查的编译器。对于 gcc 存在这样的工具。这种技术可以防止所有缓冲区溢出,堆和堆栈。不利的一面是,对于那些大量使用指针、速度是至关重要的程序,这种技术可能会影响性能。但是 在大多数情况下,该技术运行得非常好。
Stackguard 工具实现了比一般性边界检查更为有效的技术。它将一些数据放在已分配数据堆栈的末尾,并且以后会在缓冲区溢出可能发生前,查看这些数据是否仍然在那里。这 种模式被称之为“金丝雀”。(威尔士的矿工将 金丝雀放在矿井内来显示危险的状况。当空气开始变得有毒时,金丝雀会昏倒,使矿工有足够时间注意到并逃离。)
Stackguard 方法不如一般性边界检查安全,但仍然相当有用。Stackguard 的主要缺点是,与一般性边界检查相比,它不能防止堆溢出攻击。一般来讲,最好用这样一个工具来保护整个操作系统,否则,由程序调用的不受保护库(譬如,标 准库)可以仍然为基于堆栈的利用代码攻击打开了大门。
类似于 Stackguard 的工具是内存完整性检查软件包,譬如,Rational 的 Purify。这类工具甚至可以保护程序防止堆溢出,但由于性能开销,这些工具一般不在产品代码中使用。
|
结束语
在本专栏的上两篇文章中,我们已经介绍了缓冲区溢出,并指导您如何编写代码来避免这些问题。我们还讨论了可帮助使您的程序安全远离可怕的缓冲区溢出的几个 工具。表 1 总结了一些编程构造,我们建议您小心使用或避免一起使用它们。如果有任何认为我们应该将其它函数加入该列表,请则通知我们,我们将更新该列表。
函数 | 严重性 | 解决方案 |
gets | 最危险 | 使用 fgets(buf, size, stdin)。这几乎总是一个大问题! |
strcpy | 很危险 | 改为使用 strncpy。 |
strcat | 很危险 | 改为使用 strncat。 |
sprintf | 很危险 | 改为使用 snprintf,或者使用精度说明符。 |
scanf | 很危险 | 使用精度说明符,或自己进行解析。 |
sscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
fscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vfscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vsprintf | 很危险 | 改为使用 vsnprintf,或者使用精度说明符。 |
vscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
vsscanf | 很危险 | 使用精度说明符,或自己进行解析。 |
streadd | 很危险 | 确保分配的目的地参数大小是源参数大小的四倍。 |
strecpy | 很危险 | 确保分配的目的地参数大小是源参数大小的四倍。 |
strtrns | 危险 | 手工检查来查看目的地大小是否至少与源字符串相等。 |
realpath | 很危险(或稍小,取决于实现) | 分配缓冲区大小为 MAXPATHLEN。同样,手工检查参数以确保输入参数不超过 MAXPATHLEN。 |
syslog | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getopt | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getopt_long | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getpass | 很危险(或稍小,取决于实现) | 在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。 |
getchar | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
fgetc | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
getc | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
read | 中等危险 | 如果在循环中使用该函数,确保检查缓冲区边界。 |
bcopy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
fgets | 低危险 | 确保缓冲区大小与它所说的一样大。 |
memcpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
snprintf | 低危险 | 确保缓冲区大小与它所说的一样大。 |
strccpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
strcadd | 低危险 | 确保缓冲区大小与它所说的一样大。 |
strncpy | 低危险 | 确保缓冲区大小与它所说的一样大。 |
vsnprintf | 低危险 | 确保缓冲区大小与它所说的一样大。 |
在我们急匆匆讲述这些基础知识时,到现在为止,已经遗漏了一些缓冲区溢出很酷的细节。在下几篇专栏文章中,我们将深入这台“引擎”的工作,并给它加点黄油。我们将详细地了解缓冲区溢出的工作原理,甚至还会演示一些利用代码。