Почему printf с одним аргументом (без спецификаторов преобразования) устарел?
в книге, которую я читаю, написано, что printf
С одним аргументом (без спецификаторов преобразования) является устаревшим. Он рекомендует заменить
printf("Hello World!");
С
puts("Hello World!");
или
printf("%s", "Hello World!");
может кто-нибудь сказать мне, почему printf("Hello World!");
- это плохо? В книге написано, что она содержит уязвимости. Что это за уязвимости?
10 ответов
printf("Hello World!");
ИМХО не уязвим, но подумайте об этом:
const char *str;
...
printf(str);
если str
случается указывать на строку, содержащую %s
спецификаторы формата, ваша программа будет демонстрировать неопределенное поведение (в основном сбой), тогда как puts(str)
просто отобразит строку как есть.
пример:
printf("%s"); //undefined behaviour (mostly crash)
puts("%s"); // displays "%s"
printf("Hello world");
в порядке и не имеет уязвимости безопасности.
проблема заключается в:
printf(p);
здесь p
- указатель на вход, который управляется пользователем. Он склонен к формат строки атаки: потребитель может ввести спецификации преобразования для того чтобы принять управление программы, например, %x
для сброса памяти или %n
перезаписать память.
отметим, что puts("Hello world")
не эквивалентно в поведении printf("Hello world")
но ... --7-->. Компиляторы обычно достаточно умны, чтобы оптимизировать последний вызов, чтобы заменить его на puts
.
далее к другим ответам,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)
это абсолютно не одно и то же; в этом случае программист не в "дословную строку" мышления; она в "строку формата" мышления.)
Я просто добавлю немного информации по поводу уязвимости здесь.
говорят, что он уязвим из-за уязвимости строкового формата 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
посмотреть этой для более полного объяснения и других примеров.
вызов printf
с строками литерального формата безопасно и эффективно, и там
существуют инструменты, которые автоматически предупреждают вас, если ваш вызов printf
с пользователей
формат строки небезопасно.
самые тяжелые атаки на printf
воспользоваться
описатель. В отличие от всех других спецификаторов формата, например %d
, %n
на самом деле
записывает значение в адрес памяти в одном из аргументов.
Это означает, что злоумышленник может перезаписать память и таким образом потенциально взять
управление вашей программой. Википедия
предоставляет более подробную информацию.
если вы называете printf
с буквальной строкой формата злоумышленник не может проникнуть
а %n
в строку формата, и вы, таким образом, в безопасности. Фактически,
gcc изменит ваш вызов на printf
в вызов puts
, значит, есть алтари
нет никакой разницы (проверьте это, запустив gcc -O3 -S
).
если вы называете printf
с предоставленной пользователем строкой формата, злоумышленник может
потенциально подкрасться %n
в строку формата и взять под свой контроль
программа. Ваш компилятор обычно предупреждает Вас, что он небезопасен, см.
-Wformat-security
. Есть также более продвинутые инструменты, которые гарантируют, что
призыв printf
безопасно даже с строками формата обеспеченными потребителем, и
они могут даже проверить, что вы передаете правильное число и тип аргументов
printf
. Например, для Java существует ошибка Google подвержена
и проверка Рамки.
это ошибочный совет. Да, если у вас есть строка времени выполнения для печати,
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. Тестируемая программа выглядит следующим образом:
#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
. (The 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 ("моя строка\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
, который включен в не включите несколько специальных предупреждений, которые помогут найти эти случаи:
-
-Wformat-nonliteral
предупредит, если вы не передадите строку litteral как спецификатор формата. -
-Wformat-security
предупредит, если вы передадите строку, которая может содержать опасную конструкцию. Это подмножество-Wformat-nonliteral
.
я должен признать, что включение -Wformat-security
выявлено несколько ошибок, которые мы имели в нашей базе кода (модуль ведения журнала, модуль обработки ошибок, модуль вывода xml, все имели некоторые функции, которые может делать неопределенные вещи, если они были вызваны с символами % в их параметре. Для информации, нашей кодовой базе сейчас около 20 лет, и даже если бы мы знали о таких проблемах, мы были очень удивлены, когда мы включили эти предупреждения, сколько из этих ошибок все еще было в кодовой базе).