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 永远是安全的。然而,如果 CopyData 和 szData 的唯一参数来自不可靠的源(如套接字或文件),则 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++ 方法就安全了,那就错了,因为有一个方法始终会被调用,即该类的虚析构函数!当然,如果某个类不调用任何方法,就应该想想它存在的必要了。
作者在文中介绍了五种具体方法,来修复缓冲区溢出问题。
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 永远是安全的。然而,如果 CopyData 和 szData 的唯一参数来自不可靠的源(如套接字或文件),则 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++ 方法就安全了,那就错了,因为有一个方法始终会被调用,即该类的虚析构函数!当然,如果某个类不调用任何方法,就应该想想它存在的必要了。