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