Дэвид Лебланк - 19 смертных грехов, угрожающих безопасности программ
Обнаружить гонку во время тестирования трудно, но существуют методики по искоренению этого греха. Одна из самых простых – прогонять тесты на быстрой многопроцессорной машине. Если вы наблюдаете отказы, которые не удается воспроизвести на однопроцессорной машине, значит, дело почти наверняка в гонке.
Для поиска ошибок, связанных с сигналами, напишите программу, которая будет один за другим посылать сигналы тестируемому приложению, и понаблюдайте за поведением последнего. Отметим, что одного прогона теста может оказаться недостаточно, так как ошибка возникает нерегулярно.
Чтобы найти гонки, возникающие из–за временных файлов, включите протоколирование операций с файловой системой или воспользуйтесь утилитами для протоколирования системных вызовов. Внимательно изучите все случаи создания файлов, посмотрите, не создаются ли файлы с предсказуемыми именами в общедоступных каталогах. Если возможно, включите в протокол информацию об использовании флага 0_EXCL, чтобы узнать, задается ли он при создании файлов в разделяемых каталогах. Особый интерес представляют ситуации, когда файл первоначально создается с неправильными правами, которые затем корректируются. Промежутка времени между двумя этими действиями достаточно для создания эксплойта. Аналогично подозрение должно вызывать любое понижение привилегий, необходимых для доступа к файлу. Если противник сумеет заставить программу работать со ссылкой вместо настоящего файла, то сможет получить доступ к данным, которых он видеть не должен.
Примеры из реальной жизни
Следующие примеры взяты из базы данных CVE (http://cve.mitre.org).
CVE–2001–1349
Цитата из бюллетеня CVE:
В программе sendmail до версии 8.11.4, а также версии 8.12.0 до 8.12.0.BetalO имеется гонка в обработчике сигнала, которая позволяет локальному пользователю провести DoS–атаку, возможно, затереть содержимое кучи и получить дополнительные привилегии.
Эта ошибка описана в статье Залевски о доставке сигналов, на которую мы уже ссылались выше. Ошибка, допускающая написание эксплойта, возникает из–за двойного освобождения памяти, на которую указывает глобальная переменная. Это происходит при повторном входе в обработчик сигнала. Хотя ни в документации по Sendmail, ни в базе уязвимостей SecurityFocus не приводится код общедоступного эксплойта, но в первоначальной статье такая ссылка (сейчас не работающая) была.
CAN–2003–1073
Цитата из бюллетеня CVE:В программе at, поставляемой в составе ОС Solaris версий с 2.6 по 9, может возникнуть гонка, позволяющая локальному пользователю удалить произвольный файл путем задания флага–г с аргументом, содержащим две точки (..) в имени задания. Для этого нужно изменить содержимое каталога после того, как программа проверит разрешение на удаление файла, но до выполнения самой операции удаления.
Этот эксплойт детально описан на странице www.securityfocus.com/archive/ 1 /308577/2003–01–27/2003–02–02/0. Помимо гонки, в нем используется еще одна ошибка: не проверяется наличие последовательности символов../ в имени файла, из–за чего планировщик at может удалить файлы, находящиеся вне каталога для хранения заданий.
CVE–2004–0849
Цитата из бюллетеня CVE:Гонка в сервере Microsoft Windows Media позволяет удаленному противнику вызвать отказ от обслуживания в сервисе Windows Media Unicast Service путем отправки специального запроса.
Подробные сведения об этой уязвимости можно найти на странице www.microsoft.com/technet/security/Bulletin/MS00–064.mspx. «Специальный» запрос переводит сервер в состояние, когда все последующие запросы не обрабатываются вплоть до перезагрузки сервиса.
Искупление греха
Прежде всего нужно разобраться в том, как правильно писать реентерабельный код. Даже если вы не намереваетесь запускать программу в многопоточной среде, могут найтись люди, которые захотят перенести ваше приложение на другую платформу и повысить его производительность за счет организации нескольких потоков. Они оценят ваше стремление избегать побочных эффектов. Говоря о переносимости, следует отметить, что в Windows не реализован системный вызов fork() и создание нового процесса обходится очень дорого, зато создание потока не влечет почти никаких накладных расходов.
Решение о том, чем пользоваться – процессами или потоками, зависит от операционной системы и специфики приложения, но в любом случае код, не имеющий побочных эффектов, будет более переносим, и возникновение гонок в нем маловероятно.
Если в программе есть параллельные контексты исполнения, будь то процессы или потоки, то доступ к разделяемым ресурсам необходимо тщательно синхронизировать. Эта тема подробно рассматривается в других книгах, мы же лишь слегка затронем ее. Вот на что нужно обращать внимание:
□ если программа возбуждает исключение, не сняв замка, возможна тупиковая ситуация в другой части программы, которая ждет освобождения этого замка. Один из способов решения этой проблемы – инкапсулировать захват и освобождение замка в объект С++, тогда в ходе раскрутки стека будет вызван деструктор объекта, который и освободит замок. Отметим, что при этом захваченный ресурс может остаться в неопределенном состоянии; в некоторых случаях лучше допустить тупиковую ситуацию, чем продолжать работу с таким ресурсом;
□ при необходимости захватить несколько замков делайте это всегда в одном и том же порядке, а освобождайте их строго в обратном порядке. Если вам кажется, что для выполнения некоторой операции нужно захватить несколько замков, обдумайте ситуацию еще раз. Возможно, найдется более элегантное и не столь сложное проектное решение;
□ старайтесь освободить замок как можно скорее. В противоречие с предыдущим параграфом отметим, что иногда наличие нескольких замков позволяет уменьшить величину захваченного ресурса, за счет чего снижается вероятность тупиковой ситуации и значительно повышается производительность программы. Это скорее искусство, чем наука. Проектируйте тщательно и советуйтесь с коллегами;
□ никогда не рассчитывайте на то, что другой процесс или поток не может прервать системный вызов. Для выполнения системного вызова может потребоваться от нескольких тысяч до нескольких миллионов машинных команд. Раз нельзя ожидать, что даже один системный вызов отработает без прерывания, не смейте и думать о том, что между двумя системными вызовами исполнение программы не будет прервано.
В обработчике сигнала или исключения единственно безопасная вещь – это вызов exit(). Наилучшие рекомендации по этому вопросу мы встречали в статье Михала Залевски «Delivering Signals for Fun and Profit: Understanding, Exploiting and Preventing Signal Handling Related Vulnerabilities*-:
□ используйте в обработчиках сигналов только реентерабельные библиотечные функции. Для этого многие известные программы придется значительно переработать. Есть, правда, половинчатое решение – написать для каждой небезопасной библиотечной функции обертывающий код, который будет проверять некоторый глобальный флаг во избежание повторного входа;
□ блокируйте доставку сигналов на время выполнения неатомарных операций и проектируйте обработчики сигналов так, чтобы они не зависели от внутреннего состояния программы (например, обработчик может безусловно поднять некоторый флаг и этим ограничиться);
□ блокируйте доставку сигналов на время нахождения в обработчике сигнала.
Для решения проблемы «момент проверки / момент использования» (TOCTOU) лучше всего создавать файлы в таком месте, куда обычные пользователи не имеют права ничего записывать. В случае каталогов такое не всегда возможно. При программировании на платформе Windows не забывайте, что с файлом (как и с любым другим объектом) можно связать дескриптор безопасности в момент создания. Задание прав доступа в момент создания объекта устраняет возможность гонки между моментами создания и определения прав доступа. Чтобы избегнуть гонки между моментом проверки существования и объекта и моментом создания нового объекта, у вас есть несколько вариантов, зависящих от типа объекта. В случае файлов самое правильное – задать флаг CREATE_NEW при вызове функции CreateFile. Тогда если файл существует, то функция завершится с ошибкой. Создание каталогов еще проще: любое обращение к функции Сгеа–teDirectory завершается с ошибкой, если каталог с указанным именем существует. Но проблема все равно может возникнуть. Предположим, что вы хотите поместить свое приложение в каталог FilesMyApp, но противник уже создал такой каталог заранее. Теперь у него есть полный доступ к этому каталогу, в том числе и право удалять из него файлы, даже если для самого файла разрешение на удаление отсутствует. Вызовы API, предназначенные для создания объектов некоторых типов, не предусматривают различий между операциями «создавать новый» и «открывать всегда». Такой вызов завершится успешно, но GetLastError вернет код ERROR_ALREADY_EXISTS. Корректный способ обработки ситуации, когда вы не хотите открывать существующий объект, таков: