Заметки о фаззинге библиотеки Uniscribe в ОС Windows

Заметки о фаззинге библиотеки Uniscribe в ОС Windows

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

Автор: Mateusz Jurczyk

Среди всех 119 уязвимостей с кодом CVE, исправленных компанией Microsoft в мартовском обновлении, было несколько брешей, связанных с обработкой шрифтов в библиотеке Uniscribe. Тема безопасности, связанная со шрифтами, уже затрагивалась в некоторых статьях в контексте ручного анализа ([1][2]) и фаззинга ([3][4]). Отличие данной заметки в том, что библиотека Uniscribe является малоизвестным компонентом уровня пользователя, и данный вектор атак ранее не рассматривался, в отличие от работы со шрифтами на уровне ядра в драйверах win32k.sys и ATMFD.DLL. В этой статье мы коротко коснемся истории событий и описания библиотеки Uniscribe, а также рассмотрим масштабирование инфраструктуры тестирования методом фаззинга и другие моменты, которые были обнаружены в ходе исследования. Отчеты обо всех упоминаемых уязвимостях, которые были отправлены в компанию Microsoft, вместе с примерами реализации можно найти на официальном багтрекере проекта Project Zero ([5]).

Введение

В ноябре 2016 года начался новый виток тестирования методом фаззинга механизмов в Windows, отвечающих за работу со шрифтами (полная схема описывается в статье [4]). На тот момент на уровне ядра все было достаточно гладко, по крайней мере при использовании техник тестирования, которые использовали мы. Однако время от времени мы возвращались к этому вопросу и экспериментировали с конфигурацией и входными параметрами в надежде найти новые уязвимости на базе существующей инфраструктуры. Через несколько дней нам удалось получить несколько примеров, которые предположительно могли приводить к краху гостевую Windows-систему, запущенную внутри эмулятора Bochs. Однако повторно эти падения воспроизвести не удалось по необъяснимым причинам. Кроме того, в процессе тестирования нам удалось получить один интересный и неожиданный результат: в одном из тестовых случаев происходил крах только в пользовательском режиме, но в то же время система в целом оставалась в работоспособном состоянии. Сей факт мог возникнуть по двум причинам: либо была брешь в нашем коде, либо происходил непредвиденный случай, связанный с обработкой шрифтов, в кольце-3. При более глубоком исследовании обнаружилось, что имело место быть необрабатываемое исключение в следующем контексте:

(4464.11b4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=0933d8bf ebx=00000000 ecx=09340ffc edx=00001b9f esi=0026ecac edi=00000009
eip=752378f3 esp=0026ec24 ebp=0026ec2c iopl=0         nv up ei pl zr na pe nc
cs=0023  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00010246
USP10!ScriptPositionSingleGlyph+0x28533:
752378f3 668b4c5002      mov     cx,word ptr [eax+edx*2+2] ds:002b:09340fff=????

К тому моменту мы еще не до конца осознавали, что наши инструменты могли активировать любые уязвимости, связанные с обработкой шрифтов, и не только на уровне ядра (несмотря на то, что в прошлом уже были опубликованы и исправлены несколько брешей, например, CVE-2016-7274 [6]). Как следствие, наша система тестирования методом фаззинга не была адаптирована для поиска ошибок на уровне пользователя, и любые подобные падения, вызывавшие перезапуск машины, оставались полностью недетектируемыми системой поиска дыр.

Мы быстро определили, что библиотека usp10.dll, согласно документации Microsoft [7], относилась к обработчику текстов в кодировке Unicode библиотеки Uniscribe. Достаточно большой модуль размером 600-800 кб в зависимости от версии и разрядности системы, отвечающий за отображение текста в кодировке Unicode, о чем, собственно, и намекает имя компонента. С точки зрения безопасности важно отметить, что часть кода, написанного на C++, наследуется из Windows 2000 и включает обработку различных сложных структур, относящихся к шрифтам TrueType/OpenType, вдобавок к тому, что уже реализовано в ядре. Библиотека Uniscribe в основном касается специфических продвинутых типографских таблиц: «GDEF», «GSUB», «GPOS», «BASE», «JSTF», а также, в некоторой степени, «OS/2», «cmap» и «maxp». До кода, связанного с этими таблицами, можно добраться при помощи простого вызова DrawText [8] или эквивалентной API-функции с текстом, закодированным в Unicode, и шрифтом, контролируемым злоумышленником. Поскольку для запуска большей части кода библиотеки специальные вызовы не требуются, у нас вырисовывается перспективный вектор атак в приложениях, использующих GDI для отображения текста со шрифтами из недостоверных источников. В подтверждение нашей гипотезы служат результаты трассировки стека во время первоначального падения и тот факт, что подобное происходило в программе, которая не включает какой-либо код, имеющий отношение к библиотеке usp10.dll:

0:000> kb
ChildEBP RetAddr
0026ec2c 09340ffc USP10!otlChainRuleSetTable::rule+0x13
0026eccc 0133d7d2 USP10!otlChainingLookup::apply+0x7d3
0026ed48 0026f09c USP10!ApplyLookup+0x261
0026ef4c 0026f078 USP10!ApplyFeatures+0x481
0026ef98 09342f40 USP10!SubstituteOtlGlyphs+0x1bf
0026efd4 0026f0b4 USP10!SubstituteOtlChars+0x220
0026f250 0026f370 USP10!HebrewEngineGetGlyphs+0x690
0026f310 0026f370 USP10!ShapingGetGlyphs+0x36a
0026f3fc 09316318 USP10!ShlShape+0x2ef
0026f440 09316318 USP10!ScriptShape+0x15f
0026f4a0 0026f520 USP10!RenderItemNoFallback+0xfa
0026f4cc 0026f520 USP10!RenderItemWithFallback+0x104
0026f4f0 09316124 USP10!RenderItem+0x22
0026f534 2d011da2 USP10!ScriptStringAnalyzeGlyphs+0x1e9
0026f54c 0000000a USP10!ScriptStringAnalyse+0x284
0026f598 0000000a LPK!LpkStringAnalyse+0xe5
0026f694 00000000 LPK!LpkCharsetDraw+0x332
0026f6c8 00000000 LPK!LpkDrawTextEx+0x40
0026f708 00000000 USER32!DT_DrawStr+0x13c
0026f754 0026fa30 USER32!DT_GetLineBreak+0x78
0026f800 0000000a USER32!DrawTextExWorker+0x255
0026f824 ffffffff USER32!DrawTextExW+0x1e

Как видно из кода выше, функции библиотеки Uniscribe вызывались внутри user32.dll через библиотеку lpk.dll (Language Pack; Языковый пакет). Как только новый вектор атаки был более менее изучен, мы решили выполнить тесты методом фаззинга. Большая часть инфраструктуры была готова, поскольку тестирование на уровне ядра и на уровне пользователя имеет много общего. Нам нужно было доработать фильтрацию входных данных, поиграться с конфигурацией мутатора, настроить систему и реализовать логику детектирования крахов на уровне пользователя (на уровне системы тестирования и методов эмулятора Bochs). Все эти этапы подробно описаны ниже. Через несколько дней все запланированное было реализовано, после чего мы получили информацию по более 80 падениям по уникальным адресам, которая нуждалась в анализе и сортировке. В таблице ниже показан перечень проблем, найденных во время первой фазы тестирования и отправленных в компанию Microsoft в декабре 2016 года.

Краткий обзор результатов

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


Номер трекера

Тип доступа к памяти во время краха

Функция, участвующая в крахе

CVE

1022

Некорректная запись n байт (memcpy)

usp10!otlList::insertAt

CVE-2017-0108

1023

Некорректное чтение / запись 2 байт

usp10!AssignGlyphTypes

CVE-2017-0084

1025

Некорректная запись n байт (memset)

usp10!otlCacheManager::GlyphsSubstituted

CVE-2017-0086

1026

Некорректная запись n байт (memcpy)

usp10!MergeLigRecords

CVE-2017-0087

1027

Некорректная запись 2 байт

usp10!ttoGetTableData

CVE-2017-0088

1028

Некорректная запись 2 байт

usp10!UpdateGlyphFlags

CVE-2017-0089

1029

Некорректная запись n байт

usp10!BuildFSM и соседние функции

CVE-2017-0090

1030

Некорректная запись n байт

usp10!FillAlternatesList

CVE-2017-0072

Все эти уязвимости, кроме одной, активируются через стандартный вызов функции DrawText и приводят к нарушению целостности памяти кучи. Единственное исключение – проблема #1030, относящаяся к задокументированной API-функции ScriptGetFontAlternateGlyphs из библиотеки Uniscribe. Эта процедура отвечает за получения списка альтернативных начертаний (или глифов) для конкретного символа, и самое интересное заключается в том, что эта брешь не является следствием операций с любыми внутренними структурами. Проблема заключается в некорректной обработке аргумента cMaxAlternates, что приводит записи в буфер pAlternateGlyphs выходных данных большего размера, чем разрешено в вызывающей функции. Сей факт означает, что переполнение буфера не имеет отношения к конкретному типу памяти. В зависимости от типа указателя, передаваемого клиенту, переполнение может возникать в стеке, куче или статической памяти. Эксплуатируемость уязвимости зависела бы от архитектуры приложения и параметров, используемых при компиляции.  Следует признать, что пока не очень понятно, в каких реальных клиентах используется эта функция, и соответствуют ли эти клиенты условиям, способствующим осуществлению атаки.

Более того, мы добились 27 уникальных крахов, вызванных некорректным чтением памяти по непустым адресам, что потенциально может привести к раскрытию конфиденциальной информации, хранимой адресном пространстве процесса. Из-за большого объема сведений в этих падениях мы не смогли детально проанализировать каждый крах или хотя бы устранить избыточность данных. Мы сделали разделение по адресу исключения верхнего уровня и добавили всю информацию на страницу багтрекера под номером #1031:

  1. usp10!otlMultiSubstLookup::apply+0xa8
  2. usp10!otlSingleSubstLookup::applyToSingleGlyph+0x98
  3. usp10!otlSingleSubstLookup::apply+0xa9
  4. usp10!otlMultiSubstLookup::getCoverageTable+0x2c
  5. usp10!otlMark2Array::mark2Anchor+0x18
  6. usp10!GetSubstGlyph+0x2e
  7. usp10!BuildTableCache+0x1ca
  8. usp10!otlMkMkPosLookup::apply+0x1b4
  9. usp10!otlLookupTable::markFilteringSet+0x1a
  10. usp10!otlSinglePosLookup::getCoverageTable+0x12
  11. usp10!BuildTableCache+0x1e7
  12. usp10!otlChainingLookup::getCoverageTable+0x15
  13. usp10!otlReverseChainingLookup::getCoverageTable+0x15
  14. usp10!otlLigCaretListTable::coverage+0x7
  15. usp10!otlMultiSubstLookup::apply+0x99
  16. usp10!otlTableCacheData::FindLookupList+0x9
  17. usp10!ttoGetTableData+0x4b4
  18. usp10!GetSubtableCoverage+0x1ab
  19. usp10!otlChainingLookup::apply+0x2d
  20. usp10!MergeLigRecords+0x132
  21. usp10!otlLookupTable::subTable+0x23
  22. usp10!GetMaxParameter+0x53
  23. usp10!ApplyLookup+0xc3
  24. usp10!ApplyLookupToSingleGlyph+0x6f
  25. usp10!ttoGetTableData+0x19f6
  26. usp10!otlExtensionLookup::extensionSubTable+0x1d
  27. usp10!ttoGetTableData+0x1a77

Позже выяснилось, что эти 27 крахов были связаны с 21 уязвимостью под кодовыми номерами CVE-2017-0083, CVE-2017-0091, CVE-2017-0092 и в диапазоне от CVE-2017-0111 до CVE-2017-0128, которые были исправлены компанией Microsoft в бюллетене безопасности MS17-011.

В конце мы также отправили отчеты о 7 проблемах, связанных с разыменованием пустого указателя, без указания срока давности в надежде, что устранение любой из этих проблем потенциально может привести к тому, что наш фаззер обнаружит более серьезные дыры. 17 марта из центра Microsoft Security Response Center (MSRC) ответили, что найденные случаи отнесены к проблемам, связанным с отказом в обслуживании, не представляющим особой угрозы. В ближайшее время в бюллетени безопасности добавлять исправления не планируется.

Входные данные, конфигурация мутации и подстройка тестовой среды

Сбор входных примеров – вероятно один из наиболее важных этапов при подготовке тестирования методом фаззинга. Особенно когда не используется обратная связь касательно покрытия кода, что делает невозможным подстройку входных данных к более оптимальной форме. В нашем распоряжении уже было несколько сборников шрифтов, оставшихся от предыдущих тестов. Мы решили использовать тот же набор файлов, который помог нам обнаружить 18 уязвимостей (см. раздел “Preparing the input corpus” в статье [4]). Первоначально входные данные генерировались при помощи специального алгоритма фильтрации (corpus distillation algorithm) на большой выборке шрифтов, собранных в интернете, при помощи библиотеки FreeType2 с открытым исходным текстом. В целом коллекция шрифтов занимала около 2.4 Гб (14848 файлов TrueType и 4659 файлов OpenType). Чтобы адаптировать набор шрифтов для библиотеки Uniscribe, мы оставили только файлы, содержащие как минимум одну из таблиц “GDEF”, “GSUB”, “GPOS”, “BASE” и “JSTF”, которые обрабатываются в Uniscribe. В итоге осталось 3768 шрифтов TrueType и 2520 шрифтов OpenType общим размером 1.68 Гб. У оставшихся шрифтов намного большая вероятность активации уязвимостей в библиотеке Uniscribe по сравнению с удаленными файлам. Именно с этим набором входных данных мы стали работать в дальнейшем.
Конфигурация мутатора похожа на ту, которую мы делали в процессе работы с ядром. Использовались следующие алгоритмы:

  • Манипуляция битами (bit-flipping).
  • Замена байтов на случайные значения (byte-flipping).
  • Перенос данных из одного места в другое (chunk spew).
  • Вставка «специальных целых типов» размером 2 и 4 байта (INT_MIN, INT_MAX и т. д.) в различных порядках следования байтов (special ints).
  • Для бинарной арифметики.

с предварительно вычисленными диапазонами пропорций мутации для каждой таблицы. Единственное изменение, сделанные специально под библиотеку Uniscribe, - добавление мутаций для таблиц “BASE” и “JSTF”, которые ранее не были учтены.

Последний по очередности, но не последний по важности, был добавлен функционал, связанный с фаззингом гостевой системы (guest fuzzing), отвечающий за запуск тестируемых API-функций, связанных с обработкой шрифтов (в основном отображающих все начертания шрифтов различных размеров, а также запрашивающих количество свойств и т. д.). Некоторая часть нужного кода запускалась автоматически через функцию user32!DrawText без необходимости какой-либо модификации. С другой стороны, мы хотели, чтобы покрытие кода библиотеки Uniscribe было настолько обширным, насколько возможно. Полный перечень всех доступных функций извне можно найти в MSDN [9]. После беглого осмотра документации были добавлены вызовы следующих функций: ScriptCacheGetHeight, ScriptGetFontProperties, ScriptGetCMap, ScriptGetFontAlternateGlyphs, ScriptSubstituteSingleGlyph и ScriptFreeCache. Эта идея оказалась успешной, поскольку мы смогли найти вышеупомянутую уязвимость в функции ScriptGetFontAlternateGlyphs. Более того, мы решили удалить вызовы API-функций GetKerningPairs и GetGlyphOutline, поскольку логика этих методов находилась на уровне ядра, а мы сфокусировались на пользовательской части. Поэтому эти функции не помогли бы найти новых уязвимостей в библиотеке Uniscribe, а только замедляли бы процесс фаззинга в целом. Кроме упомянутых небольших изменений в целом инфраструктура тестирования осталась прежней.

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

Детектирование крахов

Первый шаг на пути эффективного обнаружения крахов – отключение специальных пулов (Special Pool) для драйверов win32k.sys и ATMFD.DLL (которые вызывают только издержки и не приносят плюсов при работе в пользовательском режиме) и включение опции PageHeap в приложении Application Verifier во время процедуры тестирования. Эти настройки позволяют увеличить шансы детектирования некорректных доступов к памяти и увеличивают надежность воспроизведения краха и отсев ненужной информации.

Благодаря тому, что код из библиотеки usp10.dll, тестируемый методом фаззинга, работал в том же контексте, что и остальная логика, и нам не нужно было писать полноценный Windows-отладчик для отслеживания другого процесса. Вместо столь сложных телодвижений мы просто установили функцию SetUnhandledExceptionFilter в качестве обработчика исключений верхнего уровня, которая вызывалась каждый раз, когда в внутри процесса генерировалось фатальное исключение. Роль обработчика заключалась в отсылке состояния контекста в CPU во время падения (передаваемого через ExceptionInfo->ContextRecord) в гипервизор (в нашем случае, инструментарию эмулятора Bochs) через гипервызов, связанный с выводом отладочной информации. Кроме того, в отчете содержалась информация о том, по какому адресу памяти возникло падение.

Во время фаззинга системы обработки шрифтов на уровне ядра крахи детектировались при помощи обратного вызова функции BX_INSTR_RESET, предусмотренной в эмуляторе Bochs. Этот подход работал, поскольку гостевая система была настроена на автоматическую перезагрузку во время критической ошибки, после чего запускался обработчик bx_instr_reset. Наипростейший способ интеграции этого подхода при фаззинге на уровне пользователя – добавление вызова ExitWindowsEx в конце обработчика исключений. Таким образом, вся связка будет работать «из коробки» без влезания в существующий инструментарий эмулятора Bochs. Однако при использовании этого метода потерялась бы информация о местонахождении краха, что делает невозможным автоматическое удаление избыточной информации. Чтобы решить эту проблемы, мы ввели новый гипервызов, получающий адрес инструкции, на которой происходил крах, в качестве аргумента от гостевой системы. Затем эта информации передавалась далее в инфраструктуру фаззинга. Когда все крахи стали сгруппированы по адресу исключения, мы сэкономили массу времени на последующую обработку и значительно ограничили количество тестовых случаев для последующего анализа.

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

Заключение

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

Действия и результаты доказывают, что фаззинг - очень универсальная техника и большая часть компонентов системы тестирования может быть легко применена и в других случаях, особенно тех, которые затрагивают схожие форматы файлов. Наконец, было доказано, что возможно выполнять фаззинг не только на уровне ядра в ОС Windows, но и на пользовательском уровне независимо от среды, используемой на хосте (в нашем случае, это была операционная система Linux). Несмотря на то, что в эмуляторе Bochs x86 возникают значительные накладные расходы, замедляющие процесс тестирования, по сравнению с родной средой выполнения, эту систему можно масштабировать и, таким образом, добиваться увеличения количества итераций в секунду. В качестве еще одно интересного факта можно отметить, что проблемы #993 (загрузка ветвей реестра на уровне ядра в Windows), #1042 (EMF+ процессинг в GDI+), #1052 и #1054 (обработка цветового профиля), исправленные недавно, также были найдены методом фаззинга системы Windows, запущенной в эмуляторе Bochs. Лишь с небольшим изменением входных данных, тестовой инфраструктуры и стратегий мутации :).

Ссылки

  1. The “One font vulnerability to rule them all” series starting with https://googleprojectzero.blogspot.com/2015/07/one-font-vulnerability-to-rule-them-all.html
  2. https://googleprojectzero.blogspot.com/2015/09/enabling-qr-codes-in-internet-explorer.html
  3. https://googleprojectzero.blogspot.com/2016/06/a-year-of-windows-kernel-font-fuzzing-1_27.html
  4. https://googleprojectzero.blogspot.com/2016/07/a-year-of-windows-kernel-font-fuzzing-2.html
  5. https://bugs.chromium.org/p/project-zero/issues/list?can=1&q=product%3Auniscribe+fixed%3A2017-mar-14
  6. http://blogs.flexerasoftware.com/secunia-research/2016/12/microsoft_windows_loaduvstable_heap_based_buffer_overflow_vulnerability.html
  7. https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd374091(v=vs.85).aspx
  8. https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd162498%28v=vs.85%29.aspx
  9. https://msdn.microsoft.com/pl-pl/library/windows/desktop/dd374093(v=vs.85).aspx

Мир сходит с ума и грянет киберапокалипсис. Подпишись на наш Телеграм канал, чтобы узнать первым, как выжить в цифровом кошмаре!