以文本方式查看主题 - 中文XML论坛 - 专业的XML技术讨论区 (http://bbs.xml.org.cn/index.asp) -- 『 C/C++编程思想 』 (http://bbs.xml.org.cn/list.asp?boardid=61) ---- 修复缓冲区溢出问题 (http://bbs.xml.org.cn/dispbbs.asp?boardid=61&rootid=&id=86927) |
-- 作者:卷积内核 -- 发布时间:9/28/2010 8:55:00 AM -- 修复缓冲区溢出问题 为什么会出现缓冲区溢出 出现缓冲区溢出需要具备很多条件,包括: 使用非类型安全的语言,如 C/C++。 首先,缓冲区溢出主要出现在 C 和 C++ 中,因为这些语言不执行数组边界检查和类型安全检查。C/C++ 允许开发人员创建非常接近硬件运行的程序,从而允许直接访问内存和计算机寄存器。其结果可以获得优异的性能;很难有任何应用程序能象编写得很好的 C/C++ 应用程序运行得那样快。其他语言中也会出现缓冲区溢出,但很少见。如果出现这种错误,通常不是由开发人员造成的,而是运行时环境的错误。 下面我们来看一个示例。 以下代码有什么错误? void CopyData(char *szData) { // 使用 cDest 令人惊讶的是,这段代码可能没有什么错误!这完全取决于 CopyData() 的调用方式。例如,以下代码是安全的: char *szNames[] = {"Michael","Cheryl","Blake"}; 这段代码是安全的,因为名字是硬编码的,并且知道每个字符串在长度上不超过 32 个字符,因此调用 strcpy 永远是安全的。然而,如果 CopyData 和 szData 的唯一参数来自不可靠的源(如套接字或文件),则 strcpy 将复制该数据,直到碰到空字符为止;如果此数据的长度大于 32 个字符,则 cDest 缓冲区将溢出,并且在内存中该缓冲区以外的任何数据将遭到破坏。不幸的是,在这里,遭到破坏的数据是来自 CopyData 的返回地址,这意味着当 CopyData 完成时,它仍然在由攻击者指定的位置继续执行。这真糟糕! 其他数据结构也同样敏感。假设某个 C++ 类的 V 表遭到破坏,如下面这段代码: void CopyData(char *szData) { foo.Init(); 此示例假定 CFoo 类具有虚方法,以及一个 V 表或该类方法的地址列表(与所有 C++ 类一样)。如果由于 cDest 缓冲区被覆盖而破坏了 V 表,则该类的任何虚方法(在此例中是 Init() )都可能调用攻击者指定的地址,而不是 Init() 的地址。顺便说一句,如果认为您的代码不调用任何 C++ 方法就安全了,那就错了,因为有一个方法始终会被调用,即该类的虚析构函数!当然,如果某个类不调用任何方法,就应该想想它存在的必要了。 修复缓冲区溢出 迁移到托管代码 遵循以下重要规则 要求代码传递缓冲区的长度。 要求代码传递缓冲区的长度 void Function(char *szName) { 此代码的问题在于函数不能判断 szName 的长度,这意味着将不能安全地复制数据。函数应知道 szName 的大小: void Function(char *szName, DWORD cbName) { 然而,您不能想当然地信任 cbName 。攻击者可以设置该名称和缓冲区大小,因此必须进行检查! 探测内存 void Function(char *szName, DWORD cbName) { // 探测 // 复制并使用 szName 此代码将尝试向目标缓冲区写入值 0x42。您可能会想,为什么要这样做而不是直接复制缓冲区呢?通过向目标缓冲区的末尾写入一个固定的已知值,可以在源缓冲区太大时,强制代码失败。同时这样也可以在开发过程中及早发现开发错误。与其运行攻击者的恶意有效代码,还不如让程序失败。这就是不复制攻击者的缓冲区的原因。 注意:您只能在调试版中这样做,以便在测试过程中捕获缓冲区溢出。 说实话,探测虽然很有用,但它并不能使您免遭攻击。真正安全的办法是编写防范性的代码。您会注意到代码已经具有防范性了。它将检查进入函数的数据是否不超过内部缓冲区 szBuff 。然而,有些函数在处理或复制不可靠的数据时,如果使用不当,则会存在潜在的严重安全问题。这里的关键是不可靠的数据。在检查代码的缓冲区溢出错误时,应跟踪数据在代码中的流向,并检查各种数据假设。当您意识到有些假设不正确时,您也许会惊异于所发现的错误。 #define SIZE(b) (sizeof(b)) 如果您需要提示,请注意每个字符串处理函数的最后一个参数。要放弃吗?在我给出答案之前,我经常会开玩笑说,如果您禁用“不安全”的字符串处理函数,而使用较为安全的 n 版本,则恐怕您要在修复新产生的错误中度过您的余生。以下便是原因所在。首先,最后那个参数不是目标缓冲区的总体大小。它是缓冲区剩余空间的大小,代码每次向 buff 添加内容时,buff 都会有实质的减小。第二个问题是,即使用户传递了缓冲区大小,他们通常也是逐一减小的。那么在计算字符串大小时,您有没有包含末尾的空字符?当我针对这个问题进行读者调查时,通常是对半分。其中一半认为在计算缓冲区大小时确实要考虑末尾空字符,另外一半则不这么认为。第三,在某些情况下,n 版本可能不会以空字符作为结果字符串的结束字符,因此请一定要阅读文档。 使用 /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 字符串。 if (pECB->GetServerVariable (pECB->ConnID,
|
W 3 C h i n a ( since 2003 ) 旗 下 站 点 苏ICP备05006046号《全国人大常委会关于维护互联网安全的决定》《计算机信息网络国际联网安全保护管理办法》 |
31.250ms |