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

2.5.7 strncpy()和strncat()

strncpy()和strncat()函数与strcpy()和strcat()函数是类似的,但每个函数都有一个额外的size_t类型的参数n用于限制要被复制的字符数量。这些函数可以被认为是截断型的复制和拼接函数。

strncpy()库函数与strcpy()完成相似的功能,但前者允许指定一个最大大小n:


1  char *strncpy( 
2    char * restrict s1, const char * restrict s2, size_t n 
3  ); 

strncpy()函数的用法如下面的示例所示:


strncpy(dest, source, dest_size - 1); 
dest[dest_size - 1] = '\0'; 

因为strncpy()函数不能保证用空字符终止目标字符串,所以程序员必须小心,以确保目标字符串是正确地以空字符终止的,并且没有覆盖最后一个字符。

C标准的strncpy()函数经常被推荐为strcpy()函数“更安全”的替代品。然而,strncpy()容易发生串终止错误,详情见随后介绍的“C11附录K边界检查接口”

strncat()函数具有以下签名:


1  char *strncat( 
2    char * restrict s1, const char * restrict s2, size_t n
3  );

strncat()函数从s2指向的数组追加不超过n个字符(空字符和它后面的字符不追加)到s1指向的字符串结尾。s2最初的字符覆盖了s1末尾的空字符。终止空字符总是被附加到结果字符串。因此,在s1指向的数组中的最大字符数量是strlen(s1) + n +1。

必须谨慎使用strncpy()和strncat()函数,或根本不使用它们,尤其是在有更不易出错的替代品的时候。下面是一个实际代码的例子,它是从现有的代码中将strcpy()和strcat()简单地变换成strncpy()和strncat()函数得到的:


strncpy(record, user, MAX_STRING_LEN - 1); 
strncat(record, cpw, MAX_STRING_LEN - 1); 

问题在于strncat()的最后一个参数不应该是缓冲区的总长度,而应该是调用strncpy()后缓冲区剩余的长度。这两个函数都要求指定剩余的长度而不是缓冲区的总长度。由于剩余的长度在每次添加或删除数据时都会改变,因此程序员必须跟踪这些改变或重新计算剩余长度。这个过程很容易出错,并且可能会导致漏洞。下面的调用在使用strncat()连接字符串之前正确地计算了剩余的空间:


strncat(dest, source, dest_size-strlen(dest)-1)

用strncpy()和strncat()作为strcpy()和strcat()函数替代品的另一个问题是,当结果字符串被截断时,这两个函数都没有提供一个状态代码或报告。两个函数都返回目标缓冲区的指针,这就要求程序员付出大量的劳动来确定结果字符串是否被截断了。

strncpy()还存在一个性能问题,当源数据被复制完之后,它会将目标字符串填满null字符。虽然对这种行为并没有什么合理的解释,但有很多程序已经依赖于这个特性,因此很难变动这种行为。

利用strncpy()和strncat()函数作为strcpy()和strcat()的替代函数,这种角色超出了它们的用途。这些函数的最初目的是允许复制和连接一个子字符串。然而,这些函数都容易发生缓冲区溢出和空字符终止错误。

C11附录K边界检查接口。C11附录K指定strncpy_s()和strncat_s()函数作为strncpy()和strncat()很接近的替代品。

strncpy_s()从源字符串中复制不超过指定数目的连续字符(空字符后面的字符将不被复制)到目标字符数组中。strncpy_s()函数具有以下签名:


1  errno_t strncpy_s( 
2    char * restrict s1,
3    rsize_t s1max,
4    const char * restrict s2,
5    rsize_t n
6  );

strncpy_s()函数有一个额外的参数用于给出目标数组的大小,以防止缓冲区溢出。如果发生运行时约束违反,则目标数组被设置为空字符串,以增加问题的能见度。

以下两个条件之一发生时,strncpy_s()函数停止从源字符串复制到目标数组:

1.源字符串的空终止字符被复制到目标字符串。

2.已复制由n参数指定的数量的字符。

如果空字符终结符不是从源字符串复制的,那么提供的目标结果会另外包括一个空字符终结符。操作的结果,包括空终结符,必须能够容纳在目标字符串中,否则就会发生运行时约束违反。该函数永远不会修改目标数组外的存储。

strncpy_s()函数返回0表示成功。如果输入参数是无效的,它会返回一个非零值并把目标字符串设置为空字符串。如果源或目标指针为NULL,或者目标字符串的最大长度为0或大于RSIZE_MAX,则输入验证失败。如果指定复制的字符数量大于RSIZE_MAX,则输入同样被认为是无效的。

一个strncpy_s()操作在其指定复制的字符数量超过目标字符串的最大长度时仍然可能执行成功,前提是源字符串实际包含的字符数目小于目标字符串的最大长度。如果需要复制的字符数大于或等于目标字符串的最大长度并且源字符串比目标缓冲区长,则该操作将会失败。

因为源字符串中的字符数目受限于n参数,且目标字符串有一个单独的参数用于给出目标数组元素的最大数量,所以strncpy_s()函数可以安全地复制子串,而不只是整个字符串或它的尾部。

由于意外的字符串截断可能是一个安全漏洞,因此strncpy_s()不截断源字符串(由空终结符和n参数分隔),以适应目标。截断是一种违反运行时约束的动作。然而,有一个习惯用法,允许一个程序使用strncpy_s()函数来强制截断。如果参数n是目标字符串大小减1,则strncpy_s()将复制整个源字符串至目标字符串或截断它,以便它能在目标数组中容纳下(一如既往的,结果将是以空字符结尾的)。例如,下面的调用将把src复制到dest数组,从而在dest中产生一个正确地以空字符结尾的字符串。当dest已满(包括空终结符)或当src的所有内容都已被复制时,复制操作将停止。


strncpy_s(dest, sizeof dest, src, (sizeof dest)-1)

虽然OpenBSD的strlcpy()函数与strncpy()类似,但它更类似于strcpy_s(),而不是strncpy_s()。不像strlcpy(),strncpy_s()支持检查运行时约束,如目标数组的大小,而且它不会截断字符串。

因为必须指定目标缓冲区的大小和要追加的字符的最大数目,所以使用strncpy_s()函数是不太可能会引入安全漏洞的。考虑下面的定义:


1  char src1[100] = "hello"; 
2  char src2[7] = {'g','o','o','d','b','y','e'}; 
3  char dst1[6], dst2[5], dst3[5]; 
4  errno_t r1, r2, r3; 

因为目标字符数组有足够的存储空间,所以下面对strncpy_s()的调用将r1赋予0值并把序列hello\0赋值给dst1:


r1 = strncpy_s(dst1, sizeof(dst1), src1, sizeof(src1)); 

下面的调用给r2赋予0值并把序列“good\0”赋值给dst2:


r2 = strncpy_s(dst2, sizeof(dst2), src2, 4);

然而,没有足够的空间来把src1字符串复制到dst3。因此,如果以下对strncpy_s()的调用返回,r3被赋予一个非零值且dst3[0]被赋值'\0':

r3 = strncpy_s(dst3, sizeof(dst3), src1, sizeof(src1));

如果用strncpy()来代替strncpy_s(),目标数组dst3将不会正确地以空结尾。

strncat_s()函数从源字符串附加不超过指定数目的连续字符(空字符后面的字符将不会被复制)到目标字符数组中。源字符串的首字符会覆盖目标数组原来结尾的空字符。如果没有从源字符串复制空字符,则在附加后的字符串结尾写入一个空字符。

strncat_s()函数具有以下签名/原型。


1  errno_t strncat_s(
2    char * restrict s1,
3    rsize_t s1max,
4    const char * restrict s2,
5    rsize_t n
6  );

如果源或目标指针为NULL,或者目标缓冲区的最大长度等于0或大于RSIZE_MAX,则运行时约束违反发生且strncat_s()函数返回一个非零值。如果目标数组已满或者没有足够的空间供完全附加源字符串,函数也将失败。strncat_s()函数还保证目标字符串是空字符终止的。

strncat_s()函数有一个额外的参数用于给出目标数组的大小,以防止缓冲区溢出。在目标数组的原始字符串加上从源数组追加的新字符必须在目标数组中能容纳下并以空字符终止,以避免运行时违反约束。如果发生运行时约束违反,则目标数组被设置为空字符串,以增加问题的能见度。

当以下两个条件中的某个首先发生时,strncat_s()函数将停止把源字符串附加到目标数组中。

1.以空字符结尾的源字符串已被复制到目标数组。

2.由n参数指定的数量的字符已被复制。

如果空字符终结符不是从源字符串复制的,就会为目标数组另外提供一个空字符终结符。产生的结果,包括空终结符,必须能在目标数组中容纳,否则就会发生运行时约束违反。strncat_s()函数永远不会修改目标数组外的存储。

因为源字符串的字符数目受限于n参数,且有一个单独的参数用于给出目标数组元素的最大数量,所以strncat_s()函数可以安全地追加一个子串而不只是整个字符串或它的尾部。

由于意外的字符串截断可能是一个安全漏洞,因此strncat_s()不截断源字符串(以空终结符和n参数指定的),以适应目标数组。截断是一种运行时约束违反。然而,有一个习惯用法,允许一个程序使用strncat_s()函数来强制截断。如果参数n为留在目标数组的元素数量减1,那么strncat_s()将整个源字符串追加至目的地或截断它,以适应目标数组的大小(一如既往的,结果将以空字符结尾)。例如,下面的调用将把src追加到dest数组,从而在dest产生一个正确地以空字符结尾的字符串。当dest已满(包括空终结符),或src的所有内容均已附加时,将停止拼接:


1  strncat_s( 
2    dest, 
3    sizeof dest, 
4    src, 
5    (sizeof dest) - strnlen_s(dest, sizeof dest) - 1 
6  ); 

虽然OpenBSD的strlcat()函数类似于strncat(),但它更类似于strcat_s(),而不是strncat_s()。不同于strlcat(),strncat_s()支持检查运行时约束,如目标数组的大小,并且它不会截断字符串。

如果错误地指定目标缓冲区的最大长度和要复制的字符数量,strncpy_s()和strncat_s()函数仍然能造成缓冲区溢出。

动态分配函数。ISO/IEC TR 24731-2[ISO/IEC TR 24731-2:2010]描述了strndup()函数,它也可以用来作为strncpy()函数的另一个替代函数。ISO/IEC TR 24731-2没有定义任何strncat()函数的替代函数。strndup()函数和strdup()函数的功能相当,在一个新的,看似使用malloc()分配的内存块中复制提供的字符串,唯一的例外是strndup()函数最多复制n加1个字节到新分配的内存中,并用空字节终止新字符串。如果字符串的长度大于n,那么只复制n个字节。如果n大于字符串的长度,那么字符串中的所有字节都被复制到新的内存缓冲区,包括用于终止的空字节。新创建的字符串将永远是被正确地终止的。分配的字符串必须通过把返回的指针传递给free()来回收内存。

替代函数总结。表2.7总结了本节描述的截断复制的一些替代函数。

表2.7 截断复制函数

表2.8总结了本节描述的截断拼接的一些替代的函数。TR 24731-2没有定义替代的截断拼接函数。

表2.8 截断级联函数