为什么printf具有单个参数(没有转换说明符)?

Iu *_*Tub 99 c security printf format-specifiers puts

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

printf("Hello World!");
Run Code Online (Sandbox Code Playgroud)

puts("Hello World!");
Run Code Online (Sandbox Code Playgroud)

要么

printf("%s", "Hello World!");
Run Code Online (Sandbox Code Playgroud)

谁能告诉我为什么printf("Hello World!");是错的?书中写道它包含漏洞.这些漏洞是什么?

Jab*_*cky 119

printf("Hello World!"); 恕我直言不容易受到影响,但考虑一下

const char *str;
...
printf(str);
Run Code Online (Sandbox Code Playgroud)

如果str恰好指向包含%s格式说明符的字符串,则程序将显示未定义的行为(主要是崩溃),而puts(str)只是按原样显示字符串.

例:

printf("%s");   //undefined behaviour (mostly crash)
puts("%s");     // displays "%s"
Run Code Online (Sandbox Code Playgroud)

  • @black:`puts`"大概"更快,这可能是人们推荐它的另一个原因,但它并不是_actually_更​​快.我只是打印了"你好,世界!",两次都是1,000,000次.用`printf`花了0.92秒.用`puts`花了0.93秒.在效率方面有些事情需要担心,但`printf`与`puts`不是其中之一. (38认同)
  • 除了导致程序崩溃之外,格式字符串还有许多其他漏洞.有关详细信息,请参阅此处:https://en.wikipedia.org/wiki/Uncontrolled_format_string (21认同)
  • @KonstantinWeitz:但是(a)我没有使用gcc,而且(b)没关系_why_声称"`puts`更快"是假的,它仍然是假的. (10认同)
  • 另一个原因是`puts`可能会更快. (9认同)
  • @KonstantinWeitz:我提供的证据是与黑方正在制作的索赔用户(相反).我只是想澄清程序员不应该担心因为这个原因而调用`puts`.(但如果你想争论它:如果你能找到任何现代编译器的任何现代编译器,我会感到惊讶,其中`puts`在任何情况下都比`printf`快得多.) (6认同)
  • @SteveSummit由于输出的瓶颈是屏幕缓冲区或文件缓冲区,您的测试很可能存在缺陷.将输出传递给/ dev/null并查看性能差异. (4认同)
  • `puts("%s");`实际上会显示`%s \n`为`puts()`附加一个换行符. (3认同)
  • @SteveSummit,你的实验没有提供"puts与printf的速度相同"的证据,它提供的证据表明,一个程序调用"带有文字格式字符串的printf而没有编译成程序集的格式参数运行速度与使用等效程序一样快put,对于一些未指定的编译器".我只想澄清一下. (3认同)
  • 不,我没有在屏幕上打印1,000,000次.我把它传入`tail`,它可能会或可能不会像重定向到`/ dev/null`那样扰乱结果.但无论如何:我不同意这样的测试是"有缺陷的".如果有的话,输出重定向到`/ dev/null`的测试是有缺陷的,因为它是不现实的.有趣的说法不是"`printf`与`puts`的速度大致相同",而是"`printf`与`puts`*在实践中的速度大致相同*".(再次,我们所讨论的是"使用`puts`而不是'printf`,因为它更快"是好建议.) (3认同)
  • @SteveSummit"Hello World"不是一个好的测试.首先,在字符串的奇偶校验中,它们不会做同样的事情(`puts`附加一个`\n`,这可能会导致进一步的开销).其次,`printf`使用更长的格式字符串变得越来越慢,因为它_____要搜索`%`,不像`puts.最后,你检查汇编是否'printf`被优化为`puts`(可能你是做了'printf("Hello World \n")`)? (2认同)
  • 当只有一个参数时,`gcc`自动将`printf`转换为`puts`,格式字符串不包含任何%-field,并且以''\n'`终止.无需为此激活优化.只需看看`gcc -S`生成的汇编代码. (2认同)

oua*_*uah 73

printf("Hello world");

很好,没有安全漏洞.

问题在于:

printf(p);
Run Code Online (Sandbox Code Playgroud)

where p是指向由用户控制的输入的指针.它易于 格式化字符串攻击:用户可以插入转换规范来控制程序,例如%x转储内存或%n覆盖内存.

请注意,puts("Hello world")是不是在行为等同printf("Hello world"),但到printf("Hello world\n").编译器通常足够聪明,可以优化后一个调用以替换它puts.

  • 当然,如果用户可以控制`p`,那么`printf(p,x)`就会有问题.所以问题是*不是*只使用一个参数来使用`printf`而是使用用户控制的格式字符串. (10认同)
  • @HagenvonEitzen这在技术上是正确的,但很少有人故意使用用户提供的格式字符串.当人们写'printf(p)`时,这是因为他们没有意识到它是一个格式字符串,他们只是认为他们正在打印文字. (2认同)

Lig*_*ica 32

除了其他答案,printf("Hello world! I am 50% happy today")是一个容易犯的错误,可能导致各种令人讨厌的内存问题(它是UB!).

它只是更简单,更容易和更强大,"要求"程序员在他们想要一个逐字字符串而不是其他任何东西时绝对清楚.

这就是你的printf("%s", "Hello world! I am 50% happy today")利益所在.这完全是万无一失的.

(史蒂夫,当然printf("He has %d cherries\n", ncherries)绝对不是一回事;在这种情况下,程序员不是"逐字字符串"的心态;她是"格式字符串"的心态.)

  • 这不值得争论,我理解你所说的关于逐字与格式字符串思维方式的内容,但是,并非所有人都这么认为,这是一刀切的规则之一.说"永远不会用'printf`打印常量字符串'就像说"总是写`if(NULL == p)`.这些规则可能对某些程序员有用,但不是全部.在这两种情况下(不匹配)无论如何,现代编译器都会对错误提出警告,因此人为规则甚至不那么重要. (2认同)
  • @Voo `printf("%s", "hello")` 会比 `printf("hello")` 慢,所以有一个缺点。一个小的,因为 IO 几乎总是比这种简单的格式化慢,但有一个缺点。 (2认同)
  • @Yakk 我怀疑这会更慢 (2认同)

P1k*_*chu 16

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

据说因printf字符串格式漏洞而易受攻击.在您的示例中,字符串是硬编码的,它是无害的(即使从未完全建议像这样的硬编码字符串).但是指定参数的类型是一个很好的习惯.举个例子:

如果有人将格式字符串字符放在printf中而不是常规字符串中(例如,如果要打印程序stdin),printf将尽可能地在堆栈中使用.

它(并且仍然)非常习惯于利用程序来探索堆栈以访问隐藏信息或绕过身份验证.

例(C):

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

如果我把这个程序作为输入 "%08x %08x %08x %08x %08x\n"

printf ("%08x %08x %08x %08x %08x\n"); 
Run Code Online (Sandbox Code Playgroud)

这指示printf函数从堆栈中检索五个参数并将它们显示为8位填充的十六进制数字.所以可能的输出可能如下所示:

40012980 080628c4 bffff7a4 00000005 08059c04
Run Code Online (Sandbox Code Playgroud)

对一个更完整的解释和其他的例子.


Kon*_*itz 12

printf使用文字格式字符串调用是安全有效的,如果printf使用用户提供的格式字符串调用不安全,则存在自动警告您的工具.

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

如果您printf使用文字格式字符串调用,攻击者无法潜入%n您的格式字符串,因此您是安全的.实际上,gcc会将你的调用改为printf调用puts,所以没有任何区别(通过运行来测试gcc -O3 -S).

如果printf使用用户提供的格式字符串进行调用,攻击者可能会潜入%n您的格式字符串,并控制您的程序.您的编译器通常会警告您,他的不安全,请参阅 -Wformat-security.还有更高级的工具可以确保printf即使用户提供的格式字符串也可以安全地调用,甚至可以检查是否传递了正确的参数数量和类型 printf.例如,对于Java,有Google的Error ProneChecker Framework.


Ste*_*mit 11

这是错误的建议.是的,如果你有一个运行时字符串要打印,

printf(str);
Run Code Online (Sandbox Code Playgroud)

非常危险,你应该经常使用

printf("%s", str);
Run Code Online (Sandbox Code Playgroud)

相反,因为通常你永远不知道是否str可能包含一个%标志.但是,如果你有一个编译时常量字符串,那么没有任何错误

printf("Hello, world!\n");
Run Code Online (Sandbox Code Playgroud)

(除此之外,这是有史以来最经典的C程序,实际上来自Genesis的C编程书.所以任何人都不赞成使用这种用法是相当异端的,而且我会有点冒犯!)


sup*_*cat 9

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


pas*_*pkT 8

由于没有人提到,我会添加一个关于他们表现的说明.

在正常情况下,假设没有使用编译器优化(即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;
}
Run Code Online (Sandbox Code Playgroud)

两者都是用gcc -Wall -O0.编译的.时间是用来衡量的time ./a.out > /dev/null.以下是典型运行的结果(我运行了五次,所有结果都在0.002秒内).

对于printf()变体:

real    0m0.416s
user    0m0.384s
sys     0m0.033s
Run Code Online (Sandbox Code Playgroud)

对于fputs()变体:

real    0m0.297s
user    0m0.265s
sys     0m0.032s
Run Code Online (Sandbox Code Playgroud)

如果你有一个长的字符串,这个效果会被放大.

#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;
}
Run Code Online (Sandbox Code Playgroud)

对于printf()变体(运行三次,实际加/减1.5秒):

real    0m39.259s
user    0m34.445s
sys     0m4.839s
Run Code Online (Sandbox Code Playgroud)

对于fputs()变体(运行三次,真正的正负0.2s):

real    0m12.726s
user    0m8.152s
sys     0m4.581s
Run Code Online (Sandbox Code Playgroud)

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

  • 它不会使你的测试无效,因为`fputs()`经常与字符串常量一起使用,并且优化机会是你想要的点的一部分.这就是说,用动态生成的字符串添加一个带有`fputs的测试运行( )`和`fprintf()`将是一个很好的补充数据点. (2认同)

Ábr*_*dre 6

printf("Hello World\n")
Run Code Online (Sandbox Code Playgroud)

自动编译为

puts("Hello World")
Run Code Online (Sandbox Code Playgroud)

您可以通过反汇编您的可执行文件来检查它:

push rbp
mov rbp,rsp
mov edi,str.Helloworld!
call dword imp.puts
mov eax,0x0
pop rbp
ret
Run Code Online (Sandbox Code Playgroud)

运用

char *variable;
... 
printf(variable)
Run Code Online (Sandbox Code Playgroud)

会导致安全问题,不要那样使用printf!

因此你的书实际上是正确的,使用带有一个变量的printf已被弃用但你仍然可以使用printf("my string \n"),因为它会自动成为puts

  • 这种行为实际上完全取决于编译器. (12认同)
  • 这是误导.你说'A编译成B`,但实际上你的意思是'A和B编译成C`. (5认同)

Pat*_*ter 5

对于gcc,可以启用用于检查printf()和的特定警告scanf()

gcc文档指出:

-Wformat包含在中-Wall。在过去的检查中,选择格式的某些方面更多的控制-Wformat-y2k-Wno-format-extra-args-Wno-format-zero-length-Wformat-nonliteral-Wformat-security,和-Wformat=2可用,但不包括在-Wall

-Wformat其在中启用-Wall选项,不会使一些特殊的警告,帮助找到这些情况:

  • -Wformat-nonliteral 如果您没有传递字符串格式的格式说明符,则会发出警告。
  • -Wformat-security如果您传递的字符串可能包含危险的构造,则会发出警告。这是的子集-Wformat-nonliteral

我必须承认,启用功能-Wformat-security揭示了我们代码库中的几个错误(日志记录模块,错误处理模块,xml输出模块,所有这些功能都有一些函数,如果使用参数中的%字符调用它们,它们可能会执行未定义的操作。有关信息,我们的代码库现在已有20多年的历史了,即使我们意识到了这类问题,当我们启用这些警告时,我们仍然对代码库中仍然有多少个错误感到非常惊讶。