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

问题描述 投票:96回答:10

在我正在阅读的一本书中,有人写道,printf只有一个参数(没有转换说明符)。它建议替代

printf("Hello World!");

puts("Hello World!");

要么

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

有人能告诉我为什么printf("Hello World!");错了吗?书中写道它包含漏洞。这些漏洞是什么?

c security printf format-specifiers puts
10个回答
118
投票

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

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

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

例:

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

4
投票

对于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不会启用几个有助于查找这些情况的特殊警告:

  • 如果你没有传递字符串文字作为格式说明符,qazxsw poi会发出警告。
  • 如果你传递一个可能包含危险结构的字符串,-Wformat-nonliteral会发出警告。它是-Wformat-security的一个子集。

我必须承认,启用-Wformat-nonliteral会发现我们的代码库中存在的一些错误(日志记录模块,错误处理模块,xml输出模块,如果在参数中调用了%字符,则所有函数都可以执行未定义的操作。 ,我们的代码库现在已经有20年的历史了,即使我们意识到这些问题,当我们启用这些警告时,我们仍然非常惊讶这些错误中有多少仍然存在于代码库中)。


73
投票

printf("Hello world");

很好,没有安全漏洞。

问题在于:

printf(p);

其中p是指向用户控制的输入的指针。它很容易出现format strings attacks:用户可以插入转换规范来控制程序,例如,%x转储内存或%n来覆盖内存。

请注意,puts("Hello world")printf("Hello world")的行为不同,而是与printf("Hello world\n")相同。编译器通常足够聪明,可以优化后一个调用,用puts替换它。


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)绝对不是一回事;在这种情况下,程序员不是“逐字串”的心态;她是“格式字符串”的心态。)


16
投票

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

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

如果有人将格式字符串字符放在printf中而不是常规字符串中(例如,如果要打印程序stdin),printf将在堆栈中使用任何可能的内容。

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

例(C):

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

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

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

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

40012980 080628c4 bffff7a4 00000005 08059c04

有关更完整的解释和其他示例,请参阅this


12
投票

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

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

如果你用文字格式的字符串调用printf,攻击者就不能将%n潜入你的格式字符串,因此你是安全的。事实上,gcc会将你对printf的调用改为对puts的调用,因此没有任何区别(通过运行gcc -O3 -S来测试)。

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


11
投票

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

printf(str);

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

printf("%s", str);

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

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

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


9
投票

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


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;
}

两者都是用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

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

#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()变体(跑了三次,真正加/减0.2s):

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

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


6
投票
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

© www.soinside.com 2019 - 2024. All rights reserved.