天天看點

Strlcpy 和 strlcat—— 一緻的、安全的字元串拷貝和串接函數

引用此文隻為自己的知識收集所用,如需引用請看下面作者和譯者的聲明

英文原文: http://www.gratisoft.us/todd/papers/strlcpy.html

英文作者: Todd C. Miller,   Theo de Raadt

譯者:林海楓

譯本位址:http://blog.csdn.net/linyt/archive/2009/07/27/4383328.aspx

注:本譯文版權由譯者所擁有,歡迎轉載,但請注明譯者和原文,請匆用于任何商業用途。

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 年獲得姗姗來遲的科羅拉多州大學計算機科學專業學士學位。可以使用郵件位址 [email protected] 與他聯系。

Theo de Raadt 自 1990 年起加入免費 Unix 作業系統。他早期的開發工作包括移植 Minix 到 sun3/50 和 amiga ,以及移植 PDP-11 BSD 2.9 到 68030 計算機。作為 NetBSD 項目的創始人之一, Theo 的工作内容為維護和改進很多系統部件,包括 sparc 端口和免費的 YP 實作,這一實作被大多數免費系統使用。 Theo 在 1995 年建立 OpenBSD 項目,項目集中(??)在安全,內建加密系統和代碼正确性等方面。 Theo 全職工作于提升 OpenBSD 項目。可通過郵件位址 [email protected] 與他聯系。

參考資料

[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.

本文來自CSDN部落格,轉載請标明出處:http://blog.csdn.net/linyt/archive/2009/07/27/4383328.aspx