Дэвид Лебланк - 19 смертных грехов, угрожающих безопасности программ
Целый класс ошибок касается взаимодействий с файлами и другими объектами. Способов попасть в беду так много, что даже не перечесть. Вот лишь несколько примеров. Ваша программа должна создать временный файл, поэтому она сначала проверяет, нет ли уже файла с тем же именем, и если нет, то создает его.
Обычное дело, не правда ли? Так–то оно так, но этой сценарий открывает возможность для атаки – противник угадывает имя временного файла и после запуска вашей программы создает ссылку на что–нибудь важное. Вашей программе не повезло, она открывает ссылку, которая ведет на файл, выбранный противником, после чего некоторые ее действия могут привести к эскалации привилегий. Если вы удалите файл, противник сможет подставить вместо него вредоносную программу. Если вы затрете файл, то в дальнейшем это может стать причиной сбоя в какой–то другой программе. Если файл доступен непривилегированным процессам, то вы можете изменить его права и предоставить противнику право записи в какой–то конфиденциальный файл. При наихудшем развитии событий ваша программа может установить для файла режим запуска от имени пользователя root (setuid root), и в результате приложение, выбранное противником, получит административные привилегии.
А, вы работаете на платформе Windows и довольно ухмыляетесь, полагая, что все это к вам не относится? Заблуждение! Вот что может произойти в Windows. При запуске любого сервиса создается именованный канал, по которому диспетчер сервисов (Service Control Manager) посылает сервису управляющие команды. Диспетчер работает от имени System – самой привилегированной учетной записи в системе. Противник определяет, как называется канал, находит сервис, запускаемый от имени обычного пользователя (по умолчанию таких несколько), а затем присоединяется к каналу, притворившись диспетчером сервисов. Для устранения ошибки были предприняты следующие меры: во–первых, имя канала сделано непредсказуемым, что заметно уменьшает шансы противника, а во–вторых, в Windows Server 2003 олицетворение другого пользователя стало привилегией. Если вы думаете, что Windows не поддерживает ссылки, то ошибаетесь; поищите в документации раздел CreateHardLink. Но ссылки на файлы – не единственная возможность. В Windows есть множество других именованных объектов: файлы, каналы, мьютексы, участки разделяемой памяти, рабочие столы и т. д. Любой может стать источником проблем, если программа не ожидает, что он вообще существует.
Греховность кода
Хотя для иллюстрации мы выбрали С, подобный код можно написать на любом языке, поскольку языковой специфики в нем почти нет. Ошибка заложена уже в проекте и связана с непониманием нюансов операционной системы и способа обойти их. Нам неизвестны языки, которые бы существенно затрудняли образование условий для возникновения гонки. Взгляните на этот пример:
...char* temp;
FILE* pTempFile;
tmp = _tempnam("/tmp", "MyApp");
pTempFile = fopen(tmp, "w+");
Выглядит совершенно безобидно, но противник может предсказать, каким будет следующее имя временного файла. При прогоне на машине автора повторные вызовы порождали файлы с именами MyAppl, МуАрр2, МуАррЗ и т. д. Если эти файлы создаются в области, куда противнику разрешено писать, то он сумеет заранее создать временный файл, возможно, заменив его ссылкой. Если программа создает несколько временных файлов, то задача противника заметно упрощается.
Родственные грехи
Мы рассмотрели несколько взаимосвязанных проблем. Основной грех заключается в неумении писать код, корректно работающий в условиях одновременного исполнения. С ним также связаны ошибки контроля доступа, описанные в грехе 12, и генерирование недостаточно случайных чисел (см. грех 18). Почти все гонки при работе с временными файлами возникают вследствие ошибок установки прав доступа, усугубленных использованием старых версий операционных систем, в которых нет надлежащим образом защищенных каталогов для хранения временных файлов, создаваемых конкретным пользователем. В большинстве современных операционных систем каждому пользователю выделяется собственное рабочее пространство, а даже если это не так, всегда можно создать такую область внутри начального каталога пользователя.
Недостаточная «случайность» случайных чисел проявляется в ситуации, когда вы хотите создать файл, каталог или другой объект с уникальным именем в общедоступной области. Если применяется генератор псевдослучайных чисел или – того хуже – увеличение некоторого счетчика на единицу, то противник часто может предсказать, каким будет следующее имя, а это первый шаг на пути к хаосу. Отметим, что многие библиотечные функции для создания временных файлов гарантируют лишь уникальность имени, но не его непредсказуемость. Если вы создаете файлы или каталоги в общедоступной области, то для порождения имен применяйте генератор случайных чисел криптографического качества. Один такой способ описан в главе 23 книги Майкл Ховард, Дэвид Лебланк «Защищенный код», 2–ое издание (Русская редакция, 2004). Хотя он предназначен для Windows, но сам подход является переносимым.
Где искать ошибку
Гонки чаще всего возникают при следующих условиях:
□ Несколько потоков или процессов должны осуществлять запись в один и тот же ресурс. Ресурсом может быть разделяемая память, файловая система (например, когда несколько Web–приложений манипулируют файлами в общем каталоге), другие хранилища данных, как, скажем, реестр Windows, и даже база данных. Это может быть даже одна разделяемая переменная!
□ Файлы или каталоги создаются в общедоступных областях, например в каталоге для временных файлов (/tmp или /usr/tmp в UNIX–подобных системах).
□ В обработчиках сигналов.
□ В нереентерабельных функциях в многопоточном приложении или вызываемых из обработчика сигнала. Отметим, что в системах Windows сигналы практически бесполезны и этой проблеме не подвержены.
Выявление ошибки на этапе анализа кода
Внимательно присмотритесь как к собственным, так и к библиотечным функциям, которыми вы пользуетесь. Нереентерабельным является код, манипулирующий переменными, объявленными вне локальной области видимости, например глобальными или статическими. Любая функция, в которой используется статическая переменная, реентерабельной не является. Хотя применение глобальных переменных вообще осуждается, поскольку усложняет сопровождение программы, но сами по себе они еще не создают гонок. Следующее, на что надо обратить внимание, – это бесконтрольное изменение таких переменных. Например, статический член класса в С++ разделяется всеми экземплярами класса и, следовательно, оказывается глобальной переменной. Если этот член инициализируется в момент первого обращения к классу, а затем только читается, то ничего страшного не случится. Если же некоторая переменная обновляется, то необходимо ставить замки, чтобы обновление не осуществлялось одновременно разными частями программы. Особое внимание следует обращать на обработчики сигналов, поскольку они могут оказаться нереентерабельными, даже если для остальной программы эта проблема не актуальна.
Внимательно изучите код обработчиков сигналов, в частности те данные, которыми они манипулируют.
Следующий случай – это внешние процессы, которые могут прерывать вашу программу. Тщательно изучите места, где файлы или каталоги создаются в общедоступных областях, обращайте внимание на предсказуемость имен.
Найдите все случаи создания файлов (к примеру, временных) в разделяемых каталогах (/tmp или /usr/tmp в UNIX, Windowstemp в Windows). При создании такого файла библиотечной функцией ореп() надо указывать флаг 0_EXCL (или его эквивалент), а если он создается функцией CreateFile() -флаг CREATE_NEW. В этом случае вызов завершится неудачно, если файл с указанным именем уже существует. Поместите обращения к этим функциям в цикл, который будет пробовать новые случайные имена, пока не создаст файл. Если имя выбирается действительно случайно (следите, чтобы имя файла содержало только допустимые в вашей системе символы), то почти наверняка потребуется только одна итерация. К сожалению, функция fopen() из стандартной библиотеки С не позволяет указать флаг 0_EXCL, поэтому применяйте функцию ореп() с последующим преобразованием возвращенного дескриптора в указатель FILE*. В операционных системах Microsoft системные вызовы типа CreateFile не только более гибки, но и работают быстрее. Никогда не полагайтесь на функции, подобные mktemp(3), поскольку они создают предсказуемые имена файлов; противник может создать файл точно с таким же именем. В командных интерпретаторах UNIX нет встроенных операций для создания имен временных файлов, а конструкции типа Is > / tmp/ list. $ $ способствуют возникновению гонки. Поэтому в shell–сценариях нужно пользоваться функцией mktemp(l).
Тестирование
Обнаружить гонку во время тестирования трудно, но существуют методики по искоренению этого греха. Одна из самых простых – прогонять тесты на быстрой многопроцессорной машине. Если вы наблюдаете отказы, которые не удается воспроизвести на однопроцессорной машине, значит, дело почти наверняка в гонке.