В нашем предыдущем исследовании (https://research.checkpoint.com/2018/50-adobe-cves-in-50-days/), мы использовали WinAFL (https://github.com/googleprojectzero/winafl) для фаззинга приложений пользовательского пространства работающих в Windows, и обнаружили более 50 уязвимостей в Adobe Reader и Microsoft Edge.
Для нашего следующего испытания, мы решили пойти дальше чего-то большего: сделать фаззинга ядра Windows. В качестве дополнительного бонуса, мы можем взять наши ошибки в пользовательском пространстве и использовать их вместе с любыми ошибками ядра, которые мы обнаружим, чтобы создать полную цепочку - потому что удаленное выполнение кода без побега/повышения привилегий в песочнице в настоящее время практически ничего не стоят.
Помня о цели, мы решили исследовать ландшафт фаззера ядра, посмотреть, какие у нас есть варианты для достижения нашей цели, и, возможно, серьезно изменить существующие инструменты, чтобы они лучше соответствовали нашим потребностям.
В этом техническом документе упоминается доклад, который мы провели на OffensiveCon (https://www.offensivecon.org/speakers/2020/netanel-ben-simon-yoav-alon.html) и BlueHat IL(https://www.bluehatil.com/abstracts#collapse-FuzzingWindowsKernel) в начале этого года.
Изучение фаззеров ядра
У нас большой опыт работы с AFL (https://github.com/google/AFL) и WinAFL (https://github.com/googleprojectzero/winafl), поэтому мы начали наше путешествие с поиска аналогичного фаззера, который можно использовать для атаки на ядро Windows.
Короткий поиск в Google неизбежно привел нас к kAFL (https://github.com/RUB-SysSec/kAFL), AFL с `k`, так как префикс звучит именно так, как нам нужно.
KAFL
kAFL (https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-schumilo.pdf) - исследовательский фазер из университета Ruhr-Universität Бохума, который использует фаззинг в стиле AFL для атаки на ядра ОС. На первый взгляд, это было именно то, что мы искали. kAFL поддерживает Linux, macOS и Windows и использовался для поиска уязвимостей в файловой системе Ext4 ядра Linux и в macOS.
Принцип kAFL аналогичен AFL, но, поскольку он нацелен на ядра ОС, ему необходимо больше работать над циклом фаззинга. Цикл фаззинга - это процесс, в котором в каждом цикле один контрольный пример проверяется относительно его цели и обрабатывается обратная связь.
Рисунок 1: Цикл фаззинга.
При первом запуске kAFL, фаззер (1) запускает несколько виртуальных машин, на которых работает целевая ОС, из сохраненного состояния. В снимке виртуальной машины есть предварительно загруженный агент (2), работающий внутри виртуальной машины.
Агент (2) и фаззер (1) взаимодействуют для продвижения процесса фаззинга. Агент работает в пользовательском пространстве и начинает связываться с фаззером через гипер-вызовы (https://wiki.xenproject.org/wiki/Hypercall) и отправляет адреса диапазона целевого драйвера на фаззер. Адреса ограничивают трассировки покрытия кода только для диапазонов, которые предоставляет агент.
В начале цикла, фаззер отправляет агенту ввод (3) через общую память. kAFL использует стратегию мутации, аналогичную AFL, для создания новых входных данных.
Затем агент уведомляет гипервизор о начале (4) сбора покрытия. Затем агент отправляет (5) входные данные целевому компоненту ядра: например, если мы нацеливаемся на драйвер с именем test.sys (6), который отвечает за анализ сжатых изображений, агент отправляет сгенерированный ввод драйверу для его тестирования.
Наконец, агент просит прекратить (7) сбор покрытия с KVM (8), и фаззер обрабатывает трассировку покрытия. Реализация покрытия kAFL использует Intel Processor Trace (IntelPT или IPT - https://software.intel.com/en-us/blogs/2013/09/18/processor-tracing) для механизма обратной связи покрытия.
Когда гостевая ОС пытается запустить, остановить или (9) собрать покрытие, она выполняет гипер-вызов на KVM (https://www.linux-kvm.org/page/Main_Page).
Механизм обнаружения сбоев в kAFL работает следующим образом:
Рисунок 2: Обнаружение крэша kAFL.
Агент (1) внутри виртуальной машины выполняет гипервызов (2) для KVM с адресами функцией BugCheck и BugCheckEx. KVM (3), в свою очередь, патчит (4) эти адреса с помощью шелл-кода (5), который выполняет гипервызов при выполнении.
Поэтому, когда машина сталкивается с багом, ядро вызывает исправленные версии функций BugCheck или BugCheckEx для выполнения гипервызова, чтобы уведомить(6) фаззер о сбое.
Теперь, когда мы понимаем механизмы, мы рассмотрели, как это можно отрегулировать в соответствии с нашими потребностями в средах Windows.
Что атаковать?
Ядро Windows огромно, содержит десятки миллионов строк кода(https://techcommunity.microsoft.com/t5/Windows-Kernel-Internals/One-Windows-Kernel/ba-p/267142) и миллионы исходных файлов(https://github.com/dwizzzle/Presentations/blob/master/David Weston - Keeping Windows Secure - Bluehat IL 2019.pdf).
Наше внимание сосредоточено на деталях, которые доступны из пространства пользователя. Эти части довольно сложны и могут быть использованы для Локального Повышения Привилегий (LPE).
Исходя из нашего опыта, AFL подходит для следующих целей:
- Быстрые цели, которые могут выполнять более 100 итераций в секунду.
- Парсеры - особенно для двоичных форматов.
Это соответствует тому, что написал Михал Залевский в README файле AFL: "По умолчанию, механизм мутации afl-fuzz оптимизирован для компактных форматов данных - например, изображений, мультимедиа, сжатых данных, синтаксиса регулярных выражений или сценариев оболочки. Он несколько менее подходит для языков с особенно многословным и избыточным словообразованием, особенно в том числе HTML, SQL или JavaScript". Мы искали подходящие цели в ядре Windows (рисунок 3).
Рисунок 3: Компоненты ядра Windows.
Вот цели, про которые мы говорили.
- Файловые системы, такие как NTFS, FAT, VHD и другие.
- Кусты реестра.
- Крипто/Целостность кода (CI).
- PE Формат.
- Шрифты (которые были перемещены в пространство пользователя, начиная с Windows 10).
- Графические драйверы.
Типичная ошибка ядра в Windows
Мы вернулись назад и посмотрели на довольно типичную ошибку ядра - CVE-2018-0744 (https://bugs.chromium.org/p/project-zero/issues/detail?id=1389):
Рисунок 4: Типичная ошибка в win32k.
Эта программа содержит несколько системных вызовов, которые принимают в качестве входных данных высоко-структурированные данные, такие как структуры, константы (магические числа), указатели функций, строки и флаги.
Кроме того, существует зависимость между системными вызовами: вывод одного системного вызова используется как вход для других системных вызовов. Этот тип структуры очень распространен в случае ошибок ядра, когда последовательность системных вызовов используется для достижения ошибочного состояния, когда возникает уязвимость.
Важность структурированного понимания фаззинга и примеров можно найти здесь (https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md).
Поверхность атаки ядра Windows: kAFL против фазера системных вызовов
После того, как мы наблюдали ошибку, описанную выше, мы поняли, что использование фаззера в стиле AFL ограничит нас относительно небольшими частями ядра. Большая часть ядра Windows доступна из системных вызовов, которые включают в себя высоко-структурированные данные, но использование kAFL ограничит нас двоичными синтаксическими анализаторами в ядре, такими как драйверы устройств, файловые системы, формат PE, реестр и другие. Эти части относительно невелики по сравнению с объемом кода, доступным из системных вызовов. Поэтому, если бы у нас был фаззер системного вызова, мы могли бы потенциально охватить больше поверхностей для атак, таких как управление виртуальной памятью, диспетчер процессов, графика, пользовательские функции, gdi, безопасность, сеть и многие другие.
В этот момент мы поняли, что нам нужно искать системный фаззер.
Представляем Syzkaller
Syzkaller(https://github.com/google/syzkaller) - это структурированный фаззер ядра (a.k.a умный фазер системных вызовов).
Он поддерживает несколько операционных систем и работает на нескольких типах компьютеров (Qemu, GCE, мобильные телефоны и т.д.) и нескольких архитектурах (x86-64, aarch64).
На сегодняшний день Syzkaller (https://syzkaller.appspot.com/upstream) обнаружил 3700 ошибок в ядре Linux, по скромным меркам, одна из шести найденных ошибок - это ошибки безопасности.
Syzkaller является структурно-ориентированным фаззером, то есть имеет описание для каждого системного вызова.
Описания системного вызова записываются в текстовые файлы с использованием `go`-подобного синтаксиса(https://github.com/google/syzkaller...ptions_syntax.md#syscall-description-language).
Syz-sysgen является одним из инструментов Syzkaller и используется для анализа и форматирования описаний системных вызовов. Когда этот процесс успешно завершен, он преобразует текстовые файлы в код `go`, которые скомпилированы вместе с кодом фаззера, в исполняемый файл syz-fuzzer.
Syz-fuzzer - основной исполняемый файл для управления процессом фаззинга в гостевой виртуальной машине.
Syzkaller имеет собственный синтаксис для описания программ, системных вызовов, структур, объединений и многого другого. Сгенерированные программы также называются программами syz. Пример можно найти здесь (https://github.com/google/syzkaller/blob/master/docs/syscall_descriptions.md#syscall-descriptions).
Syzkaller использует несколько стратегий мутаций для мутации существующих программ. Syzkaller сохраняет программы, которые обеспечивают новое покрытие кода в формате syz, в базе данных. Эта база данных также известна как corpus.
Это позволяет нам остановить фаззер, внести изменения и продолжить с того же места, где мы остановились.
Рисунок 5: Архитектура Syzkaller (Linux).
Основной двоичный файл Syzkaller называется syz-manager (1). Когда он запускается, он выполняет следующие действия:
Загружает корпус (2) программ из более ранних запусков, запускает несколько тестовых (3) машин, копирует исполняемые файлы (6) и сам фаззер (5) на машину, используя ssh (4), и запускает программу Syz-fuzzer (5).
Затем Syz-fuzzer (5) выбирает корпус из менеджера и начинает генерировать программы. Каждая программа отправляется обратно менеджеру на ответственное хранение в случае сбоя. Затем Syz-fuzzer отправляет программу через межпроцессорное взаимодействие (7) исполнителю (6), который запускает системные вызовы (8) и собирает покрытие из ядра (9), KCOV в случае Linux.
KCOV (https://www.kernel.org/doc/html/v4.17/dev-tools/kcov.html) - это инструментальная функция времени компиляции, которая позволяет нам из пространства пользователя получать покрытие кода для каждого потока во всем ядре.
Если обнаруживается новая трасса покрытия, фаззер (11) сообщает об этом менеджеру.
Syzkaller стремится стать неконтролируемым фаззером, что означает, что он пытается автоматизировать весь процесс фаззинга. Примером этого свойства является то, что в случае сбоя Syzkaller порождает несколько машин-репродукторов для выделения сбойных программ syz из журнала программ. Воспроизводители стараются максимально свести к минимуму сбойную программу. Когда процесс завершится, большую часть времени Syzkaller будет воспроизводить либо программу syz, либо код C, который воспроизводит сбой. Syzkaller также может извлечь список сопровождающих из git и отправить им подробную информацию о сбое.
Syzkaller поддерживает ядро Linux и имеет впечатляющие результаты. Глядя на Syzkaller, мы подумали: если бы мы только могли фаззить ядро Linux в Windows. Это привело нас к изучению WSL.
WSLv1
Подсистема Windows для Linux (WSL) - это уровень совместимости для непосредственного запуска двоичных файлов Linux в Windows. Она переводит системными вызовы Linux в функции Windows. Первая версия была выпущена в 2016 году и включает в себя 2 драйвера: lxcore и lxss.
Она была разработан для запуска команд bash и ядра Linux для разработчиков.
WSLv1 использует облегченный процесс, называемый процессом pico, для размещения двоичных файлов Linux и специальных драйверов, называемых поставщиками pico, для обработки системных вызовов из процессов pico (дополнительную информацию см. здесь: (https://channel9.msdn.com/Blogs/Seth-Juarez/Windows-Subsystem-for-Linux-Architectural-Overview), (https://docs.microsoft.com/en-us/archive/blogs/wsl/windows-subsystem-for-linux-overview)).
Почему WSL?
Поскольку WSL относительно похож на ядро Linux, мы можем повторно использовать большую часть существующей грамматики для Linux и двоичных файлов syz-executor и syz-fuzzer, которые совместимы со средой Linux.
Мы хотели найти ошибки для Повышения Привилегий (PE), но WSL v1 не поставляется по умолчанию, и его может быть сложно использовать из песочницы, поскольку он выполняется в процессе другого типа (процесс PICO).
Но мы подумали, что было бы лучше получить опыт работы с Syzkaller на Windows с минимальными изменениями.
И началось портирование кода
Сначала мы установили дистрибутив Linux из магазина Microsoft и использовали Ubuntu в качестве нашего дистрибутива. Мы начали с добавления сервера ssh с командой "apt install openssh-server" и настроили ключи ssh. Далее мы хотели добавить поддержку трассировки покрытия. К сожалению, ядро Windows является закрытым исходным кодом и не предоставляет инструментарий времени компиляции, такой как KCOV в Linux.
Мы подумали о нескольких альтернативах, которые помогут нам получить трассировку покрытия:
- Использование эмулятора типа QEMU/BOCHS и добавление инструментария покрытия.
- Использование статических бинарных инструментов, как в pe-afl (https://github.com/wmliang/pe-afl).
- Использование гипервизора с выборкой покрытия, как в apple-pie (https://github.com/gamozolabs/applepie).
- Использование поддержки оборудования для покрытия, как Intel-PT.
Мы решили использовать Intel-PT, потому что он обеспечивает трассировки для скомпилированных двоичных файлов во время выполнения, он относительно быстр и предоставляет полную информацию о покрытии, что означает, что мы можем получить начальный указатель инструкций (IP) для каждого базового блока, который мы посетили, в исходном порядке.
Использование Intel-PT из нашей виртуальной машины, на которой работает целевая ОС, требует нескольких модификаций KVM.
Мы использовали большие части патчей kAFL kvm для поддержки покрытия Intel-PT.
Кроме того, мы создали KCOV-подобный интерфейс с помощью гипер-вызовов, поэтому, когда исполнитель пытается запустить, остановить или собрать покрытие, он исполняет гипер-вызовы.
Symbolizer #1
Механизм обнаружения сбоев Syzkaller считывает выходные данные консоли виртуальной машины и использует предопределенные регулярные выражения для обнаружения ошибки типа паники ядра, предупреждений и т.д.
Нам обязательно был нужен механизм обнаружения сбоев для нашего порта, поэтому мы могли вывести на выходную консоль предупреждение, которое Syzkaller может перехватить.
Для обнаружения BSOD мы использовали технику kAFL.
Мы пропатчили BugCheck и BugCheckEx с помощью шелл-кода, который выполняет гипевызов и уведомляет о сбое, записывая уникальное сообщение в выходную консоль QEMU.
Мы добавили регулярное выражение в syz-manager для обнаружения сообщений о сбоях с выходной консоли QEMU. Чтобы улучшить обнаружение ошибок в ядре, мы также использовали Driver Verifier со специальными пулами для обнаружения повреждений пула («verifyier/flags 0x1/driver lxss.sys lxcore.sys»).
Общая проблема с фаззерами заключается в том, что они сталкиваются с одной и той же ошибкой много раз.
Чтобы избежать повторяющихся багов, Syzkaller требует уникальный вывод для каждого сбоя.
Наш первый подход состоял в том, чтобы извлечь несколько относительных адресов из стека, которые находятся в пределах диапазонов модулей, которые мы отслеживаем, и распечатать их на выходной консоли QEMU.
Санитарная проверка
Перед запуском фаззера мы хотели убедиться, что он действительно может обнаружить реальную ошибку, иначе мы просто тратим процессорное время. К сожалению, в то время мы не смогли найти общедоступный PoC с реальной ошибкой для выполнения этого теста.
Поэтому мы решили исправить определенный поток в одном из системных вызовов для эмуляции ошибки.
Фаззер смог его найти, что было хорошим знаком, и мы запустили фуззер.
Первая попытка фазинга
Наш механизм обнаружения сбоев был основан на kAFL, где мы исправили функции BugCheck и BugCheckEx с помощью шелл-кода, который запускает гипервызов при сбое, который перехватывает Патч-Гард. Он для этого и был разработан (https://en.wikipedia.org/wiki/Kernel_Patch_Protection#Technical_overview).
Чтобы обойти эту проблему, мы добавили драйвер, который запускается при загрузке и регистрирует обратный вызов bugcheck с помощью ядра, используя функцию KeRegisterBugCheckCallback (https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-keregisterbugcheckcallback). Теперь, когда ядро дает сбой, оно вызывает наш драйвер, который затем запускает гипервызов, уведомляющий фаззер о сбоя.
Мы снова запустили фаззер и получили новую ошибку с другим кодом ошибки. Мы попытались воспроизвести сбой, чтобы помочь нам понять его, и обнаружили, что выполнение анализа первопричин из смещений и случайного мусора из стека затруднено.
Мы решили, что нам нужен лучший подход для получения информации о сбоях.
Symbolizer #2
Мы попытались запустить "kd"на нашей хост-машине под Wine для создания стека вызовов, но это не сработало, так как генерация стека вызовов заняла около 5 минут.
Такой подход создает узкое место для нашего фаззера. В процессе воспроизведения Syzkaller пытается свести к минимуму программы сбоя, насколько это возможно, и будет ожидать стек вызовов с каждой попыткой минимизации, чтобы определить, происходит ли такой же сбой.
Поэтому мы решили использовать удаленную машину Windows с KD и туннелировать все соединения udp. Это на самом деле хорошо работало, но когда мы увеличили его до 38 машин, соединения были разорваны, и Symbolizer подумал что все зависло.
Symbolizer #3
В этот момент мы спросили себя, как KD и WinDBG могут генерировать стек вызовов?
Ответ: они должны используют фукцнию StackWalk из библиотеки DbgHelp.dll.
Для генерации стека вызовов нам нужны StackFrame, ContextRecord и ReadMemoryRoutine.
Рисунок 7: Архитектура Symbolizer.
На рисунке 7 показана архитектура:
Мы получили стек, регистры и адреса драйверов из гостевой машины гостя, используя гипервизор KVM обратно в QEMU. Эмулятор QEMU отправил стек на удаленный компьютер с Windows, где наш symbolizer вызывает функцию StackWalk со всеми соответствующими аргументами и извлекает стек вызовов. Стек вызовов был распечатан обратно на консоль.
Эта архитектура была вдохновлена Bochspwn для Windows (https://github.com/googleprojectzero/bochspwn).
Теперь, когда мы получаем новый крэш, это выглядит так:
Symbolizer #4
Работа машины под управлением Windows вместе с нашим фаззером не идеальна, и мы подумали, как трудно будет реализовать минимальный отладчик ядра на языке `go` и скомпилировать его в Syzkaller.
Мы начали с парсера и сборщика файлов PDB. После этого мы реализовали раскрутку стека x64, используя информацию о раскрутке, хранящуюся в PE файле.
Последняя часть заключалась в том, чтобы реализовать серию KD, которая работала довольно медленно, поэтому мы начали работать над KDNET и после того, как мы закончили, интегрировали его в Syzkaller.
Это решение было намного лучше, чем предыдущие решения.
Наш механизм дедупликации теперь основан на ошибочном кадре. Мы также получаем код ошибки функции BugCheck, регистры и стек вызовов.
Стабильность покрытия
Другой проблемой, с которой мы столкнулись, была стабильность покрытия.
Syzkaller использует несколько потоков для поиска гонок данных.
Например, когда сгенерированная программа имеет 4 системных вызова, она может разделить ее на два потока, чтобы один поток запускал системные вызовы 1 и 2, а другой поток - системные вызовы 3 и 4.
В нашей реализации покрытия мы использовали один буфер на процесс. На практике, запуск одной и той же программы несколько раз приведет к различным трассам покрытия при каждом запуске.
Нестабильность покрытия ухудшает способность фаззеров находить новые и интересные пути кода и баги.
Мы хотели решить эту проблему, изменив нашу реализацию покрытия, чтобы она была похожа на реализацию KCOV.
Мы знали, что KCOV отслеживает покрытие для каждого потока, и мы хотели иметь этот механизм.
Для создания KCOV-подобных трасс нам понадобится:
- Отслеживание потоков в KVM для замены буферов.
- Добавление информации о дескрипторе потока в наш API гипервызова.
Для отслеживания потоков нам понадобился хук для переключения контекста. Мы знаем, что можем получить текущий поток из глобального сегмента:
Рисунок 8: Функция KeGetCurrentThread.
Мы посмотрели, что происходит во время переключения контекста, и мы нашли инструкцию swapgs в функции, которая обрабатывает переключение контекста. Когда происходят замены, это провоцирует вызов VMExit, который гипервизор может перехватить.
Рисунок 9: swapgs внутри функции SwapContext.
Это означает, что если мы можем отслеживать замены, мы также можем отслеживать изменения потоков в гипервизоре KVM.
Это выглядело как хорошая точка для отслеживания переключения контекста и обработки IntelPT для отслеживаемых потоков.
Это позволило нам подключать и переключать буферы ToPa при каждом переключении контекста. Записи ToPa описывают Intel-PT физические адреса, по которым он может записать вывод трассировки.
Нам все еще оставалось решить несколько мелких проблем:
- Отключение служб и автоматически загружаемых программ, а также ненужных служб для ускорения загрузки.
- Обновление Windows случайно перезапустило наши машины и потребило много ресурсов процессора.
- Защитник Windows случайно убил наш фаззера.
В общем (https://support.microsoft.com/en-us/help/15055/windows-7-optimize-windows-better-performance), мы настроили нашу гостевую машину на лучшую производительность.
Результаты WSL фазинга
В целом, мы фаззили WSL в течение 4 недель и использовали 38 виртуальных процессоров. В конце у нас был рабочий прототип и гораздо лучшее понимание того, как работает Syzkaller.
Мы нашли 4 ошибки DoS и несколько взаимоблокировок. Однако мы не обнаружили какой-либо уязвимости в системе безопасности, что нас разочаровало, тогда мы решили перейти к целевому PE.
Движение к реальной цели
Фаззинг WSL был хорошим способом познакомиться с Syzkaller в Windows. Но в этот момент мы хотели вернуться к настоящей цели повышения привилегий - той, которая по умолчанию поставляется с Windows и доступна из различных песочниц.
Мы посмотрели на поверхность атаки ядра Windows и решили начать с подсистемы Win32k. Win32k - это сторона ядра подсистемы Windows, которая является инфраструктурой графического интерфейса операционной системы. Это также общая цель для локального повышения привилегий (LPE), потому что она доступна из многих песочниц.
Она включает в себя две подсистемы:
- Диспетчер окон, также известная как User.
- Интерфейс графического устройства, также известная как GDI.
Подсистема имеет много системных вызовов (~ 1200), что означает, что она является хорошей целью для грамматических фаззеров (как показано ранее CVE-2018-0744).
Начиная с Windows 10, win32k разделена на несколько драйверов: win32k, win32kbase и win32kfull.
Чтобы заставить Syzkaller работать для подсистемы win32k, нам пришлось изменить несколько вещей:
- Скомпилировать фаззер и исполняемые файлы для Windows.
- Изменения, связанные с ОС.
- Раскрытие системных звонков Windows в фаззер.
- Кросс-компиляция с mingw ++ для удобства.
Настройки подсистемы Win32k
Начиная с исходного кода фаззера, мы добавили соответствующую реализацию для Windows, такую как каналы, разделяемая память и многое другое.
Грамматика является важной частью фаззера, который мы подробно объясним позже.
Затем мы перешли к исправлению исполнителя для кросс-компиляции с использованием MinGW. Нам также пришлось пофиксить разделяемую память, пайпы и отключить режим ветвления, так как он не существует в Windows.
В рамках компиляции грамматики syz-sysgen генерирует файл заголовка (syscalls.h), который включает все имена\числа системных вызовов.
В случае с Windows мы остановились на экспортированных оболочках системных вызовов и функциях WinAPI (например, таких как CreateWindowExA и NtUserSetSystemMenu).
Большая часть оболочки syscalls экспортируется в библиотеки win32u.dll и gdi32.dll. Чтобы представить их нашему исполняемому файлу, мы использовали gendef (https://sourceforge.net/p/mingw-w64/wiki2/gendef/) для генерации файлов определений из dll. Затем мы использовали mingw-dlltool для создания библиотечных файлов и в конце концов связали их с главным файлов.
Санитарная проверка
Как мы уже говорили ранее, мы хотели убедиться, что наш фаззер способен воспроизводить старые ошибки, так как в противном случае мы тратим процессорное время.
На этот раз мы взяли настоящий баг (CVE-2018-0744, см. Рисунок 4), и мы хотели ее воспроизвести. Мы добавили соответствующие системные вызовы и позволили фаззеру найти его, но, к сожалению, это не удалось. Мы подозревали, что у нас есть ошибка, поэтому мы написали программу syz и использовали syz-execprog, Syzkaller для непосредственного выполнения программ syz, чтобы убедиться, что она работает. Системные вызовы были успешно вызваны, но, к сожалению, система не сломалась.
Через некоторое время мы поняли, что фаззер работает под сеансом 0. Все службы, включая нашу службу ssh, являются консольными приложениями, которые выполняются в сеансе 0 и не предназначены для работы с графическим интерфейсом. Таким образом, мы изменили его для запуска в качестве обычного пользователя в сеансе 1. Как только мы это сделали, Syzkaller смог успешно воспроизвести ошибку.
Мы пришли к выводу, что мы всегда должны тестировать новый код, эмулируя ошибки или воспроизводя старые.
Проверка стабильности
Всего мы добавили 15 новых функций и снова запустили фаззер.
Мы получили первый сбой в функции win32kfull!_OpenClipboard, это был сбой типа Use-After-Free. Но по какой-то причине, этот сбой не воспроизводился на других машинах. Сначала мы подумали, что это из-за другой ошибки, которую мы создали, но сбой был воспроизведён на той же машине, но без фаззера.
Стек вызовов и сбойная программа не помогли нам понять, что случилось.
Поэтому, мы пошли и посмотрели в IDA где произошел сбой:
Рисунок 11: Сбойное место - win32kfull! _OpenClipboard.
Мы заметили, что сбой происходит внутри условного блока, где он зависит от флага поставщика ETW: Win32kTraceLoggingLevel.
Этот флаг включен на некоторых машинах и выключен на других, поэтому мы заключаем, что мы, вероятно, получили A/B-тестовую машину.
Мы зарепортили об этом баге и снова установили Windows.
Мы снова запустили фаззер и получили новую ошибку, на этот раз отказ в обслуживании в функции RegisterClassExA. На данный момент наша мотивация взлетела до небес, потому что если 15 системных вызовов привели к 2 ошибкам, это означает, что 1500 системных вызовов приведут к 200 ошибкам.
Грамматика в win32k
Поскольку ранее не было публичного исследования системного вызова win32k, нам пришлось создавать правильную грамматику с нуля.
Мы получили первый сбой в функции win32kfull!_OpenClipboard, это был сбой типа Use-After-Free. Но по какой-то причине, этот сбой не воспроизводился на других машинах. Сначала мы подумали, что это из-за другой ошибки, которую мы создали, но сбой был воспроизведён на той же машине, но без фаззера.
Нашей первой мыслью было, что, возможно, мы сможем автоматизировать этот процесс, но мы столкнулись с двумя проблемами:
Во-первых, заголовочных файлов Windows недостаточно для создания грамматики, поскольку они не предоставляют важную информацию для фаззера системного вызова, например уникальные строки, некоторые параметры DWORD фактически являются флагами, а многие структуры определены как LPVOID.
Во-вторых, многие системные вызовы просто не документированы (например, функция NtUserSetSystemMenu).
К счастью, многие части Windows являются технически открытым исходным кодом:
- Windows NT Leaked sources – https://github.com/ZoloZiak/WinNT4
- Windows 2000 Leaked sources – https://github.com/pustladi/Windows-2000
- ReactOS (Leaked w2k3 sources?) – https://github.com/reactos/reactos
- Windows Research Kit – https://github.com/Zer0Mem0ry/ntoskrnl
Мы искали каждый системный вызов в MSDN и в просочившихся источниках, а также проверяли его с помощью IDA и WinDBG.
Многие сгенерированные нами сигнатуры функций было легко создать, но некоторые были настоящим кошмаром - включали множество структур, недокументированных аргументов, некоторые системные вызовы имели 15 и более аргументов.
После нескольких сотен системных вызовов мы снова запустили фаззер и получили 3 уязвимости GDI и несколько ошибок типа Отказ в Обслуживании.
На данный момент мы рассмотрели несколько сотен системных вызовов в win32k. Мы хотели найти больше ошибок. Таким образом, мы пришли к выводу, что пришло время углубиться в поиски дополнительной информации о подсистеме Win32k и выйти на более сложные поверхности атаки.
Фаззеры не являются магией, но, чтобы найти ошибки, мы должны убедиться, что мы покрываем большинство поверхностей атаки по нашей цели.
Мы вернулись назад, чтобы прочитать больше о подсистемеWin32k, понять старые ошибки и классы ошибок. Затем мы попытались поддержать недавно изученные поверхности атаки нашего фаззера.
Одним из примеров является GDI Shared Handle. _PEB! GdiSharedHandleTable - это массив указателей на структуру, которая содержит информацию об общих дескрипторах GDI между всеми процессами.
Мы добавили это в Syzkaller, добавив псевдо-системный вызов GetGdiHandle (тип, индекс), который получает тип дескриптора и индекса. Эта функция перебирает массив таблиц общих дескрипторов GDI от инициализации до индекса и возвращает последний дескриптор того же типа, что и запрошенный.
Это привело к CVE-2019-1159(https://cpr-zero.checkpoint.com/vulns/cprid-2132/), Use-After-Free, запускаемому одним системным вызовом с глобальным дескриптором GDI, который создается при загрузке.
Результаты
Мы фаззили почти 1,5 месяца и запускали все это на 60 процессорах.
Мы нашли 10 уязвимостей (3 pending , 1 дубликат)
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1014
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1096
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1159
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1164
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1256
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1286
Мы также обнаружили 3 ошибки DoS, 1 сбой в WinLogon и несколько взаимоблокировок.
LPE → RCE?
Локальные ошибки повышения привилегий - это круто, но как насчет удаленного выполнения кода?
Представляем WMF - формат метафайлов Windows.
WMF - это формат файла изображения. Он был разработан еще в 1990-х годах и поддерживает как векторную графику, так и растровые изображения. Microsoft расширила этот формат на протяжении многих лет как следующие форматы
- EMF
- EMF+
- EMFSPOOL
Microsoft также добавила функцию в этот формат, которая позволяет добавлять записи, которые воспроизводятся для воспроизведения графического вывода. Когда эти записи воспроизводятся, анализатор изображений вызывает системный вызов NtGdi. Вы можете прочитать больше об этом формате в лекции j00ru(https://j00ru.vexillium.org/slides/2016/pacsec.pdf).
Количество системных вызовов, принимающих файл EMF, ограничено, но, к счастью для нас, мы обнаружили уязвимость в функции StretchBlt, которая принимает файл EMF.
Резюме
Нашей целью было найти ошибки ядра Windows с помощью фаззера.
Мы начали исследовать ландшафт фаззеров в ядре Windows, и поскольку у нас был опыт работы с фаззерами стиля AFL, мы искали тот, который работает аналогично, и нашли kAFL.
Мы смотрели на kAFL и искали поверхности атаки в ядре Windows, но быстро выяснили, что фаззер системного вызова может достичь гораздо большего количества поверхностей атаки.
Мы искали fuzzers для системных вызовов и нашли Syzkaller.
В этот момент мы начали портировать его на WSL, так как он наиболее похож на ядро Linux, и мы могли получить некоторый опыт работы с Syzkaller в Windows. Мы реализовали инструментарий покрытия для ядра Windows с помощью IntelPT. Мы поделились механизмом обнаружения сбоев, который использовался для устранения дубликатов ошибок. Мы нашли несколько проблем со стабильностью покрытия и поделились своим решением.
После того, как мы обнаружили некоторые баги типа DoS, мы решили перейти к реальной цели файла типа PE - подсистемы win32k - но нам пришлось реализовать недостающие части в Syzkaller. Затем мы провели проверку работоспособности и стресс-тест, чтобы убедиться, что фаззер не тратит процессорное время. После этого мы потратили много времени на написание грамматики, чтение о нашей цели и в конечном итоге добавление поддержки вновь изученных частей подсистемы Win32k обратно в фаззер.
В целом, наше исследование привело нас к обнаружению 8 уязвимостей, ошибок DoS и взаимоблокировок в ядре Windows 10.
Источник: https://research.checkpoint.com/2020/bugs-on-the-windshield-fuzzing-the-windows-kernel/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
Для нашего следующего испытания, мы решили пойти дальше чего-то большего: сделать фаззинга ядра Windows. В качестве дополнительного бонуса, мы можем взять наши ошибки в пользовательском пространстве и использовать их вместе с любыми ошибками ядра, которые мы обнаружим, чтобы создать полную цепочку - потому что удаленное выполнение кода без побега/повышения привилегий в песочнице в настоящее время практически ничего не стоят.
Помня о цели, мы решили исследовать ландшафт фаззера ядра, посмотреть, какие у нас есть варианты для достижения нашей цели, и, возможно, серьезно изменить существующие инструменты, чтобы они лучше соответствовали нашим потребностям.
В этом техническом документе упоминается доклад, который мы провели на OffensiveCon (https://www.offensivecon.org/speakers/2020/netanel-ben-simon-yoav-alon.html) и BlueHat IL(https://www.bluehatil.com/abstracts#collapse-FuzzingWindowsKernel) в начале этого года.
Изучение фаззеров ядра
У нас большой опыт работы с AFL (https://github.com/google/AFL) и WinAFL (https://github.com/googleprojectzero/winafl), поэтому мы начали наше путешествие с поиска аналогичного фаззера, который можно использовать для атаки на ядро Windows.
Короткий поиск в Google неизбежно привел нас к kAFL (https://github.com/RUB-SysSec/kAFL), AFL с `k`, так как префикс звучит именно так, как нам нужно.
KAFL
kAFL (https://www.usenix.org/system/files/conference/usenixsecurity17/sec17-schumilo.pdf) - исследовательский фазер из университета Ruhr-Universität Бохума, который использует фаззинг в стиле AFL для атаки на ядра ОС. На первый взгляд, это было именно то, что мы искали. kAFL поддерживает Linux, macOS и Windows и использовался для поиска уязвимостей в файловой системе Ext4 ядра Linux и в macOS.
Принцип kAFL аналогичен AFL, но, поскольку он нацелен на ядра ОС, ему необходимо больше работать над циклом фаззинга. Цикл фаззинга - это процесс, в котором в каждом цикле один контрольный пример проверяется относительно его цели и обрабатывается обратная связь.
Рисунок 1: Цикл фаззинга.
При первом запуске kAFL, фаззер (1) запускает несколько виртуальных машин, на которых работает целевая ОС, из сохраненного состояния. В снимке виртуальной машины есть предварительно загруженный агент (2), работающий внутри виртуальной машины.
Агент (2) и фаззер (1) взаимодействуют для продвижения процесса фаззинга. Агент работает в пользовательском пространстве и начинает связываться с фаззером через гипер-вызовы (https://wiki.xenproject.org/wiki/Hypercall) и отправляет адреса диапазона целевого драйвера на фаззер. Адреса ограничивают трассировки покрытия кода только для диапазонов, которые предоставляет агент.
В начале цикла, фаззер отправляет агенту ввод (3) через общую память. kAFL использует стратегию мутации, аналогичную AFL, для создания новых входных данных.
Затем агент уведомляет гипервизор о начале (4) сбора покрытия. Затем агент отправляет (5) входные данные целевому компоненту ядра: например, если мы нацеливаемся на драйвер с именем test.sys (6), который отвечает за анализ сжатых изображений, агент отправляет сгенерированный ввод драйверу для его тестирования.
Наконец, агент просит прекратить (7) сбор покрытия с KVM (8), и фаззер обрабатывает трассировку покрытия. Реализация покрытия kAFL использует Intel Processor Trace (IntelPT или IPT - https://software.intel.com/en-us/blogs/2013/09/18/processor-tracing) для механизма обратной связи покрытия.
Когда гостевая ОС пытается запустить, остановить или (9) собрать покрытие, она выполняет гипер-вызов на KVM (https://www.linux-kvm.org/page/Main_Page).
Механизм обнаружения сбоев в kAFL работает следующим образом:
Рисунок 2: Обнаружение крэша kAFL.
Агент (1) внутри виртуальной машины выполняет гипервызов (2) для KVM с адресами функцией BugCheck и BugCheckEx. KVM (3), в свою очередь, патчит (4) эти адреса с помощью шелл-кода (5), который выполняет гипервызов при выполнении.
Поэтому, когда машина сталкивается с багом, ядро вызывает исправленные версии функций BugCheck или BugCheckEx для выполнения гипервызова, чтобы уведомить(6) фаззер о сбое.
Теперь, когда мы понимаем механизмы, мы рассмотрели, как это можно отрегулировать в соответствии с нашими потребностями в средах Windows.
Что атаковать?
Ядро Windows огромно, содержит десятки миллионов строк кода(https://techcommunity.microsoft.com/t5/Windows-Kernel-Internals/One-Windows-Kernel/ba-p/267142) и миллионы исходных файлов(https://github.com/dwizzzle/Presentations/blob/master/David Weston - Keeping Windows Secure - Bluehat IL 2019.pdf).
Наше внимание сосредоточено на деталях, которые доступны из пространства пользователя. Эти части довольно сложны и могут быть использованы для Локального Повышения Привилегий (LPE).
Исходя из нашего опыта, AFL подходит для следующих целей:
- Быстрые цели, которые могут выполнять более 100 итераций в секунду.
- Парсеры - особенно для двоичных форматов.
Это соответствует тому, что написал Михал Залевский в README файле AFL: "По умолчанию, механизм мутации afl-fuzz оптимизирован для компактных форматов данных - например, изображений, мультимедиа, сжатых данных, синтаксиса регулярных выражений или сценариев оболочки. Он несколько менее подходит для языков с особенно многословным и избыточным словообразованием, особенно в том числе HTML, SQL или JavaScript". Мы искали подходящие цели в ядре Windows (рисунок 3).
Рисунок 3: Компоненты ядра Windows.
Вот цели, про которые мы говорили.
- Файловые системы, такие как NTFS, FAT, VHD и другие.
- Кусты реестра.
- Крипто/Целостность кода (CI).
- PE Формат.
- Шрифты (которые были перемещены в пространство пользователя, начиная с Windows 10).
- Графические драйверы.
Типичная ошибка ядра в Windows
Мы вернулись назад и посмотрели на довольно типичную ошибку ядра - CVE-2018-0744 (https://bugs.chromium.org/p/project-zero/issues/detail?id=1389):
Рисунок 4: Типичная ошибка в win32k.
Эта программа содержит несколько системных вызовов, которые принимают в качестве входных данных высоко-структурированные данные, такие как структуры, константы (магические числа), указатели функций, строки и флаги.
Кроме того, существует зависимость между системными вызовами: вывод одного системного вызова используется как вход для других системных вызовов. Этот тип структуры очень распространен в случае ошибок ядра, когда последовательность системных вызовов используется для достижения ошибочного состояния, когда возникает уязвимость.
Важность структурированного понимания фаззинга и примеров можно найти здесь (https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md).
Поверхность атаки ядра Windows: kAFL против фазера системных вызовов
После того, как мы наблюдали ошибку, описанную выше, мы поняли, что использование фаззера в стиле AFL ограничит нас относительно небольшими частями ядра. Большая часть ядра Windows доступна из системных вызовов, которые включают в себя высоко-структурированные данные, но использование kAFL ограничит нас двоичными синтаксическими анализаторами в ядре, такими как драйверы устройств, файловые системы, формат PE, реестр и другие. Эти части относительно невелики по сравнению с объемом кода, доступным из системных вызовов. Поэтому, если бы у нас был фаззер системного вызова, мы могли бы потенциально охватить больше поверхностей для атак, таких как управление виртуальной памятью, диспетчер процессов, графика, пользовательские функции, gdi, безопасность, сеть и многие другие.
В этот момент мы поняли, что нам нужно искать системный фаззер.
Представляем Syzkaller
Syzkaller(https://github.com/google/syzkaller) - это структурированный фаззер ядра (a.k.a умный фазер системных вызовов).
Он поддерживает несколько операционных систем и работает на нескольких типах компьютеров (Qemu, GCE, мобильные телефоны и т.д.) и нескольких архитектурах (x86-64, aarch64).
На сегодняшний день Syzkaller (https://syzkaller.appspot.com/upstream) обнаружил 3700 ошибок в ядре Linux, по скромным меркам, одна из шести найденных ошибок - это ошибки безопасности.
Syzkaller является структурно-ориентированным фаззером, то есть имеет описание для каждого системного вызова.
Описания системного вызова записываются в текстовые файлы с использованием `go`-подобного синтаксиса(https://github.com/google/syzkaller...ptions_syntax.md#syscall-description-language).
Syz-sysgen является одним из инструментов Syzkaller и используется для анализа и форматирования описаний системных вызовов. Когда этот процесс успешно завершен, он преобразует текстовые файлы в код `go`, которые скомпилированы вместе с кодом фаззера, в исполняемый файл syz-fuzzer.
Syz-fuzzer - основной исполняемый файл для управления процессом фаззинга в гостевой виртуальной машине.
Syzkaller имеет собственный синтаксис для описания программ, системных вызовов, структур, объединений и многого другого. Сгенерированные программы также называются программами syz. Пример можно найти здесь (https://github.com/google/syzkaller/blob/master/docs/syscall_descriptions.md#syscall-descriptions).
Syzkaller использует несколько стратегий мутаций для мутации существующих программ. Syzkaller сохраняет программы, которые обеспечивают новое покрытие кода в формате syz, в базе данных. Эта база данных также известна как corpus.
Это позволяет нам остановить фаззер, внести изменения и продолжить с того же места, где мы остановились.
Рисунок 5: Архитектура Syzkaller (Linux).
Основной двоичный файл Syzkaller называется syz-manager (1). Когда он запускается, он выполняет следующие действия:
Загружает корпус (2) программ из более ранних запусков, запускает несколько тестовых (3) машин, копирует исполняемые файлы (6) и сам фаззер (5) на машину, используя ssh (4), и запускает программу Syz-fuzzer (5).
Затем Syz-fuzzer (5) выбирает корпус из менеджера и начинает генерировать программы. Каждая программа отправляется обратно менеджеру на ответственное хранение в случае сбоя. Затем Syz-fuzzer отправляет программу через межпроцессорное взаимодействие (7) исполнителю (6), который запускает системные вызовы (8) и собирает покрытие из ядра (9), KCOV в случае Linux.
KCOV (https://www.kernel.org/doc/html/v4.17/dev-tools/kcov.html) - это инструментальная функция времени компиляции, которая позволяет нам из пространства пользователя получать покрытие кода для каждого потока во всем ядре.
Если обнаруживается новая трасса покрытия, фаззер (11) сообщает об этом менеджеру.
Syzkaller стремится стать неконтролируемым фаззером, что означает, что он пытается автоматизировать весь процесс фаззинга. Примером этого свойства является то, что в случае сбоя Syzkaller порождает несколько машин-репродукторов для выделения сбойных программ syz из журнала программ. Воспроизводители стараются максимально свести к минимуму сбойную программу. Когда процесс завершится, большую часть времени Syzkaller будет воспроизводить либо программу syz, либо код C, который воспроизводит сбой. Syzkaller также может извлечь список сопровождающих из git и отправить им подробную информацию о сбое.
Syzkaller поддерживает ядро Linux и имеет впечатляющие результаты. Глядя на Syzkaller, мы подумали: если бы мы только могли фаззить ядро Linux в Windows. Это привело нас к изучению WSL.
WSLv1
Подсистема Windows для Linux (WSL) - это уровень совместимости для непосредственного запуска двоичных файлов Linux в Windows. Она переводит системными вызовы Linux в функции Windows. Первая версия была выпущена в 2016 году и включает в себя 2 драйвера: lxcore и lxss.
Она была разработан для запуска команд bash и ядра Linux для разработчиков.
WSLv1 использует облегченный процесс, называемый процессом pico, для размещения двоичных файлов Linux и специальных драйверов, называемых поставщиками pico, для обработки системных вызовов из процессов pico (дополнительную информацию см. здесь: (https://channel9.msdn.com/Blogs/Seth-Juarez/Windows-Subsystem-for-Linux-Architectural-Overview), (https://docs.microsoft.com/en-us/archive/blogs/wsl/windows-subsystem-for-linux-overview)).
Почему WSL?
Поскольку WSL относительно похож на ядро Linux, мы можем повторно использовать большую часть существующей грамматики для Linux и двоичных файлов syz-executor и syz-fuzzer, которые совместимы со средой Linux.
Мы хотели найти ошибки для Повышения Привилегий (PE), но WSL v1 не поставляется по умолчанию, и его может быть сложно использовать из песочницы, поскольку он выполняется в процессе другого типа (процесс PICO).
Но мы подумали, что было бы лучше получить опыт работы с Syzkaller на Windows с минимальными изменениями.
И началось портирование кода
Сначала мы установили дистрибутив Linux из магазина Microsoft и использовали Ubuntu в качестве нашего дистрибутива. Мы начали с добавления сервера ssh с командой "apt install openssh-server" и настроили ключи ssh. Далее мы хотели добавить поддержку трассировки покрытия. К сожалению, ядро Windows является закрытым исходным кодом и не предоставляет инструментарий времени компиляции, такой как KCOV в Linux.
Мы подумали о нескольких альтернативах, которые помогут нам получить трассировку покрытия:
- Использование эмулятора типа QEMU/BOCHS и добавление инструментария покрытия.
- Использование статических бинарных инструментов, как в pe-afl (https://github.com/wmliang/pe-afl).
- Использование гипервизора с выборкой покрытия, как в apple-pie (https://github.com/gamozolabs/applepie).
- Использование поддержки оборудования для покрытия, как Intel-PT.
Мы решили использовать Intel-PT, потому что он обеспечивает трассировки для скомпилированных двоичных файлов во время выполнения, он относительно быстр и предоставляет полную информацию о покрытии, что означает, что мы можем получить начальный указатель инструкций (IP) для каждого базового блока, который мы посетили, в исходном порядке.
Использование Intel-PT из нашей виртуальной машины, на которой работает целевая ОС, требует нескольких модификаций KVM.
Мы использовали большие части патчей kAFL kvm для поддержки покрытия Intel-PT.
Кроме того, мы создали KCOV-подобный интерфейс с помощью гипер-вызовов, поэтому, когда исполнитель пытается запустить, остановить или собрать покрытие, он исполняет гипер-вызовы.
Symbolizer #1
Механизм обнаружения сбоев Syzkaller считывает выходные данные консоли виртуальной машины и использует предопределенные регулярные выражения для обнаружения ошибки типа паники ядра, предупреждений и т.д.
Нам обязательно был нужен механизм обнаружения сбоев для нашего порта, поэтому мы могли вывести на выходную консоль предупреждение, которое Syzkaller может перехватить.
Для обнаружения BSOD мы использовали технику kAFL.
Мы пропатчили BugCheck и BugCheckEx с помощью шелл-кода, который выполняет гипевызов и уведомляет о сбое, записывая уникальное сообщение в выходную консоль QEMU.
Мы добавили регулярное выражение в syz-manager для обнаружения сообщений о сбоях с выходной консоли QEMU. Чтобы улучшить обнаружение ошибок в ядре, мы также использовали Driver Verifier со специальными пулами для обнаружения повреждений пула («verifyier/flags 0x1/driver lxss.sys lxcore.sys»).
Общая проблема с фаззерами заключается в том, что они сталкиваются с одной и той же ошибкой много раз.
Чтобы избежать повторяющихся багов, Syzkaller требует уникальный вывод для каждого сбоя.
Наш первый подход состоял в том, чтобы извлечь несколько относительных адресов из стека, которые находятся в пределах диапазонов модулей, которые мы отслеживаем, и распечатать их на выходной консоли QEMU.
Санитарная проверка
Перед запуском фаззера мы хотели убедиться, что он действительно может обнаружить реальную ошибку, иначе мы просто тратим процессорное время. К сожалению, в то время мы не смогли найти общедоступный PoC с реальной ошибкой для выполнения этого теста.
Поэтому мы решили исправить определенный поток в одном из системных вызовов для эмуляции ошибки.
Фаззер смог его найти, что было хорошим знаком, и мы запустили фуззер.
Первая попытка фазинга
Наш механизм обнаружения сбоев был основан на kAFL, где мы исправили функции BugCheck и BugCheckEx с помощью шелл-кода, который запускает гипервызов при сбое, который перехватывает Патч-Гард. Он для этого и был разработан (https://en.wikipedia.org/wiki/Kernel_Patch_Protection#Technical_overview).
Чтобы обойти эту проблему, мы добавили драйвер, который запускается при загрузке и регистрирует обратный вызов bugcheck с помощью ядра, используя функцию KeRegisterBugCheckCallback (https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/nf-wdm-keregisterbugcheckcallback). Теперь, когда ядро дает сбой, оно вызывает наш драйвер, который затем запускает гипервызов, уведомляющий фаззер о сбоя.
Мы снова запустили фаззер и получили новую ошибку с другим кодом ошибки. Мы попытались воспроизвести сбой, чтобы помочь нам понять его, и обнаружили, что выполнение анализа первопричин из смещений и случайного мусора из стека затруднено.
Мы решили, что нам нужен лучший подход для получения информации о сбоях.
Symbolizer #2
Мы попытались запустить "kd"на нашей хост-машине под Wine для создания стека вызовов, но это не сработало, так как генерация стека вызовов заняла около 5 минут.
Такой подход создает узкое место для нашего фаззера. В процессе воспроизведения Syzkaller пытается свести к минимуму программы сбоя, насколько это возможно, и будет ожидать стек вызовов с каждой попыткой минимизации, чтобы определить, происходит ли такой же сбой.
Поэтому мы решили использовать удаленную машину Windows с KD и туннелировать все соединения udp. Это на самом деле хорошо работало, но когда мы увеличили его до 38 машин, соединения были разорваны, и Symbolizer подумал что все зависло.
Symbolizer #3
В этот момент мы спросили себя, как KD и WinDBG могут генерировать стек вызовов?
Ответ: они должны используют фукцнию StackWalk из библиотеки DbgHelp.dll.
Для генерации стека вызовов нам нужны StackFrame, ContextRecord и ReadMemoryRoutine.
Рисунок 7: Архитектура Symbolizer.
На рисунке 7 показана архитектура:
Мы получили стек, регистры и адреса драйверов из гостевой машины гостя, используя гипервизор KVM обратно в QEMU. Эмулятор QEMU отправил стек на удаленный компьютер с Windows, где наш symbolizer вызывает функцию StackWalk со всеми соответствующими аргументами и извлекает стек вызовов. Стек вызовов был распечатан обратно на консоль.
Эта архитектура была вдохновлена Bochspwn для Windows (https://github.com/googleprojectzero/bochspwn).
Теперь, когда мы получаем новый крэш, это выглядит так:
Symbolizer #4
Работа машины под управлением Windows вместе с нашим фаззером не идеальна, и мы подумали, как трудно будет реализовать минимальный отладчик ядра на языке `go` и скомпилировать его в Syzkaller.
Мы начали с парсера и сборщика файлов PDB. После этого мы реализовали раскрутку стека x64, используя информацию о раскрутке, хранящуюся в PE файле.
Последняя часть заключалась в том, чтобы реализовать серию KD, которая работала довольно медленно, поэтому мы начали работать над KDNET и после того, как мы закончили, интегрировали его в Syzkaller.
Это решение было намного лучше, чем предыдущие решения.
Наш механизм дедупликации теперь основан на ошибочном кадре. Мы также получаем код ошибки функции BugCheck, регистры и стек вызовов.
Стабильность покрытия
Другой проблемой, с которой мы столкнулись, была стабильность покрытия.
Syzkaller использует несколько потоков для поиска гонок данных.
Например, когда сгенерированная программа имеет 4 системных вызова, она может разделить ее на два потока, чтобы один поток запускал системные вызовы 1 и 2, а другой поток - системные вызовы 3 и 4.
В нашей реализации покрытия мы использовали один буфер на процесс. На практике, запуск одной и той же программы несколько раз приведет к различным трассам покрытия при каждом запуске.
Нестабильность покрытия ухудшает способность фаззеров находить новые и интересные пути кода и баги.
Мы хотели решить эту проблему, изменив нашу реализацию покрытия, чтобы она была похожа на реализацию KCOV.
Мы знали, что KCOV отслеживает покрытие для каждого потока, и мы хотели иметь этот механизм.
Для создания KCOV-подобных трасс нам понадобится:
- Отслеживание потоков в KVM для замены буферов.
- Добавление информации о дескрипторе потока в наш API гипервызова.
Для отслеживания потоков нам понадобился хук для переключения контекста. Мы знаем, что можем получить текущий поток из глобального сегмента:
Рисунок 8: Функция KeGetCurrentThread.
Мы посмотрели, что происходит во время переключения контекста, и мы нашли инструкцию swapgs в функции, которая обрабатывает переключение контекста. Когда происходят замены, это провоцирует вызов VMExit, который гипервизор может перехватить.
Рисунок 9: swapgs внутри функции SwapContext.
Это означает, что если мы можем отслеживать замены, мы также можем отслеживать изменения потоков в гипервизоре KVM.
Это выглядело как хорошая точка для отслеживания переключения контекста и обработки IntelPT для отслеживаемых потоков.
Это позволило нам подключать и переключать буферы ToPa при каждом переключении контекста. Записи ToPa описывают Intel-PT физические адреса, по которым он может записать вывод трассировки.
Нам все еще оставалось решить несколько мелких проблем:
- Отключение служб и автоматически загружаемых программ, а также ненужных служб для ускорения загрузки.
- Обновление Windows случайно перезапустило наши машины и потребило много ресурсов процессора.
- Защитник Windows случайно убил наш фаззера.
В общем (https://support.microsoft.com/en-us/help/15055/windows-7-optimize-windows-better-performance), мы настроили нашу гостевую машину на лучшую производительность.
Результаты WSL фазинга
В целом, мы фаззили WSL в течение 4 недель и использовали 38 виртуальных процессоров. В конце у нас был рабочий прототип и гораздо лучшее понимание того, как работает Syzkaller.
Мы нашли 4 ошибки DoS и несколько взаимоблокировок. Однако мы не обнаружили какой-либо уязвимости в системе безопасности, что нас разочаровало, тогда мы решили перейти к целевому PE.
Движение к реальной цели
Фаззинг WSL был хорошим способом познакомиться с Syzkaller в Windows. Но в этот момент мы хотели вернуться к настоящей цели повышения привилегий - той, которая по умолчанию поставляется с Windows и доступна из различных песочниц.
Мы посмотрели на поверхность атаки ядра Windows и решили начать с подсистемы Win32k. Win32k - это сторона ядра подсистемы Windows, которая является инфраструктурой графического интерфейса операционной системы. Это также общая цель для локального повышения привилегий (LPE), потому что она доступна из многих песочниц.
Она включает в себя две подсистемы:
- Диспетчер окон, также известная как User.
- Интерфейс графического устройства, также известная как GDI.
Подсистема имеет много системных вызовов (~ 1200), что означает, что она является хорошей целью для грамматических фаззеров (как показано ранее CVE-2018-0744).
Начиная с Windows 10, win32k разделена на несколько драйверов: win32k, win32kbase и win32kfull.
Чтобы заставить Syzkaller работать для подсистемы win32k, нам пришлось изменить несколько вещей:
- Скомпилировать фаззер и исполняемые файлы для Windows.
- Изменения, связанные с ОС.
- Раскрытие системных звонков Windows в фаззер.
- Кросс-компиляция с mingw ++ для удобства.
Настройки подсистемы Win32k
Начиная с исходного кода фаззера, мы добавили соответствующую реализацию для Windows, такую как каналы, разделяемая память и многое другое.
Грамматика является важной частью фаззера, который мы подробно объясним позже.
Затем мы перешли к исправлению исполнителя для кросс-компиляции с использованием MinGW. Нам также пришлось пофиксить разделяемую память, пайпы и отключить режим ветвления, так как он не существует в Windows.
В рамках компиляции грамматики syz-sysgen генерирует файл заголовка (syscalls.h), который включает все имена\числа системных вызовов.
В случае с Windows мы остановились на экспортированных оболочках системных вызовов и функциях WinAPI (например, таких как CreateWindowExA и NtUserSetSystemMenu).
Большая часть оболочки syscalls экспортируется в библиотеки win32u.dll и gdi32.dll. Чтобы представить их нашему исполняемому файлу, мы использовали gendef (https://sourceforge.net/p/mingw-w64/wiki2/gendef/) для генерации файлов определений из dll. Затем мы использовали mingw-dlltool для создания библиотечных файлов и в конце концов связали их с главным файлов.
Санитарная проверка
Как мы уже говорили ранее, мы хотели убедиться, что наш фаззер способен воспроизводить старые ошибки, так как в противном случае мы тратим процессорное время.
На этот раз мы взяли настоящий баг (CVE-2018-0744, см. Рисунок 4), и мы хотели ее воспроизвести. Мы добавили соответствующие системные вызовы и позволили фаззеру найти его, но, к сожалению, это не удалось. Мы подозревали, что у нас есть ошибка, поэтому мы написали программу syz и использовали syz-execprog, Syzkaller для непосредственного выполнения программ syz, чтобы убедиться, что она работает. Системные вызовы были успешно вызваны, но, к сожалению, система не сломалась.
Через некоторое время мы поняли, что фаззер работает под сеансом 0. Все службы, включая нашу службу ssh, являются консольными приложениями, которые выполняются в сеансе 0 и не предназначены для работы с графическим интерфейсом. Таким образом, мы изменили его для запуска в качестве обычного пользователя в сеансе 1. Как только мы это сделали, Syzkaller смог успешно воспроизвести ошибку.
Мы пришли к выводу, что мы всегда должны тестировать новый код, эмулируя ошибки или воспроизводя старые.
Проверка стабильности
Всего мы добавили 15 новых функций и снова запустили фаззер.
Мы получили первый сбой в функции win32kfull!_OpenClipboard, это был сбой типа Use-After-Free. Но по какой-то причине, этот сбой не воспроизводился на других машинах. Сначала мы подумали, что это из-за другой ошибки, которую мы создали, но сбой был воспроизведён на той же машине, но без фаззера.
Стек вызовов и сбойная программа не помогли нам понять, что случилось.
Поэтому, мы пошли и посмотрели в IDA где произошел сбой:
Рисунок 11: Сбойное место - win32kfull! _OpenClipboard.
Мы заметили, что сбой происходит внутри условного блока, где он зависит от флага поставщика ETW: Win32kTraceLoggingLevel.
Этот флаг включен на некоторых машинах и выключен на других, поэтому мы заключаем, что мы, вероятно, получили A/B-тестовую машину.
Мы зарепортили об этом баге и снова установили Windows.
Мы снова запустили фаззер и получили новую ошибку, на этот раз отказ в обслуживании в функции RegisterClassExA. На данный момент наша мотивация взлетела до небес, потому что если 15 системных вызовов привели к 2 ошибкам, это означает, что 1500 системных вызовов приведут к 200 ошибкам.
Грамматика в win32k
Поскольку ранее не было публичного исследования системного вызова win32k, нам пришлось создавать правильную грамматику с нуля.
Мы получили первый сбой в функции win32kfull!_OpenClipboard, это был сбой типа Use-After-Free. Но по какой-то причине, этот сбой не воспроизводился на других машинах. Сначала мы подумали, что это из-за другой ошибки, которую мы создали, но сбой был воспроизведён на той же машине, но без фаззера.
Нашей первой мыслью было, что, возможно, мы сможем автоматизировать этот процесс, но мы столкнулись с двумя проблемами:
Во-первых, заголовочных файлов Windows недостаточно для создания грамматики, поскольку они не предоставляют важную информацию для фаззера системного вызова, например уникальные строки, некоторые параметры DWORD фактически являются флагами, а многие структуры определены как LPVOID.
Во-вторых, многие системные вызовы просто не документированы (например, функция NtUserSetSystemMenu).
К счастью, многие части Windows являются технически открытым исходным кодом:
- Windows NT Leaked sources – https://github.com/ZoloZiak/WinNT4
- Windows 2000 Leaked sources – https://github.com/pustladi/Windows-2000
- ReactOS (Leaked w2k3 sources?) – https://github.com/reactos/reactos
- Windows Research Kit – https://github.com/Zer0Mem0ry/ntoskrnl
Мы искали каждый системный вызов в MSDN и в просочившихся источниках, а также проверяли его с помощью IDA и WinDBG.
Многие сгенерированные нами сигнатуры функций было легко создать, но некоторые были настоящим кошмаром - включали множество структур, недокументированных аргументов, некоторые системные вызовы имели 15 и более аргументов.
После нескольких сотен системных вызовов мы снова запустили фаззер и получили 3 уязвимости GDI и несколько ошибок типа Отказ в Обслуживании.
На данный момент мы рассмотрели несколько сотен системных вызовов в win32k. Мы хотели найти больше ошибок. Таким образом, мы пришли к выводу, что пришло время углубиться в поиски дополнительной информации о подсистеме Win32k и выйти на более сложные поверхности атаки.
Фаззеры не являются магией, но, чтобы найти ошибки, мы должны убедиться, что мы покрываем большинство поверхностей атаки по нашей цели.
Мы вернулись назад, чтобы прочитать больше о подсистемеWin32k, понять старые ошибки и классы ошибок. Затем мы попытались поддержать недавно изученные поверхности атаки нашего фаззера.
Одним из примеров является GDI Shared Handle. _PEB! GdiSharedHandleTable - это массив указателей на структуру, которая содержит информацию об общих дескрипторах GDI между всеми процессами.
Мы добавили это в Syzkaller, добавив псевдо-системный вызов GetGdiHandle (тип, индекс), который получает тип дескриптора и индекса. Эта функция перебирает массив таблиц общих дескрипторов GDI от инициализации до индекса и возвращает последний дескриптор того же типа, что и запрошенный.
Это привело к CVE-2019-1159(https://cpr-zero.checkpoint.com/vulns/cprid-2132/), Use-After-Free, запускаемому одним системным вызовом с глобальным дескриптором GDI, который создается при загрузке.
Результаты
Мы фаззили почти 1,5 месяца и запускали все это на 60 процессорах.
Мы нашли 10 уязвимостей (3 pending , 1 дубликат)
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1014
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1096
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1159
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1164
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1256
- https://portal.msrc.microsoft.com/en-US/security-guidance/advisory/CVE-2019-1286
Мы также обнаружили 3 ошибки DoS, 1 сбой в WinLogon и несколько взаимоблокировок.
LPE → RCE?
Локальные ошибки повышения привилегий - это круто, но как насчет удаленного выполнения кода?
Представляем WMF - формат метафайлов Windows.
WMF - это формат файла изображения. Он был разработан еще в 1990-х годах и поддерживает как векторную графику, так и растровые изображения. Microsoft расширила этот формат на протяжении многих лет как следующие форматы
- EMF
- EMF+
- EMFSPOOL
Microsoft также добавила функцию в этот формат, которая позволяет добавлять записи, которые воспроизводятся для воспроизведения графического вывода. Когда эти записи воспроизводятся, анализатор изображений вызывает системный вызов NtGdi. Вы можете прочитать больше об этом формате в лекции j00ru(https://j00ru.vexillium.org/slides/2016/pacsec.pdf).
Количество системных вызовов, принимающих файл EMF, ограничено, но, к счастью для нас, мы обнаружили уязвимость в функции StretchBlt, которая принимает файл EMF.
Резюме
Нашей целью было найти ошибки ядра Windows с помощью фаззера.
Мы начали исследовать ландшафт фаззеров в ядре Windows, и поскольку у нас был опыт работы с фаззерами стиля AFL, мы искали тот, который работает аналогично, и нашли kAFL.
Мы смотрели на kAFL и искали поверхности атаки в ядре Windows, но быстро выяснили, что фаззер системного вызова может достичь гораздо большего количества поверхностей атаки.
Мы искали fuzzers для системных вызовов и нашли Syzkaller.
В этот момент мы начали портировать его на WSL, так как он наиболее похож на ядро Linux, и мы могли получить некоторый опыт работы с Syzkaller в Windows. Мы реализовали инструментарий покрытия для ядра Windows с помощью IntelPT. Мы поделились механизмом обнаружения сбоев, который использовался для устранения дубликатов ошибок. Мы нашли несколько проблем со стабильностью покрытия и поделились своим решением.
После того, как мы обнаружили некоторые баги типа DoS, мы решили перейти к реальной цели файла типа PE - подсистемы win32k - но нам пришлось реализовать недостающие части в Syzkaller. Затем мы провели проверку работоспособности и стресс-тест, чтобы убедиться, что фаззер не тратит процессорное время. После этого мы потратили много времени на написание грамматики, чтение о нашей цели и в конечном итоге добавление поддержки вновь изученных частей подсистемы Win32k обратно в фаззер.
В целом, наше исследование привело нас к обнаружению 8 уязвимостей, ошибок DoS и взаимоблокировок в ядре Windows 10.
Источник: https://research.checkpoint.com/2020/bugs-on-the-windshield-fuzzing-the-windows-kernel/
Автор перевода: yashechka
Переведено специально для портала xss.pro (c)
Последнее редактирование: