在我正在阅读的一本书中,printf
不推荐使用单个参数(没有转换说明符)。建议换
printf("Hello World!");
和
puts("Hello World!");
或者
printf("%s", "Hello World!");
有人能告诉我为什么printf("Hello World!");
错了吗?书中写到它包含漏洞。这些漏洞是什么?
在我正在阅读的一本书中,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
是指向由用户控制的输入的指针。它容易受到 格式化字符串攻击:用户可以插入转换规范来控制程序,例如%x
转储内存或%n
覆盖内存。
请注意,puts("Hello world")
在行为上不等价于printf("Hello world")
但printf("Hello world\n")
。编译器通常足够聪明,可以优化后一个调用以将其替换为puts
.
Further to the other answers, printf("Hello world! I am 50% happy today")
is an easy bug to make, potentially causing all manner of nasty memory problems (it's UB!).
It's just simpler, easier and more robust to "require" programmers to be absolutely clear when they want a verbatim string and nothing else.
And that's what printf("%s", "Hello world! I am 50% happy today")
gets you. It's entirely foolproof.
(Steve, of course printf("He has %d cherries\n", ncherries)
is absolutely not the same thing; in this case, the programmer is not in "verbatim string" mindset; she is in "format string" mindset.)
我将在这里添加一些有关漏洞部分的信息。
由于 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
有关更完整的说明和其他示例,请参阅此内容。
使用文字格式字符串调用是安全且高效的,并且如果您使用用户提供的格式字符串printf
调用不安全,则存在自动警告您的工具。printf
最严重的攻击printf
利用了%n
格式说明符。与所有其他格式说明符相反,例如%d
,%n
实际上将值写入格式参数之一中提供的内存地址。这意味着攻击者可以覆盖内存,从而可能控制您的程序。维基百科
提供了更多细节。
如果您printf
使用文字格式字符串进行调用,攻击者无法将 a 潜入%n
您的格式字符串,因此您是安全的。实际上,gcc 会将您的呼叫更改printf
为对 的呼叫puts
,因此几乎没有任何区别(通过运行测试gcc -O3 -S
)。
如果您printf
使用用户提供的格式字符串进行调用,攻击者可能会潜入%n
您的格式字符串并控制您的程序。您的编译器通常会警告您他的不安全,请参阅
-Wformat-security
. 还有一些更高级的工具可以确保调用printf
是安全的,即使使用用户提供的格式字符串,它们甚至可能会检查您是否将正确数量和类型的参数传递给
printf
. 例如,对于 Java,有Google 的 Error Prone
和Checker Framework。
这是误导性的建议。是的,如果您有要打印的运行时字符串,
printf(str);
非常危险,您应该始终使用
printf("%s", str);
相反,因为通常您永远无法知道是否str
可能包含%
符号。但是,如果您有一个编译时常量字符串,则没有任何问题
printf("Hello, world!\n");
(除此之外,这是有史以来最经典的 C 程序,从字面上看来自创世纪的 C 编程书籍。所以任何反对这种用法的人都是相当异端的,我会有点被冒犯!)
一个相当令人讨厌的方面printf
是,即使在杂散内存读取只能造成有限(和可接受的)危害的平台上,格式化字符之一%n
也会导致下一个参数被解释为指向可写整数的指针,并导致迄今为止输出的要存储到由此标识的变量中的字符数。我自己从未使用过该功能,有时我使用我编写的轻量级 printf 样式方法,仅包含我实际使用的功能(并且不包括该功能或任何类似功能),但提供接收到的标准 printf 函数字符串来自不可靠的来源可能会暴露超出读取任意存储能力的安全漏洞。
由于没有人提到,我会添加一个关于他们的表现的注释。
在正常情况下,假设没有使用编译器优化(即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()
变体(运行 3 次,实际加/减 1.5 秒):
real 0m39.259s
user 0m34.445s
sys 0m4.839s
对于fputs()
变体(运行 3 次,实数加/减 0.2 秒):
real 0m12.726s
user 0m8.152s
sys 0m4.581s
注意:检查 gcc 生成的程序集后,我意识到 gcc 优化了对fputs()
调用的fwrite()
调用,即使使用-O0
. (printf()
调用保持不变。)我不确定这是否会使我的测试无效,因为编译器会在编译fwrite()
时计算字符串长度。
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
对于 gcc,可以启用特定的检查警告printf()
和scanf()
.
gcc 文档指出:
-Wformat
包含在-Wall
. 为了更好地控制格式检查的某些方面,选项-Wformat-y2k
、-Wno-format-extra-args
、-Wno-format-zero-length
、-Wformat-nonliteral
、-Wformat-security
和-Wformat=2
可用,但不包含在-Wall
.
在选项中启用的-Wformat
which-Wall
不会启用有助于查找这些情况的几个特殊警告:
-Wformat-nonliteral
如果您没有将字符串作为格式说明符传递,则会发出警告。-Wformat-security
如果您传递可能包含危险构造的字符串,则会发出警告。它是 的子集-Wformat-nonliteral
。我不得不承认,启用-Wformat-security
揭示了我们在代码库中存在的几个错误(日志记录模块、错误处理模块、xml 输出模块,如果在参数中使用 % 字符调用它们,它们都有一些函数可以做未定义的事情。有关信息,我们的代码库现在已经有 20 年的历史了,即使我们意识到这些问题,当我们启用这些警告时,我们仍然感到非常惊讶,其中有多少错误仍然存在于代码库中)。
除了涵盖任何附带问题的其他解释清楚的答案之外,我想对所提供的问题给出一个准确而简洁的答案。
为什么
printf
不推荐使用单个参数(没有转换说明符)?
通常不printf
推荐使用带有单个参数的函数调用,并且在正确使用时也没有漏洞,因为您总是应该编码。
C 全世界的用户,从状态初学者到状态专家都使用printf
这种方式将简单的文本短语作为输出到控制台。
此外,有人必须区分这个唯一的参数是字符串文字还是指向字符串的指针,这是有效但通常不使用的。对于后者,当然,当指针未正确设置为指向有效字符串时,可能会出现不方便的输出或任何类型的未定义行为,但如果格式说明符与相应的参数不匹配,这些事情也会发生多个论点。
当然,作为唯一参数提供的字符串具有任何格式或转换说明符也是不正确和正确的,因为不会发生转换。
也就是说,给出一个简单的字符串文字"Hello World!"
,就像您在问题中提供的那样,在该字符串中没有任何格式说明符的唯一参数:
printf("Hello World!");
根本没有被弃用或“不良做法”,也没有任何漏洞。
事实上,许多 C 程序员开始并开始学习和使用 C 甚至一般的编程语言,将 HelloWorld 程序和此printf
语句作为同类中的第一个。
如果它们被弃用,它们就不会那样了。
在我正在阅读的一本书中,
printf
不推荐使用单个参数(没有转换说明符)。
好吧,那我会把重点放在这本书或作者本身上。如果作者真的这样做,在我看来,不正确的断言,甚至在没有明确解释他/她为什么这样做的情况下教导(如果这些断言在该书中提供的确实是字面上等效的),我会认为这是一本糟糕的书。与此相反,一本好书将解释为什么要避免某种编程方法或功能。
根据我上面所说的,printf
仅使用一个参数(字符串文字)并且没有任何格式说明符在任何情况下都不会被弃用或被视为“坏习惯”。
您应该问作者,他的意思是什么,甚至更好,请提醒他澄清或更正下一版的相关部分或一般印记。