• XSS.stack #1 – первый литературный журнал от юзеров форума

Fuzzing Год фаззинга шрифтов ядра Windows №2: методы

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
В 1 части мы обсудили мотивацию и результаты наших многолетних усилий по фаззингу против движка шрифтов ядра Windows, после чего последовал анализ двух ошибок с Keen Team и Hacking Team, которые последовали в результате этой работы. Хотя сами ошибки, безусловно, забавны, мы находим еще более интересными методы и решения, которые мы приняли, чтобы сделать проект таким же эффективным, каким он оказался. Хотя несколько методов, которые мы использовали, были очень специфичны для конкретной цели, мы считаем, что большинство идей по-прежнему применимы к большинству усилий по фаззингу, поскольку мы разработали их благодаря многолетнему опыту, не ограничивающемуся только ядром Windows. Наслаждайся!

Технические подробности

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

Подготовка входных данных

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

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

- Шрифты TrueType (расширение .TTF, обрабатывается win32k.sys),
- Шрифты OpenType (расширение .OTF, обрабатывается ATMFD.DLL),
- Растровые шрифты (расширения .FON, .FNT, обрабатывается win32k.sys),
- Шрифты Type 1 (расширения .PFB, .PFM, .MMM, обрабатываются ATMFD.DLL).


Тем не менее, мы решили в конечном итоге исключить Тип 1 по нескольким причинам:

- Windows требует как минимум два соответствующих файла (.PFB и .PFM) для загрузки одного шрифта Type 1.
- Формат структурно очень прост, наиболее сложной частью являются CharStrings, которые мы уже тщательно проверили.
- Большая часть логики обработки шрифтов используется в ATMFD.DLL как для форматов Type 1, так и для OpenType.


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

Поскольку мы не собирались проводить измерения покрытия кода ядра Windows, мы не могли провести обычный фазинг большого количества шрифтов, автоматически сгенерированных различными инструментами или просканированных из Интернета. Однако, благодаря предыдущему фаззингу FreeType2, мы создали несколько корпусов для этого проекта. Мы выбрали файл с высокой избыточностью покрытия (до 100 образцов на одну базовую пару блоков) и извлекли только шрифты TTF и OTF, в результате чего была получена коллекция из 19507 файлов — 14848 файлов TrueType и 4659 файлов OpenType — потребляющих около 2,4 ГБ памяти дискового пространства.

Использование корпуса, адаптированного для одной части программного обеспечения, для фаззинга другой сопряжено с риском того, что некоторые функции, присутствующие в последнем, не будут покрыты образцами для первого, и без охвата они никогда не будут обнаружены сами по себе. В данном случае я пошел на компромисс, так как риск был частично снижен за счет избыточности в корпусе и того факта, что FreeType, вероятно, имеет наиболее полную поддержку форматов TrueType и OpenType, которая должна охватывать большинство или все форматы Windows. Наконец, использование легкодоступного корпуса сэкономило нам много времени, позволив запустить фаззер, как только остальная инфраструктура заработает.

Мутация TTF и OTF

По нашему опыту, оптимальный выбор стратегии мутации является одной из наиболее важных частей эффективного фаззинга, поэтому обычно стоит потратить некоторое время на настройку конфигурации или даже на написание обработки для конкретного формата. Удобно, что форматы TrueType и OpenType имеют общую структуру чанков, называемую SFNT (https://en.wikipedia.org/wiki/SFNT). Короче говоря, это макет файла, состоящий из нескольких таблиц, идентифицируемых 4-байтовыми тегами, каждая из которых структурирована документированным образом и служит разным целям. Дизайн обратно совместим, но его также легко расширить с помощью таблиц, специфичных для поставщика. В настоящее время существует около 50 таблиц, но только около 20 можно считать важными, и еще меньше обязательных. Кроме того, их также можно классифицировать в зависимости от того, являются ли они специфичными для TrueType, специфичными для OpenType или общими для этих двух. Подробную информацию о наиболее фундаментальных таблицах можно найти в спецификации Microsoft OpenType ( https://www.microsoft.com/en-us/Typography/OpenTypeSpecification.aspx).

Важное замечание о таблицах SFNT заключается в том, что все они совершенно разные. Они различаются по длине, структуре, важности, характеру хранимых данных и т. д. Имея это в виду, кажется разумным относиться к каждому из них индивидуально, а не одинаково. Однако почти все публично обсуждаемые фаззеры подходили к шагу мутации противоположным образом: сначала мутируя файл TTF/OTF целиком (не учитывая его внутреннюю структуру), а затем исправляя контрольные суммы таблицы в заголовке, т.е. Windows не сразу отказывалась бы их загружать. На наш взгляд, это было крайне неоптимально.

Чтобы получить некоторые конкретные цифры, мы просмотрели наш корпус шрифтов и обнаружили, что в среднем существует около 10 таблиц, изменение которых влияет на успешность загрузки и отображения шрифта. Интуитивно понятно, что стратегия мутации наиболее эффективна, когда результирующие файлы успешно обрабатываются тестируемым программным обеспечением в 50 % случаев и аналогичным образом не анализируются в остальных 50% случаев. Это указывает на то, что тестовые случаи находятся на грани допустимости, и показывает, что конфигурация не является ни слишком агрессивной, ни слишком свободной. В случае шрифтов SFNT, если одна из таблиц слишком сильно повреждена, весь файл не сможет загрузиться, независимо от содержимого других таблиц. Если мы обозначим вероятность успешной загрузки каждой таблицы с определенной стратегией мутации как
1651660372953.png
, то вероятность для всего файла будет равна
1651660382441.png
, или:

1651660344764.png



или если мы предположим, что вероятности для всех таблиц должны быть равными (мы хотим фаззить их одинаково):


1651660355026.png



Если мы подставим усредненное значение

1651660362674.png



Итак, в результате мы хотели настроить мутатор таким образом, чтобы для каждой таблицы ее измененная форма все еще могла быть успешно обработана ~93% времени (по крайней мере, в среднем).

Алгоритмами, которые мы выбрали для изменения таблиц, были Bitflipping (переворачивает 1-4 последовательных бита в случайно выбранных байтах), Bytflipping (заменяет случайно выбранные байты другими значениями), Chunkspew (копирует данные из одного места во входных данных в другое), Special Ints (вставляет специальные, магические целые числа во входные данные) и Add Sub Binary (добавляет и вычитает случайные целые числа из двоичных данных). Поскольку каждый из алгоритмов воздействовал на данные по-разному, нам нужно было определить среднее наиболее оптимальное соотношение мутаций для каждой пары таблица/алгоритм.

Чтобы облегчить задачу, мы разработали программу, которая загружала каждый файл в корпусе, выполняла итерации по каждой таблице и алгоритму и определяла коэффициент мутации, который приводил бы к ~ 93% успешности загрузки шрифта через деление пополам. После обработки всех файлов мы усреднили числа, установили некоторые общие коэффициенты мутации для таблиц, мутации которых не повлияли на загрузку шрифта (но потенциально они все еще могли быть где-то проанализированы), и в итоге получили следующую таблицу:

1651660397003.png


Даже с приведенными выше точными числами это всего лишь средние результаты, основанные на всем корпусе, поэтому использование одних и тех же значений для всех шрифтов не сработает. Это особенно верно, поскольку размер и сложность входных выборок сильно различаются: от 8 килобайт до 10 мегабайт. Следовательно, плотность и распределение данных в таблицах также различаются, и для достижения желаемого уровня успеха синтаксического анализа требуются индивидуальные коэффициенты мутаций. Чтобы решить эту проблему и предоставить фаззеру еще большую свободу, вместо того, чтобы использовать фиксированное соотношение в каждой итерации, на каждом проходе фаззер будет выбирать временное соотношение из диапазона
1651660422652.png
, являющегося рассчитанной идеальной скоростью.

С приведенной выше конфигурацией и тривиальным фрагментом кода для дизассемблирования, изменения и повторной сборки файлов SFNT (написанных в нашем случае на C++) мы теперь могли мутировать шрифты умным и глупым способом. Хотя мы все еще просто случайным образом переворачивали биты и использовали такие же простые алгоритмы, у нас была некоторая степень контроля над тем, как Windows будет реагировать на средний мутировавший шрифт.

15 из 16 уязвимостей, обнаруженных в ходе работы, были обнаружены с указанной выше конфигурацией мутаций.

Генерация TTF-программ

Добавление некоторой "умной" логики в совершенно глупый процесс фаззинга является значительным улучшением, но важно понимать, что этого может быть недостаточно для обнаружения определенных классов ошибок обработки шрифтов. Большая часть большинства файлов TTF и OTF, иногда даже большинство, потребляется программами набросков глифов, написанными для выделенных виртуальных машин TrueType/PostScript. Эти виртуальные машины довольно уязвимы в контексте фаззинга, поскольку они прерывают выполнение программы, как только встречается недопустимая инструкция. Следовательно, тупые алгоритмы, работающие с битовыми потоками "черного ящика", не зная их структуры, будут использовать очень ограниченную часть виртуальной машины, поскольку каждая программа, скорее всего, выйдет из строя после выполнения всего нескольких инструкций.

Это не означает, что обычные мутации не могут быть полезны для поиска ошибок VM. Уязвимости, которые могли быть вызваны заменой одной инструкции на другую или перестановкой аргумента инструкции, вполне могли быть обнаружены таким образом, и на самом деле они были в прошлом. Вот почему мы допустили мутации в таблицах "prep", "fpgm" и "glyf", которые содержали программы TrueType. На самом деле уязвимость в обработчике инструкций «IUP» действительно была обнаружена с помощью тупого фаззера.

Тем не менее, мы подозревали, что могли быть также ошибки, которые требовали определенных, длинных конструкций правильных инструкций, чтобы проявить себя. Подозрение также подтвердилось проверкой безопасности кода обработки CharString в ATMFD, где большинство сбоев можно было спровоцировать только тремя или более конкретными инструкциями. Крайне маловероятно (вплоть до нереалистичности в разумные сроки) создание таких конструкций глупым алгоритмом, особенно при отсутствии покрытия. Поскольку я уже провел недели за аудитом виртуальной машины, реализованной в ATMFD, не было необходимости проводить ее дальнейшее фазз-тестирование. Проблема должна была быть решена только для TTF, и наша идея заключалась в том, чтобы использовать специальный генератор программ TrueType, который выдавал бы потоки структурно правильных инструкций (со случайными аргументами) и встраивал их в существующие законные шрифты TTF. Ниже мы приступим к описанию того, как нам удалось добиться такого эффекта.

Шрифты в формате SFNT (как TTF, так и OTF) можно разбить на удобочитаемые файлы .ttx (со структурой XML) с помощью утилиты ttx.py в рамках проекта FontTools (https://github.com/behdad/fonttools) и собрать обратно в двоичную форму. Это очень полезно для удобного просмотра внутренних структур шрифтов и их модификации любым желаемым способом. Проект также понимает инструкции TrueType и может дизассемблировать и собирать обратно целые программы, написанные на этом языке. Потоки инструкций расположены внутри тегов <assembly></assembly>, как показано ниже:

<?xml version="1.0" encoding="utf-8"?>

<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="2.4">



<glyf>



<TTGlyph name=".notdef" xMin="128" yMin="0" xMax="896" yMax="1638">



<instructions><assembly>

PUSH[ ] /* 16 values pushed */

6 111 2 9 7 111 1 8 5 115 3 3 8 6 115 1

SVTCA[0]

MDAP[1]

MIRP[01101]

SRP0[ ]



</assembly></instructions>

</TTGlyph>



</glyf>



</ttFont>

Мы начали с небольшой предварительной обработки: мы преобразовали все файлы .ttf в соответствующие версии .ttx и написали короткий скрипт Python (используя xml.etree.ElementTree) для удаления тела всех тегов <assembly>. Встраивание сгенерированных программ в существующие законные шрифты во многом превосходит их вставку в один минималистичный шаблон TTF. Инструкции TrueType тесно связаны с другими характеристиками своих шрифтов (точки, контуры, другие настройки), поэтому их разнообразный набор (по аналогии с мутационным фаззингом) не мог бы нам навредить, а мог бы только помочь.

Следующим шагом было написать собственно генератор, который бы генерировал инструкции и встраивал их в нужные места файлов .ttx. Большая часть работы заключалась в чтении спецификации набора инструкций TrueType (https://www.microsoft.com/typography/otspec/ttinst.htm) и реализации генератора каждой инструкции в отдельности. Некоторые из инструкций (например, RTHG, ROFF или FLIPON) были тривиальны для обработки, в то время как другие были намного сложнее. В общем, чтобы убедиться, что программы корректны и не вылетают слишком быстро, пришлось добавить обработку и других частей шрифта:

- Подсчитать количество контуров и точек в каждом глифе, чтобы иметь возможность генерировать их действительные индексы в качестве аргументов инструкции.
- Следовать указателям текущей зоны (ZP0, ZP1, ZP2), установленным инструкциями SZP0, SZP1, SZP2, SZPS, чтобы иметь возможность генерировать действительные индексы контуров и точек.
- Расширить таблицу "CVT" до 32768 элементов со случайными значениями.
- Настроить многие поля в таблице "maxp" для нашего удобства, чтобы мы не нарушали какие-либо ограничения, произвольно установленные шрифтом хостинга.


После нескольких дней разработки и тестирования мы получили скрипт tt_generate.py, который принимал файлы .ttx в качестве входных данных и создавал другие файлы .ttx со всеми правильно настроенными структурами и содержащими длинные потоки в основном правильных инструкций для каждого глифа на выходе. После перекомпиляции с помощью ttx.py шрифт можно было загрузить и отобразить аналогично тестовым примерам, созданным с помощью немого фаззинга.

Первой уязвимостью, обнаруженной генератором, была, очевидно, ошибка "IUP", поскольку для этого требовалось только установить ZP2 в 0 с помощью инструкций SZP2/SZPS, а затем вызвать инструкцию IUP. Поскольку эта последовательность генерировалась очень часто, нам пришлось активно избегать ее в коде (всегда устанавливая ZP2 в 1 перед отправкой IUP) до того, как было выпущено исправление для этой проблемы. Затем генератор обнаружил еще одну уникальную ошибку (на этот раз пропущенную глупым проходом) во 2-й итерации фаззинга (issue #507 (https://bugs.chromium.org/p/project-zero/issues/detail?id=507)): переполнение буфера на основе пула в функциях отрисовки текста (win32k!or_all_N_wide_rotated_need_last и другие родственные подпрограммы).

С одной уникальной серьезной ошибкой и одним столкновением с результатом мутационного фаззинга мы по-прежнему считаем, что усилия, затраченные на написание генератора TTF, стоили того. :)

Фаззинг ядра в Bochs

Одно из фундаментальных предположений проекта заключалось в том, чтобы сделать фаззинг масштабируемым, предпочтительно в системах Linux без поддержки виртуализации. Эта предпосылка значительно ограничила наш выбор инфраструктуры, поскольку мы говорим о фаззинге ядра Windows, которое должно работать как единое целое, но также требует некоторого гипервизора над ним, чтобы иметь возможность обнаруживать сбои, сохранять тестовые случаи и так далее. Проект qemu казался интуитивной идеей, но любой, кто знаком с нашим исследованием Bochspwn (http://j00ru.vexillium.org/?p=1695), знает, что нам очень нравится эмулятор Bochs x86 (http://bochs.sourceforge.net/) и его инструментальная поддержка. Системы, работающие в режиме полной эмуляции, очень медленные (эффективная частота до ~100 МГц), но потенциально с тысячами машин замедление в 20-50 раз по-прежнему легко компенсируется. Кроме того, у нас уже был большой опыт работы с Windows на Boch, поэтому мы могли приступить к реализации логики фаззинга в инструментарии, не беспокоясь о каких-либо непредвиденных проблемах начального уровня.

Чтобы сократить количество циклов внутри гостевой системы и ускорить работу, мы решили, что мутация/генерация шрифта будет происходить вне Windows, в инструментарии Bochs. Единственное, за что будет отвечать эмулируемая система, — это запрос входных данных от гипервизора, загрузка их в систему и отображение всех глифов в различных стилях и размерах точек. Вся гостевая логика была включена в единую программу harness.exe, которая автоматически запускалась во время загрузки и начинала тестировать шрифты как можно быстрее.

Вышеупомянутая конструкция требовала некоторого канала связи между harness и инструментами Bochs. Как это сделать? Если мы осознаем, что запускаем эмулятор и, таким образом, контролируем каждую часть выполнения виртуального процессора, ответ становится очевидным: путем обнаружения некоторого особого состояния, которое никогда не возникает при обычном выполнении, но может быть легко установлено нашим процессом внутри виртуальной машины. В нашем случае мы решили использовать инструкцию LFENCE x86 в качестве канала связи между двумя средами. Код операции фактически не используется и (почти) никогда не используется во время обычной работы системы, что делает его идеальным кандидатом для этой функции. Его выполнение можно обнаружить с помощью обратного вызова BX_INSTR_AFTER_EXECUTION:

void bx_instr_after_execution(unsigned cpu, bxInstruction_c *i);

В нашем дизайне код операции передавался через регистр EAX, а параметры ввода/вывода — через регистры ECX/EDX. Всего мы реализовали три операции, облегчающие копирование данных шрифта в гостевую систему, информирование хоста об успехе или неудаче его загрузки и отправку текстовых отладочных сообщений:

- REQUEST_DATA(lpBuffer) — отправляется программой для получения свежих данных шрифта для проверки ядра. Аргумент lpBuffer содержит виртуальный адрес буфера, в который гипервизор должен копировать данные.

- SEND_STATUS(dwStatus) — отправляется системой для информирования хоста об успешной загрузке самого последнего шрифта.

- DBG_PRINT(lpMessage) – отсылается обвязкой для логирования текстового сообщения.


Кроме того, данные, полученные в ответ на первое сообщение REQUEST_DATA, всегда были исходными, неизмененными данными файла образца. Это было сделано для того, чтобы программа могла извлекать действительные структуры LOGFONT, описывающие шрифты, содержащиеся в файле, которые затем использовались для их загрузки и отображения во всех последующих итерациях. Рабочий процесс показан на схеме ниже:

1651660488026.png


Как видите, схема очень проста. Отправка отладочных сообщений и информации о состоянии позволила агрегировать информацию о времени выполнения из сеансов фаззинга и время от времени проверять, что они работают правильно, и цель среднего коэффициента загрузки шрифтов в 50% была действительно достигнута. Шаг изменения/генерации шрифта, реализованный в инструментарии, конечно же, был синхронным, а это означает, что выполнение инструкции LFENCE в гостевой системе не вернется до тех пор, пока образцы данных не будут скопированы в память. Этап мутации был довольно быстрым, так как был написан на C++ и выполнялся в том же процессе. Вариант генерации программы TTF был намного медленнее, поскольку он включал запуск внепроцессного генератора Python, а затем скрипт ttx.py для сборки шрифта обратно в двоичную форму. Однако по сравнению с периодом времени тестирования одного шрифта внутри гостевой системы (от 0,5 с до нескольких минут) это по-прежнему незначительные накладные расходы.

Еще один интересный момент заключается в том, что копирование данных шрифта в память процесса пользовательского режима внутри виртуальной машины в целом было довольно простым (с использованием функции write_lin_mem, аналогичной read_lin_mem от Bochspwn, см. реализацию в mem_interface.cc (https://github.com/j00ru/kfetch-toolkit/blob/master/instrumentation/mem_interface.cc)), мы должны были убедиться, что виртуальный адрес, переданный гипервизору, имел соответствующее отображение физической памяти (т. е. был зафиксирован, а не выгружен). Память, выделенная в пространстве пользователя Windows с помощью стандартных распределителей или VirtualAlloc (https://msdn.microsoft.com/en-us/library/windows/desktop/aa366887(v=vs.85).aspx ), по умолчанию доступна для страниц, что может быть адресовано либо с помощью функции VirtualLock (https://msdn.microsoft.com/en-us/library/windows/desktop/aa366895(v=vs.85).aspx), либо путем простой записи данных в выделенную область памяти, что также гарантирует, что она останется отображенной в ближайшем будущем.

Благодаря этому дизайну мы теперь могли масштабировать фаззинг шрифтов ядра Windows на любое количество машин, независимо от основной операционной системы (при условии, что она могла запускать Bochs) и от того, была ли доступна поддержка аппаратной виртуализации или нет. Опять же, единственной серьезной проблемой была скорость гостевых систем, но с имеющимися в нашем распоряжении ресурсами было легко сбалансировать накладные расходы эмулятора и выйти далеко за пределы возможностей одного высокопроизводительного ПК. Мы также предприняли ряд шагов, чтобы максимально оптимизировать протестированную установку Windows (как с точки зрения размера, так и с точки зрения фонового выполнения), что подробно описано в одном из следующих разделов.

The client harness

Наряду с мутацией и генерацией шрифтов таким образом, который делает наиболее вероятным сбой тестируемого программного обеспечения, исчерпывающая проверка путей кода обработки ввода, вероятно, является наиболее важной частью фаззинга. Даже если у нас есть самые умные мутаторы, самые эффективные механизмы обнаружения ошибок и миллионы машин для использования, автоматизированное тестирование не принесет многого, если оно никогда не запустит уязвимый код. Кроме того, даже если код действительно выполняется, но затрагивает только 1% входных данных в процессе, наши шансы вызвать сбой значительно (и излишне) ограничены. Вот почему мы полагали, что нужно уделить много внимания реализации клиентского интерфейса, чтобы гарантировать, что все поверхности атаки, связанные со шрифтами, будут проверены на всех данных, включенных в каждый входной шрифт. Этот подход был полной противоположностью некоторым другим попыткам фаззинга в прошлом, когда все тестирование шрифтов состояло из запуска стандартной программы Windows Font Viewer для проверки искаженных шрифтов или использования ее только для отображения символов в пределах печатного диапазона ASCII со стилем по умолчанию и размера точки.

Начнем с того, что каждый файл может содержать несколько шрифтов, число которых возвращается при успешном вызове функции AddFontResource (https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd183326(v=vs.85).aspx). Чтобы перечислить их так, чтобы операционная система могла их различить, мы использовали недокументированную функцию GetFontResourceInfoW (экспортируемую gdi32.dll) с параметром QFR_LOGFONT. Функция работает с файлами, сохраненными на диске, и с использованием определенного аргумента возвращает массив структур LOGFONT ( https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd145037(v=vs.85).aspx), каждая из которых описывает один встроенный шрифт. Вот почему была необходима первоначальная пробная итерация: извлечь дескрипторы из исходного, немодифицированного файла шрифта (гарантируя согласованность информации).

Пробный прогон также был единственным случаем, когда harness.exxe работал с файловой системой. Для фактической загрузки шрифтов для их тестирования мы вызвали функцию AddFontMemResourceEx (https://msdn.microsoft.com/en-us/library/windows/desktop/dd183325(v=vs.85).aspx), которая устанавливает шрифты непосредственно из памяти, а не из файлов, что в данном случае привело к огромному повышению производительности. Структуры LOGFONT были переданы непосредственно как параметр функции CreateFontIndirect (https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd183500(v=vs.85).aspx), которая создавала объекты шрифта GDI и возвращала дескрипторы типа HFONT. Затем дескрипторы передавались в SelectObject (https://msdn.microsoft.com/en-us/library/windows/desktop/dd162957(v=vs.85).aspx), устанавливая шрифт в качестве текущего в указанном контексте устройства. В нашей программе мы создали пять объектов шрифта для каждого извлеченного LOGFONT: один с их исходным содержимым и четыре со свойствами высоты, ширины, курсива, подчеркивания и зачеркивания, выбранными полуслучайно (с постоянным сидом srand()).

Для каждого такого объекта шрифта мы хотели отобразить все поддерживаемые глифы. Как мы быстро выяснили, функция GetFontUnicodeRanges (https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd144887(v=vs.85).aspx) была разработана именно для этой задачи: перечисление диапазонов юникодных символов, которые имеют соответствующие контуры в шрифте. С этой информацией мы вызывали API DrawText (https://msdn.microsoft.com/en-us/library/windows/desktop/dd162498(v=vs.85).aspx ) для каждых 10 последующих символов. Кроме того, мы также вызвали функцию GetKerningPairs (https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd144895(v=vs.85).aspx) один раз для каждого шрифта и функцию GetGlyphOutline(https://msdn.microsoft.com/en-us/library/windows/desktop/dd144891(v=vs.85).aspx) с различными параметрами для каждого глифа. В целом, программа работала очень быстро — все операции выполнялись в процессе, почти не взаимодействуя с системой, кроме как через системные вызовы, работающие с тестируемыми шрифтами и связанными с ними графическими объектами. С другой стороны, мы считаем, что ему удалось использовать большую часть соответствующей поверхности атаки, что в некоторой степени подтверждается тем фактом, что 16 сбоев были вызваны четырьмя различными системными вызовами: NtGdiGetTextExtentExW, NtGdiAddFontResourceW, NtGdiGetCharABCWidthsW и NtGdiGetFontUnicodeRanges.

Оптимизация системы и выявление багчеков

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

Мы предприняли ряд действий для оптимизации скорости и размера системы:

- Изменили графическую тему на классическую.
- Отключили все сервисы, которые не были абсолютно необходимы для работы системы (оставили только несколько критических).
- Установите режим загрузки Минимальная/Безопасная загрузка с VGA, чтобы загружались только основные драйверы режима ядра.
- Удаллили все компоненты Windows по умолчанию (игры, веб-браузер и т. д.).
- Установили параметр "Настроить для лучшей производительности" в свойствах системы.
- Изменили оболочку по умолчанию в реестре с explorer.exe на harness, так что Explorer вообще никогда не запускался.
- Удалили все элементы из автозапуска.
- Отключили индексацию диска.
- Отключили пейджинг.
- Отключили гибернация.
- Удалили большинство ненужных файлов и библиотек из каталога C:\Windows.


Со всеми вышеуказанными изменениями нам удалось уменьшить размер образа диска примерно до 7 гигабайт и запустить его в Bochs менее чем за пять минут. Конечно, при подготовке образов для 2-й и 3-й итерации многие изменения пришлось отменить, чтобы обновить операционную систему, а затем применить обратно.

Интересно, что одно из наших оптимизационных изменений повлияло на воспроизводимость ошибки № 684( https://bugs.chromium.org/p/project-zero/issues/detail?id=684). Хотя об ошибке в Microsoft сообщили в тот же день, что и о других результатах третьей итерации, через две недели поставщик сообщил нам, что не может получить копию. После некоторого обмена информацией мы более внимательно изучили возможные различия в конфигурации между установкой по умолчанию и нашей тестовой средой. Затем мы быстро поняли, что в рамках настройки параметра "Настроить для лучшей производительности" в "Свойствах системы" параметр "Сглаживание краев экранных шрифтов" был снят, хотя по умолчанию он включен. Оказалось, что действительно уязвимость проявлялась только при отключенной опции. Когда Microsoft подтвердила, что теперь может воспроизвести проблему, мы обновили дату сообщения в трекере, и поставщик исправил ошибку в новом 90-дневном окне.

Когда дело доходит до обнаружения системных ошибок, мы выбрали самое простое решение, предписывая Windows автоматически перезапускаться при возникновении критического системного сбоя:

1651660537251.png


На стороне инструментов Bochs перезагрузка системы может быть тривиально обнаружена с помощью следующего обратного вызова:


void bx_instr_reset(unsigned cpu, unsigned type);

Мы могли полагаться на перезагрузку системы как на индикатор сбоя, потому что это была единственная причина, по которой Windows могла завершить работу: система была настроена на неограниченную работу, и почти никакие другие службы или процессы не работали в фоновом режиме (т. Инструмент обновления). Одним из основных недостатков этого решения было то, что в момент перезагрузки системы никакая контекстная информация (регистры, флаги, трассировка стека) больше не была доступна, не говоря уже о доступе к дампу сбоя системы или любой другой информации, связанной с сбоем. Единственным следом багчека был сам файл шрифта, но благодаря таким механизмам, как специальные пулы и наличие точной копии среды фаззинга, подготовленной для воспроизведения, этого оказалось в основном достаточно. Мы смогли воспроизвести почти все сбои, происходящие в Bochs, исключительно на основе сохраненных тестовых случаев.

Конечно, в этой области есть много возможностей для совершенствования. Лучший инструментарий мог бы попытаться отфильтровать исключения ядра и получить оттуда информацию о контексте ЦП, подключиться к доступу к диску, чтобы перехватить файл аварийного дампа, сохраняемый в файловой системе, или, по крайней мере, сделать снимок экрана "Синего экрана смерти", чтобы получить общее представление о сбое (тип проверки ошибок, четыре аргумента). Однако, поскольку упрощенная реализация работала у нас так надежно, а ее настройка заняла всего несколько минут, мы не чувствовали необходимости ее усложнять.

Воспроизведение сбоев

Воспроизведение сбоя вообще было одним из самых простых этапов процесса. Мы использовали виртуальную машину VirtualBox с точно такой же средой, что и фаззер, и модифицировали систему загрузки шрифтов, чтобы загружать шрифт из файла, а не запрашивать его с помощью инструкции LFENCE. Затем нам также пришлось написать простую программу-репродюсер, которая следовала очень простой схеме:


1. Загружала имя последнего обработанного образца из файла progress.txt (если он существует).
2.Проверяла, не сохранены ли аварийные дампы в C:\Windows\Minidumps.
- Если они есть, копировала его в общий каталог VirtualBox под именем последнего файла шрифта.
- Запускала WinDbg, используя его для создания текстового отчета о аварийном дампе (!analyze -v) и копировала его в тот же каталог.
- Удаляла аварийный дамп локально.
3.Список файлов во входном каталоге, пропускала их до тех пор, пока не будет найден последний обработанный файл. Выбералае следующий и сохраните его имя в progress.txt.
4. Запускала модифицированную обвязку несколько раз (для верности, в общем-то должно хватить).
- В случае сбоя система автоматически сохраняла аварийный дамп и перезагружается.
5. Если мы все еще работаем в этот момент, это означает, что сбой невоспроизводим.

Чтобы убедиться, что при обработке следующего образца не сохраняется поврежденное состояние системы, перезапустите систему «вручную» (через ExitWindowsEx API (https://msdn.microsoft.com/pl-pl/library/windows/desktop/aa376868(v=vs.85).aspx)).

Благодаря вышеописанному алгоритму, как только воспроизведение было завершено, мы удобно получили список каталогов, соответствующих аварийным образцам, и содержащий как двоичные аварийные дампы, так и их текстовые представления, которые мы могли легко просмотреть или обработать другими способами. Выполняя полную перезагрузку системы, мы также могли быть на 100% уверены, что каждый сбой системы действительно был вызван одним конкретным шрифтом, поскольку никакие ранее загруженные шрифты не могли повлиять на результат. Обратной стороной было, конечно, увеличение времени, которое занимает весь процесс, но с чрезвычайно оптимизированной системой, которая загружалась менее чем за 50 секунд и обычно менее 10 секунд выполнялась для тестирования шрифта, пропускная способность составляла где-то около 1500 образцов в день, что было приемлемо для обработки вывода первого (самого аварийного) запуска всего за несколько дней.

Единственными изменениями, примененными к системе воспроизведения по сравнению с системой фаззинга, были добавление программы воспроизведения в автозагрузку, сокращение времени нахождения в загрузочном экране "Восстановление после ошибки Windows" (появляющемся после сбоя системы) до 1 секунды, настройка общего доступа. Папки и установите WinDbg, чтобы иметь возможность генерировать текстовые фрагменты аварийного дампа в самой гостевой системе. Мы также подготовили соответствующую 64-разрядную воспроизводимую виртуальную машину Windows 7 с большим объемом оперативной памяти, чтобы потенциально предоставить Special Pools больше памяти для работы, но это оказалось не очень полезным, поскольку сбои обычно либо воспроизводились в обеих сборках, либо не ни на одном из них.

Минимизация образцов

Существует два типа минимизации выборки, которые могут быть выполнены в контексте шрифтов SFNT: табличная гранулярность и байтовая гранулярность (в рамках каждой таблицы). Конечно, оба они имеют смысл только для результатов мутационного фаззинга. Сообщая о сбоях в Microsoft, мы выполняли только гранулярную минимизацию таблицы первого уровня, которая служила двум целям: удовлетворить наше любопытство и помочь устранить дубликаты сбоев. Особенно в первой итерации, когда нам приходилось иметь дело с десятками различных трассировок стека, было очень полезно знать, какая проверка ошибок была вызвана мутациями в какой таблице (таблицах).

Инструмент минимизации был тривиален по своей конструкции: он использовал нашу существующую библиотеку C++ SFNT для загрузки как исходных, так и измененных образцов и отображал список одинаковых и различающихся таблиц. Затем пользователь может восстановить выбранную таблицу (вставить ее исходное содержимое вместо искаженного) и проверить, вызовет ли новый шрифт сбой. Если да, то мутации в таблице неактуальны и их можно пропустить; в противном случае по крайней мере некоторые из них были необходимы для запуска ошибки и должны были быть сохранены. После нескольких повторений у нас остались 1-3 таблицы, которые были необходимы для краха.

Гранулярная минимизация байтов была выполнена только для нескольких конкретных образцов, чтобы выяснить, какие поля в структурах шрифта были виновниками сбоев. Поскольку ни один из алгоритмов мутации не изменил размер ни одной из таблиц (только изменил данные "статическим" способом), можно было бы использовать очень простой алгоритм: на каждой итерации половина отличающихся в настоящее время байтов восстанавливалась бы в файл мутации. Если бы ошибка все еще происходила, эти байты были несущественными и о них можно было забыть. Если сбоя больше не было, некоторые байты должны были быть важными, поэтому изменения итерации были отменены, и для тестирования было выбрано другое подмножество байтов. Минимизация считалась завершенной, когда набор различающихся байтов больше не мог быть уменьшен после большого количества попыток.

Отдельный алгоритм минимизации потенциально может быть разработан для выходных данных генератора программ TTF; однако, поскольку такой уникальный сбой был только один, и мы не собирались проводить подробный анализ его основных причин, мы не стали его разрабатывать.

Будущая работа

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

Мы собрали их в списке ниже:

1. Фаззинг на основе покрытия.

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

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

Идея связана с более общей концепцией выполнения фаззинга ядра Windows с учетом покрытия (в Linux он уже есть, см. syzkaller (https://github.com/google/syzkaller)), независимо от того, являются ли это шрифтами, системными вызовами или какой-либо другой поверхностью атаки. Это, однако, предмет для отдельного исследования, которое, вероятно, лучше всего реализуется с помощью реальной виртуализации и VMI (Virtual Machine Introspection) для повышения производительности.

2.Улучшение мутации.

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

Другие идеи включают в себя проверку предположения, что мы вручную нашли все ошибки в обработке CharString и написание генератора потока инструкций для шрифтов OTF, поиск способа фазз-тестирования таблиц SFNT, которые мы ранее считали слишком хрупкими для мутации (cmap, head, hhea, name, loca и т.д.), или изменение шрифтов на основе представления, отличного от скомпилированной двоичной формы (например, файлы XML .ttx, сгенерированные FontTools).

3. Фаззинг на других версиях Windows.

Проект запускался на Windows 7 от начала и до конца с предположением, что он может содержать наибольшее количество ошибок (например, тех, которые не были перенесены из Windows 8.1 и 10). Однако возможно, что более новые платформы могут включать некоторые дополнительные функции, и их фаззинг может выявить уязвимости, которых нет в Windows 7.

4. Улучшено обнаружение сбоев Bochs.

Как упоминалось в предыдущем разделе, обнаружение сбоев изначально было реализовано на основе очень простого индикатора в виде перезагрузки машины. Это фактически ограничило количество информации о контексте сбоя, которую мы могли собрать, до нуля, что сделало невозможным исследование сбоев, которые не воспроизводились с сохраненным образцом. Одним из возможных решений может быть установка обратного вызова для события BX_INSTR_EXCEPTION, чтобы перехватывать исключения ЦП по мере их возникновения и извлекать из них контекстную информацию. Этот параметр также имеет дополнительное преимущество, заключающееся в том, что его можно использовать для сигнализации об исключениях режима ядра, которые обычно маскируются универсальными обработчиками исключений, но все же обнаруживают настоящие ошибки.

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

Резюме

Как метод тестирования программного обеспечения, фаззинг имеет очень низкую планку входа и может использоваться для достижения удовлетворительных результатов с небольшим опытом или вложенными усилиями. Тем не менее, это все еще не панацея в поиске уязвимостей, и есть много этапов, которые могут потребовать тщательной настройки или индивидуальной адаптации для конкретной цели или формата файла, особенно для нетривиальных целей, таких как ядра операционных систем с закрытым исходным кодом. В этом посте мы продемонстрировали, как мы пытались максимально улучшить процесс фаззинга шрифтов ядра Windows в пределах доступных временных ресурсов. В частности, мы вложили много сил в мутацию, генерацию и проверку входных данных достаточно эффективным способом, а также в масштабирование процесса фаззинга на тысячи машин за счет разработки специального инструментария Bochs и агрессивной оптимизации операционной системы. Результат работы в виде 16 уязвимостей высокой степени опасности показал, что методы были эффективными и улучшенными по сравнению с предыдущей работой.

Учитывая, насколько велик потенциал фаззинга и насколько широка тема, мы с нетерпением ждем дальнейшего развития и использования фаззинга.

Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://googleprojectzero.blogspot.com/2016/07/a-year-of-windows-kernel-font-fuzzing-2.html
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Какое то практическое применение у этого есть? RCE например?
Как локально так и удаленно можно. Например встроить в документ уязвимый шрифт и тем самым проэксплуатировать уязвимость удаленно. Ссылка на дополнительный материал -> тык..
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх