Ошибки программирования
Хотя мы утверждали в начале предыдущего раздела, что методы управления полномочиями в современных ОС общего назначения теоретически идеальны, это относится лишь к идеальной ОС, т. е. к спецификациям соответствующих модулей. В реальном коде встречаются отклонения от этих спецификаций, так называемые ошибки. Полный обзор и исчерпывающая классификация всех типов ошибок, по-видимому, невозможны. В этом разделе мы опишем только наиболее распространённые и опасные ошибки.
Наибольшую опасность с точки зрения безопасности представляют ошибки в модулях, связанных с проверкой ACL, авторизацией и повышением уровня привилегий процессора (например, в диспетчере системных вызовов), а в системах семейства Unix — в setuid-программах.
Одна из наиболее опасных и распространённых ошибок — это ошибка, приведённая в примере 2.4. Рассмотрим этот код снова (пример 12.2).
Пример 12.2. Пример программы, подверженной срыву стека.
/* Фрагмент примитивной реализации сервера SMTP (RFC822) */
int parse_line(FILE *socket) {
/* Согласно RFC822, команда имеет длину не более 4 байт,
а вся строка — не более 255 байт */
char cmd[5], args[255];
fscanf(socket, "%s %s\n", cmd, args);
/* Остаток программы нас не интересует */
}
Видно, что наша программа считывает строку из сетевого соединения, которая должна состоять из двух полей, разделённых пробелом, и заканчиваться символом перевода строки. В соответствии со спецификациями протокола SMTP, первое поле (команда) не может превышать четырёх символов, а строка целиком не может быть длиннее 255 байт. Если наш партнёр на другом конце соединения полностью соответствует требованиям RFC 0822, наш код будет работать без проблем. Однако серьёзные проблемы возникнут, если нам передадут строку, не соответствующую этим требованиям.
Превышение длины команды не представляет большой опасности: лишние байты будут записаны в начало массива args и потеряны при записи в его собственное поле. Однако настоящая опасность заключается в превышении длины всей строки. Для нашего партнёра не составит труда отправить строку длиной более 255 символов без перевода строки. Это приведёт к переполнению буфера args, за которым в памяти идёт заголовок стекового кадра с адресом возврата нашей подпрограммы.
Если злоумышленник достаточно квалифицирован, он может подменить команду на кусок кода и поддельный стековый кадр, который в качестве адреса возврата будет указывать на его код. В этом случае, при попытке возврата управления, программа передаст управление на подставленный код. Количество вредоносных действий, которые может содержать этот код, неисчислимо.
Рис. 12.21. Срыв буфера
Если вредитель не может передать и исполнить код (например, из-за защиты стека от исполнения), даже повреждения стекового кадра будет достаточно для аварийного завершения работы сетевого сервиса, что также является неприятным. Ошибки такого рода называются переполнением буфера (buffer overrun) или срывом буфера. Если буфер находится в стеке, говорят о срыве стека. Срывы буфера могут происходить не только в сетевых сервисах, но и в приложениях, считывающих файлы. Особенно опасны ошибки в драйверах сетевых протоколов нижнего уровня, так как атака затрагивает ядро ОС. Срыв буфера в модулях, осуществляющих парольную авторизацию, может позволить злоумышленнику не только разрушить стековый кадр, но и изменить переменную, сигнализирующую об успешной проверке пароля.
Наибольшую опасность представляет срыв буфера в ситуациях, когда спецификация протокола или формата файла гласит, что длина того или иного поля не может превышать определённого количества байтов, однако нарушить это ограничение возможно. Это может быть связано с использованием не счётчика байтов, а маркера конца пакета или с разрядностью счётчика байтов, позволяющего представлять значения, превышающие установленный предел.
Примечание: Ошибки такого рода часто используются в атаках, но важно понимать, что они основаны на ошибках в коде атакуемой системы. Без таких ошибок злоумышленник не смог бы осуществить атаку.
При программировании на языке C основными источниками таких ошибок являются стандартные процедуры, такие как gets и fscanf. Процедура gets непригодна для безопасного использования, так как невозможно указать размер буфера, и она не контролирует его заполнение. Вместо неё следует использовать функцию fgets, которая принимает размер буфера в качестве параметра. Рекомендуется использовать её, а также проверять размер каждого поля, как показано ниже:
fscanf(socket, "%4s %255s\n", cmd, args);
Однако автоматическая проверка правильности всех форматных спецификаторов невозможна, и поэтому использование fscanf и других подобных процедур не рекомендуется.
Распространение червя Морриса через срыв буфера
Широко известен срыв буфера в программе fingerd, которая входила в систему BSD Unix. На процессорах VAX и MC68000 отсутствовала защита страниц памяти от исполнения, поэтому любая страница памяти, доступная для чтения, могла быть исполнена.
Червь Морриса использовал эту уязвимость, передавая кусок кода и поддельный стековый кадр, который передавал управление на код. Этот код запускал командный интерпретатор с привилегиями суперпользователя, который, в свою очередь, использовал их для загрузки вируса в систему.
Ошибка была обнаружена в 1987 году и быстро исправлена. Современные системы используют безопасные версии программы fingerd. Однако атаки через срыв буфера продолжают возникать. Например, червь Code Red, который поразил серверы IIS в 2001 году, использовал уязвимости срыва буфера для распространения. Эта атака повлекла за собой несколько других атак, что показало важность защиты от таких уязвимостей.
Срывы буфера остаются одним из наиболее распространённых типов уязвимостей: согласно базе данных CERT, они составляют 27% всех задокументированных проблем. В языках программирования, таких как C++ и Java, разработка программы, подверженной срыву буфера, сложнее, но это всё ещё возможно. В Java защита памяти более сложная, что делает такие атаки невозможными, однако ошибки, связанные с неправильной обработкой исключений, могут привести к аварийной остановке приложения, что является не менее серьёзной угрозой.
Кроме того, в многопоточных сервисах распространены ошибки соревнования. Для их срабатывания необходима определённая последовательность и временное согласование запросов к сервису.
Ошибка подъёма по каталогам (directory traversal bug), обнаруженная во многих HTTP-серверах, позволяет злоумышленнику через URI с последовательностями '..' подняться по файловой системе выше корневого каталога сервера и получить доступ к файлам, которые не должны быть доступны. Аналогичные ошибки встречаются и в сетевых файловых серверах.
Рис. 12.24. Ошибка подъёма по каталогам
Общим правилом, позволяющим уменьшить вероятность таких ошибок, является недоверие к входным данным. Если спецификации протокола или формата утверждают, что данные должны удовлетворять определённому условию, программа должна проверять это условие, а не просто доверяться входным данным. Тестирование программного комплекса должно включать проверку как правильности обработки допустимых данных, так и реакцию на недопустимые.