Операционная система
166,45
рейтинг
16 февраля 2015 в 19:55

Разработка → Откуда берутся бреши в безопасности программ? перевод tutorial

                                             — У нас дыра в безопасности.
                                             — Ну, хоть что-то у нас в безопасности.
                                                                                  Анекдот

image Если вы Windows-пользователь, то вам должны быть знакомы всплывающие каждый второй вторник месяца окошки, рапортующие об установке «критических обновлений безопасности». Microsoft прилагает немалые усилия, постоянно работая над исправлениями уязвимостей в своих операционных системах, но оно того стоит: в мире, где число кибератак день ото дня только возрастает, ни одна лазейка в обороне наших компьютеров не должна оставаться открытой для потенциальных злоумышленников.

Недавнее обсуждение в списке рассылки, посвящённого 66192-й SVN ревизии ReactOS, показало как это легко — внести в код ядра критическую уязвимость. Я буду использовать этот случай как пример простой, но влияющей на безопасность ошибки, а также для наглядного представления некоторых мер, которым необходимо подвергать код ядра, если вы действительно хотите получить безопасную систему.

Отыщем уязвимость


Давайте взглянем на этот код и разберёмся что к чему:

NTSTATUS
APIENTRY
NtUserSetInformationThread(IN HANDLE ThreadHandle,
                           IN USERTHREADINFOCLASS ThreadInformationClass,
                           IN PVOID ThreadInformation,
                           IN ULONG ThreadInformationLength)
{
    [...]
    switch (ThreadInformationClass)
    {
        case UserThreadInitiateShutdown:
        {
            ERR("Shutdown initiated\n");

            if (ThreadInformationLength != sizeof(ULONG))
            {
                Status = STATUS_INFO_LENGTH_MISMATCH;
                break;
            }

            Status = UserInitiateShutdown(Thread, (PULONG)ThreadInformation);
            break;
        }
        [...]
}

Это небольшой кусочек функции NtUserSetInformationThread, представляющий собой системный вызов в win32k.sys, который может быть (более-менее) напрямую вызван пользовательскими программами. Здесь ThreadInformation — указатель на некий блок с данными, а параметр ThreadInformationClass показывает, как эти данные следует интерпретировать. Если он равен UserThreadInitiateShutdown, в блоке должно быть 4-байтовое целое число. Количество переданных байт хранится в ThreadInformationLength, и, как несложно заметить, код действительно проверяет, чтобы там было именно «4», в противном случае выполнение будет прервано с ошибкой STATUS_INFO_LENGTH_MISMATCH. Но обратите внимание, что оба этих параметра приходят непосредственно из пользовательской программы, а значит, какая-нибудь зловредная закладка, вызывая эту функцию, может передать ей что угодно.

А теперь давайте посмотрим, что происходит с ThreadInformation, когда его передают в UserInitiateShutdown:


NTSTATUS
UserInitiateShutdown(IN PETHREAD Thread,
                     IN OUT PULONG pFlags)
{
    NTSTATUS Status;
    ULONG Flags = *pFlags;
    [...]
    *pFlags = Flags;
    [...]
    /* If the caller is not Winlogon, do some security checks */
    if (PsGetThreadProcessId(Thread) != gpidLogon)
    {
        // FIXME: Play again with flags...
        *pFlags = Flags;
        [...]
    }
    [...]
    *pFlags = Flags;

    return STATUS_SUCCESS;
}

Поскольку довольно большая часть этой функции пока не реализована, всё, что происходит выше — это лишь несколько циклов чтения и записи 4-байтового значения, на которое указал пользователь.

Так в чём тогда проблема?


Ну, одного только разыменования непроверенного указателя хватает, чтобы сделать возможной DoS-атаку (отказ от обслуживания) — вредоносная программа может банально выключить компьютер, не имея на это прав. Например, программа может просто передать нулевой (NULL) указатель и таким образом воспользуется уязвимостью. UserInitiateShutdown разыменует упомянутый указатель, что приведёт к BSOD'у, обычно называемому «bug check» среди разработчиков ядра. При этом вызывающий имеет возможность писать в память (тут вспоминаем, что это произвольный указатель — он может ссылаться даже на область ядра!). На первый взгляд, запись считанного из указанной области памяти значения обратно туда же выглядит не так плохо. Но в реальности может привнести достаточно проблем. Некоторые участки памяти часто изменяются с высокой интенсивностью, и восстановление ранее хранившегося там значения может, например, снизить уровень энтропии генератора случайных чисел какого-нибудь криптоалгоритма, или переписать таблицу отображения страниц памяти её старой версией, которая к этому моменту уже должна была быть уничтожена, позволяя получить доступ к большему количеству памяти, что может быть использовано для компрометации системы. Но это всё просто примеры, родившиеся по ходу — а у целенаправленно атакующих могут быть месяцы, чтобы прийти к наилучшему решению, позволяющему достичь поставленных целей, и мелкий на первый взгляд изъян в безопасности, такой как этот, может оказаться для кого-то достаточным, чтобы вытянуть с вашей машины все секреты и получить полный контроль над ней. Конечно, когда функция будет полностью реализована, она будет изменять переменную Flags, перед тем, как записать её назад, предоставляя возможность модифицировать произвольный участок памяти (ядра), причём управляемым образом — настоящий праздник для хакера.

Зная всё это, что можно исправить?


Для защиты от подобного рода проблем в ядре NT предусмотрены два механизма: probing (проверка) и SEH (структурированная обработка исключений, Structured Exception Handling). Проверка памяти избавляет от большого количества проблем, позволяя убедиться, что полученный от приложения указатель действительно ссылается на пространство памяти пользователя. Выполнение такой проверки для всех параметров-указателей даёт уверенность, что программы уровня пользователя не смогут получить доступ к памяти ядра таким способом. Однако это не спасает от нулевых, или любых других недействительных указателей. И тут на помощь приходит второй механизм, SEH: оборачивание каждого обращения к данным по сомнительным указателям (т.е. полученным от пользовательских программ) в блок обработки исключений гарантирует, что код сохранит устойчивость, даже если указатель недействителен. Код уровня ядра в этом случае предоставляет обработчик исключений, который вызывается всякий раз, когда защищённый код генерирует исключение (такое, как нарушение доступа вследствие использования неверного указателя). Обработчик исключений собирает доступные сведения (такие, как код исключения), выполняет все необходимые действия по очистке памяти и возвращает, в большинстве случаев, управление пользователю, вместе с кодом ошибки.

Давайте посмотрим на исправленные исходники (коммита r66223):


            ULONG CapturedFlags = 0;

            ERR("Shutdown initiated\n");

            if (ThreadInformationLength != sizeof(ULONG))
            {
                Status = STATUS_INFO_LENGTH_MISMATCH;
                break;
            }

            /* Capture the caller value */
            Status = STATUS_SUCCESS;
            _SEH2_TRY
            {
                ProbeForWrite(ThreadInformation, sizeof(CapturedFlags), sizeof(PVOID));
                CapturedFlags = *(PULONG)ThreadInformation;
            }
            _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
            {
                Status = _SEH2_GetExceptionCode();
            }
            _SEH2_END;

            if (NT_SUCCESS(Status))
                Status = UserInitiateShutdown(Thread, &CapturedFlags);

            /* Return the modified value to the caller */
            _SEH2_TRY
            {
                *(PULONG)ThreadInformation = CapturedFlags;
            }
            _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)
            {
                Status = _SEH2_GetExceptionCode();
            }
            _SEH2_END;


Заметьте, все обращения к небезопасному указателю ThreadInformation выполняются теперь внутри блоков _SEH2_TRY. Возникающие в них исключения будут контролируемо перехватываться кодом из блока _SEH2_EXCEPT. Кроме того, перед тем, как разыменовать указатель в первый раз, делается вызов ProbeForWrite, который возбудит исключение STATUS_ACCESS_VIOLATION или STATUS_DATATYPE_MISALIGNMENT, если обнаружится недействительный (принадлежащий к области ядра, например) указатель, или защищённая от записи память. В конце обратите внимание на введённую переменную CapturedFlags, которая передаётся в UserInitiateShutdown. Подобная хитрость упрощает операции с небезопасным параметром: чтобы не использовать SEH всякий раз при обращении к pFlags внутри функции, это значение сохраняется в доверенную область силами NtUserSetInformationThread, а потом записывается обратно в пользовательскую память, когда UserInitiateShutdown отработает. Так пропадает необходимость править саму UserInitiateShutdown, поскольку теперь она получает на вход безопасный указатель из области ядра (указатель на CapturedFlags). Результат всех этих мер — функция теперь может работать с совершенно любым набором пользовательских данных, корректных, и не очень, без риска навредить системе. Дело сделано!

Какой урок из этого нужно извлечь?


Очевидно, повышенная бдительность ещё на этапе разработки позволяет вовремя заметить строчки кода, способные стать угрозой безопасности в дальнейшем. Нельзя позволять, чтобы их становилось слишком много, потому что, честно говоря, и без них проблем безопасности наверняка и так будет много. В перспективе, если всё пойдёт по плану, мы будем постепенно отыскивать их и исправлять, выпуская регулярные обновления, вроде тех, что приходят к вам по вторникам из Windows Update Center.

Заметка на полях. Как справедливо заметил Алекс Йонеску (Alex Ionescu), сама Windows имеет уязвимость в этой же самой функции, NtUserSetInformationThread. Причём, по его словам до сих пор не закрытую и активно эксплуатируемую для всякого рода джейлбрейков устройств типа Surface RT. Впервые она была описана ещё в 2012 году известным исследователем безопасности по имени Матеуш «jooro» Юрчик (Mateusz Jurczyk) (который, кстати, частенько тусуется с нами в IRC ;]). Его статью на эту тему вы найдёте в блоге: j00ru.vexillium.org/?p=1393

Примечания от переводчика:
Обо всех опечатках, ошибках и не неточностях прошу сообщать в личных сообщениях.
В переводе участвовали: Postscripter, al-tarakanoff, Алексей Брагин, Мабу
Автор: @Jeditobe Thomas Faber
Фонд ReactOS
рейтинг 166,45
Операционная система
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Комментарии (9)

  • +12
    Хотелось бы знать, что не понравилось хабраюзерам, молчаливо поставившим топику отрицательную оценку.
    • +4
      Читая заголовок, я ожидал насладиться глубоким исследованием со статистикой и погружением в дебри бессознательных процессов, таящихся в душе у программиста.

      А увидел баянную ошибку, которая кроме эмоции «о, опять классические грабли» ничего не вызвала.

      И да, где тут реверс-инжиниринг (если взять код из SVN — реверс-инжиниринг, то я — трамвай) и C++?
      • +3
        Заголовок авторский, статья — перевод, автор с удовольствием ознакомится с Вашей обратной связью по электронной почте.

        баянную ошибку

        Кому-то баян, а кому-то в новинку. Туториал же.
        • –6
          Я лингвист никудышный, но заголовок:
          How do security issues happen?

          В данном контексте означает «Как [здесь просится вписать „у нас“] возникли проблемы с безопасностью», что лучше соответствует содержанию статьи.
      • +6
        Это же мода такая: дать громкий заголовок, наделать кучу левых тегов, в начало темы воткнуть интригующую картинку и налить воды в содержание.
        Наиболее распространенный вид тем на БХ.
  • –1
    Да уж. Правда, после того, как продемонстрировали OpenSSH Heartbleed, нас уже ничем не удивить…
  • +3
    Интересно, почему до сих пор SVN, почему не git.

    И еще интересна была бы статья или хотя бы описание того, как именно работают эти «перехватчики исключений» на уровне ассемблера. А то магия какая-то.
    • 0
      Про git отвечу анекдотом:
      Сидит папа-программист за компьютером. Подходит сынишка и спрашивет:
      — Папа, почему солнышко утром всходит, а вечером заходит?
      Папа нехотно отрывается от монитора…
      — А ты проверял?
      — Проверял.
      — Хорошо проверял?
      — Хорошо.
      — Работает?
      — Работает.
      — Каждый день?
      — Да, каждый день.
      — Тебя устраивает?
      — Да, устраивает.
      — Тогда ради бога, сынок, ничего не трогай и ничего не меняй!!!


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

      На гит мы возможно перейдем, но позже. К примеру, смена билдсистемы с rbuild на cmake у нас заняла не менее года, оказалось очень много скрытых нюансов.
  • 0
    Сорри коллеги. Мне не нравится код ни до ни после исправления (

    Конечно я может быть не понимаю всех нюансов кодирования на уровне ядра, но все же… функция очень длинная, я бы разделили ее на три по одной для каждого из поддерживаемых значений ThreadInformationClass и вызывал их из NtUserSetInformationThread.

    Кроме того я бы перенес вызов UserEnterExclusive() туда где он действительно необходим, во всяком случае после проверки аргументов, которые наверняка можно проверять без эксклюзивного режима.

    ThreadHandle используется не во всех кейсах switch-а, я бы делал вызов ObReferenceObjectByHandle(ThreadHandle, ...) только по необходимости и только для тех ThreadInformationClass для которых он действительно нужен, и опять же после проверки аргументов.
    Зачем захватывать ресурсы которые не нужны?

    Сообщение ERR(«Shutdown initiated\n») на самом деле не всегда будет говорить правду. Потому что если аргументы не пройдут проверку UserInitiateShutdown() не будет вызван и соотвественно сообщение Shutdown initiated в логах будет говорить не правду.

    Кейс когда нужно проверить указатель на возможность чтения или записи и вернуть NTSTATUS наверное не единственный. Для этого можно иметь одну inline функцию или макрос которая(ый) будет использоваться везде.

    И эта функция не единственная где есть похожие проблемы (

    Можете мне сказать что предлагаемые изменения сделают код более длинным, добавят в стек исполнения еще один вызов функции что негативно повлияет на производительность. Это все правда. Но они точно улучшат читабельность и понятность кода что приведет к повышению безопасности.

    Как то так… Может быть я и не прав.

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

Самое читаемое Разработка