[Перевод] Откуда берутся бреши в безопасности программ?
— У нас дыра в безопасности. — Ну, хоть что-то у нас в безопасности. Анекдот Если вы 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, Алексей Брагин, Мабу