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

Why is printf with a single argument (without conversion specifiers) deprecated?

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

printf("Hello World!");

puts("Hello World!");

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

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

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

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

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

示例:

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

printf("Hello world");

很好,没有安全漏洞。

问题在于:

printf(p);

其中 p 是指向由用户控制的输入的指针。容易出现format strings attacks:用户可以插入转换规范来控制程序,例如,%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

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

printf(str);

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

printf("%s", str);

而不是,因为通常您永远无法知道 str 是否可能包含 % 符号。但是,如果你有一个编译时 constant 字符串,那么

就没有任何问题
printf("Hello, world!\n");

(除此之外,这是有史以来最经典的 C 程序,字面上来自创世记的 C 编程书籍。因此,任何反对这种用法的人都是相当异端的,我个人会有点被冒犯!)

除其他答案外,printf("Hello world! I am 50% happy today") 是一个容易犯的错误,可能会导致各种严重的内存问题(这是 UB!)。

对于 "require" 程序员来说,绝对清楚 当他们想要一个逐字的字符串而不需要其他任何东西时

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

(史蒂夫,当然 printf("He has %d cherries\n", ncherries) 绝对不是一回事;在这种情况下,程序员不处于 "verbatim string" 思维模式;她处于 "format string" 思维模式。)

我将在此处添加一些关于漏洞的信息

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

如果有人将格式字符串字符而不是常规字符串放入您的 printf 中(例如,如果您想打印程序标准输入),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

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

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

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

如果你用文字格式字符串调用printf,攻击者就无法偷偷摸摸 a %n 到您的格式字符串中,这样您就安全了。实际上, gcc 会将您对 printf 的调用更改为对 puts 的调用,因此这里乱七八糟 没有任何区别(通过 运行 gcc -O3 -S 进行测试)。

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

由于没有人提及,我将添加有关其性能的注释。

在正常情况下,假设没有使用编译器优化(即 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()变体(运行三次,真实plus/minus 1.5s):

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

对于fputs()变体(运行三次,真实plus/minus0.2s):

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

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

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

gcc 文档指出:

-Wformat is included in -Wall. For more control over some aspects of format checking, the options -Wformat-y2k, -Wno-format-extra-args, -Wno-format-zero-length, -Wformat-nonliteral, -Wformat-security, and -Wformat=2 are available, but are not included in -Wall.

-Wall 选项中启用的 -Wformat 不会启用一些有助于查找这些情况的特殊警告:

  • -Wformat-nonliteral 如果您不传递字符串文字作为格式说明符,将发出警告。
  • 如果您传递的字符串可能包含危险结构,
  • -Wformat-security 将发出警告。它是 -Wformat-nonliteral.
  • 的子集

我不得不承认启用 -Wformat-security 揭示了我们代码库中的几个错误(日志记录模块、错误处理模块、xml 输出模块,所有这些都有一些函数可以做未定义的事情,如果他们在他们的参数中被调用了 % 字符。关于信息,我们的代码库现在大约有 20 年的历史,即使我们意识到这些问题,当我们启用这些警告时,我们仍然非常惊讶这些错误中有多少仍然存在在代码库中)。

除了包含任何附带问题的其他解释清楚的答案外,我想对所提供的问题给出一个准确而简洁的答案。


Why is printf with a single argument (without conversion specifiers) deprecated?

使用单个参数的 printf 函数调用通常 已弃用,并且在 when 正确使用时也没有漏洞正如您将要编写的代码一样。

C 全世界的用户,从状态初学者到状态专家都使用 printf 这种方式将一个简单的文本短语作为输出到控制台。

此外,有人必须区分这个唯一的参数是字符串文字还是指向字符串的指针,这是有效但不常用的。对于后者,当然,当指针未正确设置为指向有效字符串时,可能会出现不方便的输出或任何类型的 Undefined Behavior,但如果格式说明符与相应的字符串不匹配,也会出现这些情况通过给出多个参数来论证。

当然,作为唯一参数提供的字符串具有任何格式或转换说明符也是不正确的,因为不会发生转换。

就是说,像 "Hello World!" 这样的简单字符串文字作为唯一的参数,在该字符串中没有任何格式说明符,就像您在问题中提供的那样:

printf("Hello World!");

没有被弃用或“错误做法”,也没有任何漏洞。

事实上,许多 C 程序员开始并开始学习和使用 C 甚至一般编程语言,HelloWorld 程序和这个 printf 语句作为同类中的第一个。

如果它们被弃用,它们就不会是那样了。

In a book that I'm reading, it's written that printf with a single argument (without conversion specifiers) is deprecated.

好吧,那我就把重点放在书上或者作者本身。如果作者真的这样做,在我看来,不正确断言,甚至在没有明确解释为什么 he/she的情况下教导它(如果那些断言确实与那本书中提供的字面意思相同),我会认为它是一本 糟糕 的书。一本的书,相反,应该解释为什么来避免某些类型的编程方法或函数。

根据我上面所说的,使用 printf 只有一个参数(字符串文字)和 没有 任何格式说明符在任何情况下都不会被弃用或考虑作为 "bad practice".

你应该问问作者,他的意思是什么,或者更好的是,介意他澄清或更正下一版或一般印记的相关部分。