Пришло время переосмыслить безопасность OpenBSD

Автор оригинала: Ted Unangst
  • Перевод
OpenBSD позиционируетcя как защищённая ОС. Однако за последние несколько месяцев в системе найден ряд уязвимостей. Конечно, в этом нет ничего экстраординарного. Хотя некоторые уязвимости довольно необычные. Можно даже сказать, критические. У разработчиков OpenBSD несколько принципов, как обеспечить безопасность. Вот два из них:

  • избегать ошибок;
  • минимизировать риск ошибок.

Не все согласны, что этих принципов достаточно, чтобы строить защищённые системы. Мне кажется, есть смысл изучить, работает ли подход OpenBSD, или он изначально обречён.

Для иллюстрации я выбрал не все, а только несколько интересных багов, которые случайно совпадают с темой нашего разговора.

libc auth


Функции auth запускают хелперы без проверки argv. Отчёт. Патч.

Это поразительно простая ошибка, но, вероятно, она не показалась слишком очевидной во время код-ревью. Мне кажется, отчасти причина в некоторой путанице в том, кто отвечает за проверку входных данных. Вы можете вызвать каждый из трёх задействованных компонентов: программу, библиотеку или login_passwd — и разумно предположить, что проверку выполняет кто-то другой. В конце концов, я думаю, виновной признали библиотеку, потому что именно для неё появился патч, но лично мне её код на первый взгляд не кажется однозначно неправильным.

Более интересная часть истории заключается в том, что даже с упомянутой ошибкой libc функция login_passwd не была бы уязвима таким образом, если бы не другой баг. В 2001 году login_passwd переписали для поддержки kerberos, и, возможно, именно тогда ввели то, что является настоящей причиной ошибки. Предложение аутентификации запросно-ответного типа (как в системе s/key) возвращает аутентифицированное состояние, а не молчание. Много лет спустя код kerberos удалили, но часть кода для его поддержки осталась, как и введённая ошибка.

Если бы код kerberos тщательно почистили, то баг с аутентификацией всё равно остался бы (там были и некоторые другие связанные проблемы с парсингом argv), но его влияние, безусловно, сильно бы уменьшилось.

Не так легко провести корректный парсинг argv в контексте безопасности. Многие логичные советы и подходы тут не работают. Я только отмечу, что эта уязвимость выскочила после очередного обсуждения проблемы с именами файлов в Unix/Linux/POSIX, хотя ведущий минус (дефис) на самом деле не относится к названию файла.

ld.so


Из кода ld.so не удалены плохие переменные окружения. Отчёт. Патч.

Что-то похожее на ошибку памяти. Но нет. Баг здесь заключается в привязке успеха одной операции — разбиения переменной окружения — к удалению этой переменной. Весьма самоуверенно.

Конечно, сторонники различных систем типизации будут уверять, что обработают эти операции в правильном порядке, но не факт, что это поможет. Код C не вылетал с ошибкой из-за отсутствия обработки ошибок или потому что ошибка распределения памяти осталась незаметной.

ftp


ftp следует редиректам на локальные файлы. Отчёт. Патч.

Баг NetBSD ftp с редиректами — это же давно типичный пример бесконтрольных функций. И вот опять та же ошибка (к счастью, с незначительными последствиями)! Ребята, ну сидите дома. Не добавляйте в свои программы дополнительные функции.

smtpd from


smtpd не может проверить некоторые адреса отправителей. Отчёт. Патч. Комментарий.

Думаю, что в комментарии Жиля всё сказано, но напомню предысторию. Давным-давно вся почта хранилась локально у каждого пользователя в /var/mail в файлах mbox. Это не очень здорово, потому что существует вероятность повреждения, если mua удалит электронное письмо, пока mda доставляет новое (не говоря уже о других проблемах типа искажения поля From). Поэтому файл mbox нужно заблокировать. Но блокировка в сетевой файловой системе работает ненадёжно. Так что вместо блокировки мы задействуем lock-файлы. Однако нужно прийти к согласию по определённому протоколу блокировки, ведь файлами mbox владеет пользователь, а самим каталогом — рут. Таким образом, чтобы фактически изменить файл mbox, нужно каждый раз запускать вспомогательную функцию setuid. Ну, вот уже первая проблема. Ещё один пережиток старых времён заключается в настройках mda, которую можно использовать не просто как программу, а как конвейер оболочки. Народ прописывает что-то вроде spam-assassin | mail.mda, а это вы уже не сможете просто передать execve().

Здесь очень много сложностей, которые корнями уходят в прошлое. И, к сожалению, этот проблемный код нельзя просто заменить. Электронная почта используется давным-давно, и на её основе построены очень сложные системы и рабочие процессы, поэтому очень трудно просто вырезать один кусок кода — и вставить новый. Несмотря на различные уровни разделения привилегий, родительский процесс с правами рута по-прежнему во многом доверяет своим менее привилегированным дочерним процессам. Он выполнит команды и аргументы, которые получит.

Избежать этого кажется так же просто, как переключиться на формат хранения maildir, но это требует различных изменений во многих местах. Стандартный mua mail не понимает этот формат. Как по мне, так mbox давно отжил своё, но многих он по-прежнему устраивает, а процедура обновления, возможно, не полностью прозрачна и не пройдёт автоматически.

smtpd read


Чтение за пределами области в smtpd можно использовать для выполнения команд. Отчёт. Патч.

Здесь реально проблема безопасности памяти. Отправив несколько забавных строк состояния, удалённый smtp-сервер может внедрить в очередь smtpd команды для выполнения. Когда электронное письмо помещается в очередь для повторной доставки, smtpd добавляет в заголовок некоторую информацию о месте назначения, чтобы знать, какую команду следует выполнить (см. выше). Эта атака похожа на «контрабанду» http-запросов. Если вы можете сгенерировать «отлуп» с неожиданными командами в заголовке, то smtpd выполнит их при попытке повторной доставки.

Как и выше, одна из проблем заключается в том, что наиболее чувствительные части smtpd находятся слишком близко к поверхности атаки. Мне кажется, истинная проблема в том, что smtpd хранит собственные метаданные внутри данных электронной почты. Из-за этого становится возможной атака с рассинхронизацией парсинга. Если бы процитированный ответ с сервера хранился совершенно отдельно от файла с инструкциями по доставке, то чтение за пределами границ не нанесло бы особого вреда.

Эта уязвимость мне кажется показательной, потому что мы здесь видим опасность смешивания данных с разными уровнями доверия. Эту опасность никогда не обсуждали. А она точно есть.

Выводы


Конечно, вывод был понятен с самого начала, но мы всё равно об этом скажем.

Думаю, некоторые из этих ошибок помогают продемонстрировать, насколько жизненно важны для безопасности такие принципы, как удаление устаревших интерфейсов и снижение сложности кода. По большей части, провал произошёл из-за того, что этим принципам не следовали до конца, а не потому, что сами принципы испорчены или несостоятельны. Некоторые вещи ускользают из виду, но я не согласен, что разработчикам необходима какая-то сверхчеловеческая бдительность. Тут легко давать бесполезные советы, типа будь внимательнее, не делай ошибок и git gud [слэнговое геймерское выражение означает 'get good', то есть «поправляйся, выздоравливай» — прим. пер.]. Но я думаю, что у OpenBSD более серьёзные проблемы.

Даже OpenBSD может рискнуть безопасностью ради утилитарной практичности. Вот почему долгое время не вносятся изменения в некоторые устаревшие проекты. Так что, возможно, урок состоит в том, что эффективных принципов нужно придерживаться всегда, а не только тогда, когда это удобно. Хотя зачастую трудно сделать правильный выбор.

Три самые серьёзные уязвимости, auth и две smtpd, более или менее пригодны для эксплуатации только из-за архитектурных проблем, которые выходят за рамки оригинального бага. Они должны были остаться просто мелкими недочётами, которые демонстрируют, что в хорошо защищённой системе не обязательно нужно стремиться к идеальному коду, то есть мелкие ошибки допустимы — и они не повляют на безопасность. Увы, бывает трудно выявить недостатки дизайна в абстрактном виде. И все части системы по отдельности выглядят защищёнными, но если их соединить, то могут появиться слабые места.

Разделение привилегий — ключевой компонент безопасности OpenBSD, а в его основе лежит межпроцессная коммуникация. Есть смысл более пристально присмотреться к тому, какие проблемы могут возникнуть с повреждёнными процессами. Защищённые браузеры всё больше усиляют защиту и затрудняют атаки. В частности, smtpd должен быть защищён от повреждения памяти в сетевых задачах. Но вызывает тревогу та лёгкость, с которой он способен управлять родительским процессом.

Только одну ошибку можно было предотвратить, используя более безопасный язык. Да, наверное, существует какая-то программная идиома, которая в некоторых случаях способна помочь, если следовать ей с религиозным упорством. Но я пока не уверен, что каждый программист по умолчанию корректно закодирует все соответствующие инварианты.

Написание почтового сервера — сложное дело. Особенно если вас поджимают рамки легаси.



Дата-центр «Миран»
Решения для аренды и размещения ИТ-инфраструктуры

Комментарии 6

    0
    OpenBSD считается самой безопасной лишь потому, что она крайне слабо распространена, как следствие, мало кому интересна. Из-за этого в открытом доступе нет уязвимостей для неё (ну или почти нет).
    Поэтому говорить, что она безопасна, не совсем правильно. Правильнее говорить, что она не исследована так, как другие. Тогда всё встаёт на свои места и иллюзии растворяются как мираж.

    При этом вполне возможно наличие как уязвимостей, так и эксплойтов для этой ОС, использующихся индивидуально либо в узких кругах.

    w3techs.com/technologies/details/os-openbsd
      +2
      Нет, потому что в OpenBSD готовы жертвовать чем угодно ради защищённости.
      Пример: отключили HT (hyperthreading) совсем — система стала медленнее на процессорах с HT, но защищённее.
      Поэтому говорить, что она безопасна, не совсем правильно.
      Правильнее говорить, что она более безопасна.
      Системы с закрытым кодом непонятно как исследованы, и непонятно кем. В отличие от систем с открытым кодом.
      +1
      Статья хорошая. Перевод некоторых частей кривой. Исправляю.

      You could look at any of the three components involved… and reasonably conclude that somebody else is doing the checks. Вы можете вызвать каждый из трёх задействованных компонентов… и разумно предположить, что проверку выполняет кто-то другой.


      «Вы можете обратить взгляд на каждый из трёх задействованных компонентов… и придти к разумному заключению, что проверку выполняет кто-то другой.»

      A request for challenge response auth (think s/key) would return authenticated, instead of silence. Предложение аутентификации запросно-ответного типа (как в системе s/key) возвращает аутентифицированное состояние, а не молчание.


      Смысл не искажен, но фраза кривая. И тут лучше было бы переводить would прошедшим временем, поскольку речь идет о неправильном будущем поведении, имевшем место в прошлом (future in the past). Может быть: «Предложение аутентификации запросно-ответного типа (как в системе s/key) возвращало успех аутентификации, а должно было бы не возвращать ничего».

      Many years later, the kerberos code was removed, but the refactor to support it remained, and so did the introduced bug. Много лет спустя код kerberos удалили, но часть кода для его поддержки осталась, как и введённая ошибка.


      «Много лет спустя код kerberos удалили, но следы рефакторинга для его поддержки остались, как и введённая ошибка.»

      Naughty environment variables weren’t removed in ld.so. Из кода ld.so не удалены плохие переменные окружения.


      Речь идет не об удалении чего-то из кода, а об удалении переменных окружения при выполнении этого кода. Поэтому: «ld.so не удалял опасные переменные окружения».

      ftp would follow redirects to local files. ftp следует редиректам на локальные файлы.


      Опять-таки, это лучше переводить прошедшим временем. «ftp следовал редиректам на локальные файлы.»

      The NetBSD ftp bug where it would exec redirects is my goto example for runaway features. Баг NetBSD ftp с редиректами — это же давно типичный пример бесконтрольных функций.


      Тут речь не о функциях, а о функциональности. Перевод слова «runaway» с помощью слова «бесконтрольные», конечно, корректен, но думаю, что можно было бы поискать синонимы с более подходящей эмоциональной окраской. Как насчет такого варианта: «Баг NetBSD ftp с редиректами — это же давно типичный пример функциональности, вышедшей из-под контроля»? P.S. с синонимом мне помог Google Translate.

      So you need to exec a setuid helper to actually deliver to the mbox. Таким образом, чтобы фактически изменить файл mbox, нужно каждый раз запускать вспомогательную функцию setuid.


      Речь идет о запуске вспомогательной программы (бинарника на диске), а не функции. Функция не может быть setuid. Поэтому: «Таким образом, чтобы фактически доставить письмо в mbox, нужно каждый раз запускать вспомогательную setuid-программу.»

      P.S. Разделы «smtpd from» и выводы переведены на «отлично». Думаю, что переводчику следует сходить на курсы по программированию на Си и по архитектуре операционных систем, тогда и более низкоуровневые темы станут понятны на 100% и поэтому переведутся правильно.
        –2
        Файловую систему в ней для начала надо переосмыслить. Чтобы её было хотя бы не страшно на боевой сервак поставить и чтобы она не сдохла случайно от внезапной перезагрузки. О какой безопасности может идти речь, если её придётся восстанавливать после банального скачка напряжения.

        Такая же проблема с ReactOS. Ни о какой замене Windows, даже там где она способна его заменить нет никакой речи, потому что способна сдохнуть на ровном месте из-за некорректной перезагрузки.
          +1
          Конечно, сторонники различных систем типизации будут уверять, что обработают эти операции в правильном порядке, но не факт, что это поможет. Код C не вылетал с ошибкой из-за отсутствия обработки ошибок или потому что ошибка распределения памяти осталась незаметной.

          Как сторонник различных систем типизации не могу понять этот абзац. Вообще-то да, именно строгие и выразительные системы типов могут гарантировать отсутствие подобных ошибок. Язык С же не имеет ни строгую, ни выразительную систему типов.


          слэнговое геймерское выражение означает 'get good', то есть «поправляйся, выздоравливай» — прим. пер.

          Улыбнули, спасибо! Get good в этом контексте — «повышай свой скилл». Get good at something, типа.

            +1
            Код C не вылетал с ошибкой из-за отсутствия обработки ошибок или потому что ошибка распределения памяти осталась незаметной.
            Как сторонник различных систем типизации не могу понять этот абзац. Вообще-то да, именно строгие и выразительные системы типов могут гарантировать отсутствие подобных ошибок. Язык С же не имеет ни строгую, ни выразительную систему типов.


            Непонятное предложение надо понимать (это не перевод!) так: «Код в идеале должен был упасть или вообще не скомпилироваться, но он был написан на языке C, где слишком легко не написать обработку ошибок или не заметить ошибку выделения памяти».

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое