`
helloyesyes
  • 浏览: 1306771 次
  • 性别: Icon_minigender_2
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论

五种方法避免代码中的缓冲区溢出问题

阅读更多

Michael Howard是 Microsoft Secure Windows Initiative 小组的安全程序经理,这是它最近写的一篇文章。详细探讨了修复代码溢出的方法。

作者在文中介绍了五种具体方法,来修复缓冲区溢出问题。

1、使用托管代码

2、代码应该检测缓冲区的长度

3、进行内存探测

4、编写防范性的代码

5、使用/GS进行编译

原文:

当 David LeBlanc 和我确定《Writing Secure Code》(英文)一书的目录时,我们明确地意识到必须着重介绍缓冲区溢出问题,因为已经有太多的开发人员在编写代码时犯了太多的此类错误,这些错误导致了可被人利用的缓冲区溢出的出现。在本文中,我将集中介绍为什么会出现缓冲区溢出及其修复的方法。

为什么会出现缓冲区溢出

出现缓冲区溢出需要具备很多条件,包括:

  • 使用非类型安全的语言,如 C/C++。
  • 以不安全的方式访问或复制缓冲区。
  • 编译器将缓冲区放在内存中关键数据结构旁边或邻近的位置。

现在我们来仔细看看以上每种条件。

首先,缓冲区溢出主要出现在 C 和 C++ 中,因为这些语言不执行数组边界检查和类型安全检查。C/C++ 允许开发人员创建非常接近硬件运行的程序,从而允许直接访问内存和计算机寄存器。其结果可以获得优异的性能;很难有任何应用程序能象编写得很好的 C/C++ 应用程序运行得那样快。其他语言中也会出现缓冲区溢出,但很少见。如果出现这种错误,通常不是由开发人员造成的,而是运行时环境的错误。

其次,如果应用程序从用户(或攻击者)那里获取数据,并将数据复制到应用程序所维护的缓冲区中而未考虑目标缓冲区的大小,则可能造成缓冲区溢出。换句话说,代码为缓冲区分配了 N 个字节,却将多于 N 个字节的数据复制到该缓冲区中。这就象向 12 盎司的玻璃杯中注入 16 盎司的水一样。那么多出的 4 盎司水到哪里去了呢?全溢出去了!

最后一点,也是最重要的一点,编译器通常将缓冲区放在“令人感兴趣的”数据结构旁边。例如,当某个函数的缓冲区紧邻堆栈,则在内存中该函数的返回地址紧靠在缓冲区之后。这时,如果攻击者可以使该缓冲区发生溢出,他就可以覆盖函数的返回地址,从而在返回函数时,返回到攻击者定义的地址。其他令人感兴趣的数据结构包括 C++ V 表、异常处理程序地址、函数指针等等。

下面我们来看一个示例。

以下代码有什么错误?

void CopyData(char *szData) {
   char cDest[32];
   strcpy(cDest,szData);

   // 使用 cDest
   ...
} 

令人惊讶的是,这段代码可能没有什么错误!这完全取决于 CopyData() 的调用方式。例如,以下代码是安全的:

char *szNames[] = {"Michael","Cheryl","Blake"};
CopyData(szName[1]);

这段代码是安全的,因为名字是硬编码的,并且知道每个字符串在长度上不超过 32 个字符,因此调用 strcpy 永远是安全的。然而,如果 CopyDataszData 的唯一参数来自不可靠的源(如套接字或文件),则 strcpy 将复制该数据,直到碰到空字符为止;如果此数据的长度大于 32 个字符,则 cDest 缓冲区将溢出,并且在内存中该缓冲区以外的任何数据将遭到破坏。不幸的是,在这里,遭到破坏的数据是来自 CopyData 的返回地址,这意味着当 CopyData 完成时,它仍然在由攻击者指定的位置继续执行。这真糟糕!

其他数据结构也同样敏感。假设某个 C++ 类的 V 表遭到破坏,如下面这段代码:

void CopyData(char *szData) {
   char cDest[32];
   CFoo foo;
   strcpy(cDest,szData);

   foo.Init();
}

此示例假定 CFoo 类具有虚方法,以及一个 V 表或该类方法的地址列表(与所有 C++ 类一样)。如果由于 cDest 缓冲区被覆盖而破坏了 V 表,则该类的任何虚方法(在此例中是 Init())都可能调用攻击者指定的地址,而不是 Init() 的地址。顺便说一句,如果认为您的代码不调用任何 C++ 方法就安全了,那就错了,因为有一个方法始终会被调用,即该类的虚析构函数!当然,如果某个类不调用任何方法,就应该想想它存在的必要了。

修复缓冲区溢出

现在,我们继续讨论一些更实际的内容 - 如何在您的代码中删除和防止缓冲区溢出。

迁移到托管代码

在 2002 年 2 月和 3 月,我们举办了 Microsoft Windows® Security Push 活动。在此期间,我的工作组对 8,500 多位人员在设计、编写、测试和记录安全功能方面进行了培训。我们为所有设计人员提出的一个建议就是,制定计划,将相应的应用程序和工具从本机 Win32® C++ 代码迁移到托管代码。这样做有多种原因,主要是有助于减少缓冲区溢出。在托管代码中,很难创建出包含缓冲区溢出的代码,因为所编写的代码不能直接访问指针、计算机寄存器或内存。您应当考虑,或者至少要计划将某些应用程序和工具迁移到托管代码中。例如,管理工具就是一个很好的迁移对象。当然,我们也要现实一些,因为不可能在一个晚上将所有的应用程序从 C++ 迁移到 C# 或其他托管语言中。

遵循以下重要规则

当编写 C 和 C++ 代码时,应注意如何管理来自用户的数据。如果某个函数具有来自不可靠源的缓冲区,请遵循以下规则:

  • 要求代码传递缓冲区的长度。
  • 探测内存。
  • 采取防范措施。

现在我们来仔细看看以上每种情况。

要求代码传递缓冲区的长度

如果任何函数调用具有类似特征,将出现一个错误:

void Function(char *szName) {
   char szBuff[MAX_NAME];
   // 复制并使用 szName
   strcpy(szBuff,szName);
}

此代码的问题在于函数不能判断 szName 的长度,这意味着将不能安全地复制数据。函数应知道 szName 的大小:

void Function(char *szName, DWORD cbName) {
   char szBuff[MAX_NAME];
   // 复制并使用 szName
   if (cbName < MAX_NAME)
      strncpy(szBuff,szName,MAX_NAME-1);
}

然而,您不能想当然地信任 cbName。攻击者可以设置该名称和缓冲区大小,因此必须进行检查!

探测内存

如何判别 szNamecbName 是有效的?您相信用户会提供有效的值吗?一般来说,答案是否定的。验证缓冲区大小是否有效的一个简单方法是探测内存。以下代码段显示了如何在代码的调试版中完成这一验证过程:

void Function(char *szName, DWORD cbName) {
   char szBuff[MAX_NAME];
   
#ifdef _DEBUG

   // 探测
   memset(szBuff, 0x42, cbName);
#endif

   // 复制并使用 szName
   if (cbName < MAX_NAME)
      strncpy(szBuff,szName,MAX_NAME-1);
}

此代码将尝试向目标缓冲区写入值 0x42。您可能会想,为什么要这样做而不是直接复制缓冲区呢?通过向目标缓冲区的末尾写入一个固定的已知值,可以在源缓冲区太大时,强制代码失败。同时这样也可以在开发过程中及早发现开发错误。与其运行攻击者的恶意有效代码,还不如让程序失败。这就是不复制攻击者的缓冲区的原因。

注意:您只能在调试版中这样做,以便在测试过程中捕获缓冲区溢出。

采取防范措施

说实话,探测虽然很有用,但它并不能使您免遭攻击。真正安全的办法是编写防范性的代码。您会注意到代码已经具有防范性了。它将检查进入函数的数据是否不超过内部缓冲区 szBuff。然而,有些函数在处理或复制不可靠的数据时,如果使用不当,则会存在潜在的严重安全问题。这里的关键是不可靠的数据。在检查代码的缓冲区溢出错误时,应跟踪数据在代码中的流向,并检查各种数据假设。当您意识到有些假设不正确时,您也许会惊异于所发现的错误。

需要注意的函数包括诸如 strcpystrcatgets 等常见函数。但也不能排除所谓的 strcpystrcat 的“安全的 n 版本”- strncpy 和 strncat。这些函数被认为使用起来更安全、可靠,因为它们允许开发人员限制复制到目标缓冲区中的数据的大小。然而,开发人员在使用这些函数时也会出错!请看以下这段代码。您能看出其中的缺点吗?

#define SIZE(b) (sizeof(b))
char buff[128];
strncpy(buff,szSomeData,SIZE(buff));
strncat(buff,szMoreData,SIZE(buff));
strncat(buff,szEvenMoreData,SIZE(buff));

如果您需要提示,请注意每个字符串处理函数的最后一个参数。要放弃吗?在我给出答案之前,我经常会开玩笑说,如果您禁用“不安全”的字符串处理函数,而使用较为安全的 n 版本,则恐怕您要在修复新产生的错误中度过您的余生。以下便是原因所在。首先,最后那个参数不是目标缓冲区的总体大小。它是缓冲区剩余空间的大小,代码每次向 buff 添加内容时,buff 都会有实质的减小。第二个问题是,即使用户传递了缓冲区大小,他们通常也是逐一减小的。那么在计算字符串大小时,您有没有包含末尾的空字符?当我针对这个问题进行读者调查时,通常是对半分。其中一半认为在计算缓冲区大小时确实要考虑末尾空字符,另外一半则不这么认为。第三,在某些情况下,n 版本可能不会以空字符作为结果字符串的结束字符,因此请一定要阅读文档。

如果编写 C++ 代码,请考虑使用 ATL、STL、MFC 或者您最喜欢的字符串处理类来处理字符串,而不要直接处理字节。唯一潜在的不足是可能出现性能的下降,但总的来说,大部分这些类的使用都会使代码更加强大和可维护。

使用 /GS 进行编译

Visual C++® .Net 中的这个新的编译时选项会在某些函数的堆栈框架中插入值,有助于减少基于堆栈的缓冲区溢出的潜在弱点。请记住,此选项不会修复您的代码,也不能删除任何错误。它只是象一个棒球运动的捕手,帮助您减少某些类的缓冲区溢出变为可被人利用的缓冲区溢出的潜在可能性,以免攻击者向过程中写入代码并执行。可以把它视为一个很小的保险措施。请注意,对于使用 Win32 应用程序向导创建的新的本机 Win32 C++ 项目,将默认启用此选项。此外,Windows .NET Server 编译时也使用了此选项。有关详细信息,请参阅 Brandon Bray 的 Compiler Security Checks In Depth(英文)。

排除隐患

下面我给出了一些代码,其中至少包含一处安全隐患。您能找出来吗?我将在下一篇文章中公布答案!

WCHAR g_wszComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];

// 获取服务器名称并将其转换为 Unicode 字符串。
BOOL GetServerName (EXTENSION_CONTROL_BLOCK *pECB) {
   DWORD   dwSize = sizeof(g_wszComputerName);
   char    szComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];

   if (pECB->GetServerVariable (pECB->ConnID,
            "SERVER_NAME",
            szComputerName,
            &dwSize)) {
   // 其余代码被略去

Michael Howard 是 Microsoft Secure Windows Initiative 小组的安全程序经理,也是《Writing Secure Code》(英文)的作者之一。他的主要工作就是确保人们设计、构建、测试和记录无缺陷的安全系统。他最喜欢的话是“尺有所短,寸有所长”。

分享到:
评论

相关推荐

    缓冲区溢出实验

    - **根本原因**:C/C++等语言中的一些标准库函数(如`strcpy()`、`strcat()`等)缺乏边界检查机制,容易引发缓冲区溢出问题。 #### 具体案例分析 - **案例简介**:实验中通过一个简单的C语言程序示例来模拟堆溢出...

    防止缓冲区溢出教程源代码

    在编程领域,特别是C/C++语言中,缓冲区溢出是一种常见的安全问题,它可能导致程序崩溃、数据丢失,甚至被恶意攻击者利用执行任意代码。本教程源代码着重讲解如何防止这种现象,确保程序的稳定性和安全性。 首先,...

    windows 缓冲区溢出

    《Windows缓冲区溢出:保护与突破》 缓冲区溢出是计算机安全领域的一个重要话题,尤其在Windows操作系统中,...同时,保持警惕,定期更新系统和软件,避免使用存在已知漏洞的程序,也是预防缓冲区溢出攻击的重要措施。

    缓冲区溢出示例代码课件ppt

    课件中的示例代码可能包括了触发缓冲区溢出的示例,以及如何通过编程手段避免溢出。例如,可能会展示一个简单的C语言函数,该函数接收用户输入并存储到固定大小的缓冲区中,通过故意超过缓冲区长度的输入来演示溢出...

    缓冲区溢出攻击实例

    "缓冲区溢出攻击实例" 缓冲区溢出攻击是一种常见的网络攻击方式,它可以让攻击者获得系统的...缓冲区溢出攻击是一种非常危险的网络攻击方式,我们需要了解缓冲区溢出的原理和防范措施,以避免缓冲区溢出攻击的危害。

    Q版缓冲区溢出教程源代码

    《Q版缓冲区溢出教程源代码》是一个专注于讲解缓冲区溢出安全问题的资源,主要面向对计算机安全有兴趣或从事相关工作的读者。缓冲区溢出是计算机编程中常见的安全漏洞,尤其在C/C++等低级语言中,如果不妥善处理,...

    有关缓冲区溢出攻击的代码

    1. 缓冲区溢出攻击定义:缓冲区溢出攻击是一种攻击方式,攻击者可以通过向缓冲区中写入过长的数据,使得缓冲区溢出,并覆盖程序的返回地址,从而跳转到攻击者编写的攻击代码的位置上,开始运行攻击代码。 2. 缓冲区...

    缓冲区溢出原理及植入代码的分析研究.pdf

    缓冲区溢出是一种常见的软件安全漏洞,主要发生在使用C或C++编程语言编写的程序中。这些语言允许程序员直接操作内存,如果没有适当的边界检查,就会引发溢出问题。当程序试图将超过预定长度的数据写入缓冲区时,超出...

    Q版缓冲区溢出教程 学习缓冲区溢出的好教材

    在Q版缓冲区溢出教程中,你将深入理解这一过程,并学习如何避免此类问题。 教程可能涵盖以下核心内容: 1. **缓冲区与内存管理**:了解内存分配、释放以及栈和堆内存的工作原理,这是理解缓冲区溢出的基础。 2. *...

    缓冲区溢出程序代码分析

    缓冲区溢出是一种常见的安全漏洞,本实验旨在帮助理解缓冲区溢出的基本原理及其潜在危害。通过实践操作,掌握如何利用C语言来编写简单的缓冲区溢出代码,并进一步探讨如何防范此类攻击。 #### 缓冲区溢出概述 缓冲...

    缓冲区溢出攻击

    缓冲区溢出攻击是一种常见的网络安全威胁,它利用软件设计中的缺陷,尤其是与内存管理相关的漏洞,来破坏程序的正常运行,甚至获取对系统的非法控制。在本例中,我们将聚焦于Windows XP平台上的CCproxy服务,该服务...

    C++缓冲区溢出实验

    在编程领域,特别是系统安全和逆向工程中,缓冲区溢出是一种常见的编程错误,它可能导致程序崩溃、数据丢失,甚至被恶意利用执行任意代码。C++作为一门静态类型的语言,由于其对内存管理的特性,也存在缓冲区溢出的...

    缓冲区溢出原理.doc

    缓冲区溢出是一种系统攻击的手段,借助于在程序缓冲区编写超出其长度的代码,造成溢出,从而破坏其堆栈,使程序执行攻击者在程序地址空间中早已安排好的代码,以达到其目的。缓冲区溢出攻击之所以常见,是因为它太...

    缓冲区溢出答辩材料

    了解缓冲区溢出及其利用方法对于安全研究人员和软件开发者至关重要,因为它能帮助他们在设计和实现程序时采取适当的预防措施,避免此类漏洞的出现。通过这样的实验,我们可以深入理解内存安全,学习如何检测和修复...

    缓冲区溢出基本原理(相当适合初学溢出者)

    缓冲区溢出是软件开发中一个重要的安全性问题,了解其基本原理对于开发安全可靠的软件至关重要。通过正确管理和验证用户输入,结合使用现代编程语言的安全特性,可以在很大程度上避免此类漏洞的发生。希望本文能够...

    安全编程之缓冲区溢出.7z

    **安全编程之缓冲区溢出** ...通过深入理解缓冲区溢出,开发者可以编写更安全的代码,避免潜在的安全风险,保护用户数据和系统的稳定性。同时,对于安全研究人员而言,理解溢出机制也是发现和修复漏洞的关键。

    Q版缓冲区溢出教程

    缓冲区溢出是计算机安全领域中的一个重要概念,它涉及到程序设计和系统安全。本教程将通过生动的故事形式,深入浅出地解释缓冲区溢出的原理、如何利用它以及如何进行有效的防御。 缓冲区,简单来说,就是程序中用于...

    Windows缓冲区溢出资料

    Windows操作系统中的缓冲区溢出是一种常见的安全漏洞,主要出现在C/C++等编程语言中,由于程序员在处理输入数据时没有正确地检查边界,导致程序试图写入超出分配内存区域的数据,从而可能破坏程序运行环境,甚至允许...

Global site tag (gtag.js) - Google Analytics