Почему 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")но чтобыprintf("Hello world\n"). Компиляторы обычно достаточно умны, чтобы оптимизировать последний вызов, чтобы заменить его на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 а то Checker Рамки.
это ошибочный совет. Да, если у вас есть строка времени выполнения для печати,
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. (Тег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.The
-Wformat, который включен в не включите несколько специальных предупреждений, которые помогут найти эти случаи:
-Wformat-nonliteralпредупредит, если вы не передадите строку litteral в качестве спецификатора формата.-Wformat-securityпредупредит, если вы передадите строку, которая может содержать опасную конструкцию. Это подмножество-Wformat-nonliteral.я должен признать, что включение
-Wformat-securityвыявлено несколько ошибок, которые мы имели в нашей кодовой базе (модуль регистрации, модуль обработки ошибок, модуль вывода xml, все имели некоторые функции, которые может делать неопределенные вещи, если они были вызваны с % символов в их параметре. Для информации, нашей кодовой базе сейчас около 20 лет, и даже если мы знали о таких проблемах, мы были очень удивлены, когда мы включили эти предупреждения, сколько из этих ошибок все еще было в кодовой базе).
Comments