Format-string уязвимость в старом программном обеспечении опасна не тем, что звучит сложно, а тем, что её часто оставляют «на потом». Особенно если сервис давно работает, исходники потеряны, а обновляться страшно из-за совместимости. На практике такие ошибки встречаются в старом C/C++ коде, сетевых демонах, CLI-утилитах, логировании и встроенных устройствах. Их нужно закрывать системно: найти опасные вызовы, поправить формат, проверить сборку и снизить ущерб там, где быстрый патч невозможен.
Если сервис доступен из интернета, принимает данные от клиентов или уже есть сообщение об уязвимости в вашей версии, сначала ограничьте доступ к нему. Не начинайте с долгого аудита на боевой машине. Изоляция, VPN, firewall-правила или отключение публичного интерфейса могут дать несколько дней или недель на нормальный ремонт.
- Почему старые версии чаще становятся проблемой
- Где чаще всего искать уязвимый код
- Порядок работ, если нужно закрыть уязвимость без лишней паники
- Какой вариант защиты выбрать
- Если исходники есть: не обновляйте всё сразу без необходимости
- Если исходников нет или версия закрытая
- Экранирование процентов — временная мера, не лечение
- Сборка старого ПО с защитой от последствий
Почему старые версии чаще становятся проблемой
Сама по себе старая версия не гарантирует наличие format-string уязвимости. Проблема в том, что в старом ПО обычно совпадает сразу несколько рисков:
- код писали до массового использования строгих предупреждений компилятора;
- в логах и ошибках часто напрямую печатали пользовательские строки;
- проект собран без PIE, RELRO, FORTIFY_SOURCE и других современных защит;
- вендор уже не выпускает обновления или обновляет только новые ветки;
- команда боится менять бинарник, потому что система годами работает «как есть».
Format-string ошибка появляется, когда строка формата берётся из ненадёжного источника. Например, пользовательский ввод, HTTP-запрос, аргумент командной строки, имя файла или строка из сети передаётся в функцию, которая ожидает шаблон формата.
Где чаще всего искать уязвимый код
В первую очередь смотрите не на «страшные» места, а на обычное логирование и вывод ошибок. Именно там разработчики часто писали так:
syslog(LOG_INFO, user_input);
printf(error_message);
fprintf(stderr, request_line);
Правильно было бы явно сказать, что пользовательская строка — это данные, а не формат:
syslog(LOG_INFO, "%s", user_input);
printf("%s", error_message);
fprintf(stderr, "%s", request_line);
Опасные функции, которые стоит проверить в старом коде:
printf(),fprintf(),sprintf(),snprintf();syslog()и обёртки над ним;err(),warn(),setproctitle()и похожие variadic-функции;- самописные макросы логирования;
- старые библиотеки, которые печатают строки, полученные из сети или из конфигурации.
Главный признак проблемы: первый аргумент формата не является строковым литералом или приходит извне. Особенно подозрительны вызовы вида printf(buf), syslog(priority, msg), snprintf(out, len, user_format).
Порядок работ, если нужно закрыть уязвимость без лишней паники
- Соберите список старых компонентов. Запишите версии, где они запускаются, кто их обслуживает, есть ли исходники, можно ли собрать заново и кто зависит от этого сервиса.
- Определите поверхность атаки. Интернет-доступ, локальная сеть, CLI для обычных пользователей, логирование HTTP-запросов, SNMP, telnet, старые API — всё это влияет на срочность.
- Найдите подозрительные вызовы. Начните с поиска
printf,fprintf,syslog,sprintf,snprintfи самописных логеров. Поиск не заменяет аудит, но быстро даёт список кандидатов. - Проверьте, кто управляет форматом. Если формат задаётся константой в коде — обычно нормально. Если приходит из аргументов, файла, сети, переменной окружения или пользовательского ввода — это риск.
- Исправьте корень проблемы. Замените вызовы так, чтобы пользовательская строка шла через
"%s", а не становилась форматом. - Соберите с предупреждениями компилятора. Включите проверки формата и по возможности доведите их до ошибок сборки.
- Проверьте на тестовом стенде. Старое ПО часто ломается не от патча, а от новой сборки, новых флагов или другой версии libc.
- Добавьте компенсационные меры. Если полный ремонт невозможен сразу, ограничьте доступ, запускайте сервис с минимальными правами и мониторьте падения.
Какой вариант защиты выбрать
| Ситуация | Что делать | Что это даёт | Ограничения |
|---|---|---|---|
| Есть исходники и известна уязвимая функция | Исправить вызовы формата и пересобрать | Самый чистый вариант: убирается причина ошибки | Нужно тестирование и аккуратный релиз |
| Есть исходники, но нельзя обновлять всю версию | Перенести минимальный патч в старую ветку | Меньше риска, чем полный апгрейд | Нужно следить, чтобы патч не потерялся при следующей сборке |
| Исходников нет, но есть вендорская версия | Запросить исправленную сборку или firmware | Ответственность за совместимость остаётся у поставщика | Если вендор не поддерживает версию, придётся изолировать или менять продукт |
| Сервис доступен извне, а патча пока нет | Ограничить доступ, закрыть публичный порт, включить VPN или ACL | Быстро снижает шанс эксплуатации | Не устраняет уязвимость внутри программы |
| Код старый, но сервис внутренний | Запланировать патч, включить аудит и hardened-сборку | Позволяет закрыть риск без аварийной остановки | Внутренний доступ тоже может быть опасен при компрометации сети |
| Патч невозможен, а замена займёт месяцы | Изолировать, запускать без root-прав, мониторить падения, готовить замену | Снижает ущерб и даёт время | Это временная мера, а не нормальное состояние |
Если исходники есть: не обновляйте всё сразу без необходимости
В старом ПО полный апгрейд часто опаснее точечного патча. Если старая версия работает на специфичной libc, старом ядре или с нестандартной конфигурацией, перенос на новую ветку может сломать больше, чем исправить.
Нормальный путь выглядит так:
- найти конкретный опасный вызов;
- понять, откуда берётся строка формата;
- заменить её на литерал формата;
- добавить тест с процентами во входных данных;
- собрать с предупреждениями;
- проверить на стенде, максимально похожем на production.
Например, если старый демон пишет в лог путь из HTTP-запроса, опасный вариант:
syslog(LOG_INFO, http_request_path);
Исправление простое, но его часто забывают сделать во всех местах:
syslog(LOG_INFO, "%s", http_request_path);
Для поиска кандидатов можно начать с команд вроде:
grep -RInE 'printf|fprintf|sprintf|snprintf|syslog' src/
После этого не нужно верить поиску на слово. grep только показывает места, где стоит посмотреть. Дальше нужен ручной разбор: что именно передаётся как формат, можно ли это контролировать извне, есть ли обёртки, макросы и старые библиотеки.
Если исходников нет или версия закрытая
С закрытым старым ПО задача становится не «починить код», а «снизить риск до приемлемого уровня и подготовить замену». Здесь лучше работать по слоям:
- узнать, есть ли у вендора исправленная версия или advisory;
- если есть — обновить хотя бы компонент, а не всю систему;
- если нет — закрыть внешний доступ к уязвимому интерфейсу;
- разрешить подключение только с доверенных адресов;
- запустить сервис с минимальными правами;
- отключить неиспользуемые функции;
- настроить мониторинг падений, core dump и необычных перезапусков;
- заложить замену продукта в план модернизации.
Бинарный патч «на коленке» имеет смысл только если у вас есть специалисты, тестовый стенд и понимание ABI. Иначе можно получить ситуацию, когда format-string закрыли, но сервис начал падать от изменения памяти, версий библиотек или поведения libc.
Экранирование процентов — временная мера, не лечение
Иногда команда быстро меняет входные данные так, чтобы проценты превращались в безопасную строку. Это может помочь как аварийный костыль, но только если сделано везде и одинаково. Если хотя бы один путь обходит фильтр, риск остаётся.
Лучше не превращать пользовательскую строку в безопасный формат, а вообще не передавать её как формат. То есть не «чистить %», а писать "%s" и передавать строку аргументом.
Если другой дороги нет, экранируйте на границе входа и сразу ставьте задачу на нормальный патч. Такой обход нельзя считать закрытием уязвимости.
Сборка старого ПО с защитой от последствий
Hardened-сборка не исправляет format-string ошибку. Она снижает шанс, что атака приведёт к полноценному зах
