Почему printf с единственным аргументом (без спецификаторов преобразования) устарел?
В книге, которую я читаю, написано, что printf
с единственным аргументом (без спецификаторов преобразования) устарел. Он рекомендует заменить
printf("Hello World!");
с
puts("Hello World!");
или
printf("%s", "Hello World!");
Может кто-нибудь сказать мне, почему printf("Hello World!");
не так? В книге написано, что он содержит уязвимости. Каковы эти уязвимости?
Ответы
Ответ 1
printf("Hello World!");
ИМХО не уязвим, но рассмотрим это:
const char *str;
...
printf(str);
Если str
указывает на строку, содержащую спецификаторы формата %s
, ваша программа будет демонстрировать поведение undefined (в основном, сбой), тогда как puts(str)
будет просто отображать строку как есть.
Пример:
printf("%s"); //undefined behaviour (mostly crash)
puts("%s"); // displays "%s"
Ответ 2
printf("Hello world");
в порядке и не имеет уязвимости безопасности.
Проблема заключается в следующем:
printf(p);
где p
- указатель на вход, который контролируется пользователем. Он подвержен форматированию атак типа: пользователь может вставлять спецификации преобразования для управления программой, например %x
, чтобы сбрасывать память или %n
для перезаписывания памяти.
Обратите внимание, что puts("Hello world")
не эквивалентен поведению printf("Hello world")
, а <<27 > . Компиляторы обычно достаточно умны, чтобы оптимизировать последний вызов, чтобы заменить его на puts
.
Ответ 3
В дополнение к другим ответам, 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)
абсолютно не то же самое, в этом случае программист не находится в стиле "стенографической строки", она находится в стиле "формат строки" ).
Ответ 4
Я просто добавлю немного информации относительно части уязвимости здесь.
Он сказал, что уязвим из-за уязвимости формата 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
См. this для более полного объяснения и других примеров.
Ответ 5
Это ошибочный совет. Да, если у вас есть строка времени выполнения для печати,
printf(str);
довольно опасен, и вы всегда должны использовать
printf("%s", str);
потому что в общем случае вы никогда не узнаете, может ли str
содержать знак %
. Однако, если у вас есть постоянная строка времени компиляции, ничего нечего с
printf("Hello, world!\n");
(Среди прочего, это самая классическая программа на C, когда-либо, буквально из книги программирования C в книге Бытия. Поэтому любой, кто осуждает это использование, является довольно еретическим, и я, например, был бы несколько оскорблен!)
Ответ 6
Вызов printf
с буквальными строками формата безопасен и эффективен, и там
существуют инструменты для автоматического предупреждения, если ваш вызов printf
с пользователем
если строки формата небезопасны.
Наиболее серьезные атаки на printf
используют формат %n
спецификатор. В отличие от всех других спецификаторов формата, например, %d
, %n
на самом деле
записывает значение в адрес памяти, предоставленный в одном из аргументов формата.
Это означает, что злоумышленник может перезаписать память и, следовательно,
управление вашей программой. Wikipedia
предоставляет более подробную информацию.
Если вы вызываете printf
с литеральной строкой формата, злоумышленник не может подкрасться
a %n
в строку формата, и вы, таким образом, будете в безопасности. По факту,
gcc изменит ваш вызов на printf
на вызов puts
, поэтому там litteraly
не имеет никакой разницы (проверьте это, запустив gcc -O3 -S
).
Если вы вызываете printf
с введенной пользователем строкой формата, злоумышленник может
потенциально прокрасться %n
в строку формата и взять под свой контроль
программа. Ваш компилятор обычно предупреждает вас, что он небезопасен, см.
-Wformat-security
. Существуют также более совершенные инструменты, которые гарантируют, что
вызов printf
является безопасным даже с предоставленными пользователем строками формата и
они могут даже проверить, что вы передаете правильное число и тип аргументов
printf
. Например, для Java существует Google Error Prone
и Checker Framework.
Ответ 7
Довольно неприятный аспект printf
заключается в том, что даже на платформах, где считывание блуждающей памяти может вызвать только ограниченный (и приемлемый) вред, один из символов форматирования %n
вызывает следующий аргумент, который будет интерпретироваться как указатель на целое число, доступное для записи, и приводит к тому, что количество выводимых символов до сих пор хранится в переменной, идентифицированной им. Я никогда не использовал эту функцию самостоятельно, и иногда я использую легкие методы стиля printf, которые я написал, чтобы включать только те функции, которые я фактически использую (и не включаю в них ни один или что-то подобное), но загружая стандартные функции printf, полученные строки из ненадежных источников может выявить уязвимости безопасности, выходящие за пределы возможности читать произвольное хранилище.
Ответ 8
Поскольку никто не упомянул, я бы добавил примечание относительно их эффективности.
При нормальных обстоятельствах, если не использовать оптимизацию компилятора (т.е. printf()
фактически вызывает printf()
, а не fputs()
), я ожидал бы, что printf()
будет работать менее эффективно, особенно для длинных строк. Это связано с тем, что printf()
должен анализировать строку, чтобы проверить, есть ли какие-либо спецификации преобразования.
Чтобы подтвердить это, я провел несколько тестов. Тестирование выполняется на Ubuntu 14.04, с gcc 4.8.4. Моя машина использует процессор Intel i5. Проверяемая программа выглядит следующим образом:
#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()
во время компиляции.
Ответ 9
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 ( "моя строка \n" ), потому что она автоматически станет puts
Ответ 10
Для 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
будет предупреждать, если вы не передадите строку litteral в качестве спецификатора формата.
-
-Wformat-security
будет предупреждать, если вы передадите строку, которая может содержать опасную конструкцию. Это подмножество -Wformat-nonliteral
.
Я должен признать, что включение -Wformat-security
выявило несколько ошибок, которые мы имели в нашей кодовой базе (модуль протоколирования, модуль обработки ошибок, модуль вывода xml, все имели некоторые функции, которые могли бы делать undefined вещи, если они были вызваны с% символов в их параметре. Для информации наша кодовая база сейчас составляет около 20 лет, и даже если мы знали об этих проблемах, мы были очень удивлены, когда мы включили эти предупреждения, сколько из этих ошибок осталось в кодовой базе).