Текст книги "19 смертных грехов, угрожающих безопасности программ"
Автор книги: Майкл Ховард
Жанр: Зарубежная компьютерная литература, Зарубежная литература
сообщить о неприемлемом содержимом
Текущая страница: 8 (всего у книги 26 страниц) [доступный отрывок для чтения: 9 страниц]
Служба монтирования файловых систем в ОС IRIX позволяет смонтировать систему дистанционно, пользуясь вызовами RPC. Обычно она по умолчанию включена. Оказалось, что вплоть до обнаружения ошибки в 2002 году многие проверки файла, которые сервер выполнял при получении запроса, были реализованы с помощью системного вызова рореп(). Передаваемая ему информация поступала непосредственно от пользователя, поэтому поставленная в нужном месте точка с запятой позволяла противнику выполнять команды от имени root.
Искупление греха
Очевидное решение – никогда не запускать никаких интерпретаторов команд. Но это не всегда практично, особенно если речь идет о работе с базой данных. Можно было бы сказать иначе: если вы все–таки обращаетесь к оболочке, не передавайте ей данные, поступившие извне. Но и этот совет столь же непрактичен.
Единственная разумная рекомендация – проверять входные данные. Путь к искуплению греха часто состоит всего из двух шагов:
1) проверьте данные и убедитесь, что они корректны;
2) предпримите необходимые действия, если данные некорректны.
Контроль данныхНа самом верхнем уровне у вас есть две возможности. Можно проверять все, что передается внешнему процессу, или только те данные, которые поступают из источника, не заслуживающего доверия. Оба варианта приемлемы, если вы производите проверку тщательно.
Обычно внешние данные имеет смысл проверять непосредственно перед использованием. На то есть две причины. Во–первых, таким образом гарантируется, что данные будут проверены на любом пути, ведущем к их использованию. Во–вторых, смысл данных часто проще всего понять непосредственно перед использованием. А, понимая смысл, вы сможете наилучшим образом выполнить проверку. Кроме того, такой подход позволяет защититься от непреднамеренной порчи данных после начальной проверки.
Но наилучшей является стратегия глубоко эшелонированной обороны. Разумно проверять данные и в момент поступления, чтобы избежать риска использовать непроверенные данные где–то в другом месте, особенно в ситуации, когда таких мест много.
Есть три основных способа обеспечить корректность данных:
□ подход «все кроме». Вы ищете свидетельства того, что данные некорректны, а если не находите, то принимаете их;
□ подход «только такие». Вы сравниваете данные с заведомо корректными, а все остальные отвергаете (даже если есть шанс, что ничего страшного не произойдет);
□ «закавычивание». Вы преобразуете данные таким образом, чтобы избежать всякого риска.
Всем им свойствен общий недостаток: вы можете что–то проглядеть. В случае подхода «все кроме» и «закавычивания» это, очевидно, может иметь плачевные последствия для безопасности. На самом деле, отвергая все данные, кроме тех, что кажутся корректными, вы, скорее всего, получите небезопасную программу, так как перечень символов, которые могут иметь специальный смысл, довольно велик. В некоторых системах почти все символы, кроме букв и цифр, могут оказаться специальными. «Закавычивание» часто гораздо труднее реализовать, чем кажется на первый взгляд. Например, при попытке сделать это для некоторых командных процессоров чаще всего строку с входными данными просто заключают в двойные кавычки. Но если вы не проявите осторожность, противник сможет сам включить в строку кавычки. Кроме того, для некоторых процессоров (например, командных оболочек в UNIX) есть метасимволы, которые интерпретируются даже внутри кавычек.
Чтобы убедиться в том, насколько сложна эта задача, попробуйте сами записать все метасимволы, имеющие специальный смысл для UNIX. Включите все, что может интерпретироваться как управляющие символы. Насколько длинный список у вас получится?
В наш перечень вошли все пунктуационные символы, кроме @, _, +,: и запятой. Но полной уверенности, что эти символы во всех случаях безопасны, у нас нет. Возможно, существуют интерпретаторы, для которых и они могут быть управляющими.
Быть может, вы полагаете, что некоторые из упомянутых символов никогда не имеют специального смысла. Скажем, знак минуса? Увы, если минус находится в начале слова, то может интерпретироваться как признак окончания флагов команды. А как насчет символа А? Вы не знали, что он применяется для подстановок? Ну а знак процента? Хотя при интерпретации в качестве метасимвола он, скорее всего, безопасен, но все же может быть метасимволом, поскольку используется для управления заданиями. Вместо знака тильды (~) в начале слова иногда подставляется начальный каталог пользователя, а в остальных случаях он метасимволом не является. Однако и такое использование может привести к раскрытию информации, особенно если цель противника – увидеть ту часть файловой системы, которую программа видеть не должна. Например, вы могли бы поместить свое приложение в каталог /home/blah/ и запретить во входных данных две подряд идущие точки. Тем не менее противник сможет добраться до любого файла в этом каталоге, добавив к имени файла префикс ~.
Даже пробел может считаться управляющим символом, поскольку разделяет семантически значимые аргументы или команды. И в таком качестве используются многие символы, помимо пробела, а именно: символ табуляции, перевода строки, возврата каретки, перевода страницы и вертикальной табуляции.
К тому же есть еще управляющие символы типа Ctrl–D или NULL, которые также могут приводить к нежелательным эффектам.
В общем, гораздо надежнее работать со списком «только эти». В случае списка «все кроме» вы должны быть абсолютно уверены, что рассмотрели все возможности. Но даже разрешительного подхода на основе списка «только эти» может оказаться недостаточно. Знать, как действует тот или иной символ, все равно необходимо, поскольку иначе вы можете включить в список разрешенных пробелы или тильду, не понимая, какие последствия это будет иметь для безопасности программы.
Другая проблема, свойственная разрешительному подходу, состоит в том, что пользователи могут быть недовольны, когда программа не дает вводить допустимые с их точки зрения символы. Например, вы можете запретить символ «+» в почтовых адресах, но найдутся люди, которые применяют этот символ, чтобы отличить тех, кому они сообщили свой адрес. И все же пропускание только разрешенных символов надежнее двух других подходов.
Рассмотрим случай, когда вы принимаете от пользователя значение, интерпретируемое как имя файла. Предположим, что проверка реализована так (код ниже написан на Python):
for char in filename:
if (not char in string.ascii_letters and not char in string.digits
and char <> '.'):
raise "InputValidationError"
Здесь разрешены точки, чтобы пользователь мог указывать имена файлов с расширением, но о символе подчеркивания мы забыли. При подходе «все кроме» вы могли бы не подумать о том, чтобы запретить косую черту, а это плохо – противник может с помощью символа косой черты и точек сформировать имя файла из другой части файловой системы, вне текущего каталога. Если бы вы применили «закавычивание», то функция проверка оказалась бы гораздо более сложной.
Для такого рода проверок часто применяют регулярные выражения. Однако в регулярном выражении, особенно сложном, легко допустить ошибку. Если вам нужны вложенные конструкции и другие подобные вещи, лучше о регулярных выражениях забыть.
Вообще говоря, с точки зрения безопасности лучше перестраховаться, чем потом кусать локти. Регулярные выражения проще, но не безопаснее, особенно когда для точного контроля нужно учитывать сложную семантику, а не просто сопоставлять с образцом.
Если проверка не проходитЕсть три основные стратегии обработки ошибок. Они не являются взаимоисключающими, и, по крайней мере, первые два способа лучше применять совместно.
□ Известить об ошибке (и, разумеется, не запускать команду, несмотря ни на что). Но думайте о том, как выглядит сообщение об ошибке. Если вы просто включите в него неверные данные, то можете нарваться на атаку с кросс–сайтовым сценарием. Не стоит также сообщать противнику слишком много информации (особенно если в ходе проверки используются данные из конфигурационного файла). Иногда лучше всего просто сказать «недопустимый символ» или что–нибудь, столь же туманное.
□ Протоколировать ошибку и все связанные с ней данные. Но следите, чтобы сам процесс протоколирования не оказался мишенью атаки; некоторые системы протоколирования принимают символы форматирования, а попытка бесхитростно записать в протокол некоторые данные (например, символ возврата каретки или перевода строки) может привести к порче протокола.
□ Модифицировать поступившие данные, заменив их значением по умолчанию или как–то преобразовав.
В общем случае мы не рекомендуем третий вариант. Вы можете ошибиться, но даже в том случае, когда правы вы, а ошибся пользователь, результат может оказаться неожиданным. Лучше совсем отказаться от операции, но сделать это безопасно.
Дополнительные защитные меры
В языке Perl есть средства, которые позволяют обнаружить такого рода ошибки во время выполнения. Это так называемый «осторожный режим» (taint mode). Идея в том, что Perl не позволит передать непроверенные данные любой из перечисленных выше функций. Однако проверка выполняется только в осторожном режиме, поэтому, не включив его, вы не получите никаких преимуществ. Кроме того, вы можете случайно отключить этот режим, предварительно ничего не проверив. Есть и другие мелкие ограничения, поэтому лучше не полагаться только на этот механизм. Тем не менее это прекрасный инструмент для тестирования, и обычно стоит задействовать его в качестве одного из средств защиты.
Для стандартных вызовов API, с помощью которых происходит обращение к командным процессорам, имеет смысл написать собственные обертки, которые фильтруют входные данных по списку разрешенных символов и возбуждают исключение, если что–то не так. Это не должно быть единственным средством контроля, поскольку часто необходима более тщательная проверка. Однако в качестве первой линии обороны сойдет, к тому же и реализовать совсем нетрудно. Можно либо заменить «плохие» функции обертками прямо в библиотеке, либо пропустить исходный текст через простейшую программу поиска, найти все места, где они встречаются, и быстро провести контекстную замену.
Другие ресурсы
□ «How То Remove Metacharacters From User–Supplied Data in CGI Scripts»: www.cert.org/tech_tips/cgi_metacharacters.html
Резюме
Рекомендуется
□ Проверяйте все входные данные до передачи их командному процессору.
□ Если проверка не проходит, обрабатывайте ошибку безопасно.
Не рекомендуется
□ Не передавайте непроверенные входные данные командному процессору, даже если полагаете, что пользователь будет вводить обычные данные.
□ Не применяйте подход «все кроме», если не уверены на сто процентов, что учли все возможности.
Стоит подумать
□ О том, чтобы не использовать регулярные выражения для проверки входных данных; лучше написать простую и понятную процедуру проверки вручную.
Грех 6.
Пренебрежение обработкой ошибок
В чем состоит грех
Безопасность подвергается серьезной угрозе, когда программист не уделяет должное внимание обработке ошибок. Иногда программа может оказаться в некорректном состоянии, но чаще все заканчивается отказом от обслуживания, так как приложение просто «падает». Эта проблема не утрачивает значимости даже в таких современных языках, как С# и Java, только в них аварийное завершение программы происходит из–за необработанного исключения, а не из–за того, что автор забыл проверить код возврата.
Суровая реальность такова: любая ненадежность программы, приводящая к краху или перезапуску, – это отказ от обслуживания, а следовательно, может угрожать безопасности, особенно если речь идет о сервере.
Очень часто причиной подобной ошибки становится некритическое копирование кода из какого–нибудь примера. Ведь обычно в примерах для простоты опускают проверку ошибок.
Подверженные греху языки
Уязвим любой язык, в котором функция извещает об ошибке с помощью кода возврата, например ASP, PHP, С и С++, а также языки, полагающиеся на исключения: С#, VB.NET и Java.
Как происходит грехопадение
Есть шесть способов впасть в этот грех:
□ раскрытие излишней информации;
□ игнорирование ошибок;
□ неправильная интерпретация ошибок;
□ бесполезные возвращаемые значения;
□ обработка не тех исключений, что нужно;
□ обработка всех исключений.
Рассмотрим каждый в отдельности.
Раскрытие излишней информацииЭта тема обсуждается в разных главах книги, а особенно в грехе 13. Ситуация типична: возникает ошибка, и вы из соображений «практичности» сообщаете пользователю все подробности случившегося, а заодно советуете, как ошибку исправить. Только вот беда – хакер получает лакомый кусок: сведения, с помощью которых он может скомпрометировать систему.
Игнорирование ошибокФункция возвращает код ошибки не просто так, а чтобы вызывающая программа могла отреагировать. Согласны, некоторые значения кодов возврата носят чисто информационный характер и зачастую необязательны. Например, значение, возвращаемое функцией printf, проверяется очень редко; если оно положительно, то равно числу выведенных символов, а–1 означает ошибку. Честно говоря, для вызывающей программы ошибка в printf не слишком существенна.
Но чаще возвращаемое значение важно. Например, в Windows есть несколько функций, позволяющих сменить учетную запись, от имени которой работает программа: ImpersonateSelf(), ImpersonateLogonUser() и SetThreadToken(). Если любая из них возвращает ошибку, значит, олицетворение не состоялось, и поток продолжает работать от имени того же пользователя, что и весь процесс.
Или возьмем ввод/вывод. Если вы вызываете функцию fopenQ, а она не может открыть файл (его не существует или к нему нет доступа), и вы не обрабатываете ошибку, то все последующие вызовы fwrite() или fread() тоже завершатся неудачно. А если вы читаете из файла данные, а потом как–то их используете, то приложение может «грохнуться».
В языках, поддерживающих исключения, именно они являются основным механизмом для передачи информации об ошибках. Java пытается заставить программиста обрабатывать ошибки, проверяя во время компиляции, что исключения обрабатываются (или, по крайней мере, ответственность за обработку исключения делегируется вызывающей программе). Однако есть исключения, которые могут возбуждаться в самых разных местах, поэтому Java не требует, чтобы они обрабатывались. Типичный пример – NullPointerException. Это печально, так как любое исключение – признак логической ошибки; если оно возникло, то довольно трудно восстановить нормальную работу программы, пусть даже вы его перехватываете.
Но и для тех исключений, которые Java заставляет обработать, компилятор не в состоянии проконтролировать, что вы делаете это сколько–нибудь разумным образом. Часто в этом случае просто завершают программу, даже не пытаясь восстановиться, а это все тот же отказ от обслуживания. Еще того хуже и, как это ни грустно, гораздо более распространена практика включать пустой обработчик исключения, в результате чего оно распространяется дальше. Но об этом позже.
Неправильная интерпретация ошибокНекоторые функции, например recv() (для чтения из сокета), ведут себя просто странно. recv() может вернуть одно из трех значений. В случае успешного завершения возвращается длина сообщения в байтах. Если в буфере сокета ничего нет и удаленный хост выполнил аккуратное размыкание соединения (orderly shutdown), то recv() возвращает 0. В противном случае возвращается–1, а в переменную errno записывается код ошибки.
Бесполезные возвращаемые значенияНекоторые функции из стандартной библиотеки С попросту опасны, например strncpy не возвращает никакого уведомления об ошибке, а лишь указатель на целевой буфер независимо от того, как завершилась операция копирования. Если в результате вызова произошло переполнение буфера, то вы получите указатель на переполненный буфер! Если вам нужен был довод в пользу отказа от использования этих ужасных функций С, так вот он!
Обработка не тех исключений, что нужноВ языках, поддерживающих исключения, надо внимательно относиться к тому, какие именно исключения вы обрабатываете. Например, стандарт С++ гласит:
Функция выделения памяти извещает об ошибке, возбуждая исключение bad_alloc. В этом случае никакая инициализация не проведена.
Но так, к сожалению, бывает не всегда. Например, в библиотеке Microsoft Foundation Classes оператор new в случае ошибки может возбуждать исключение CMemoryException, а многие современные компиляторы С++ (в том числе Microsoft Visual С++ и gcc) позволяют использовать спецификацию std::nothrow, чтобы предотвратить возбуждение исключения оператором new. Поэтому если ваша программа готова обрабатывать исключения типа FooException, а код внутри блока try/catch возбуждает Bar Exception, то программа завершится аварийно, ибо перехватывать Bar Exception некому. Разумеется, можно было бы перехватить все исключения, но это тоже плохо, а почему, мы расскажем в следующем разделе.
Обработка всех исключенийКазалось бы, тема этого раздела – обработка всех исключений – прямо противоположна названию греха «Пренебрежение обработкой ошибок», но на самом деле то и другое тесно связано. Обрабатывать все исключения так же плохо, как вообще не обрабатывать ошибки. Тем самым программа «глотает» ошибки, о которых ничего не знает, которые не может обработать или – и это самое страшное–которые маскируют логические дефекты. Если вы притворяетесь, что никакой ошибки не произошло, то скрытые «баги», о которых вы ничего не знаете, рано или поздно проявятся и программа «умрет» от «непостижимой» причины, да так, что отладить ее будет очень непросто.
Греховность C/C++В примере ниже автор проверяет неинформативное значение, возвращенное функцией, – strncpy просто возвращает указатель на начало целевого буфера. Эта информация бесполезна.
char dest[19];
char *p = strncpy(dest, szSomeLongDataFromAHax0r, 19);
if (p) {
// все сработало отлично, поинтересуемся значением dest или p
}
Переменная р указывает на начало dest вне зависимости от того, что произошло внутри strncpy. А между тем эта функция не завершает буфер нулем, если длина исходных данных больше или равна размеру буфера dest. При взгляде на этот код закрадывается подозрение, что автор просто не понимает, что именно возвращает strncpy, ожидая, что в случае ошибки получит NULL. Ох, грехи наши тяжкие!
Следующий код тоже часто встречается на практике. Да, здесь возвращаемое значение проверяется, но только внутри макроса assert, который исчезнет из программы, как только будет выключен отладочный режим. Кроме того, не проверяются аргументы функции, но это уже другая тема.
Греховность C/C++ в WindowsDWORD OpenFileContents(char *szFileName) {
assert(szFileName != NULL);
assert(strlen(szFileName) > 3);
FILE *f = fopen(szFileName, "r");
assert(f);
// Можно работать с файлом
return 1;
}
Мы уже говорили, что в Windows есть функции олицетворения, которые могут завершаться неудачно. Более того, в Windows Server 2003 появилась новая привилегия, которая разрешает выполнять олицетворение только определенным учетным записям, например службам (LocalSystem, LocalService и NetworkService), а также администраторам. Следовательно, ваша программа может не сработать при таком вызове функции олицетворения:
ImpersonateNamedPipeClient(hPipe);
DeleteFile(szFileName);
RevertToSelf();
Проблема в том, что если процесс работает от имени LocalSystem, а вызывает этот код низкопривилегированный пользователь, то обращение к DeleteFile может завершиться с ошибкой, так как у пользователя нет доступа к файлу. Надо полагать, именно этого вы и хотели. Но если функция олицетворения возвращает ошибку, то поток продолжает работать в контексте той учетной записи, от имени которой был запущен процесс. А это LocalSystem, и у нее, скорее всего, есть право на удаление файла! Итак, низкопривилегированный пользователь только что удалил ценный файл!
В следующем примере обрабатываются все вообще исключения. Механизм структурной обработки исключений (SEH) в Windows работает для любого языка:
char *ReallySafeStrCopy(char *dest, const char *src) {
__try {
return strcpy(dst, src);
} __except (EXCEPTION_EXECUTE_HANDLER) {
// замаскировать ошибку
}
return dst;
}
Если в strcpy происходит ошибка из–за того, что длина src больше длины dest или src равно NULL, то вы понятия не имеете, в каком состоянии осталось приложение. Содержимое dst корректно? В зависимости от того, где размещен буфер dest, каково состояние кучи или стека? Ничего не известно, но приложение будет продолжать работать, возможно, даже несколько часов, пока, наконец, с грохотом не рухнет. Поскольку разрыв во времени между моментом ошибки и окончательным сбоем так велик, никакая отладка не поможет. Не поступайте так.
Правообладателям!
Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.Читателям!
Оплатили, но не знаете что делать дальше?