使用 C 或 C++ 开发安全代码是可能的,但这两种语言都包含一些基本的设计决策,使得编写安全代码更加困难。C 和 C++ 容易出现缓冲区溢出,迫使程序员进行自己的内存管理,并且它们的类型系统相当宽松。对于系统程序(例如操作系统内核),C 和 C++ 是不错的选择。对于应用程序,C 和 C++ 经常被过度使用。强烈考虑使用更高级别的语言,至少对于大多数应用程序而言。但显然,存在许多用 C 和 C++ 编写的现有程序不会被完全重写,并且许多开发人员可能会选择使用 C 和 C++ 进行开发。
C 和 C++ 程序最大的安全问题之一是缓冲区溢出;有关更多信息,请参见第 6 章。C 还有一个额外的弱点,即不支持异常,这使得编写忽略严重错误情况的程序变得容易。
C 和 C++ 的另一个问题是,开发人员必须进行自己的内存管理(例如,使用 malloc()、alloc()、free()、new 和 delete),并且未能正确执行此操作可能会导致安全漏洞。更严重的问题是,程序可能会错误地释放不应该释放的内存(例如,因为它已经被释放了)。这可能导致立即崩溃或被利用,从而允许攻击者执行任意代码;请参阅 [Anonymous Phrack 2001]。某些系统(例如许多 GNU/Linux 系统)默认情况下根本不防止双重释放,并且尚不清楚那些试图保护自己的系统是否真正不可破坏。尽管我没有看到任何关于这个主题的文章,但我怀疑在 C++ 中使用不正确的调用(例如,混合使用 new 和 malloc())可能会产生类似的效果。例如,在 2002 年 3 月 11 日,宣布 zlib 库存在此问题,影响了许多使用它的程序。因此,在 GNU/Linux 上测试程序时,您应该将环境变量 MALLOC_CHECK_ 设置为 1 或 2,并且您可以考虑使用环境变量设置为 0、1、2 来执行您的程序。此变量的原因在 GNU/Linux malloc(3) 手册页中进行了解释:
有各种工具可以处理这个问题,例如 Electric Fence 和 Valgrind;有关更多信息,请参见第 11.7 节。如果未释放未使用的内存(例如,使用 free()),则未使用的内存可能会累积 - 如果累积了足够的未使用内存,则程序可能会停止工作。因此,攻击者可能会利用未使用的内存来创建拒绝服务。理论上,攻击者可能会导致内存碎片化并导致拒绝服务,但这通常是一种相当不切实际且低风险的攻击。最新版本的 Linux libc(高于 5.4.23)和 GNU libc (2.x) 包含一个 malloc 实现,可以通过环境变量进行调整。当设置 MALLOC_CHECK_ 时,将使用一种特殊的(效率较低的)实现,该实现旨在容忍简单错误,例如使用相同参数对 free() 进行双重调用,或单个字节的溢出(差一错误)。但是,并非所有此类错误都可以防止,并且可能导致内存泄漏。如果 MALLOC_CHECK_ 设置为 0,则任何检测到的堆损坏都将被静默忽略;如果设置为 1,则会在 stderr 上打印诊断信息;如果设置为 2,则会立即调用 abort()。这可能很有用,因为否则崩溃可能会在很久之后才发生,并且问题的真正原因很难追踪。
在声明类型时,尽可能严格。在可以的情况下,使用 ``enum'' 定义枚举值(而不仅仅是具有特殊值的 ``char'' 或 ``int'')。这对于 switch 语句中的值特别有用,编译器可以用来确定是否已涵盖所有合法值。在适当的情况下,如果值不能为负数,请使用 ``unsigned'' 类型。
C 和 C++ 中的一个复杂之处在于字符类型 ``char'' 可以是有符号或无符号的(取决于编译器和机器)。当高位设置为 1 的有符号 char 保存在整数中时,结果将是一个负数;在某些情况下,这可能是可利用的。一般来说,在处理可能具有大于 127 (0x7f) 的值的字符数据时,对于缓冲区、指针和强制转换,请使用 ``unsigned char'' 而不是 char 或 signed char。
根据定义,C 和 C++ 在类型检查支持方面相当宽松,但您至少可以提高其检查级别,以便可以自动检测到一些错误。尽可能多地打开编译器警告,并更改代码以使用它们干净地编译,并在单独的头文件 (.h) 中严格使用 ANSI 原型,以确保所有函数调用都使用正确的类型。对于使用 gcc 的 C 或 C++ 编译,至少使用以下编译标志(这将打开大量警告消息)并尝试消除所有警告(请注意,使用 -O2 是因为某些警告只能通过在更高优化级别执行的数据流分析来检测)
gcc -Wall -Wpointer-arith -Wstrict-prototypes -O2 |
许多 C/C++ 编译器可以检测到不准确的格式字符串。例如,如果您使用 gcc 的 __attribute__() 功能(C 扩展)来标记此类函数,则 gcc 可以警告您创建的函数的不准确格式字符串,并且您可以使用该功能而不会使您的代码不可移植。以下是您可以放在头文件 (.h) 中的示例
/* in header.h */ #ifndef __GNUC__ # define __attribute__(x) /*nothing*/ #endif extern void logprintf(const char *format, ...) __attribute__((format(printf,1,2))); extern void logprintva(const char *format, va_list args) __attribute__((format(printf,1,0))); |
避免 C/C++ 开发人员常犯的错误。例如,注意不要在您想表达 ``=='' 时使用 ``=''。