Почему 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 лет, и даже если бы мы знали о таких проблемах, мы были очень удивлены, когда мы включили эти предупреждения, сколько из этих ошибок все еще было в кодовой базе).