为什么printf只有一个参数(没有转换说明符)不推荐使用?

在我正在阅读的一本书中,写了带有一个参数(不带转换说明符)的printf被弃用。 它build议替代

 printf("Hello World!"); 

 puts("Hello World!"); 

要么

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

有人可以告诉我为什么printf("Hello World!"); 是错的? 它是写在书中,它包含漏洞。 这些漏洞是什么?

printf("Hello World!"); 恕我直言,不容易,但考虑到这一点:

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

如果str恰好指向包含%s格式说明符的string,程序将显示未定义的行为(大部分是崩溃),而puts(str)将仅显示string。

例:

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

printf("Hello world");

是好的,没有安全漏洞。

问题在于:

 printf(p); 

其中p是指向由用户控制的input的指针。 它很容易格式化string攻击 :用户可以插入转换规范来控制程序,例如%x转储内存或%n覆盖内存。

请注意, puts("Hello world")在行为上与printf("Hello world")而是printf("Hello world\n") 。 编译器通常足够聪明,可以优化后者的调用来replaceputs

除了其他的答案之外, printf("Hello world! I am 50% happy today")是一个很容易造成的bug,可能会导致各种令人讨厌的内存问题(这是UB!)。

只要程序员想要一个逐字串而没有其他任何东西时 ,它就更简单,更简单,更强大。

这就是printf("%s", "Hello world! I am 50% happy today") 。 这完全是万无一失的。

(Steve,当然是printf("He has %d cherries\n", ncherries)绝对不是一回事,在这种情况下,程序员并不是“逐字串”的心态,而是“格式化”的心态。 )

我将在这里添加一些有关漏洞部分的信息。

据说由于printfstring格式的漏洞,它是脆弱的。 在你的例子中,string是硬编码的,这是无害的(即使硬编码这样的string永远不会被完全推荐)。 但是指定参数的types是一个很好的习惯。 以这个例子:

如果有人将格式string字符放在printf中,而不是常规string(例如,如果要打印程序标准input),printf将采取任何他可以在堆栈上进行的操作。

现在(现在仍然)非常习惯于利用程序来探索堆栈来访问隐藏的信息或绕过authentication。

例(C):

 int main(int argc, char *argv[]) { printf(argv[argc - 1]); // takes the first argument if it exists } 

如果我把这个程序input为"%08x %08x %08x %08x %08x\n"

 printf ("%08x %08x %08x %08x %08x\n"); 

这指示printf函数从堆栈中检索五个参数,并将其显示为8位填充的hex数字。 所以可能的输出可能如下所示:

 40012980 080628c4 bffff7a4 00000005 08059c04 

看到这个更完整的解释和其他例子。

这是错误的build议。 是的,如果您有打印的运行时string,

 printf(str); 

是相当危险的,你应该总是使用

 printf("%s", str); 

相反,因为通常你永远不会知道str是否可能包含%符号。 但是,如果你有一个编译时常量string,没有任何问题

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

(除此之外,这是C程序中最经典的C程序,字面意思是来自“创世纪”的C编程书籍,所以任何贬低这个用法的人都是相当邪教的,而我一个人会有点冒犯他人!

使用文字格式string调用printf是安全和有效的,并且存在一些工具可以在用户提供的格式string对printf的调用不安全时自动发出警告。

printf最严重的攻击利用%n格式说明符。 与所有其他格式说明符(例如%d相反, %n实际上是将一个值写入其中一个格式参数中提供的内存地址。 这意味着攻击者可以覆盖内存,从而有可能控制你的程序。 维基百科提供了更多细节。

如果用string格式string调用printf ,攻击者就不能将%n隐藏到格式string中,因此您是安全的。 实际上,gcc会把你的调用改为printf ,所以在这里没有任何区别(通过运行gcc -O3 -Stesting)。

如果使用用户提供的格式string调用printf ,攻击者可能潜入%n到您的格式string中,并控制您的程序。 你的编译器通常会警告你他的不安全,请参阅-Wformat-security 。 还有一些更高级的工具可以确保printf的调用在用户提供的格式string中是安全的,他们甚至可能会检查是否将正确数量和types的parameter passing给printf 。 例如,对于Java,有Google的错误倾向和检查器框架 。

printf一个相当讨厌的方面是,即使在杂散内存读取的平台上只能造成有限(和可接受)的伤害的平台之一,格式化字符%n的一个导致下一个参数被解释为指向可写整数的指针,并且使得到此为止输出的字符的数量被存储到由此识别的variables中。 我从来没有使用过这个function,有时候我使用了轻量级的printf风格的方法,我只写了一些我实际使用的function(不包括那一个或者类似的东西),但是提供了标准的printf函数string从不可靠的来源可能暴露的安全漏洞超出了读取任意存储的能力。

由于没有人提到,我会添加一个关于他们的performance的笔记。

在正常情况下,假设没有使用编译器优化(即printf()实际调用printf()而不是fputs() ),我期望printf()执行效率较低,特别是对于长string。 这是因为printf()必须parsingstring来检查是否有任何转换说明符。

为了证实这一点,我已经运行了一些testing。 testing在Ubuntu 14.04上执行,使用gcc 4.8.4。 我的机器使用Intel i5 cpu。 正在testing的程序如下:

 #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来测量的。 以下是典型运行的结果(我运行了五次,所有结果都在0.002秒之内)。

对于printf()变体:

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

对于fputs()变体:

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

如果你有长的string,这个效果会被放大。

 #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()变体(运行三次,实际加/减1.5s):

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

对于fputs()variables(运行三次,实际加/减0.2s):

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

注意:在检查gcc生成的程序集之后,我意识到gcc优化了对fwrite()调用的fputs() fwrite()调用,即使使用-O0 。 ( printf()调用保持不变。)我不确定这是否会使我的testing失效,因为编译器会在编译时计算fwrite()的string长度。

 printf("Hello World\n") 

自动编译

 puts("Hello World") 

你可以通过diassembling你的可执行文件来检查它:

 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与一个variables已弃用,但你仍然可以使用printf(“我的string\ n”),因为它会自动成为投入

对于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会发出警告,如果你不传递一个string作为格式说明符。
  • -Wformat-security会警告你传递一个可能包含危险结构的string。 它是-Wformat-nonliteral的一个子集。

我不得不承认,启用-Wformat-security揭示了我们在代码库中的一些错误(日志模块,error handling模块,xml输出模块,都有一些函数可以做未定义的事情,如果在参数中用%字符调用的话对于信息,我们的代码库现在已经有20年了,即使我们意识到了这些问题,当我们启用这些警告时,仍然有多less这些错误仍在代码库中,我们感到非常惊讶。