Не зная брода, не лезь в воду. Часть вторая

    Ужасный printf
    В этот раз я хочу поговорить о функции printf. Все наслышаны об уязвимостях в программах, и что функции наподобие printf объявлены вне закона. Но одно дело знать, что лучше не использовать эти функции. А совсем другое — понять почему. В этой статье я опишу две классических уязвимости программ, связанных с printf. Хакером после этого вы не станете, но, возможно, по-новому взгляните на свой код. Вдруг, вы реализуете аналогичные уязвимые функции, даже не подозревая об этом.

    СТОП. Подожди читатель, не проходи мимо. Я знаю, что ты увидел слово printf. И уверен, что автор статьи сейчас расскажет банальную историю о том, что функция не контролирует типы передаваемых аргументов. Нет! Статья будет не про это, а именно про уязвимости. Заходи почитать.

    Предыдущая заметка находится здесь: Часть первая.

    Введение


    Взглянем на вот эту строчку:
    printf(name);

    Она кажется простой и безобидной. А между тем, в ней скрывается как минимум два способа, чтобы атаковать программу.

    Начнем статью с демонстрационного примера, где есть эта строчка. Код может показаться вам странноватым. Так оно и есть. Оказалось не так просто написать программу, чтобы потом её атаковать. Дело в оптимизации, которую производит компилятор. Получается, что если написать слишком простую программу, то компилятор создает такой код, что ломать там нечего. Он использует регистры, а не стек для хранения данных, встраивает функции и тому подобное. Можно написать код с лишними действиями и циклами, чтобы компилятору не хватило свободных регистров, и он начал помещать данные в стек. К сожалению, пример получается слишком большой и запутанный. Про всё это можно написать отдельную детективную историю, но не будем.

    Представленный пример является компромиссом между сложностью и необходимостью не дать компилятору «схлопнуть в ничто» слишком простой код. Признаюсь, немного я себе всё равно помог. Я отключил некоторые виды оптимизации, используемые в Visual Studio 2010. Во-первых, был отключен ключ /GL (Whole Program Optimization). Во–вторых, я использовал атрибут __declspec(noinline).

    Прошу прощение за такое длинное вступление. Хотелось пояснить неуклюжесть программного кода. И сразу пресечь дискуссии на тему, что этот код можно написать лучше. Я знаю, что можно. Но не получается сделать код одновременно и коротким, и чтобы можно было показать уязвимость.

    Демонстрационный пример


    Полный код и проект для Visual Studio 2010 доступен здесь.
    const size_t MAX_NAME_LEN = 60;
    enum ErrorStatus {
      E_ToShortName, E_ToShortPass, E_BigName, E_OK
    };
    
    void PrintNormalizedName(const char *raw_name)
    {
      char name[MAX_NAME_LEN + 1];
      strcpy(name, raw_name);
    
      for (size_t i = 0; name[i] != '\0'; ++i)
        name[i] = tolower(name[i]);
      name[0] = toupper(name[0]);
    
      printf(name);
    }
    
    ErrorStatus IsCorrectPassword(
      const char *universalPassword,
      BOOL &retIsOkPass)
    {
      string name, password;
      printf("Name: "); cin >> name;
      printf("Password: "); cin >> password;
      if (name.length() < 1) return E_ToShortName;
      if (name.length() > MAX_NAME_LEN) return E_BigName;
      if (password.length() < 1) return E_ToShortPass;
    
      retIsOkPass = 
        universalPassword != NULL &&
        strcmp(password.c_str(), universalPassword) == 0;
      if (!retIsOkPass)
        retIsOkPass = name[0] == password[0];
    
      printf("Hello, ");
      PrintNormalizedName(name.c_str());
    
      return E_OK;
    }
    
    int _tmain(int, char *[])
    {
      _set_printf_count_output(1);
      char universal[] = "_Universal_Pass_!";
      BOOL isOkPassword = FALSE;
      ErrorStatus status =
        IsCorrectPassword(universal, isOkPassword);
      if (status == E_OK && isOkPassword)
        printf("\nPassword: OK\n");
      else
        printf("\nPassword: ERROR\n");
      return 0;
    }

    Функция _tmain() вызывает функцию IsCorrectPassword(). Если пароль верен или если он совпадает с магическим словом "_Universal_Pass_!", то программа выводит строку «Password: OK». Целью атак будет добиться, чтобы программа выводила именно эту строку.

    Функция IsCorrectPassword() запрашивает у пользователя имя и пароль. Пароль считается корректным, если он совпадает с переданным в функцию магическим словом. Также он корректен, если первая буква пароля совпадает с первой буквой имени.

    Вне зависимости от того, введен правильный пароль или нет, программа приветствует пользователя. Для этого вызывается функция PrintNormalizedName().

    В функции PrintNormalizedName() всё самое интересное. Именно в ней, находится обсуждаемый «printf(name);». Подумайте, как с помощью этой строчки можно обмануть программу. Если знаете как, то дальше можно не читать.

    Что делает функция PrintNormalizedName()? Она печатает имя, сделав первую букву заглавной, а остальные маленькими. Например, если ввести имя «andREy2008», то она распечатает «Andrey2008».

    Первая атака


    Предположим мы не знаем правильный пароль. Но знаем, что где-то есть некий магический пароль. Попробуем его поискать, используя printf(). Если адрес этого пароля есть где-то в стеке, то у нас есть шанс на успех. Есть идеи, как увидеть этот пароль на экране?

    Даю подсказку. Функция printf() относится к семейству функций с переменным количеством аргументов. Работают такие функции так. В стек записывается произвольное количество данных. Функция printf() не знает, сколько данных записано в стек и какой у них тип. Она руководствуется исключительно строкой форматирования. Если написано "%d%s", то значит, из стека следует извлечь одно значение типа int и один указатель. Так как функция printf() не знает, сколько аргументов ей передали, то она может заглянуть глубже в стек и распечатать данные, которые никакого к ней отношения не имеет. Как правило, это приводит к access violation или к распечатке мусора. Однако, этим мусором можно воспользоваться.

    Рассмотрим, как может выглядеть стек в момент, когда мы вызываем функцию printf():
    Рисунок 1. Схематическое расположение данных в стеке.
    Рисунок 1. Схематическое расположение данных в стеке.

    Вызов функции «printf(name);» имеет только один аргумент, который является строкой форматирования. Это значит, что если мы вместо имени мы введём "%d", то распечатаем данные, которые лежат в стеке до адреса возврата в функцию PrintNormalizedName(). Попробуем:

    Name: %d
    Password: 1
    Hello, 37
    Password: ERROR

    Пока данное действие малоосмысленно. Как минимум, вначале мы должны распечатать адреса возврата и всё содержимое буфера char name[MAX_NAME_LEN + 1];, который тоже расположен в стеке. И только потом, возможно, мы доберемся до чего-то интересного.

    Если злоумышленник не имеет возможности дизассемблировать или отладить программу, то ему сложно понять, найдет он что-то в стеке или нет. Тем не менее, он может действовать следующим образом.

    В начале, ввести: "%s". Потом ввести "%x%s". Потом ввести "%x%x%s" и так далее. Этим хакер будет перебирать по-очереди данные в стеке, и пытаться распечатать их как строку. Здесь ему помогает то, что все данные в стеке выровнены, как минимум по границе 4 байта.

    Если честно, действуя так, у нас ничего не получится. Мы превысим лимит в 60 символов, так и не распечатав ничего полезного. На помощь нам придет "%f", который предназначен для печати значений типа double. Следовательно, с его помощью мы сможем двигаться по стеку сразу по 8 байт.

    И вот она — долгожданная строчка:

    %f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%x(%s)

    Результат:
    Рисунок 2. Распечатка пароля. Нажмите на рисунок для увеличения.
    Рисунок 2. Распечатка пароля. Нажмите на рисунок для увеличения.

    Попробуем эту строчку в качестве волшебного пароля:

    Name: Aaa
    Password: _Universal_Pass_!
    Hello, Aaa
    Password: OK

    Ура! Мы смогли найти и вывести на экран приватные данные, к которым программа не планировала дать нам доступ. Причем, обратите внимание, для этого нет необходимости иметь доступ к самому двоичному коду программы. Достаточно усердия и настойчивости.

    Выводы по первой атаке


    Подобный способ получения приватных данных следует обдумать более широко. При разработке программ, содержащих функции с переменным количеством аргументов подумайте, существуют ли ситуации, когда через них могут утечь данные во внешний мир. Это может быть лог-файл, пакет, передаваемый по сети, и так далее.

    В рассмотренном случае атака стала возможна из-за того, что на вход функции printf() поступает строка, которая может содержать управляющие команды. Чтобы этого избежать, было достаточно написать так:
    printf("%s", name);


    Вторая атака


    Вы знаете, что функция printf() может модифицировать память? Скорее всего, вы про это читали, но забыли. Речь идет о спецификаторе "%n". Он позволяет записать по указанному адресу количество символов, которые уже распечатала функция printf().

    Если честно, атака, основанная на спецификаторе "%n" носит исключительно исторический характер. Начиная с Visual Studio 2005 возможность использования "%n" по умолчанию отключена. Чтобы провести эту атаку мне пришлось явно разрешить этот спецификатор. Вот это магическое действие:
    _set_printf_count_output(1);

    Чтобы стало понятнее, приведу пример использования "%n":
    int i;
    printf("12345%n6789\n", &i);
    printf( "i = %d\n", i );

    Вывод программы:

    123456789
    i = 5

    Как добраться до нужного указателя, находящегося в стеке, мы уже узнали. А теперь у нас в руках есть инструмент, который позволяет модифицировать память по этому указателю.

    Конечно, пользоваться этим неудобно. Во-первых, мы можем записать только сразу 4 байта (размер типа int). Если нам нужно большое число, то вначале функция printf() будет должна вывести очень много символов. Чтобы этого не делать, может помочь спецификатор "%00u". Спецификатор влияет на значение текущего количества выведенных байт. Подробнее вникать в тонкости не будем.

    В нашем случае всё проще. Нам достаточно записать в переменную isOkPassword любое значение, неравное 0. Адрес этой переменной передаются в функцию IsCorrectPassword(), а значит, находится где-то в стеке. Пусть вас не смущает, что переменная передается как ссылка. На низком уровне, ссылка является обыкновенным указателем.

    Вот строка, которая позволит нам модифицировать переменную IsCorrectPassword:

    %f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f%f %n

    Спецификатор "%n" не учитывает количество символов, выведенных с помощью таких спецификаторов, как "%f". Поэтому, перед "%n" поставим один пробел, чтобы записать в isOkPassword значение 1.

    Пробуем:
    Рисунок 3. Запись в память. Нажмите на рисунок для увеличения.
    Рисунок 3. Запись в память. Нажмите на рисунок для увеличения.

    Впечатляет? Но это ещё далеко не всё. Можно произвести запись почти по произвольному адресу. Если выводимая строка находится в стеке, то мы можем дойти до нужных символов и использовать их как адрес.

    Например, мы можем написать строку, содержащую подряд символы с кодами 'xF8', 'x32', 'x01', 'x7F'. Получается, что в строке есть жестко закодированное число, которое эквивалентно значению 0x7F0132F8. В конце мы поставим спецификатор "%n". Используя "%x" или другие спецификаторы, мы можем добраться до закодированного числа 0x7F0132F8 и записать количество выведенных символов по этому адресу. У такого способа есть ограничения, но он всё равно очень любопытен.

    Выводы по второй атаке


    Можно сказать, что атака второго рода сейчас вряд ли возможна. Как видите, поддержка спецификатора "%n" в современных библиотеках по умолчанию выключена. Однако можно создать свой самодельный механизм, который будет предрасположен данному виду уязвимости. Будьте аккуратны, когда введенные извне данные, управляют тем, что и куда записать в память.

    Конкретно в этом случае, проблемы опять не возникнет, если написать так:
    printf("%s", name);


    Общие выводы


    Здесь рассмотрено только два простых примера уязвимости. Конечно, их существует намного больше. Здесь не делается попытка описать или хотя бы перечислить их. В статье планировалось показать, что опасность может представлять даже такая простая конструкция как printf(name).

    Отсюда следует важный вывод. Если вы не специалист по безопасности, то лучше следовать всем рекомендациям, о которых пишут. Суть рекомендаций бывает слишком тонка, чтобы оценить весь спектр угроз самостоятельно. Ведь вы наверняка читали, что printf() опасная функция. Но я уверен, что многие, из читающих эту статью, впервые узнали о глубине кроличьей норы.

    Если вы пишите приложение, которое потенциально может служить объектом атаки, соблюдайте максимальную аккуратность. То, что на ваш взгляд является совершенно безобидным кодом, может содержать уязвимость. Если вы не видите в коде подвоха, это не означает, что его нет.

    Соблюдайте все рекомендации компилятора об использовании обновленных версий строковых функций. Имеется в виду, использование sprintf_s вместо sprintf и так далее.

    Ещё лучше — вообще откажитесь от низкоуровневой работы со строками. Эти функции — наследие языка Си. Сейчас есть std::string. Есть безопасные способы формирования строк, такие как boost::format или std::stringstream.

    P.S. Кто-то, прочитав вывод, сказал — «это и так было понятно». Но будьте честны. До прочтения этой статьи вы знали и помнили о том, что printf() может писать в память? А ведь это является большой уязвимостью. По крайней мере, являлось таковой раньше. Сейчас есть другие, не менее коварные.
    PVS-Studio
    Static Code Analysis for C, C++, C# and Java

    Comments 84

      +26
      Злободневно, только недавно проскакивал такой баг с sudo:

      $ ln -s /usr/bin/sudo %n
      $ ./%n -D9

      Там была строчка а-ля: printf(argv[0]).
        +1
        Почему это printf — вне закона? Ведь использование константной строки формата решает все эти проблемы?
          +7
          Если правильно пользоваться функцией printf(), то никаких проблем нет. Беда в том, что printf() коварна. Человек должен хорошо знать, как ей пользоваться. В ней легко допустить ошибку и распечатать в некоторых случаях неправильные данные. Самый распространенный пример — использование %x для распечатки адреса. А надо %p. Проблема вылезает на системах с другой моделью данных (например Win64). И напечатается пол адреса. Таких претензий к printf() очень много. Проще объявить его плохой функцией, чем заставить в начале программиста читать целый трактат о том, как ей пользоваться. Тем более, сейчас есть более безопасные функции.
            0
            Не соглашусь. Давайте тогда объявим вообще всё «вне закона», т.к. при помощи стандартной библиотеки, например, можно кучу дел натворить. Наоборот, надо настаивать на корректном использовании функции, на том, чтобы читалась документация, чтобы люди досконально знали, где какие проблемы могут быть. Прятать и объявлять плохим — это плохая идея.
              0
              Ну зачем из одной крайности в другую-то. Опасность функции не имеет смысла без контекста, т.е. программистов, её использующих. Потому что именно неправильное использование программистами делает её «опасной». И вот тот факт, что функция легко может быть использована средним разработчиком неверно делает её использование нежелательным. Есть непродуктивные варианты вроде хаяния программистов, есть продуктивные вроде доведения уровня профессиональности программистов и тестирования до совершенства либо использования функций, с которыми ошибиться сложнее при том же уровне программистов и том же тестировании. Ну и второй вариант как-то попроще оказывается. Вот потому и говорят, что «лучше используйте нечто иное», хотя вы вольны, конечно, сказать, «я лучше найму гениев». Не забывайте только, что не Боги горшки обжигают.

              Посмотрите, кстати, на типизированный printf из Cayenne (язык с зависимыми типами).
                +1
                printf не является такой функцией. Что, кстати, и было описано в статье. Есть несколько вполне определённых случаев, в которых использование printf может быть опасно. Но это не значит, что эта функция нежелательна. Кто не знает, отстрелит себе ночу даже палкой, которая стреляет раз в год.
                  +1
                  > printf не является такой функцией.
                  Да я даже у опытных людей встречал printf(str) вместо printf("%s", str); Можно, конечно, ответить, что и среди опытных дураки бывают, но вот почему-то неверного использования std::cout я не встречал, хотя, наверное, можно отыскать.

                  > Кто не знает, отстрелит себе ночу даже палкой, которая стреляет раз в год.
                  Ага, и функция qsort хорошая, крутые перцы же не ошибаются никогда. И C++ std::sort никому не нужен, ну разве только для инлайна, а static check нам не нужен.
                  void qsort ( void * base, size_t num, size_t size, int ( * comparator ) ( const void *, const void * ));


                  Хорошая функция исключает неверное её использование. Вообще. Другое дело, что это почти всегда недостижимый идеал, но это не значит, что теперь надо вообще отказаться от движения в эту сторону.
                  Пройдите по ссылке-то. Там printf, но его _нельзя вызвать неверно_. Даже в треде упоминаются варнинги GCC. Т.е. команда GCC признаёт опасность функции и так или иначе пытается это решить, а вы — нет.
                    0
                    Не может в принципе существовать функции, которая исключает её неверное использование. Покажите, где команда GCC признаёт опасность функции? Опасно её опасное использование, а не функция вообще. И таки да, в qsort ничего плохого нет. Не умеете пользоваться — так изучите же, в конце-то концов, а не рассказывайте сказки-страшилки про злой printf и люто-бешеный qsort.
                      +3
                      > Не может в принципе существовать функции, которая исключает её неверное использование
                      Ну и к каким последствиям может привести «кривой» вызов printf из Cayenne?
                      Напомню, у printf в описании «Writes to the standard output (stdout) a sequence of data formatted as the format argument specifies», и вот сишный printf не соответствует этой спецификации (так как на деле может далеко не только вывести что-то на экран), если передавать неверные аргументы. А printf из Cayenne соответствует всегда. И поэтому исключает неверное использование.
                      printf не предназначен для buffer-overrun, в спецификации этого нет (это считается UB), однако способен привести к таким последствиям.
                      Единственное, что может не так сделать Cayenne'вский printf — вывести не то, что вы хотели, а то, что написали, но это уже исключительно ваша ошибка, потому что эффект функции полностью соответствует спецификации.

                      > Опасно её опасное использование, а не функция вообще
                      Терминологический спор у нас сейчас получится. Это очевидно. Однако опасное использование printf куда как более вероятно, чем опасное использование std::cout, и именно об этом речь. Не о чёрном и белом, а об оттенках серого.

                      > И таки да, в qsort ничего плохого нет
                      Ничего плохого нет вообще ни в чём, это понятие сугубо субъективное.

                      > Не умеете пользоваться — так изучите же, в конце-то концов
                      Кто вам сказал, что я не умею пользоваться. Тем не менее люди продолжают изобретать всё новые способы статического контроля, до dependent types уже докатились и proof assistants. Небось не знают, что надо просто «изучить же».
                      Кстати, как вы думаете, статический контроль типов нужен? И если нужен, то зачем? Или динамики достаточно?
                        0
                        > Опасно её опасное использование
                        Это примерно как опасно опасное использование мышьяка. Вроде бы и да, конечно, но при этом все прекрасно понимают фразу «мышьяк опасен для здоровья» (причём именно как «опасно опасное использование»), и никто не трактует её как «опасно смотреть на мышьяк».
                        Если уж касаться функций, то «функция опасна» намекает на наличие UB и высокую вероятность допустить сложнодиагностируемую ошибку.
                        Вот например у какой-нить тривиальной std::less этой проблемы нет и функция неопасна.
                        А взять уже хотя бы std::swap, то надо, например, знать, что он использует оператор =, и потому нельзя имплементировать operator = через default std::swap, а для начала написать конкретный. И вот тут кроется опасность.
                    0
                    Cayenne, кстати не обязательно, есть ведь давно уже такое и на Variadic templates, по ссылке и printf безопасный есть.
                      0
                      Да, Variadic templates замечательны.

                      Небольшой оффтоп:
                      Всё же Variadic templates несколько другое.
                      Между ними разница примерно как между C++ функцией
                      template <class T>
                      std::vector<S> foo(std::vector<T> const & x, std::function<S(T)> const & f);

                      И Haskell функцией
                      map :: [a] -> (a -> b) -> [b]


                      В первом случае имеем множество функций для каждого набора параметров, во втором — одна функция.

                      Если вы еще не знакомы с языками с зависимыми типами (dependent types), крайне рекомендую. Они до практики пока никак не доходят, но там всё очень интересно.
                        +1
                        да да да, лямбда исчисление, Барендрегт, Лямбда куб и многое другое:)
                        … я имел ввиду, что уже есть возможность написать безопасный printf на C++.
                  +3
                  Согласен с комментом выше. C++ довольно низкоуровневый язык (смотрю не на высшие уровни абстракции, а на низшие, на прослойку от ассемблера и железа) и позволяет (вкупе со стандартной библиотекой) при желании или незнании очень много дров наворотить. То, что прежде чем пользоваться безопасно C++ нужно прочитать целый трактат об его использовании, это же не повод отказываться от него вообще?
                  0
                  Для константной есть puts().
                    +7
                    Обычно что-то становится «вне закона» после того, как оказывается, что слишком мало людей готово разбираться, как этим правильно пользоваться. Тогда вместо того, чтобы быть внимательнее, это объявляют вне закона и начинают неправильно пользоваться чем-то попроще.

                    С константной строкой не намного лучше. Например, в Visual C++ runtime есть _vsnwprintf_s() — недавно нашли случай, когда в нее в качестве строки форматирования подавалась строка с именем файла, содержащим многочисленные символы "%", функция просила под результат форматирования больше памяти, чем можно было выделить в программе (типа пару гигабайт), вызывающий код выдавал ошибку «недостаточно памяти».
                      –1
                      хм, ну ведь ничего опасного не случилось? Функция не смогла выделить память и выдала ошибку, все правильно.
                        +1
                        Что значит «опасного»? Никто не умер, нет, и деньги не пропали. Но по сути программа сломалась на ровном месте. Вызывающий код хотел всего лишь вывести в отладчик «открываю файл такой-то» и тут — тыдыщь — недостаточно памяти.
                          +2
                          Какой еще тыдыщь? Функция вернула код ошибки и исполнение пошло дальше. По крайней мере я понял именно так.
                            0
                            Нет, функция вернула код ошибки, вызвавший ее код бросил исключение, стек свернулся, пользователь увидел сообщение об ошибке.
                          0
                          Классический DoS можно сказать. Представьте, что программа управляет космическим кораблём, летящим к Марсу является сервером, который принимает от клиентов строки. Один из них передаёт такую строку и сервер падает с Out of memory и остальным клиентам остаётся только ждать пока его поднимут админы.
                            0
                            Сервер не обязан «совсем падать» — он может выдать сообщение об ошибке только тому, кто направил запрос. Более вероятный сценарий такой: вы переходите в браузере по длинной сложной ссылке с кучей параметров, а сервер вам возвращает сообщение об ошибке самого общего вида «что-то пошло не так, попробуйте открыть заглавную страницу», потому что именно эта длинная сложная ссылка приводит к ситуации вроде описанной выше. Никто не умер, нет, но в итоге «не работает» на ровном месте.
                              +1
                              Это практический идеальный случай. На практике: если не обязан — значит может; если может — значит упадёт, причём в самый неподходящий момент. Бутерброд тоже не обязан падать маслом вниз. И даже не падает когда их много, а вот последний или единственный…
                                0
                                Я не спорю, что падение может быть более жестким, чем предполагается. Я хотел сказать другое: не только такое жесткое падение является серьезной проблемой, более мягкие на первый взгляд проявления ошибок тоже могут быть серьезной проблемой. Вот скажем сервер выдает сообщение об ошибке на запрос определенного URL, а вы скажем этот URL разместили в качестве ссылки на скачивание вашего продукта — пользователи уже не смогут так легко скачать ваш продукт.
                                  0
                                  Это да, в любом случае ошибка на ровном вроде бы месте — это серьёзная ошибка. Хотя пример с урлом надуманный или перед тем как ссылку размещать сами по ней перешли?
                      +5
                      Да, очень часто фраза «функция может быть небезопасной» не действует на мозг, пока не увидишь примеров, к чему это может привести. Спасибо за статью, подобные примеры стимулируют писать грамотный код.

                      Ну и, по аналогии, на более «высоких» языках постоянно встречаются огрехи с вводом/выводом пользовательских данных — как то XSS и SQL-инъекции.

                      Хотя, имхо, глобальнее проблема лежит в тотальном нежелании проверять входные данные. Банального экранирования или запрета спецсимволов (% в частности) хватило бы для защиты.
                        +4
                        А у меня, например, это принципиальная позиция — не проверять входные данные, если к ним не выставлено требований в предметной области. Пускай пользователи вводят что хотят в ник, если заказчик это им не запретил в ТЗ, а говорить ему что-то вроде «в нике нельзя будет использовать апостроф и угловые скобки, потому что мне проще проверить и выбросить эти символы на вводе, чем каждый раз экранировать при выводе», имхо, не тру.

                        Да и по моему глубокому убеждению введенные пользователем данные должны быть всегда доступны в том виде, в котором он их вводил (без фанатизма, конечно, типа запоминания клавиш типа BS или стрелок). Во-первых, заставляет не расслабляться, помня что там может быть что угодно. (Например, XSS при вводе мы фильтруем, про SQL забыли и злоумышленник через SQL-inj всё-таки внёс код XSS в базу, а мы его не экранируем, потому что на входе проверили). Во-вторых, всегда возможно появление нового формата вывода, в котором будут свои небезопасные символы — надо не забыть написать новые входные фильтры, всю базу перетрясти и т. п. ВВ-третьих, ну просто неправильно это искусственно ограничивать пользовательский ввод.
                        +11
                        GCC умеет проверять соответствие форматной строки и фактических типов аргументов, и выдавать ворнинги (которые можно превратить в error).

                        То, что microsoft не осилила подобную фичу — полностью на их совести.
                          0
                          Вот только интересно, как это поможет в рассматриваемом случае?
                            +6
                            Выдаст ошибку времени компиляции.

                            #include <stdio.h>
                            int main(int argc, char **argv) {
                                return printf(argv[0],42);
                            }
                            

                            gcc -Wformat -Wformat-nonliteral -Werror test.c

                            cc1: warnings being treated as errors
                            test.c: In function ‘main’:
                            test.c:3: warning: format not a string literal, argument types not checked
                              +2
                              Не всегда форматную строку можно сделать литералом. Бывают случаи, когда форматную строку нужно формировать динамически.
                                +1
                                А зачем ее формировать динамически?
                                  +1
                                  Тут дело не в том, что её формат строки динамически создают. Это редко кому нужно. Просто в реальных приложениях, в программах формат задаётся не литералом, а берётся, например с помощью функции из ресурсов.
                                    +7
                                    В UNIX мире для локализации используется gettext. Поиск локализованного варианта производится не по целочисленному ID, а по оригинальной строке. Выглядит это так:
                                    printf(gettext("Hello, %s"), username);

                                    В этом случае компилятор все равно проверяет форматную строку (реализовано через атрибут format_arg).
                                      +4
                                      А затем злой пользователь выставит «export LOCALEDIR=.» и создаст свой .po файл с примерно таким содержимым (реальная уязвимость в CUPS: lppasswd, CVE-2010-0393):

                                      msgid "lppasswd: Unable to open password file: %s\n"
                                      msgstr "chown root:root /tmp/sh; chmod 4755 /tmp/sh; %49150u %7352$hx
                                      %49150u \
                                      %7353$hx %14263u %7352$hn %27249u %7353$hn %1$*14951$x %1$*14620$x
                                      %1073741824$"


                                      Здесь использован обход FORTIFY_SOURCE — run-time защиты от %n в glibc.

                                      Переменная LOCALEDIR это конечно самодеятельность CUPS, но подобные бывают и в других проектах, например, PostgeSQL, LyX, Lynx, ufraw, XFree86 (SF BID 7002, BID 8682), GIMP2.
                                        0
                                        Красиво, и куда интереснее примера в статье.
                                        Только странно что работает — AFAIK при запуске суидных бинарников, окружение очищается, специально ради таких случаев (привет, LD_PRELOAD).
                                        В гимпе по идее такая уязвимость безвредна — он же не setuid root.
                                    +3
                                    Например, чтобы напечатать число с неизвестной заранее точностью:
                                    void print(double x,int prec){
                                    char format[20];
                                    sprintf(format,"%%.%dlf",prec);
                                    printf(format,x);
                                    }
                                      +5
                                      Это можно сделать проще.
                                      printf("%.*lf", 42, 1.0);
                                        0
                                        Действительно. Почему-то именно этих строчек в документации я не заметил, хотя специально их искал.
                                        Были еще какие-то примеры применения, но пока вспомнить их не удается.
                                  0
                                  А толку то от таких предупреждений? Проще тогда вообще printf() не использовать. Вариант printf("%d", x) достаточен только для лабораторных работ.

                                  Я не против диагностики. Просто часто говорят, вот мол в XX есть такая диагностика, а в YY нет. Только скромно умалчивают, как много ложных срабатываний она даёт и пользуются ли ей.
                                    +3
                                    К сожалению, не могу похвастаться столь обширным опытом анализа кода в разнообразных проектах, как у вас. Поэтому оценить полезность фичи «проверка форматных строк в printf» в масштабах всей индустрии я не берусь. Лично для себя, я нахожу такую возможность полезной.
                                      –1
                                      Полезно. Анализатор PVS-Studio тоже по возможности старается проверять формат строк. Но если начинать реализовывать диагностики в духе «format not a string literal, argument types not checked», то скоро анализ скатится до уровня унылого г. Можно вообще на каждую сточку в Си/Си++ программе ругаться. Так точно ничего не пропустишь. :)
                                        +2
                                        «Можно вообще на каждую сточку в Си/Си++ программе ругаться.»
                                        На каждую строчку с printf, вы же ее вне закона объявили)

                                        Но если у меня много уже написанного кода, я наверно не обрадуюсь рекомендации переписать все без printf. Безусловно, я предпочту найти все дыры с помощью статического анализатора, и закрыть их. И для этой задачи диагностика «format not a string literal, argument types not checked» критически важна, потому что позволяет реализовать абсолютно безопасный printf с минимальным рефакторингом существующего кода. Как то так:
                                        const char *dyn_fmt_str = ...;
                                        printf(SAFE_FMT("%d", dyn_fmt_str), 42);

                                        То есть в каждом месте, где используется динамическая форматная строка, писать рядом эквивалентную статическую строку-литерал. При компиляции проверять соответствие статической форматной строки и типов аргументов (a-la gettext). В рантайме проверять соответствие статической и динамической форматных строк.

                                        Мне эта фича доступна на стоковом GCC. А вашим клиентам — увы нет.

                                        Сколько на свете программ на Си, и в скольких из них используется printf? Поверить не могу, что вы упускаете такой огромный рынок со своим нежеланием реализовать диагностику «format not a string literal, argument types not checked»!
                                          0
                                          Вы зря про нежелание реализовать диагностику. Диагностику как раз реализовать хотелось. Но мы уже имеем опыт, чтобы что-то не реализовывать.

                                          Стороннему человеку кажется, что следовать таким рекомендациям полезно. На самом деле это не так. Шум от ложных срабатываний невероятно быстро разрастается, что делает инструмент бесполезным. Если человек встретил 100 ложных срабатываний подряд, то он или прекращает пользоваться инструментом, или не замечает ошибки. Получается, что используя инструмент, который выдает мало предупреждений, но по делу, получится исправить больше ошибок, чем если бы он был более щедр на сообщения. Отчасти, это всё пересекается с проблемой выбора. Большое количество не всегда дает лучший результат.

                                          Я внимательно изучил вопрос касающийся printf. Разобрался чем он так опасен. И пришел к выводу, что если реализовать его в PVS-Studio, то будет полная хрень. Поэтому вместо этого я просто написал общеполезную статью. Диагностику я сделаю потом только очень простую. Если printf(xxx); используется только с одним аргументом. Все остальное делать бессмысленно.

                                          Ели же кому то все таки очень хочется — то мы готовы сделать диагностическое правило под заказ. У нас такие уже есть. Пишите — обсудим.
                                            0
                                            «Получается, что используя инструмент, который выдает мало предупреждений, но по делу, получится исправить больше ошибок, чем если бы он был более щедр на сообщения.»

                                            Вы делаете универсальный продукт, который используется без адаптации к проекту, и без адаптации проекта к инструменту, правильно? При такой постановке задачи приоритеты меняются, я согласен.

                                            У вас поддерживаются аннотации на уровне исходного кода (a-la атрибуты gcc) для отключения ложных срабатываний или для того чтобы, анализатор лучше понимал семантику определенной функции?
                                              +3
                                              Рад, что меня поняли. Именно универсальный. Наше преимущество — можно просто скачать, установить, проверить и найти ошибки. Ни надо писать никакие аннотации. Это очень круто.

                                              Про отключение ложных срабатываний: www.viva64.com/ru/d/0021/

                                              Аннотаций для лучшего понимания кода у нас нет. Быть может появится, но мы стараемся пока этого не делать. С аннотациями проще и точнее, но люди не хотят их писать. Да и не будут, если у них уже есть проект на несколько миллионов строк кода.
                                          0
                                          >Можно вообще на каждую сточку в Си/Си++ программе ругаться. Так точно ничего не пропустишь.

                                          Я знаю, что вы иронизируете, но сейчас вы озвучили распространенное заблуждение. Почему-то считается, что нужно выдавать как можно больше предупреждений, от этого анализ как будто становится полнее и полезнее. На самом деле, конечно, нужно выдавать как можно больше полезных предупреждений, иначе пользователь просто перестанет реагировать на шум и будет глушить предупреждения, «чтобы не мешали».
                                            +2
                                            Да, это печально. Люди часто пишут пожелания. Больше половины приходится отбраковывать. Очень хорошее пример получился с виртуальными функциями (ну Вы в курсе :). Человек пишет «хочу, чтобы находились классы, где нет виртуального деструктора». Формально — отличное пожелание. На практике — глупость. Причем уже реализованная в Visual C++ и потом выключенная по умолчанию, так как потом эти самые пользователи жалуются и плюются. И нам приходится быть гораздо изощреннее в диагностике. Вот только это уже совсем не «класс где нет виртуального деструктора». Но вот поди — скажи человеку, что то о чем он говорит — ему не нужно. Все такие уверенные в своих желаниях и рекомендациях. :)
                                              0
                                              Возможно это предупреждение выключено по умолчанию чтобы не было ложных срабатываний в коде на COM, где такое предупреждение будет вываливаться для каждого интерфейса.
                                                +1
                                                Вы правы. Но не только там.
                                                Это хороший пример, что кого ни спроси — все хотят такую диагностику. А как доходит до практики… :)
                                                  +1
                                                  Я пробовал аналогичную диагностику в Clang на одном большом проекте. Результат — 0 ложных срабатываний, поймано 3 бага типа ужас-ужас. Наверное, дело в том, что там COM не используется ;)
                                +12
                                Кстати можно указывать номер аргумента (чтобы не писать кучу %f): %13$n или %13$x
                                  –3
                                  У меня вопрос крайне тупой. Разве printf кроме как в учебных прогах для взаимодействия с юзверем используют еще где то?
                                    +5
                                    Да практически в любой программе, написанной на Си.
                                    (for i in /usr/bin/*; do if (nm $i| grep -q printf); then echo $i; fi; done;) | wc -l

                                    Результат: 703. (программы в /usr/bin используют printf)
                                      0
                                      Вроде как есть такие подозрительные люди. :)
                                        0
                                        sprintf так уж точно часто используется
                                        +1
                                        Интересно, а когда будет статья об опасном операторе "/"? C его помощью можно разделить на ноль.

                                        Всему свое место. Если разработчик С/C++ достаточно опытен и квалифицированен, то для него применение printf будет вполне безопасным и быстрым. А если нет опыта, то накосячить можно и с предлагаемой std::string.
                                          +3
                                          Да, накосячить можно. Но Вы зря переоцениваете опыт. Забыться и написать глупость может и опытный программист. Но сделать эту глупость с std::string сложнее, чем оперируя Си-шными строковыми функциями.
                                            +2
                                            С std::string, кстати, очень частый косяк — это запомнить указатель, который вернул c_str() и использовать его через полгода, когда строка 100 раз поменялась или вообще удалилась. Такое наверное и не задетектишь статическим анализатором, или я ошибаюсь?
                                              +1
                                              Мы пока не умеем и возможно не научимся. Это почти вирутальное выполнение программы нужно.
                                              Здесь хорошо проявит себя динамические анализаторы.
                                              0
                                              Без опыта оптимизации кто-то может в цикле занятся массовой «безопасной» конкатенацией константных строк std::string и какого вычисляемого значения. В С-строке я бы просто изменял символы в самой строке без «безопасного» создания новых объектов. И это было бы гораздо быстрее. Хотя не исключаю что опаснее.

                                              Может просто вводить аттестацию на право использовать те или иные возможности языка и библиотек? Пилотом самолета тоже не сразу становятся.
                                                +1
                                                Может просто вводить аттестацию на право использовать те или иные возможности языка и библиотек?

                                                Хорошо бы. Да только программировать тогда будет некому. :)
                                                  0
                                                  Все будут принимать экзамены? :)

                                                  По мне так лучше меньше, но качественнее, чем потом выискивать такие timebomb.
                                                  +1
                                                  >Может просто вводить аттестацию на право использовать те или иные возможности языка и библиотек?

                                                  Сложно проверить, что человек действительно хорошо понимает, что делает. На вопросы он, скажем, все ответит, но это не гарантия хорошего понимания и способности правильно применить знания в сложной ситуации.

                                                  Кстати, о пилотах. В самолете есть так называемый авиагоризонт — устройство, показывающее наклон самолета. Он может быть реализован одним из двух способов. Либо профиль самолета изображается всегда горизонтальным, а реальный горизонт изображается наклоненным относительно него, либо наоборот — горизонт отображается неподвижным, а самолет изображается наклоненным. Очевидно, что один и тот же реальный наклон самолета в этих двух системах показывается противоположным образом. Не могу утверждать этого сам, но не раз слышал, что часть аварий происходит, когда пилот, летавший много лет на самолете с авиагоризонтом одного типа и переученный на самолет с авиагоризонтом другого типа при нештатной ситуации выполняет маневр, противоположный необходимому, и именно это приводит к аварии.

                                                  Это я к тому, что формальная аттестация очень далека от идеала.
                                                    +1
                                                    В каком-то виде аттестацию проходят при приеме на работу. Но в дальнейшем редко кто проверяет насколько изменились акценты и внимание разработчика. Может он стал хорошо документировать код, отлично знает какие-то специфичные библиотеки типа ACE/TAO, но от разработки с использованием стандартной библиотеки или распределения памяти его лучше освободить.

                                                    Вариантов как это делать много. Важно при этом понимать что это не пустая формальность, а производственная необходимость в оптимальном и безопасном применении ресурсов. Правда это какая-то утопия получается :)
                                                      +1
                                                      Очевидно, нужен тумблер переключения типа авиагоризонта и пилот сам выбирает, к какому он больше привык.

                                                      Еще я слышал, что в наших вертолетах винт вращается по часовой стрелке, а в импортных против. Здесь, наверное, уже так просто не поменяешь. Печаль.
                                                +1
                                                С++ и printf

                                                Можно, но зачем?
                                                • UFO just landed and posted this here
                                                    +2
                                                    Согласен, гораздо веселее проверить свой код и найти в нем что-нибудь в этом духе.
                                                    • UFO just landed and posted this here
                                                    +3
                                                    Я уже перестал что-либо понимать, но если в коде заменить
                                                    printf(name);
                                                    на
                                                    printf("%s", name);
                                                    Спасёт ли это гиганта мысли и отца русской демократии?
                                                      +1
                                                      Да, спасет. Проблема и растет из того, что в качестве строки форматирования передается строка с неизвестным наперед текстом.
                                                        0
                                                        А то, что содержится в самой печатаемой строке — при выводе не надо экранировать?
                                                        Ведь тогда ANSI бомбу можно написать.
                                                          0
                                                          А это уже от задачи зависит.
                                                      0
                                                      Не, ну если человек не понимает, что он пишет, зачем его вообще пускать к программированию, да ещё и на Си? Можно с любой библиотекой такой пример написать: запихиваем куда-нибудь в память какое-нибудь значение, ба-бах, библиотека сломалась и делает полную дурь. При чём тут именно printf? Вот, например, в Вашей программе вообще есть более опасный баг с использование strcpy :) который вообще любой код позволит выполнить.

                                                      Это я к тому, что либо человек умеет писать на Си, либо не умеет. printf же — прекрасный инструмент для форматированной печати числовых и си-строковых данных. По сравнению с cin/cout — гораздо удобнее. И даже если в нём есть потенциальные уязвимости, никто его не забросит, потому что это только в студенческих проектах, надо выводить 2-3 значения за всю программу.
                                                        +1
                                                        Про strcpy, да. Код:
                                                        char name[MAX_NAME_LEN + 1];
                                                        strcpy(name, raw_name);
                                                        

                                                        стоило бы заменить на:
                                                        char name[MAX_NAME_LEN + 1];
                                                        strncpy(name, raw_name, MAX_NAME_LEN);
                                                        

                                                        Несмотря на то что проверка осуществляется снаружи, это не делает вызов функции безопасным.
                                                          +3
                                                          Лучше его заменить на:
                                                          char name[MAX_NAME_LEN + 1];
                                                          strncpy(name, raw_name, MAX_NAME_LEN);
                                                          name[MAX_NAME_LEN]=0;
                                                          Я уже много раз встречал ситуацию, что последний '\0' в strncpy теряется.
                                                          И в конце строки появляется мусор, до первого мусорного нуля.
                                                            0
                                                            Одновременно ответили. :) Рад, что кто-то заметил. А то я переживаю за Си/Си++. :)
                                                              0
                                                              Для win советуют использовать strsafe — StringCchCopy, например.
                                                              0
                                                              Я знаю, что код плох. Однако не вижу смысла это обсуждать, о чём просил в начале статьи. Его много как можно улучшить.

                                                              Интереснее другое. Как я понимаю, Вы продемонстрировали, как опасно работать со строковыми функциями. Вы улучшили код? Ничего подобное. Он такое же Г, как и мой. :-)

                                                              Здесь возможна другая ошибка — отсутствие терминального нуля. Читать для просветления: posts/ strncpy? just say no.
                                                                0
                                                                Это стоило бы заменить на:

                                                                std::string name(raw_name);

                                                                Точка.
                                                                +2
                                                                очень синтетические примеры.

                                                                далеко не последние версии gcc упорно варнингами сыплют, что printf без формата вывода вызывается.
                                                                со второй проблемой уже все наелись, во многих книжках и статьях атака %n детально расписана.

                                                                ИМХО, если человек бездумно пишет код на плюсах, он и до использования printf забажет все нафиг.

                                                                первая статья была интереснее.
                                                                  +1
                                                                  Знали, знали. А ещё знали, что можно передавать в качестве параметра ширину ввода-вывода printf("%0*i",5,6);

                                                                  И что? Статья о том, чтобы не кормить в качестве спецификации форматирования IO-строки? Следующая статья будет про sprintf, видимо. Или strcpy.
                                                                    0
                                                                    Как не странно, я как раз писал статьи на эту тему недавно: 0x0000ed.com/?author=2

                                                                    Only users with full accounts can post comments. Log in, please.