C和C++安全编码(原书第2版)
上QQ阅读APP看书,第一时间看更新

2.6.3 对象大小检查

GNU C编译器(GCC)对于访问由指针指向的对象的大小,提供的功能有限。从4.1版本开始,GCC推出了__builtin_object_size()函数来提供这种能力。它的签名是size_t __builtin_object_size(void *ptr.int type)。其第一个参数是一个指向任何对象的指针。此指针可以指向对象的开始,但这不是必需的。例如,如果该对象是一个字符串或字符数组,则该指针可以指向数组的第一个字符或其范围中的任何字符。第二个参数提供关于被引用的对象的详细信息,并可以取从0~3的任何值。该函数返回从被引用的字节到被引用的对象的最后一个字节的字节数。

此函数仅适用于可以在编译时确定范围的对象。如果GCC不能确定被引用的是哪个对象,或者如果它不能确定这个对象的大小,则该函数返回0或–1,它们都是无效的大小。对于能够确定对象大小的编译器,该程序必须在优化级别-01或更高级别下编译。

第二个参数表示被引用的对象的详细信息。如果该参数为0或2,那么被引用的对象是包含指向的字节的最大对象,否则,所涉及的对象是包含指向的字节的最小对象。为了说明这一点区别,考虑下面的代码:


struct V { char buf1[10]; int b; char buf2[10]; } var; 
void *ptr = &var.b; 

如果把ptr传给__builtin_object_size()并把type设置为0,那么返回值是从var.b到var的末尾所包括的字节数量。(此值将至少等于sizeof(int)加上buf2数组的大小10)然而,如果type为1,则返回值为从var.b到var.b的末尾(包括它)所包括的字节数(即,sizeof(int))。

如果__builtin_object_size()无法确定指向的对象的大小,如果第二个参数是0或1,那么它返回(size_t)-1。如果第二个参数是2或3,那么它返回(size_t)0。表2.9总结type参数是如何影响__builtin_object_size()的行为的。

表2.9 type对__builtin_object_size()行为的影响

使用对象大小检查。当_FORTIFY_SOURCE已定义时,__builtin_object_size()函数用来为以下标准函数添加轻量级的缓冲区溢出保护:


memcpy()     strcpy()     strcat()      sprintf()     vsprintf()
memmove()    strncpy()    strncat()     snprintf()    vsnprintf()
memset()     fprintf()    vfprintf()    printf()      vprintf()

许多支持GCC的操作系统在默认情况下打开检查对象大小。其他操作系统则提供了宏(如_FORTIFY_SOURCE),作为一个选项来启用该功能。例如,Red Hat Linux,默认情况下没有进行保护。当把_FORTIFY_SOURCE设置为优化级别1(_FORTIFY_SOURCE=1)或更高时,采取的安全措施不应该改变合规程序的行为。_FORTIFY_SOURCE=2增加了更多的检查,但一些合规程序可能会失败。

例如,在定义了_FORTIFY_SOURCE时,memcpy()函数的实现可能会如下所示。


1  __attribute__ ((__nothrow__)) memcpy(
2    void * __restrict __dest,
3    __const void * __restrict __src,
4    size_t __len
5  ) {
6    return ___memcpy_chk(
7             __dest, __src, __len, __builtin_object_size(__dest, 0)
8           ); 9  }

当使用memcpy()和strcpy()函数时,下列行为是可能的:

1.下面的案例已知是正确的:


1  char buf[5];
2  memcpy(buf, foo, 5);
3  strcpy(buf, "abcd");

不需要进行运行时检查,因此调用memcpy()和strcpy()函数。

2.下面的案例,不知道是否正确,但可在运行时检查:


1  memcpy(buf, foo, n);
2  strcpy(buf, bar);

编译器知道对象中剩余的字节数,但不知道实际会复制的长度。在这种情况下,使用替代函数__memcpy_chk()和__strcpy_chk()。这些函数检查是否发生缓冲区溢出。如果检测到缓冲区溢出,就调用__chk_fail(),且通常在写一个诊断消息到stderr后中止应用程序。

3.下面的案例已知是不正确的:


1  memcpy(buf, foo, 6);
2  strcpy(buf, "abcde");

在编译时,编译器可以检测到缓冲区溢出。它发出警告,并在运行时调用带检查的替代函数。

4.最后一种情况是,不知道代码是否正确,并且不能在在运行时检查:


1  memcpy(p, q, n);
2  strcpy(p, q);

编译器不知道缓冲区的大小,并且不进行检查。在这些情况下,溢出不能被检测出。

了解更多:使用__builtin_object_size()。此函数可以与复制操作结合使用。例如,通过检查该数组的大小,可以安全地把一个字符串复制到一个大小固定的数组,如下所示。


01  char dest[BUFFER_SIZE];
02  char *src = /* 
合法指针 */;
03  size_t src_end = __builtin_object_size(src, 0);
04  if (src_end == (size_t) -1 && /* 
不知道 src 
是否太大 */
05      strlen(src) < BUFFER_SIZE) {
06    strcpy(dest, src);
07  } else if (src_end <= BUFFER_SIZE) {
08    strcpy(dest, src);
09  } else {
10    /* src 
会使 dest
溢出 */
11  }

使用__builtin_object_size()的优点是,如果它返回一个有效的大小(而不是0或–1),那么在运行时调用strlen()函数是不必要的,并且可以被旁路,从而提高运行时的性能。

定义了_FORTIFY_SOURCE时,GCC把strcpy()实现为调用__builtin___strcpy_chk()的内联函数。否则,strcpy()函数是一个普通的glibc函数。__builtin___strcpy_chk()函数具有如下签名:


char *__builtin___strcpy_chk(char *dest, const char *src,
                             size_t dest_end)

这个函数的行为与strcpy()类似,但它首先检查dest缓冲区是否足够大,以防止缓冲区溢出。这种检查是通过dest_end参数提供的,通常是__builtin_object_size()调用的结果。这种检查通常可以在编译时执行。如果编译器能够确定不会发生缓冲区溢出,它就可以把运行时检查优化掉。同样,如果编译器确定缓冲区溢出总是发生,它会发出警告,并在运行时中止调用。如果编译器知道目标字符串的空间大小,但不知道源字符串的长度,它增加了一个运行时检查。最后,如果编译器不能保证目标字符串有足够的空间,那么它会把调用移交给没有增加检查的标准的strcpy()函数。