为什么printf只有一个参数(没有转换说明符)不推荐使用?
在我正在阅读的一本书中,写了带有一个参数(不带转换说明符)的printf
被弃用。 它build议替代
printf("Hello World!");
同
puts("Hello World!");
要么
printf("%s", "Hello World!");
有人可以告诉我为什么printf("Hello World!");
是错的? 它是写在书中,它包含漏洞。 这些漏洞是什么?
printf("Hello World!");
恕我直言,不容易,但考虑到这一点:
const char *str; ... printf(str);
如果str
恰好指向包含%s
格式说明符的string,程序将显示未定义的行为(大部分是崩溃),而puts(str)
将仅显示string。
例:
printf("%s"); //undefined behaviour (mostly crash) puts("%s"); // displays "%s"
printf("Hello world");
是好的,没有安全漏洞。
问题在于:
printf(p);
其中p
是指向由用户控制的input的指针。 它很容易格式化string攻击 :用户可以插入转换规范来控制程序,例如%x
转储内存或%n
覆盖内存。
请注意, puts("Hello world")
在行为上与printf("Hello world")
而是printf("Hello world\n")
。 编译器通常足够聪明,可以优化后者的调用来replaceputs
。
除了其他的答案之外, printf("Hello world! I am 50% happy today")
是一个很容易造成的bug,可能会导致各种令人讨厌的内存问题(这是UB!)。
只要程序员想要一个逐字串而没有其他任何东西时 ,它就更简单,更简单,更强大。
这就是printf("%s", "Hello world! I am 50% happy today")
。 这完全是万无一失的。
(Steve,当然是printf("He has %d cherries\n", ncherries)
绝对不是一回事,在这种情况下,程序员并不是“逐字串”的心态,而是“格式化”的心态。 )
我将在这里添加一些有关漏洞部分的信息。
据说由于printfstring格式的漏洞,它是脆弱的。 在你的例子中,string是硬编码的,这是无害的(即使硬编码这样的string永远不会被完全推荐)。 但是指定参数的types是一个很好的习惯。 以这个例子:
如果有人将格式string字符放在printf中,而不是常规string(例如,如果要打印程序标准input),printf将采取任何他可以在堆栈上进行的操作。
现在(现在仍然)非常习惯于利用程序来探索堆栈来访问隐藏的信息或绕过authentication。
例(C):
int main(int argc, char *argv[]) { printf(argv[argc - 1]); // takes the first argument if it exists }
如果我把这个程序input为"%08x %08x %08x %08x %08x\n"
printf ("%08x %08x %08x %08x %08x\n");
这指示printf函数从堆栈中检索五个参数,并将其显示为8位填充的hex数字。 所以可能的输出可能如下所示:
40012980 080628c4 bffff7a4 00000005 08059c04
看到这个更完整的解释和其他例子。
这是错误的build议。 是的,如果您有打印的运行时string,
printf(str);
是相当危险的,你应该总是使用
printf("%s", str);
相反,因为通常你永远不会知道str
是否可能包含%
符号。 但是,如果你有一个编译时常量string,没有任何问题
printf("Hello, world!\n");
(除此之外,这是C程序中最经典的C程序,字面意思是来自“创世纪”的C编程书籍,所以任何贬低这个用法的人都是相当邪教的,而我一个人会有点冒犯他人!
使用文字格式string调用printf
是安全和有效的,并且存在一些工具可以在用户提供的格式string对printf
的调用不安全时自动发出警告。
对printf
最严重的攻击利用%n
格式说明符。 与所有其他格式说明符(例如%d
相反, %n
实际上是将一个值写入其中一个格式参数中提供的内存地址。 这意味着攻击者可以覆盖内存,从而有可能控制你的程序。 维基百科提供了更多细节。
如果用string格式string调用printf
,攻击者就不能将%n
隐藏到格式string中,因此您是安全的。 实际上,gcc会把你的调用改为printf
,所以在这里没有任何区别(通过运行gcc -O3 -S
testing)。
如果使用用户提供的格式string调用printf
,攻击者可能潜入%n
到您的格式string中,并控制您的程序。 你的编译器通常会警告你他的不安全,请参阅-Wformat-security
。 还有一些更高级的工具可以确保printf
的调用在用户提供的格式string中是安全的,他们甚至可能会检查是否将正确数量和types的parameter passing给printf
。 例如,对于Java,有Google的错误倾向和检查器框架 。
printf
一个相当讨厌的方面是,即使在杂散内存读取的平台上只能造成有限(和可接受)的伤害的平台之一,格式化字符%n
的一个导致下一个参数被解释为指向可写整数的指针,并且使得到此为止输出的字符的数量被存储到由此识别的variables中。 我从来没有使用过这个function,有时候我使用了轻量级的printf风格的方法,我只写了一些我实际使用的function(不包括那一个或者类似的东西),但是提供了标准的printf函数string从不可靠的来源可能暴露的安全漏洞超出了读取任意存储的能力。
由于没有人提到,我会添加一个关于他们的performance的笔记。
在正常情况下,假设没有使用编译器优化(即printf()
实际调用printf()
而不是fputs()
),我期望printf()
执行效率较低,特别是对于长string。 这是因为printf()
必须parsingstring来检查是否有任何转换说明符。
为了证实这一点,我已经运行了一些testing。 testing在Ubuntu 14.04上执行,使用gcc 4.8.4。 我的机器使用Intel i5 cpu。 正在testing的程序如下:
#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
如果你有很长的string,这个效果会被放大。
#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()
variables(运行三次,实际加/减0.2s):
real 0m12.726s user 0m8.152s sys 0m4.581s
注意:在检查gcc生成的程序集之后,我意识到gcc优化了对fwrite()
调用的fputs()
fwrite()
调用,即使使用-O0
。 ( printf()
调用保持不变。)我不确定这是否会使我的testing失效,因为编译器会在编译时计算fwrite()
的string长度。
printf("Hello World\n")
自动编译
puts("Hello World")
你可以通过diassembling你的可执行文件来检查它:
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与一个variables已弃用,但你仍然可以使用printf(“我的string\ n”),因为它会自动成为投入
对于gcc,可以启用特定的警告来检查printf()
和scanf()
。
gcc文档指出:
-Wformat
包含在-Wall
。 为了更好地控制格式检查的某些方面,选项-Wformat-y2k
-Wno-format-extra-args
,-Wno-format-extra-args
,-Wno-format-zero-length
,-Wformat-nonliteral
,-Wformat-security
和-Wformat=2
可用,但不包含在-Wall
。
在-Wall
选项中启用的-Wformat
不会启用几个有助于查找这些情况的特殊警告:
-
-Wformat-nonliteral
会发出警告,如果你不传递一个string作为格式说明符。 -
-Wformat-security
会警告你传递一个可能包含危险结构的string。 它是-Wformat-nonliteral
的一个子集。
我不得不承认,启用-Wformat-security
揭示了我们在代码库中的一些错误(日志模块,error handling模块,xml输出模块,都有一些函数可以做未定义的事情,如果在参数中用%字符调用的话对于信息,我们的代码库现在已经有20年了,即使我们意识到了这些问题,当我们启用这些警告时,仍然有多less这些错误仍在代码库中,我们感到非常惊讶。