Содержание статьи
«Сон разума рождает чудовищ», — гласит испанская пословица. Немного навыков системного программирования, IDA Pro в умелых руках, ну и самое главное — исходные коды Windows aka WRK, и на свет начинают выползать кошмары из сна операционной системы Windows. Не терпится узнать, какие?
Необычный взгляд на обычные вещи
Сколько раз в популярной IT-литературе описывался механизм перехода из ring3 в ring0 ОС Windows? Не счесть! При этом авторы, копипастя друг у друга фактически один и тот же текст, подробно или не очень описывали, что произойдет, если пользователь вызовет простую функцию CreateFile().
Сегодня мы попробуем взглянуть на эту проблему с несколько неожиданной стороны. По утверждениям знающих людей, существует один «proof of concept»’ный способ, позволяющий выполнять свой код на привилегированном уровне и пользоваться сервисами ядра напрямую, то есть в обход существующих ограничений, которые на тебя накладывает пользовательский (ring3) уровень. Да-да, ты не ошибся, — посмотрим, можно ли ядро системы «подергать за вымя» напрямую. Все, что тебе для этого понадобится, это хорошие знания ядра, подсистемы ввода/вывода и изворотливость (или даже извращенность :)) ума.
Речь пойдет об упомянутом мной механизме перехода из пользовательского уровня (ring3) в привилегированный уровень (уровень ядра). Попробуем поразмыслить и посмотреть на, казалось бы, со всех сторон облизанный и всем известный сценарий с другой стороны — вдруг мы что-нибудь оставили без внимания? Предположим, что у нас в руках некая 0-day уязвимость, которая позволяет скомпрометировать ядро и выполнить привилегированный код. Например, как в случае с эксплуатацией бага nt!ZwSystemDebugControl в Windows. Одна из главных проблем, без решения которой вообще не обойтись, это необходимость изыскать способ возврата в нормальное ring3-состояние после того как ты выполнишь свой ring0-код. Существует два пути, которые здесь можно использовать. Первый — это самому реализовать код выхода с использованием асмовских инструкций iret или sysexit. Второй — заюзать собственные процедуры ядра, которые оно использует для таких операций (то есть выхода из ring0 в usermod’ный режим).
Первый способ трудоемок и сложен в реализации, поэтому в рамках этой статьи я его рассматривать не буду. Второй способ, наоборот, очень даже интересен и вдобавок может быть реализован с использованием многочисленных техник. Одну из них (попытку использования ядерной функции nt!KiServiceExit) мы сейчас и рассмотрим. Для начала нам нужно будет найти функцию в ядре, так как она не экспортируется. Для этого целесообразнее всего использовать сигнатурный скан, если ты, конечно, умеешь пользоваться дизассемблером длин. Отмечу, что эта функция далеко не единственная, которую можно заюзать для наших коварных целей.
При этом надо иметь в виду, что большинство операций перехода ring0-ring3 (такие как вызов системных функций, прерывания, исключения) используют стек ядра. Это, в свою очередь, дает нам возможность воспользоваться одной из таких системных функций, чтобы вернуться в пользовательский режим из переходов «ring0-ring3», которые вызываются различными событиями в системе. Главным требованием к такой функции является то, что она должна оканчиваться асмовскими инструкциями iret/ sysexit, ответственными за переход между режимами.
Таких функций несколько:
- KiSystemCallExit;
- KiSystemCallExit2;
- KiServiceExit;
- KiServiceExit2;
- KiGetTickCount;
- Kei386EoiHelper;
- KiTrap02, KiTrap06, KiTrap0D;
- KiCallbackReturn;
То есть, теоретически, если мы не найдем KiServiceExit, то всегда можно будет попытаться найти адрес похожей функции — тем более, что архитектура ОС Windows это позволяет :).
Опять лезем в WRK и внимательно читаем описание каждой функции. И тут на свет вылезает крайне занимательная штука — оказывается, что функции KiExceptionExit и Kei386EoiHelper выполняют практически одинаковую работу! Вот описание KiExceptionExit: «Код функции передается в конце обработки исключения. Его цель — восстановить состояние машины и продолжить исполнение потока. Если контроль будет возвращен в пользовательский режим и это будет постановка APC в очередь, то контроль передается в процедуру отправки APC».
А вот описание Kei386EoiHelper: «Код функции передается в конце обработки прерывания (через макрос EXIT_INTERRUPT). Он проверяет отправку APC и выполняет макрос EXIT_ALL для выхода из прерывания». Как видишь, в обоих случаях код отвечает за транзакции, вызванные исключениями пользовательского режима и прерываниями. И тут возникает второй вопрос — если эти две функции выполняют очень похожие операции, то почему же они используются раздельно? При этом, заметь, создатели WRK честно предупреждают, что KiExceptionExit и Kei386EoiHelper идентичны. Все дело, оказывается, в волшебных пузырьках, которые скрыты в передаваемых макросу EXIT_ALL параметрах:
KiServiceExit:
EXIT_ALL NoRestoreSegs,
NoRestoreVolatile
Kei386EoiHelper:
EXIT_ALL ,,NoPreviousMode/
Примечание: только не подумай, что я запутался между KiExceptionExit и KiServiceExit, поскольку в тексте они вроде как постоянно друг друга подменяют. Для прояснения ситуации я посоветую тебе курить файл base ntoskei386trap.asm из WRK.
Теперь взглянем на реализацию макроса EXIT_ALL, а точнее — на описания известных нам параметров: NoRestoreSegs, NoRestoreVolatile и NoPreviousMode.
Параметр NoRestoreSegs означает, что обработчику выхода в пользовательский режим не нужно восстанавливать регистры DS, ES, GS. Параметр NoRestoreVolatile значит, что обработчику выхода не нужно восстанавливать так называемые волатильные регистры, а параметр NoPreviousMode говорит обработчику выхода из прерывания, что следует отказаться от копирования значения PreviousMode (сохраненного в трап-фрейме) в одно из полей структуры KTHREAD.
Ну, как тебе новость? Дальше будет еще интереснее. Когда речь идет о первых двух аргументах (NoRestoreSegs и NoRestoreVolatile), для нас неважно, есть они или нет. Эти параметры действительно важны для нормального функционирования операционной системы, когда содержание регистров должно быть сохранено и/или восстановлено при наступлении определенных событий. В данном конкретном случае нам не нужно заботиться о сохранении контекста процессора — нам сейчас главное повысить уровень наших привилегий, а основной код будет выполнен в пользовательском режиме.
Самое интересное — это параметр NoPreviousMode. Если ты не знаешь значение волшебного слова PreviousMode, то бегом учить матчасть. Вкратце скажу: это изменяемый параметр, который говорит операционной системе (например, вызову nt!KiSystemService) откуда идет вызов кода: из пользовательского режима или из ядра. Если обработчик заподозрит неладное — будет сгенерировано исключение. Здесь нужно помнить такую вещь, что вызов системных Zw*-функций может происходить как из пользовательского режима, так и непосредственно самим ядром и драйверами. В первом случае используется инструкция SYSENTER/SYSCALL (или прерывание INT0xE, которое было оставлено для совместимости вплоть до Windows 7) для перехода в привилегированный режим. Во втором случае ядро может напрямую вызвать ту или иную системную функцию, для чего, как правило, используется KiSystemService.
.text:00405FCC ; NTSTATUS __stdcall ZwOpenFile@24
.text:00405FCC mov eax, 74h
.text:00405FD1 lea edx, [esp+0x4]
.text:00405FD5 pushf
.text:00405FD6 push 8
.text:00405FD8 call KiSystemService
.text:00405FDD retn 18h
.text:00405FDD _ZwOpenFile@24 endp
Вот тут-то и проявляет себя PreviousMode — она оказывается критической для функций обработки выхода в пользовательский режим. Ведь если при выходе из привилегированного режима ядра не установить значение PreviousMode в UserMode, а оставить его «как есть», то дальнейшая работа кода будет происходить именно в режиме ядра. И создатели Microsoft милостиво предоставили в наши коварные руки инструмент, который позволяет это сделать.
Именно поэтому я акцентировал внимание на функции Kei386EoiHelper — она идеально подходит на роль того трамплина, который позволит нам остаться в привилегированном режиме. При этом нужно помнить, что эта процедура использует оба стековых регистра — EBP и ESP. Несмотря на то, что в регистре ESP содержится валидный адрес (а он используется как обычный указатель стека до тех пор, пока мы не перехватили управление), нам нужно позаботиться о правильном для нас содержании регистра EBP. Это важно, так как при атаке переполнения буфера в случае выхода по инструкции RET значение стека переписывается атакующим кодом.
К счастью для нас, валидное значение EBP может быть легко восстановлено при помощи того же регистра ESP: MOV EBP, ESP. Подведем итоги и определим приблизительный план действий: во-первых, нам нужно отыскать базовый адрес загрузки ядра, затем с помощью сигнатурного скана найти там адрес функции nt!Kei386EoiHelper, далее — заставить систему ее выполнить (например, вызвать переполнение буфера). Не забудем подправить значение регистра EBP, прыгнуть на функцию nt!Kei386EoiHelper и продолжить выполнение своего кода уже в привилегированном режиме. Примерно так :).
Pro & cons
Как видно из содержания этой статьи, существует несколько ограничений, которые делают такой сценарий маловероятным, но все же вероятным. Нам нужно каким-то образом (скажем, переполнением буфера, которое сработает внутри системного обработчика) сгенерировать исключение. Такое развитие ситуации возможно, например, при каких-либо уязвимостях ядра. Таким образом, этот метод теоретически должен позволить выполнение твоего кода на привилегированном уровне. Однако, чтобы его завести, нужен хороший толчок, роль которого и должна сыграть некая гипотетическая уязвимость, которая сможет вызвать переполнение буфера в режиме ядра. А найти такую — ой как не просто. По признаниям самих же разработчиков ОС Windows, им хватает времени лишь на то, чтобы проконтролировать и протестить только код ядра. Значит ли это, что надежность и стабильность остальных компонентов остается без внимания? Судя по количеству найденных уязвимостей в ОС Windows — очень даже может быть.
Заключение
Не о самых простых вещах идет речь в этой статье. Однако понять их не так трудно. Иногда это похоже на собирание мозаики, когда детали, которые раньше тебе казались непонятными, вдруг становились на свои места в общей картине. Я не утверждаю, что метод, описанный в статье, будет работоспособным. Хотя мне кажется, что IT-спецам, занятым в сфере безопасности, уже пора начать искать нечто подобное в сети.
Удачного компилирования и да пребудет с тобой Сила!
Links
Не забывай посещать MSDN — как программист спешу заверить, что там можно найти ответы почти на все вопросы, которые будут возникать у тебя при системном кодинге.