strlcpy  strlcat—— 一致的、安全的字符串拷贝和串接函数

Todd C. Miller

University of Colorado, Boulder

Theo de Raadt

OpenBSD project

概述

随着流行的缓冲区溢出攻击的增加,越来越多程序员开始使用带有大小,即有长度限制的字符串函数,如 strncpy() 和 strncat() 。尽管这种趋势令人十分鼓舞,但通常的标准 C 字符串函数并不是专为此而设计的。本文介绍另一种直观的,一致的,天生安全的字符串拷贝 API 。

当函数 strncpy() 和 strncat() 作为 strcpy() 和 strcat() 的安全版本来使用时,仍然存在一些安全隐患。首先,这两函数以不同的,非直观的方式来处理 NUL 结束符和长度参数,即使有经验的程序员也会混淆。其次,发生字符串截断时,也不容易检查。最后,strncpy() 函数使用 0 来填充剩余的目标字符串空间,以招致性能下降。在所有这些问题之中,由长度参数引起的混淆以及与 NUL 结束符相关的问题最严重。在审核 OpenBSD 源代码树的潜在安全漏洞时,我们发现 strncpy() 和 strncat() 猖獗误用的情况。尽管并非所有的误用都会导致可被利用的安全漏洞,但清楚地表明使用 strncpy() 和 strncat() 来实施安全的字符串操作这一准则已普遍受到误解。两个替代函数 strlcpy() 和 strlcat() 被提议通过提出一个字符串拷贝安全的 API 来解决这些问题(参阅图 1 函数原型)。这两函数保证产生包含 NUL 的字符串,以长度即字符串按占用字节的数量作为入口参数,并且提供简便的方式来检查是否有字符串截断。两者均不会清零未使用的目标空间。

引言

1996 年年中,笔者和 OpenBSD 项目的其它成员一起担任审核 OpenBSD 源代码树的工作,以寻找安全问题,并强调缓冲区溢出问题。缓冲区溢出问题 [1] 最近在论坛上如 BugTraq [2] 获得广泛的关注,并且也被广泛利用。我们发现大量的溢出是由于使用 sprintf(), strcpy() 和 strcat() 而造成无长度界限的字符串拷贝,在循环里操纵字符串时没有显式检查字符串长度也是元凶之一。除此之外,我们也发现在很多场合下,程序员已使用 strncpy() 和 strncat() 进行安全的字符串操纵,但未能领会这些 API 的精妙之处。

因此在审核代码时,我们发现不仅有必要去检查是否使用不安全的函数,如 strcpy() 和 strcat() ,同时也要检查是是否有函数strncpy() 和 strcat() 的不正确使用。检查是否正确使用并非总是显而易见,特别是使用“静态”变量或使用由 calloc() 分配的缓冲区时,这些缓冲区总是预先就填满了 NUL 结束符。我们得到一个结论:需要十分安全的函数来替代 strncpy() 和 strncat() ,从根本上简化程序员的工作,同时也使代码审核变得更容易。

size_t strlcpy(char *dst, const char *src, size_t size);
size_t strlcat(char *dst, const char *src, size_t size);

图 1 : strlcpy() 和 strlcat() 的 ANSI C 原型

普遍的误解

最普遍的误解莫过于认为函数 strncpy() 总是产生以 NUL 结束的目标字符串。然而只有当源字符串的长度小于 size 参数时,这一论断才为真。当拷贝任意长的用户输入到固定大小的缓冲区,问题就出现了。这种情况下,使用 strncpy() 最安全的方法是先将目标字符串的大小减 1 ,再传递给 strncpy 的 size 参数,然后手工给目标字符串加上 NUL 结束符。这样可以保证目标字符串总是以 NUL结尾的。严格地说,如果字符串是“静态”变量或者由 calloc() 分配的变量,完全没有必要手工给字符串加上 NUL 结束符。因为这些字符串在分配时已经清零了。然而,依赖这一特性通常会给后来维护代码的人造成混乱。

另一个误解认为把代码中的 strcpy() 和 strcat() 换成 strncpy() 和 strncat() 所引起的性能下降微不足道。对于 strncat() 来说, 确实如此 。但对于 strncpy() 来说则不是这样,因为它会把那些未用来存储字符串的字节清零。当目标字符串的大小远远大于源字符的长度时,这会导致为数不少 [**] 的性能下降。 Strncpy() 的行为因 CPU 架构和它的实现而异,因此它所带来的性能下降也因它的行为而不同。

使用 strncat() 最普遍的错误是使用不正确的 size 参数。确实要保证 strncat() 使目标字符串包含 NULL 结束符,参数 size 决不能把 NULL 字符的空间计算在内。最重要的是,参数 size 不是目标字符串本身的大小,而是为字符串预留的空间的数量。由于参数size 几乎总一个计算量,而非一个已知的常量,因此经常被错误地计算。

Strlcpy()  strlcat() 是如何简化编程的?

Strlcpy() 和 strlcat() 函数提供一个一致的,绝无 二 义的 API ,帮助程序员编写更安全的防弹代码。首先,同时也是最重的,strlcpy() 和 strlcat() 两者保证所有的目标字符串都以 NUL 字符结尾,只要提供的 size 参数为非零。其次,两个函数都把 size参数作为整个目标字符的大小。大多情况下,它的值很容易在编译时通过使用 sizeof 运算符来计算。最后, strlcpy() 和strlcat() 均不给目标字符串清零未使用的字节(而是使用 NUL 来表示字符串的结束)。

Strlcpy() 和 strlcat() 函数返回他们尝试创建的字符串的长度。对于 strlcpy() 来说,就是源字符串的长度;而对 strlcat() 来说,就是目标字符串的长度(串接前的长度)加上源字符串的长度。对于检查是否发生字符截断,程序员只需要验证回返值是否不小于size 参数。因此,就算发生截断,存储整个字符串所需的字节数现已知道,程序员可以分配一个更大的空间,接着重新拷贝字符串(如果需要的话)。返回值在语义上与 snprintf() 的返回值类似, snprintf() 由 BSD 实现并由即将来临的 C9X 标准规范化(请注意,非并当前所有的 snprintf 实现都遵循 C9X )。如果没有发生截断,程序员现在也获知了结果字符串的长度。由于通常的实践是使用 strncpy() 和 strncat() 来构建字符串,然后使用 strlen() 来获得结果字符串的长度,因此( strlcpy() 和 strlcat() )这一返回值语义非常有用。有了 strlcpy() 和 strlcat() 后,就不再需要最后一步的 strlen() 来获得字符串的长度了。

示例 1a 是有潜在缓冲区溢出的代码段( HOME 环境变量由用户所控制,可为任意长)。

strcpy(path, homedir);
strcat(path, "/");
strcat(path, ".foorc");
len = strlen(path);

示例 1a: 使用 strcpy() 和 strcat() 的代码段

示例 1b 是同样功能的代码段,不过换成了 安全 地使用 strncpy() 和 strncat()( 请注意我们不得已手工给目标字符串设置 NUL 字符 ) 。

strncpy(path, homedir,sizeof(path) - 1);
path[sizeof(path) - 1] = '/ 0';
strncat(path, "/",sizeof(path) - strlen(path) - 1);
strncat(path, ".foorc",sizeof(path) - strlen(path) - 1);
len = strlen(path);

示例 1b: 转换成使用 strncpy() 和 strncat()

示例 1c 是使用 strlcpy()/strlcat()API 的 平凡 版本。它的优点是与示例 1a 一样简洁,但不需要利用新 API 的返回值。

strlcpy(path, homedir, sizeof(path));
strlcat(path, "/", sizeof(path));
strlcat(path, ".foorc", sizeof(path));
len = strlen(path);

示例 1c: 使用 strlcpy()/strlcat() 的平凡版本

由于示例 1c 是如此的容易阅读和理解,故对它添加额外的检查显得格外简单。示例 1d 里检查返回值以确定是否有足够的空间来储存源字符串。如果没有足够空间,返回一个错误。虽然程序比以前有轻微的复杂,但更具鲁棒性,同时避免最后一步的 strlen() 调用。

len = strlcpy(path, homedir,sizeof(path));
if (len >= sizeof(path))
return (ENAMETOOLONG);
len = strlcat(path, "/",sizeof(path));
if (len >= sizeof(path))
return (ENAMETOOLONG);
len = strlcat(path, ".foorc",sizeof(path));
if (len >= sizeof(path))
return (ENAMETOOLONG);

示列 1d  检测是否截断

设计决策

在考虑 strlcpy() 和 strlcat() 应具有什么语义的时候,涌现出各种各样的想法。原先的想法是使 strlcpy() 和 strlcat() 的语义和 strncpy() 与strncat() 的相同,唯一例外是 他们总是确保目标字符串以 NUL 结尾。然而,回顾 strncat() 的普遍使用情况(和误用),我们深信 strlcat() 的 size 参数应该是整个字符串空间的大小,而不仅是剩下来未分配的字符数。起决定初返回值为拷贝字符的数目,???。很快我们决定返回值和 snprintf() 的具有相同的语义是这一个更好的选择,因为这样给予程序员最大的弹性去做截断检查和截断恢复。

性能

程序员现已开始避免使用 strncpy() 函数,原因是当目标缓冲区远远大于源字符串的长度时,该函数的性能欠佳。例如 apache 开发小组 [6] 以调用内部函数来取代 strncpy() ,并公布了性能上的提升 [7] 。同样地, ncurses [8] 软件包最近删除了所有的 strncpy() 函数调用,结果 tic 工具的运行速度提高了四倍。我们谨希望,将来更多的程序员使用 strlcpy() 提供的接口,而非使用经定制的接口。

为获得在最糟糕情况下, strncpy() 和 strlcpy() 差别的感性认识,我们运行一个测试程序,拷贝字符串“ this is just a test”1000 次到大小为 1024 字节的缓冲区。这对于 strncpy() 来说有点不公平,由于使用较短的字符串和较大的缓冲区, strncpy() 必须为缓冲区大部分空间填充上 NUL 字符。然而在实践中,使用的缓冲区通常远远大于用户预期的输入。例如,路径名缓冲区的长度为 MAXPATHLEN(1024 字节 ) ,但大多数文件名远远小于这一长度。表 1 中的平均运行时间是在使用 25Mhz 的 68040CPU 的机器 HP9000/425t 在 OpenBSD 2.5 操作系统下和使用 166Mhz 的 alpha CPU 的机器 DEC AXPPCI166 在 OpenBSD 2.5 操作系统下产生的结果。各种情况使用相同的 C 函数版本,时间为 time 工具报告结果的“ real time” 部分。

CPU 架构

函数

时间 (秒)

M 68k

Strcpy

0.137

M 68k

Strncpy

0.464

M 68k

Strlcpy

0.14

A lpha

Strcpy

0.018

A lpha

Strncpy

0.10

A lpha

Strlcpy

0.02

Table 1 : Performance timings in seconds

表 1 :性能测时结果(秒)

从表 1 可以看到, strncpy() 的计时结果远差于 strncpy() 和 strlcpy() 的结果。这可能不仅仅是因为填补 NUL 字符带来的开销,而且是因为 CPU 的数据缓存被长长的零串有效地刷新。

Strlcpy()  strlcat() 所不能及之处

尽管 strlcpy() 和 strlcat() 善长于处理大小固定的缓冲区,但仍然不能完全取代 strncpy() 和 strncat() 。在某些情况下,必须操纵那些并非真正 C 字符串的缓冲区(例如 struct utmp 中的字符串)。然而,我们认为这些“伪字符串”不应该使用在新的代码中,因为它们容易被误用,并且从我们的经验来说,这是bug 的普遍源头。此外, strlcpy() 和 strlcat() 函数并不尝试“修复” C 中的字符串处理。相反它们设计的初衷就是适合 C 字符的标准架构。如果要使用支持动态分配,任意大小缓冲区的字符串函数,可以使用 mib 软件 [9] 里的” astring” 包。

谁应该使用 strlcpy()  strlcat()?

Strlcpy() 和 strlcat() 函数首先出现在 OpenBSD 2.4 中。最近两函数被同意纳入 Solaris 的新版中。第三方包也开始使用这一 API 。例如, rsync [5] 软件包现在使用 strlcpy() ,如果 OS 不支持该函数则提供自己的版本。我们希望其它操作系统和应用程序以后会使用 strlcpy() 和 strlcat() ,而且希望经过若干时间会得到标准的接受。

下一步将是什么?

在 OpenBSD 项目中,我们计划使用 strlcpy() 和 strlcat() 替换每个 strncpy() 和 strncat() ,这是明智之举。即使 OpenBSD 中使用新 API 来编写新的代码,仍然有大量的代码在我们原先的安全审核过程中转换成 strncpy() 和 strncat() 。至今,我们继续在现有代码中发现由于错误使用 strncpy() 和strncat() 而造成的 bug 。把旧代码更改为使用 strlcpy() 和 strlcat() ,应该能(??)一些程序提速,并且能 (?) 为一些程序揭开 bug 。

可从何处获得源代码?

Strlcpy() 和 strcat() 的源代码可以免费获得,并遵循作为 OpenBSD 操作系统一部分的 BSD 协议。你同样可通过匿名 ftp 从 ftp.openbsd.org 的/pub/OpenBSD/src/lib/libc/string 目录下载代码和它的手册。 strlcpy() 和 strlcat() 的源代码分别在文件 strlcpy.c 和 strlcat.c 中。文档(使用tmac.doc troff 宏)可从 strlcpy.3 中找到。

作者信息

1993 年, Todd C. Miller 接管 sudo 软件包的维护工作,并从此参加免费软件社区。他作为活跃的开发者加入 OpenBSD 项目。 Todd 于 1997 年获得姗姗来迟的科罗拉多州大学计算机科学专业学士学位。可以使用邮件地址 Todd.Miller@cs.colorado.edu 与他联系。

Theo de Raadt 自 1990 年起加入免费 Unix 操作系统。他早期的开发工作包括移植 Minix 到 sun3/50 和 amiga ,以及移植 PDP-11 BSD 2.9 到 68030 计算机。作为 NetBSD 项目的创始人之一, Theo 的工作内容为维护和改进很多系统部件,包括 sparc 端口和免费的 YP 实现,这一实现被大多数免费系统使用。Theo 在 1995 年建立 OpenBSD 项目,项目集中(??)在安全,集成加密系统和代码正确性等方面。 Theo 全职工作于提升 OpenBSD 项目。可通过邮件地址deraadt@openbsd.org 与他联系。

参考资料

[1] Aleph One. ``Smashing The Stack For Fun And Profit.'' Phrack Magazine Volume Seven, Issue Forty-Nine.

[2] BugTraq Mailing List Archives. http://www.geek-girl.com/bugtraq/. This web page contains searchable archives of the BugTraq mailing list.

[3] Brian W. Kernighan, Dennis M. Ritchie. The C Programming Language, Second Edition. Prentice Hall, PTR, 1988.

[4] International Standards Organization. ``C9X FCD, Programming languages /*- C'' http://wwwold.dkuug.dk/jtc1/sc22/open/n2794/ This web page contains the current draft of the upcoming C9X standard.

[5] Andrew Tridgell, Paul Mackerras. The rsync algorithm. http://rsync.samba.org/rsync/tech_report/. This web page contains a technical report describing the rsync program.

[6] The Apache Group. The Apache Web Server. http://www.apache.org. This web page contains information on the Apache web server.

[7] The Apache Group. New features in Apache version 1.3. http://www.apache.org/docs/new_features_1_3.html. This web page contains new features in version 1.3 of the Apache web server.

[8] The Ncurses (new curses) home page. http://www.clark.net/pub/dickey/ncurses/. This web page contains Ncurses information and distributions.

[9] Forrest J. Cavalier III. ``Libmib allocated string functions.'' http://www.mibsoftware.com/libmib/astring/. This web page contains a description and implementation of a set of string functions that dynamically allocate memory as necessary.

转自:http://blog.csdn.net/linyt/article/details/4383328

strlcpy和strlcat——一致的、安全的字符串拷贝和串接函数相关推荐

  1. C语言编写字符串拷贝(strcpy)函数详解以及assert函数

    目录 一.strcpy函数 原型声明 功能 说明 代码及运行结果 二.自己编写strcpy函数 代码一及运行结果 代码二(改进)及运行结果 代码三(进一步改进)及assert函数 在这里解释一下什么是 ...

  2. C语言-字符串拷贝(2)strncpy

    C语言-字符串拷贝(2)strncpy 1. strncpy 1 定义: 2 原型: 3 功能: 4 说明: 5 缺点: 6 实现-原始版1 7 实现-原始版2 8. n 的长度大于 strlen(d ...

  3. python输入字符串转换为公式_将python字符串转化成长表达式的函数eval实例

    爬一个网页时,要保存的数据都没有encode,就导致保存下来的中文都变成unicode了... 那么,怎么把一个表示字符串的unicode还原成unicode呢? 函数eval(expression) ...

  4. python字符串和字节串有什么区别_对于Python中的字节串bytes和字符串以及转义字符的新的认识...

    事情的起因是之前同学叫我帮他用Python修改一个压缩包的二进制内容用来做fuzz,根据他的要求,把压缩包test.rar以十六进制的方式打开,每次修改其中一个十六进制字符串并保存为一个新的rar用来 ...

  5. linux c strdup 字符串拷贝

    #include <string.h> char *strdup(const char *s); 函数介绍: strdup()函数是c语言中常用的一种字符串拷贝库函数,一般和free()函 ...

  6. linux strcpy 用法,由Linux中管道的buffer,浅谈C语言中char类型字符串拷贝使用strcpy()和=赋值符号的区别...

    今天在写父子进程用两个单向管道通信时,出现了错误: Segmentation fault (core dumped) 打开core文件发现: 附上源码: 1 #include 2 #include 3 ...

  7. 字符串拷贝函数memcpy()、strncpy()和snprintf()性能之比较

    问题: 函数memcpy(dest, src, sizeof(dest)).strncpy(dest, src, sizeof(dest))和snprintf(dest, sizeof(dest), ...

  8. 【C 语言】字符串拷贝 ( 函数形参使用推荐方法 | 凡是涉及 修改指针指向 的操作一律创建新的 指针变量 执行 | 引入 辅助 局部 指针变量 )

    文章目录 一.函数形参使用推荐方法 二.完整代码示例 一.函数形参使用推荐方法 在函数中 , 形参 中的 指针变量 , 不建议直接使用 ; 推荐 在 函数中 , 定义 局部 指针变量 , 接收 形参中 ...

  9. 【C 语言】字符串拷贝 ( 指针使用前判空 | while 空语句 )

    文章目录 一.指针使用前判空 二.while 空语句 三.代码示例 一.指针使用前判空 在任何指针使用之前 , 要先进行判空 ; 尤其是 函数 形参 指针 , 使用前先 判断该 指针变量 是一个合法的 ...

最新文章

  1. 3行Python代码就能获取海量数据?
  2. YARN的内存和CPU配置优化
  3. arping 帮助——翻译
  4. Kafka删除topic Note: This will have no impact if delete.topic.enable is not【另外强烈推荐一个kafka小工具】
  5. [LeetCode]题解(python):058-Length of Last Word
  6. pcb板材的tg是什么_做到这6点,PCB过回焊炉不会出现板弯及板翘!
  7. *多叉树的树形背包常见建模方法
  8. SAP License:SAP中的一些问题及处理
  9. Custom Components 翻译
  10. python实现登录抓取_Python网页抓取、模拟登录
  11. 容器中Bean的生命周期
  12. JavaScript将JSON转换为字符串
  13. android常用的存储方式,Android数据的四种存储方式
  14. DP动态规划思想讲解
  15. matlab截取图像像素,图像中获取图像的像素值
  16. 如何导出专业的工程图纸(附工图模板)
  17. Springboot集成MybatisPlus、Druid
  18. linux误删文件恢复的方法 ----debugfs
  19. netlink实现驱动和应用层通信
  20. (4)小程序 - 家庭收支系统

热门文章

  1. 开年赢好运!程序员加油包等你免费拿
  2. ClickHouse:人群圈选业务的大杀器
  3. 内容流量管理的关键技术:多任务保量优化算法实践
  4. 调查问卷 | 中国云原生用户调研,邀您参与!
  5. 优酷播控实践:基于规则引擎的投放管控模型
  6. 过程化技术:打造「开放世界」的秘密
  7. 模型解析之独立顶点的筛选
  8. OCM备考 三. Managing Database Availability 之RMAN日常操作
  9. 微信小程序家庭记账本开发进度二
  10. 022-红黑树(三)