分类：电脑/网络
问题：我的电脑出现内存溢出提示是怎么回事 
回答：你好，  在程序员设计的代码中包含的“溢出”漏洞实在太多了。本文将给大家介绍内存溢出问题的产生根源、巨大危害和解决途径。 一、为什么会出现内存溢出问题？ 导致内存溢出问题的原因有很多，比如： (1) 使用非类型安全(non-type-safe)的语言如 C/C++ 等。 (2) 以不可靠的方式存取或者复制内存缓冲区。 (3) 编译器设置的内存缓冲区太靠近关键数据结构。 下面来分析这些因素： 1. 内存溢出问题是 C 语言或者 C++ 语言所固有的缺陷，它们既不检查数组边界，又不检查类型可靠性(type-safety)。众所周知，用 C/C++ 语言开发的程序由于目标代码非常接近机器内核，因而能够直接访问内存和寄存器，这种特性大大提升了 C/C++ 语言代码的性能。只要合理编码，C/C++ 应用程序在执行效率上必然优于其它高级语言。然而，C/C++ 语言导致内存溢出问题的可能性也要大许多。其他语言也存在内容溢出问题，但它往往不是程序员的失误，而是应用程序的运行时环境出错所致。 2. 当应用程序读取用户(也可能是恶意攻击者)数据，试图复制到应用程序开辟的内存缓冲区中，却无法保证缓冲区的空间足够时(换言之，假设代码申请了 N 字节大小的内存缓冲区，随后又向其中复制超过 N 字节的数据)。内存缓冲区就可能会溢出。想一想，如果你向 12 盎司的玻璃杯中倒入 16 盎司水，那么多出来的 4 盎司水怎么办？当然会满到玻璃杯外面了！ 3. 最重要的是，C/C++ 编译器开辟的内存缓冲区常常邻近重要的数据结构。现在假设某个函数的堆栈紧接在在内存缓冲区后面时，其中保存的函数返回地址就会与内存缓冲区相邻。此时，恶意攻击者就可以向内存缓冲区复制大量数据，从而使得内存缓冲区溢出并覆盖原先保存于堆栈中的函数返回地址。这样，函数的返回地址就被攻击者换成了他指定的数值；一旦函数调用完毕，就会继续执行“函数返回地址”处的代码。非但如此，C++ 的某些其它数据结构，比如 v-table 、例外事件处理程序、函数指针等，也可能受到类似的攻击。 好，闲话少说，现在来看一个具体的例子。 请思考：以下代码有何不妥之处？ void CopyData(char *szData) { char cDest[32]; strcpy(cDest,szData); // 处理 cDest ... }  奇怪，这段代码好象没什么不对劲啊！确实，只有调用上述 CopyData() 才会出问题。例如：这样使用 CopyData() 是安全的： char *szNames[] = {"Michael","Cheryl","Blake"}; CopyData(szName[1]); 为什么呢？因为数组中的姓名("Michael"、"Cheryl"、"Blake")都是字符串常量，而且长度都不超过 32 个字符，用它们做 strcpy() 的参数总是安全的。再假设 CopyData 的唯一参数 szData 来自 socket 套接字或者文件等不可靠的数据源。由于 strcpy 并不在乎数据来源，只要没遇上空字符，它就会一个字符一个字符地复制 szData 的内容。此时，复制到 cDest 的字符串就可能超过 32 字符，进而导致内存缓冲区 cDest 的溢出；溢出的字符就会取代内存缓冲区后面的数据。不幸的是，CopyData 函数的返回地址也在其中！于是，当 CopyData 函数调用完毕以后，程序就会转入攻击者给出的“返回地址”，从而落入攻击者的圈套！授人以柄，惨！ 前面提到的其它数据结构也可能受到类似的攻击。假设有人利用内存溢出漏洞覆盖了下列 C++ 类中的 v-table ： void CopyData(char *szData) { char cDest[32]; CFoo foo; strcpy(cDest,szData); foo.Init(); } 与其它 C++ 类一样，这里的 CFoo 类也对应一个所谓的 v-table，即用于保存一个类的全部方法地址的列表。若攻击者利用内存溢出漏洞偷换了 v-table 的内容，则 CFoo 类中的所有方法，包括上述 Init() 方法，都会指向攻击者给出的地址，而不是原先 v-table 中的方法地址。顺便说一句，即使你在某个 C++ 类的源代码中没有调用任何方法，也不能认为这个类是安全的，因为它在运行时至少需要调用一个内部方法——析构器(destructor)！当然，如果真有一个类没有调用任何方法，那么它的存在意义也就值得怀疑了。 二、解决内存溢出问题 不要太悲观，下面讨论内存溢出问题的解决和预防措施。 1、改用受控代码  2002 年 2 月和 3 月，微软公司展开了 Microsoft Windows Security Push 活动。在此期间，我所在的小组一共培训了超过 8500 人，教授他们如何在设计、测试和文档编制过程中解决安全问题。在我们向所有程序设计人员提出的建议中，有一条就是：紧跟微软公司软件开发策略的步伐，将某些应用程序和工具软件由原先基于本地 Win32 的 C++ 代码改造成基于 .NET 的受控代码。我们的理由很多，但其中最根本的一条，就是为了解决内存溢出问题。基于受控代码的软件发生内存溢出问题的机率要小得多，因为受控代码无法直接存取系统指针、寄存器或者内存。作为开发人员，你应该考虑(至少是打算)用受控代码改写某些应用程序或工具软件。例如：企业管理工具就是很好的改写对象之一。当然，你也应该很清楚，不可能在一夜之间把所有用 C++ 开发的软件用 C# 之类的受控代码语言改写。 2、遵守黄金规则 当你用 C/C++ 书写代码时，应该处处留意如何处理来自用户的数据。如果一个函数的数据来源不可靠，又用到内存缓冲区，那么它就必须严格遵守下列规则： 必须知道内存缓冲区的总长度。  检验内存缓冲区。  提高警惕。  让我们来具体看看这些“黄金规则”： （1）必须知道内存缓冲区的总长度。 类似这样的代码就有可能导致 bug ： 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) strcpy(szBuff,szName); } 这样虽然有所改进，可它似乎又过于信任 cbName 了。攻击者仍然有机会伪造 czName 和 szName 两个参数以谎报数据长度和内存缓冲区长度。因此，你还得检检这两个参数！ （2）检验内存缓冲区 如何知道由参数传来的内存缓冲区长度是否真实呢？你会完全信任来自用户的数据吗？通常，答案是否定的。其实，有一种简单的办法可以检验内存缓冲区是否溢出。请看如下代码片断： void Function(char *szName, DWORD cbName) { char szBuff[MAX_NAME]; // 检测内存 szBuff[cbName] = 0x42; // 复制并使用 szName  if (cbName < MAX_NAME) strcpy(szBuff,szName); } 这段代码试图向欲检测的内存缓冲区末尾写入数据 0x42 。你也许会想：真是多此一举，何不直接复制内存缓冲区呢？事实上，当内存缓冲区已经溢出时，一旦再向其中写入常量值，就会导致程序代码出错并中止运行。这样在开发早期就能及时发现代码中的 bug 。试想，与其让攻击者得手，不如及时中止程序；你大概也不愿看到攻击者随心所欲地向内存缓冲区复制数据吧。 （3）提高警惕 虽然检验内存缓冲区能够有效地减小内存溢出问题的危害，却不能从根本上避免内存溢出攻击。只有从源代码开始提高警惕，才能真正免除内存溢出攻击的危胁。不错，上一段代码已经很对用户数据相当警惕了。它能确保复制到内存缓冲区的数据总长度不会超过 szBuff 的长度。然而，某些函数在使用或复制不可靠的数据时也可能潜伏着内存溢出漏洞。为了检查你的代码是否存在内存溢出漏洞，你必须尽量追踪传入数据的流向，向代码中的每一个假设提出质疑。一旦这么做了，你将会意识到其中某些假设是错误的；然后你还会惊讶地叫道：好多 bug 呀！ 在调用 strcpy、strcat、gets 等经典函数时当然要保持警惕；可对于那些所谓的第 n 版 (n-versions) strcpy 或 strcat 函数 —— 比如 strncpy 或 strncat (其中 n = 1，2，3 ……) —— 也不可轻信。的确，这些改良版本的函数是安全一些、可靠一些，因为它们限制了进入内存缓冲区的数据长度；然而，它们也可能导致内存溢出问题！请看下列代码，你能指出其中的错误吗？ #define SIZE(b) (sizeof(b)) char buff[128]; strncpy(buff,szSomeData,SIZE(buff)); strncat(buff,szMoreData,SIZE(buff)); strncat(buff,szEvenMoreData,SIZE(buff)); 给点提示：请注意这些字符串函数的最后一个参数。怎么，弃权？我说啊，如果你是执意要放弃那些“不安全”的经典字符串函数，并且一律改用“相对安全”的第 n 版函数的话，恐怕你这下半辈子都要为了修复这些新函数带来的新 bug 而疲于奔命了。呵呵，开个玩笑而已。为何这么说？首先，它们的最后一个参数并不代表内存缓冲区的总长度，它们只是其剩余空间的长度；不难看出，每执行完一个 strn... 函数，内存缓冲区 buff 的长度就减少一些。其次，传递给函数的内存缓冲区长度难免存在“大小差一”(off-by-one)的误差。你认为在计算 buff 的长度时包括了字符串末尾的空字符吗？当我向听众提出这个问题时，得到的肯定答复和否定答复通常是 50 比 50 ，对半开。也就是说，大约一半人认为计算了末尾的空字符，而另一半人认为忽略了该字符。最后，那些第 n 版函数所返回的字符串不见得以空字符结束，所以在使用前务必要仔细阅读它们的说明文档。 3、编译选项 /GS “/GS”是 Visual C++ .NET 新引入的一个编译选项。它指示编译器在某些函数的堆栈帧(stack-frames) 中插入特定数据，以帮助消除针对堆栈的内存溢出问题隐患。切记，使用该选项并不能替你清除代码中的内存溢出漏洞，也不可能消灭任何 bug 。它只是亡羊补牢，让某些内存溢出问题隐患无法演变成真正的内存溢出问题；也就是说，它能防止攻击者在发生内存溢出时向进程中插入和运行恶意代码。无论如何，这也算是小小的安全保障吧。请注意，在新版的本地 Win32 C++ 中使用 Win32 应用程序向导创建新项目时，默认设置已经打开了此选项。同样，Windows .NET Server 环境也默认打开了此选项。关于 /GS 选项的更多信息，请参考 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)) {