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

时间:2015-07-08 11:06:38

标签: c security printf format-specifiers puts

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

printf("Hello World!");

puts("Hello World!");

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

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

11 个答案:

答案 0 :(得分:117)

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

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

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

示例:

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

答案 1 :(得分:72)

printf("Hello world");

很好,没有安全漏洞。

问题在于:

printf(p);

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

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

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

答案 3 :(得分: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-function从堆栈中检索五个参数,并将它们显示为8位填充的十六进制数字。因此可能的输出可能如下:

40012980 080628c4 bffff7a4 00000005 08059c04

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

答案 4 :(得分: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

答案 5 :(得分:11)

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

printf(str);

非常危险,你应该总是使用

printf("%s", str);

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

没有任何错误。
printf("Hello, world!\n");

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

答案 6 :(得分:9)

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

答案 7 :(得分: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.5秒):

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

对于fputs()变体(运行三次,实数加/减0.2秒):

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

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

答案 8 :(得分: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(&#34;我的字符串\ n&#34;)因为它会自动成为put

答案 9 :(得分: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选项中启用的-Wformat-nonliteral不会启用一些有助于查找这些案例的特殊警告:

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

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

答案 10 :(得分:1)

除了涵盖所有附带问题的其他解释清楚的答案外,我想对所提供的问题给出准确,简洁的答案。


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

通常不建议使用仅使用单个参数的printf函数调用,并且正常使用时也不会存在漏洞,因为您总是应该编写代码。

C世界各地的用户(从状态初学者到状态专家)都使用printf来提供简单的文本短语作为控制台输出。

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

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

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

printf("Hello World!");

完全不被 弃用或“ 不良习惯”,也没有任何漏洞。

实际上,许多C程序员开始并开始以这种HelloWorld程序和此printf语句为同类的第一语言来学习和使用C甚至一般的编程语言。

如果不赞成,那不是那个。

在我正在阅读的书中,写有不带单个参数(不带转换说明符)的printf

那么,我将重点放在书或作者本身上。在我看来,如果作者确实这样做了,则是不正确断言,甚至是在没有明确说明为什么的情况下教导他/她这样做(如果这些断言实际上是等效的)那本书中提供的内容),我认为这是一本书。与此相反,一本书应该解释为什么,以避免某些编程方法或函数。

按照我在上面所说的,仅将printf与一个参数(字符串文字)一起使用,并且将不带的任何格式说明符在任何情况下都不被弃用或视为”不良做法”。

您应该问作者,他的意思是什至是什至更好,请介意他澄清或更正下一版或总体版本说明的相关部分。