为什么strncpy不会在末尾添加空字符?

98

strncpy()据说可以防止缓冲区溢出。但如果它在不添加空字符的情况下防止了溢出,很可能随后的字符串操作也会溢出。因此,为了防止这种情况,我发现自己要做:

strncpy( dest, src, LEN );
dest[LEN - 1] = '\0';

man strncpy 给出了如下信息:

strncpy() 函数与 strcpy() 函数类似,不同之处在于只复制不多于 n 个字节的 src。因此,如果在 src 的前 n 个字节中没有空字符,则结果将不会以空字符结尾。

如果没有添加空字符,即使是看似无害的东西也可能导致问题,比如:

   printf( "FOO: %s\n", dest );

如果使用strncpy(),会出现可能导致程序崩溃的情况。是否有更好、更安全的替代方法?


1
请注意,在MacOS X(BSD)上,man页面中对'extern char *strncpy(char * restrict s1,const char * restrict s2,size_t n);'的说明如下:strncpy()函数将最多n个字符从s2复制到s1中。如果s2少于n个字符,则s1的剩余部分将用`\0'字符填充。否则,s1不会被终止。 - Jonathan Leffler
难道不应该是 dest[LEN-1] = '\0'; 吗? - codeObserver
2
这是我认为我们应该复制字符串的方法: int LEN = src.len; str* dest = new char[LEN+1]; strncpy( dest, src, LEN ); dest[LEN] = '\0'; - codeObserver
如果您确定字符串的大小不会超过目标缓冲区长度,那么在目标字符串上始终使用memset是最安全的方法。 - koolvcvc
编写你自己的函数,我认为这不应该是一个难任务。 - Megharaj
11个回答

60

strncpy()并不是用来替代更安全的strcpy()函数的,它应该用来将一个字符串插入到另一个字符串的中间。

所有这些“安全”的字符串处理函数,如snprintf()vsnprintf()等,都是在后来的标准中添加的修复措施,以缓解缓冲区溢出攻击等问题。

维基百科提到,strncat()可以作为编写自己的安全版strncpy()的替代方案:

*dst = '\0';
strncat(dst, src, LEN);

编辑

我错过了一个重要的事实,即当字符串长度大于等于 LEN 字符时,strncat() 在添加 NULL 结尾字符时会超出 LEN 字符。

无论如何,使用strncat() 而不是任何自制的解决方案(例如memcpy(..., strlen(...))/ 等等),其目的在于 strncat() 的实现可能针对库进行了目标/平台优化。

当然,您需要检查 dst 是否至少包含 nullchar,因此正确使用strncat()应该像这样:

if (LEN) {
    *dst = '\0'; strncat(dst, src, LEN-1);
}

我也承认,如果源字符串长度小于n,strncpy()对于将子字符串复制到另一个字符串中并不是非常有用,目标字符串将被截断。


34
“it is supposed to be used to insert one string in the middle of another” - 不,它的目的是将一个字符串写入到固定宽度的字段中,例如在目录条目中。这就是为什么当(且仅当)源字符串太短时,它会用 NUL 填充输出缓冲区的原因。 - Steve Jessop
5
设置*dst='\0'如何使它更安全?它仍然存在允许您在目标缓冲区末尾之外写入的原始问题。 - Adam Liss
5
好的,但是应该改为strncat(dst,src,LEN-1),因为它将写入一个额外的字符。 - Timothy Pratley
3
@Jonathan: 实际上,"safe" 数据类型将结合指向字符缓冲区的指针和该缓冲区的长度。但我们都知道这不会发生。就个人而言,我已经厌倦了所有这些努力,试图使本质上不安全的东西(程序员试图准确地尊重缓冲区的长度)变得更加安全。如果说我们当前的缓冲区溢出过多了50%,只要我们能让字符串处理安全度提高50%,那么我们就没问题了:( - Steve Jessop
2
+1,不要重复那些错误的说法,即strncpy是strcpy的安全版本——前者有其自身的问题。 - paxdiablo
显示剩余6条评论

40

最初,第七版UNIX文件系统(参见DIR(5))的目录条目将文件名限制为14个字节; 目录中的每个条目都由2个字节的i节点编号和14个字节的名称组成,null填充到14个字符,但不一定以null结尾。 我认为strncpy()是为这些目录结构设计的-或者至少对于该结构它完美地工作。

考虑:

  • 14个字符的文件名未以null结尾。
  • 如果名称少于14个字节,则填充null以达到满长度(14个字节)。

这正是以下代码实现的内容:

strncpy(inode->d_name, filename, 14);

因此,strncpy()最初是非常适合其特定应用场景的。防止空终止字符串的溢出只是一个巧合。

(请注意,将空值填充到长度14并不会带来严重的开销 - 如果缓冲区的长度为4 KB,并且您想安全地将20个字符复制到其中,则多余的4075个空值是严重的浪费,如果您反复向长缓冲区添加内容,则很容易导致二次行为。)


4
那种情况可能有些难以理解,但在数据结构中拥有固定长度的字符串字段、进行了空格填充但没有以空字符结尾并不算罕见。实际上,如果要存储固定格式的数据,这通常是最有效的方法。 - supercat

27

1
更不用说,它是便携、快速和可靠的。你仍然可能会误用它,但风险要低得多。在我看来,strncpy 应该被弃用,并用一个叫做 dirnamecpy 或类似名称的相同函数替换。strncpy 不是安全的字符串复制函数,从来都不是。 - user14554

8

ISO/IEC TR 24731指定了一些新的替代品(有关信息,请参见https://buildsecurityin.us-cert.gov/daisy/bsi/articles/knowledge/coding/317-BSI.html)。这些函数大多数都需要另外一个参数,该参数指定目标变量的最大长度,确保所有字符串都是以空字符结尾,并以_s结尾命名以将其与早期“不安全”的版本区分开来。1

不幸的是,它们仍在 gaining support 阶段,可能无法在您特定的工具集中使用。 Visual Studio的较新版本将在使用旧的不安全函数时抛出警告。

如果您的工具不支持新函数,则可以很容易地为旧函数创建自己的包装器。下面是一个示例:

errCode_t strncpy_safe(char *sDst, size_t lenDst,
                       const char *sSrc, size_t count)
{
    // No NULLs allowed.
    if (sDst == NULL  ||  sSrc == NULL)
        return ERR_INVALID_ARGUMENT;

   // Validate buffer space.
   if (count >= lenDst)
        return ERR_BUFFER_OVERFLOW;

   // Copy and always null-terminate
   memcpy(sDst, sSrc, count);
   *(sDst + count) = '\0';

   return OK;
}

你可以根据需要更改该函数,例如始终尽可能复制字符串而不溢出。实际上,如果将_TRUNCATE作为count传递给VC++实现,它就可以做到这一点。
当然,你仍然需要准确地确定目标缓冲区的大小:如果提供了一个3个字符的缓冲区,但告诉strcpy_s()它有25个字符的空间,那么你仍然会遇到麻烦。

你不能合法地定义一个以str*开头的函数,因为在C语言中该“命名空间”是保留的。 - unwind
2
但是ISO C委员会可以 - 而且确实做到了。另请参阅:https://dev59.com/73RC5IYBdhLWcg3wOOP1 - Jonathan Leffler
@Jonathan:感谢您提供自己问题的交叉参考,这提供了许多额外有用的信息。 - Adam Liss

8

Strncpy函数在程序使用者进行堆栈溢出攻击时更加安全,但它不能保护程序开发者自己犯下的错误,比如打印未以空字符结尾的字符串。

你可以通过限制printf函数打印的字符数来避免你描述的问题导致程序崩溃:

char my_string[10];
//other code here
printf("%.9s",my_string); //limit the number of chars to be printed to 9

使用精度字段来限制%s打印的字符数,必须是C语言中最晦涩难懂的特性之一。 - David Thornley
@DavidThornley 在 K&R 的 sprintf 章节中有非常清晰的文档记录。 - weston
@weston:在我工作的地方,我有Harbison&Steele。除了这两本书之外,在哪些流行的C语言书籍中提到了这一点?每个特性都应该在K&R和H&S中提到(并在标准中提到),因此如果这是晦涩的标准,那么就没有晦涩的特性。 - David Thornley
@DavidThornley,我只是想平衡一下你的评论,因为通过说“最不常见的功能之一”,这会让这个答案看起来很糟糕,人们可能会被吓到而不使用它。这是错误的,因为它是一个完全有效的、文档齐全的功能,就像精度字段的任何其他用途一样有文档记录。“模糊”似乎是一个主观的问题,因为我个人经常看到它被使用。 - weston

5

请使用这里指定的strlcpy()函数:http://www.courtesan.com/todd/papers/strlcpy.html

如果您的libc没有实现,可以尝试使用此版本:

size_t strlcpy(char* dst, const char* src, size_t bufsize)
{
  size_t srclen =strlen(src);
  size_t result =srclen; /* Result is always the length of the src string */
  if(bufsize>0)
  {
    if(srclen>=bufsize)
       srclen=bufsize-1;
    if(srclen>0)
       memcpy(dst,src,srclen);
    dst[srclen]='\0';
  }
  return result;
}

(由我于2004年编写 - 献给公共领域。)

请给我解释一下,为什么你希望结果总是源字符串的长度?在我看来,返回 srclen 会更好,因为我们可以知道实际复制了多少个字符。 - Lê Quang Duy

5

您可以使用以下方法来代替strncpy()

snprintf(buffer, BUFFER_SIZE, "%s", src);

以下是一行代码,它会从src中拷贝至多size-1个非空字符到dest,并添加一个空终止符:

static inline void cpystr(char *dest, const char *src, size_t size)
{ if(size) while((*dest++ = --size ? *src++ : 0)); }

我们正在使用宏相当于 snprintf(buffer, sizeof(buffer), "%s", src)。只要记得永远不要在 char* 目标上使用它,它就可以正常工作。 - che

3

我一直更喜欢:

 memset(dest, 0, LEN);
 strncpy(dest, src, LEN - 1);

对于后期修复的方法,这只是个人偏好问题。


1
是否将所有缓冲区初始化为零是一个有争议的话题。个人而言,在开发/调试期间,我更喜欢这样做,因为它往往会使错误更加明显,但还有许多其他(“更便宜”的)选项。 - Adam Liss
8
如果需要,strncpy()函数会填充除了dest[LEN-1]以外的其他字节,所以你只需要将dest[LEN-1]设置为0即可(请记住:strncpy(s,d,n)总是会写入n个字节!)。 - Christoph

3
在不依赖于新扩展的情况下,我曾经做过类似于这样的事情:
/* copy N "visible" chars, adding a null in the position just beyond them */
#define MSTRNCPY( dst, src, len) ( strncpy( (dst), (src), (len)), (dst)[ (len) ] = '\0')

也许甚至还包括:

/* pull up to size - 1 "visible" characters into a fixed size buffer of known size */
#define MFBCPY( dst, src) MSTRNCPY( (dst), (src), sizeof( dst) - 1)

为什么要使用宏而不是较新的“内置”函数?因为在我日常使用C开发时,需要将代码移植到许多不同的UNIX系统以及其他非UNIX(非Windows)环境中。


3

strncpy函数直接使用可用的字符串缓冲区,如果您直接使用内存,则必须知道缓冲区的大小,并且可以手动设置'\0'。

我认为在纯C中没有更好的替代方法,但是如果您在处理原始内存时非常小心,那么情况并不会太糟糕。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接