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


102

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

printf("Hello World!");

puts("Hello World!");

要么

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

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


34
注意:printf("Hello World!")并不相同puts("Hello World!")puts()附加一个'\n'。取而代之的printf("abc")fputs("abc", stdout)
chux-恢复莫妮卡2015年

5
那是什么书?我认为printf弃用的方式gets与C99中弃用的方式不同,因此您可以考虑更精确地编辑问题。
el.pescado

14
听起来您正在阅读的书不是很好-一本好书不应该只是说这样的东西“已过时”(除非作者使用这个词来描述他们自己的观点,否则这实际上是错误的),并且应该解释该用法实际上是无效和危险的,而不是显示安全/有效代码作为“不应该”的示例。
R .. GitHub停止帮助ICE

8
你能认出这本书吗?
基思·汤普森

7
请指定书名,作者和页面参考。谢谢。
Greenonline 2015年

Answers:


122

printf("Hello World!"); 恕我直言不易受攻击,但请考虑以下因素:

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

如果str碰巧指向包含%s格式说明符的字符串,则您的程序将表现出未定义的行为(通常是崩溃),而puts(str)只会按原样显示该字符串。

例:

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

21
除了导致程序崩溃之外,格式字符串还有许多其他利用方式。有关更多信息,请参见此处:en.wikipedia.org/wiki/Unulated_format_string
e.dan 2015年

9
另一个原因puts可能是速度更快。
edmz 2015年

38
@black:puts“大概”更快,这可能是人们推荐它的另一个原因,但实际上并不快。"Hello, world!"两种方式我都只打印了1,000,000次。用printf了0.92秒。用puts了0.93秒。在效率方面,有些事情值得担心,但是printfvs. puts并不是其中之一。
史蒂夫·萨米特

10
@KonstantinWeitz:但是(a)我没有使用gcc,(b)为什么puts更快” 的说法是假的也没关系,但它仍然是假的。
Steve Summit

6
@KonstantinWeitz:我提供的证据是黑人用户提出的主张(与之相反)。我只是在试图澄清,程序员不应puts为此担心。(但是,如果您想对此进行争论:如果能为任何putsprintf任何情况下都快得多的现代机器找到任何现代编译器,我会感到惊讶。)
Steve Summit

75

printf("Hello world");

很好,没有安全漏洞。

问题在于:

printf(p);

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

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


10
当然 printf(p,x),如果用户可以控制,同样会出现问题p。因此,问题在于仅使用printf一个参数,而是使用用户控制的格式字符串。
哈根·冯·埃岑

2
@HagenvonEitzen从技术上讲是正确的,但是很少有人会故意使用用户提供的格式字符串。人们写作时printf(p),是因为他们没有意识到它是格式字符串,所以他们只是认为自己正在打印文字。
Barmar

33

除了其他答案之外,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)绝对不是同一回事;在这种情况下,程序员不是“普通字符串”思维方式的;她是“格式字符串”思维方式的。)


2
这不值得争论,我理解您所说的逐字格式与格式化字符串的心态,但是,并不是每个人都这么认为,这是一个“千篇一律”的规则会发怒的原因。说“从不打印带有常量的字符串printf”就像说“总是写if(NULL == p)。这些规则可能对某些程序员有用,但并不是全部。在两种情况下(printf格式不匹配和Yoda条件匹配),现代编译器无论如何都会警告错误,因此,人为规则甚至不那么重要
史蒂夫·萨米特

1
@Steve如果使用某种东西的好处恰好为零,但缺点很多,那么是的,确实没有理由使用它。另一方面,Yoda条件确实具有不利之处,它们使代码更难阅读(您直觉地说“如果p为零”而不是“如果零为p”)。
Voo,2015年

2
@Voo printf("%s", "hello")将会比慢printf("hello"),因此存在不利之处。很小,因为IO总是比这种简单的格式化慢得多,但是有一个缺点。
Yakk-Adam Nevraumont 2015年

1
@Yakk我怀疑这将是慢
MM

gcc -Wall -W -Werror可以避免此类错误带来的严重后果。
chqrlie

17

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

据说由于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

请参阅此内容以获得更完整的说明和其他示例。


13

printf使用文字格式的字符串进行调用既安全又高效,并且如果printf使用用户提供的格式字符串进行的调用不安全,则存在一些工具可以自动警告您。

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

如果您printf使用文字格式的字符串进行调用,则攻击者无法将a潜入%n您的格式字符串中,因此很安全。实际上,gcc会将您的呼叫更改printfputs,从而将您的呼叫更改为,因此从根本上没有任何区别(通过运行进行测试gcc -O3 -S)。

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


12

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

printf(str);

非常危险,您应该始终使用

printf("%s", str);

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

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

(在其他事情中,这是有史以来最经典的C程序,从字面上看是Genesis的C编程书。因此,任何反对这种用法的人都是相当异端的人,而我会有点冒犯!)


because printf's first argument is always a constant string我不确定您的意思。
塞巴斯蒂安·马赫

正如我所说的,它"He has %d cherries\n"是一个常量字符串,这意味着它是一个编译时常量。但是,为了公平起见,笔者的意见是不是“不及格常量字符串作为printf第一个参数”,它是“不不传递一个字符串%作为printf第一个说法。”
史蒂夫·萨米特

literally from the C programming book of Genesis. Anyone deprecating that usage is being quite offensively heretical-近年来,您实际上并没有读过K&R。如今,这里有大量的建议和编码风格,不仅不赞成使用,而且还很普通。
Voo,2015年

@Voo:好吧,我们只能说并非所有被认为是不好的做法实际上都是不好的做法。(int想到“永远不要使用简单” 的建议。)
史蒂夫·萨米特

1
@Steve我不知道你在哪儿听到的,但是那肯定不是我们在那儿谈论的那种不好(坏?)的做法。别误会我的意思,因为这段时间的代码还算不错,但是您真的不想花很多时间看k&r,而只是将这些作为历史记录。这些天“
Voo

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。以下是典型运行的结果(我已经运行了5次,所有结果都在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()变体(运行了3次,实际正负1.5秒):

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

对于fputs()变体(运行了3次,正负0.2s):

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

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


2
它不会使您的测试无效fputs()(通常与字符串常量一起使用),并且优化机会是您想要进行的工作的一部分,也就是说,使用动态生成的字符串添加一个测试运行,fputs()并且fprintf()将是一个很好的补充数据点。
PatrickSchlüter'15

@PatrickSchlüter使用动态生成的字符串进行测试似乎无法解决此问题的目的,尽管... OP似乎只对要打印的字符串文字感兴趣。
user12205'7

1
即使他的示例使用字符串文字,他也没有明确声明。实际上,我认为他对本书建议的困惑是由于在示例中使用了字符串文字。对于字符串字面量,书籍的建议有些疑问,对于动态字符串,这是个好建议。
PatrickSchlüter'15

1
/dev/null有点像玩具,因为通常在生成格式化输出时,您的目标是使输出到达某个地方,而不是被丢弃。一旦添加了“实际上不丢弃数据”的时间,它们将如何比较?
Yakk-亚当·内夫罗蒙特2015年

7
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


12
此行为实际上完全取决于编译器。
Jabberwocky 2015年

6
这是误导。你说A compiles to B,但实际上你是说A and B compile to C
塞巴斯蒂安·马赫

6

对于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多年的历史了,即使我们意识到了这类问题,当我们启用这些警告时,我们仍然对代码库中仍然有多少个错误感到非常惊讶。


1

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


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

一个printf与一般的一个参数的函数调用时剔除,也没有漏洞,当使用得当,你总是要编写。

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

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

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

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

printf("Hello World!");

不是过时或者“不良做法 ”,也没有任何漏洞。

实际上,许多C程序员开始并开始使用该HelloWorld程序来学习和使用C甚至是编程语言。 printf语句是同类中的第一条。

如果它们被弃用,那不是那样的。

在我正在阅读的书中,写printf有一个参数(不带转换说明符)已被弃用。

好吧,那我就把重点放在书或作者本身上。我认为,如果作者确实是在做这样的事情,那是不正确的断言,甚至在没有明确说明他/她这样做的原因的情况下教导他们(如果这些断言在书中字面上确实是等价的),我会认为这是一本不好的书。一个很好的书,而不是说,要解释为什么以避免某种编程方法或功能。

根据我在上面所说的,在任何情况下,不建议printf仅使用一个参数(字符串文字)且不使用任何格式说明符的情况,也不认为这是“不良做法”

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


你可能会补充说,printf("Hello World!");不是等同于puts("Hello World!");无论如何,它告诉一些有关建议的作者。
chqrlie
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.