?
概要:
時下緩沖區溢出攻擊已經增加,越來越多的程序員使用帶有
size
或長度邊界的字符串函數,例如:
strncpy
和
strncat
。這的確是一個趨勢,但標準的
C
字符串函數并不是真正為這些任務而設計的。本文描述一個專門設計用于安全字符串復制的可選的、直覺的和一致的
API
。
將
strncpy
和
strncat
作為
strcpy
和
strcat
安全版本有幾個問題。兩個函數都是以不同的和非直覺的方法來處理
NULL
結尾的和長度參數,即使有經驗的程序員都有時迷惑;而檢查什么時候發生截斷也是不容易的。最后,
strncpy
用
0
來填充目標字符串剩余的部分,這是以損失性能為代價的。所有這些迷惑都是由長度參數引起的,空結束的要求也非常重要。當我們評估
OpenBSD
源樹的潛在安全漏洞的時候,我們發現大量濫用
strncpy
和
strncat
。當然,并不是所有的都導致暴露的安全漏洞,上面的這些使說明了一點:使用
strncpy
和
strncat
作為安全字符串操作容易被誤解。推薦使用的函數是
strlcpy
和
strlcat
,通過為安全字符串設計的
API
來程序這些問題(見圖
1
的函數原型)。兩個函數都保證
NUL
結尾,長度參數是以字節記數的,并且提供了檢查截斷的方法,兩個函數都不是在目標字符串中填充
0
。
?
介紹
?
在
1996
年中,作者和其他
OpenBSD
項目的成員一起承擔了對
OpenBSD
源樹的評估,為了找出安全問題;以緩沖區溢出作為開始。緩沖區溢出最近在一些論壇(例如
BugTraq
)大量關注,并且正被廣泛地開拓。我們發現大量的緩沖區溢出是由于較大的使用
sprintf
、
strcpy
、
strcat
進行的字符串復制;在循環中操作字符串而沒有明確地檢查循環變量的長度也是一個問題。另外,我們也發現許多程序員使用
strncpy
和
strncat
來進程安全字符串操作但失敗的場景。
因此,在評估代碼的時候,我們發現不僅僅檢查
strcpy
和
strcat
的不安全使用,同樣也要檢查
strncpy
和
strncat
的不正確使用。檢查正確使用并不總是明顯地,特別在靜態變量和緩沖區或
calloc
分配的緩沖區,這些都容易被忽視。我們得到結論,一個安全的
strncpy
和
strncat
是必要的,首先可以減輕程序員的工作;另外也可以是代碼評估更容易。
?
size_t strlcpy(char *dst, const char *src, size_t size);
size_t strlcat(char *dst, const char *src, size_t size);
Figure 1:
ANSI C prototypes for strlcpy() and strlcat()
通常誤解
最通常的誤解是
strncpy
空結尾的目標字符串。然而,只有源符串的長度小于
size
參數才是正確的。當用戶輸入的任意長度字符串時候,可能有問題。這種情況下最安全方法是傳遞一個小于目標字符串的
size
給
strncpy
,并且手動添加一個結束符號。這種方法下你可以總是保證目標字符穿是
NUL
結束的。嚴格地說,如果是一個靜態的字符串或一個
calloc
分配的字符串不必要手動添加一個結束符號;主要由于這些字符串在分配的時候是填充
0
的。然而這些特性時候比較迷惑的。
另外一個暗示的假定就是從
strcpy
到
strcat
代碼轉換到
strncpy
和
strncat
導致的性能下降是可以接受的。對于
strncat
來說是正確的,但同樣對于
strncpy
來說并不正確,由于
strncpy
將剩余的目標字節填充
0
,這在字符串比較大的時候可能導致可觀的性能損失。確切的損失由
CPU
架構和實現的而決定。
最常見的錯誤是使用
strncat
和一個不正確的
size
參數。然而
strncat
保證目標字符串是
NULL
結尾的,你不需要在
size
參數中為
NUL
計算機空間。最重要的,這不是目標字符串自身的大小,而是可用空間的數量。因此這個值總是要計算,并且作為一個可靠的常量,它常常也不能正確計算。
為什么
strlcpy
和
strlcat
能夠安全
?
這兩個函數提供了一個一致的、沒有二義性的
API
來幫助程序員寫比較防彈代碼。首先也是最重要的,兩個函數都能夠保證所有的目標字符串是
NUL
結尾的,給定的
size
非
0
;其次,兩個函數都將目標字符串的整個
size
作為一個
size
參數。在大多數情況下,這個值比較容易在編譯期間使用
sizeof
操作符號來計算;最后,不管是
strlcpy
還是
strlcat
都不
0
填充他們的目標字符串(而是強迫
NUL
到字符串的結尾)。
Strlcpy
和
strlcat
函數返回最終創建的字符串長度。對于
strlcpy
來說是源的長度,對于
strlcat
來說意味著目標的長度加源的長度。為了檢查截斷,程序員需要驗證返回值是否小于
size
參數。因此,如果發生截斷,可以發現已經存儲了多少個字節,并且程序員可以重新分配空間來重新復制字符串。返回值和
snprintf
在
BSD
上的實現有相同的含義。如果沒有截斷發生,程序員現在有返回值長度的字符串;這是有用的,因為通常情況用
strncpy
和
strncat
來構造字符串并且使用
strlen
來取得字符串的長度。使用
strlcpy
和
strlcat
,
strlen
就不需要了。
例子
1a
是潛在緩沖區溢出的例子(
HOME
環境變量由用戶來控制可以是任意長度)。
strcpy(path, homedir);
strcat(path, "/");
strcat(path, ".foorc");
len = strlen(path);
Example 1a:
Code fragment using strcpy() and strcat()
例子
1b
轉換到
strncpy
和
strncat
的同樣代碼片段(注意,我必須自己添加字符串結束符號)。
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);
Example 1b:
Converted to strncpy() and strncat()
例子
1c
是到
strlcpy/strlcat
的變化,其有例子
1a
一樣簡單的好處,但卻沒有利用
API
的返回值:
strlcpy(path, homedir, sizeof(path));
strlcat(path, "/", sizeof(path));
strlcat(path, ".foorc", sizeof(path));
len = strlen(path);
Example 1c:
Trivial conversion to 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);
Example 1d:
Now with a check for truncation
?
設計決策
許多思想加入判斷
strlcpy
和
strlcat
應該是什么語義。最初的想法是使
strlcpy
和
strlcat
與
strncpy
和
strncat
相同,并且始終是
NUL
結束的目標字符串。然而,當我們回過來看常用(和誤用)
strncat
說服我們
strlcat
的
size
參數應該是字符串的所有大小而不僅僅是未分配的字符的數量。返回值開始作為復制字符串的數量,由于有復制和串聯的副作用。我們很快決定返回值應該與
sprintf
一樣,這樣程序可以比較彈性的處理截斷和恢復。
性能
當目標字符串的長度比源字符串明顯大很多的時候,程序員正在避免使用
strncpy
,主要由于其降低性能。例如,
Apache
組用內部函數來代替
strncpy
并且注意到性能提升。同樣,
ncurses
包最近刪除了
strncpy
,結果比
tic
實現提高了四倍。我們的希望是,將來更多的程序員使用
strlcpy
而不是自定義的接口。
為了對性能的降低有一個感覺,我們比較
strncpy
和
strlcpy
,并且復制字符串
’’
;也就是復制
1000
次到
1024
字節的緩沖區中。這對
strncpy
有點不公平,由于使用了大的緩沖區和小的字符串,并且大緩沖區的時候,
strncpy
不得不填充多余的緩沖為
NUL
字符。實際上,通常使用的緩沖區都比用戶輸入的大,例如,路徑名稱緩沖區是
MAXPATHLEN
長(
1024
),但多數文件都比較短。表
1
中的平均運行時間在
HP9000/425t
,
25Mhz68040 CPU
運行
OPENBSD2.5
,
DEC AXPPCI166
上
166Mhz Alpha CPU
運行
OpenBSD
。所有的
case
都是相同
C
版本函數,時間由時間工具產生:
?cpu architecture?
|
?function?
|
?time (sec)?
|
m68k
|
strcpy
|
0.137
|
m68k
|
strncpy
|
0.464
|
m68k
|
strlcpy
|
0.14
|
alpha
|
strcpy
|
0.018
|
alpha
|
strncpy
|
0.10
|
alpha
|
strlcpy
|
0.02
|
表
1
:性能時間表
如我們在表
1
中看到的一樣,
strncpy
的時間是最壞的。可能的原因不僅是
NUL
填充,也可能因為
CPU
數據緩沖區被長流
0flush
的原因。
?
什么時候不要
strlcpy
和
strlcat
?
然而,
strlcpy
和
strlcat
處理固定大小的緩沖區很好,但他們不能在所有情況下代理
strncpy
和
strncat
。有時候操作緩沖區并不是真正的
C
字符串(例如,結構體
utmp
中的字符串)時候就是必要的。然而,我們爭論的這樣假冒字符串不應該用在新的編碼中,由于他們可能被誤用,并且據我們的經驗,這也是
BUG
的根源。另外,
strlcpy
和
strlcat
函數并不是
C
里面修正字符串處理的嘗試,他們設計為來適應正常的
C
字符串框架。如果你需要字符串函數支持動態分配的、任意大小的緩沖區,你可能需要檢查
asstring
包,在
MIB
軟件中。
誰使用
strlcpy
和
strlcat
?
Strlcpy
和
strlcat
函數首先出現在
OpenBSD2.4
。這些函數最近被將來的
Solaris
版本中批準。第三方包也開始收集這些
API
。例如,
rsync
包現在使用
strlcpy
并且提供它自己的版本如果
OS
不支持的話。其他的操作系統和應用程序將來使用
strlcpy
和
strlcat
是我們的希望,并且它將某個時候接受標準。
下一步是什么?
Strlcpy
和
strlcat
的源碼可以免費獲得,并且
BSD
風格的
license
是
OpenBSD
操作系統的一部分。你可以通過匿名
ftp
從
ftp.openbsd.org
下載代碼和相關的手冊;目錄為
/pub/OpenBSD/src/lib/libc/string
。
strlcpy
和
strlcat
的源碼在
strlcpy.c
和
strlcat.c
中。也可以找到相應的文檔。
作者:
Todd C. Miller
http://www.courtesan.com/todd/papers/strlcpy.html