c - puts用法




為什麼不推薦使用帶有單個參數(沒有轉換說明符)的printf? (7)

在我正在閱讀的書中,寫有不帶單個參數(不帶轉換說明符)的 printf 。 建議替代

printf("Hello World!");

puts("Hello World!");

要么

printf("%s", "Hello World!");

有人可以告訴我為什麼 printf("Hello World!"); 是錯的? 它寫在書中,其中包含漏洞。 這些漏洞是什麼?


使用文字格式的字符串調用 printf 是安全高效的,並且如果使用用戶提供的格式字符串調用 printf 是不安全的,則存在一些工具可以自動警告您。

printf 的最嚴重的攻擊利用了 %n 格式說明符。 與所有其他格式說明符相反,例如 %d%n 實際上將值寫入格式參數之一中提供的內存地址。 這意味著攻擊者可以覆蓋內存,從而有可能控製程序。 en.wikipedia.org/wiki/Uncontrolled_format_string 提供了更多細節。

如果您使用文字格式的字符串調用 printf ,則攻擊者無法將 %n 潛入您的格式字符串,因此很安全。 實際上,gcc會將您對 printf 的調用更改為對 puts 的調用,因此從根本上沒有任何區別(通過運行 gcc -O3 -S 測試)。

如果您使用用戶提供的格式字符串調用 printf ,則攻擊者可能會將 %n 潛入您的格式字符串中,並控製程序。 您的編譯器通常會警告您他不安全,請參見 -Wformat-security 。 還有一些更高級的工具可以確保即使使用用戶提供的格式字符串也可以安全地調用 printf ,並且它們甚至可以檢查是否向 printf 傳遞了正確的數字和參數類型。 例如,對於Java,有 Google的Error Prone Checker Framework


對於gcc,可以啟用特定警告來檢查 printf()scanf()

gcc文檔指出:

-Wformat 包含在 -Wall 。 為了更好地控制格式檢查的某些方面,選項 -Wformat-y2k -Wno-format-extra-args-Wno-format-extra-args-Wno-format-zero-length-Wformat-nonliteral-Wformat-security-Wformat=2 可用,但不包含在 -Wall

-Wall 選項中啟用的 -Wformat 不會啟用一些有助於查找這些情況的特殊警告:

  • -Wformat-nonliteral 如果未傳遞字符串亂碼作為格式說明符,則會發出警告。
  • 如果傳遞可能包含危險結構的字符串,則 -Wformat-security 將發出警告。 它是 -Wformat-nonliteral 的子集。

我必須承認,啟用 -Wformat-security 揭示了我們代碼庫中的幾個錯誤(日誌記錄模塊,錯誤處理模塊,xml輸出模塊,所有這些功能都有一些函數,如果使用參數中的%字符調用它們,它們可能會執行未定義的操作對於信息,我們的代碼庫現在已有20多年的歷史了,即使我們意識到了這類問題,當我們啟用這些警告時,我們仍然對代碼庫中仍然有多少個錯誤感到非常驚訝。


由於沒有人提及,因此我將添加有關其性能的註釋。

在正常情況下,假設不使用編譯器優化(即, printf() 實際上調用 printf() 而不是 fputs() ),則我希望 printf() 的執行效率較低,尤其是對於長字符串。 這是因為 printf() 必須解析字符串以檢查是否存在任何轉換說明符。

為了確認這一點,我已經進行了一些測試。 該測試是在Ubuntu 14.04和gcc 4.8.4上執行的。 我的機器使用Intel i5 cpu。 正在測試的程序如下:

#include <stdio.h>
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM");
        // or
        fputs("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM", stdout);
    }
    fflush(stdout);
    return 0;
}

兩者都用 gcc -Wall -O0 編譯。 時間是使用 time ./a.out > /dev/null 來測量的。 以下是典型運行的結果(我已經運行了5次,所有結果都在0.002秒內)。

對於 printf() 變體:

real    0m0.416s
user    0m0.384s
sys     0m0.033s

對於 fputs() 變體:

real    0m0.297s
user    0m0.265s
sys     0m0.032s

如果弦線 長,則會放大此效果。

#include <stdio.h>
#define STR "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"
#define STR2 STR STR
#define STR4 STR2 STR2
#define STR8 STR4 STR4
#define STR16 STR8 STR8
#define STR32 STR16 STR16
#define STR64 STR32 STR32
#define STR128 STR64 STR64
#define STR256 STR128 STR128
#define STR512 STR256 STR256
#define STR1024 STR512 STR512
int main() {
    int count = 10000000;
    while(count--) {
        // either
        printf(STR1024);
        // or
        fputs(STR1024, stdout);
    }
    fflush(stdout);
    return 0;
}

對於 printf() 變體(運行了3次,正負1.5秒):

real    0m39.259s
user    0m34.445s
sys     0m4.839s

對於 fputs() 變體(運行了3次,正負0.2s):

real    0m12.726s
user    0m8.152s
sys     0m4.581s

注意: 在檢查了gcc生成的程序集之後,我意識到gcc即使將 -O0 也可以將 fputs() 調用優化為 fwrite() 調用。 ( printf() 調用保持不變。)我不確定這是否會使我的測試無效,因為編譯器會在編譯時計算 fwrite() 的字符串長度。


這是誤導的建議。 是的,如果您要打印運行時字符串,

printf(str);

是非常危險的,您應該始終使用

printf("%s", str);

相反,因為通常您永遠無法知道 str 是否包含 % 符號。 但是,如果您有一個編譯 時常量 字符串,則沒有任何問題

printf("Hello, world!\n");

(除其他外,這是有史以​​來最經典的C程序,字面意思是Genesis的C編程書。因此,任何反對這種用法的人都是相當異端的人,而我會有點冒犯!)


printf("Hello World!"); 恕我直言不脆弱,但請考慮以下幾點:

const char *str;
...
printf(str);

如果 str 恰好指向包含 %s 格式說明符的字符串,則您的程序將表現出未定義的行為(通常是崩潰),而 puts(str) 只會按原樣顯示該字符串。

例:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s"

printf("Hello world");

很好,沒有安全漏洞。

問題在於:

printf(p);

其中 p 是指向由用戶控制的輸入的指針。 容易受到 格式字符串攻擊 :用戶可以插入轉換規範來控製程序,例如, %x 轉儲內存或 %n 覆蓋內存。

請注意, puts("Hello world") 行為並不等同於 printf("Hello world") 而是 printf("Hello world\n") 。 編譯器通常很聰明,可以優化後者的調用,以 puts 代替它。


printf("Hello World\n")

自動編譯為等效

puts("Hello World")

您可以通過分解可執行文件來檢查它:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret

使用

char *variable;
... 
printf(variable)

會導致安全問題, 永遠不要那樣使用printf!

因此您的書實際上是正確的,不建議使用帶有一個變量的printf,但是您仍然可以使用printf(“ my string \ n”),因為它會自動變成puts







puts