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

Статья CVE-2021-38001: краткое введение во встроенный кэш V8 и использование путаницы типов

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Некоторая справочная информация

CVE-2021-38001 рассматривался на TianFu Cup 2021. Эта ошибка использует проблему путаницы типов, возникшую во встроенном кэше V8, и может привести к удаленному выполнению кода.

В моем последнем блоге о V8 pwn я проанализировал и воспроизвел CVE-2020-6507. Его основной причиной является проблема чтения/записи OOB, возникшая на этапе JIT V8. Но чтобы эксплуатировать баг, мне вообще не нужно было понимать, как работает JIT в V8. На этот раз CVE-2021-38001 как ошибка путаницы типов требует некоторых базовых знаний о том, что такое встроенный кеш и как он работает в v8.

Кратко о встроенном кэше

Когда я впервые слышу о термине «Встроенный кэш», я понятия не имею, что он делает. Я знаю, что это какой-то кеш, но понятия не имею, где вписывается «встроенный» (я до сих пор не знаю ничего про это).

Для начала давайте взглянем на код JS:

1685610676002.png


Мы знаем, что a будет иметь свойство a.a и a.b. Это свойство должно храниться в поле свойств его JSObject. Когда мы пишем еще одну строку кода, например: console.log(a.a), и изучаем байтовый код, сгенерированный V8:

1685610685281.png


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

Анализируя a в gdb, мы видим:

1685610693859.png


и посмотрите на содержимое его памяти:

1685610728075.png


у нас есть значения свойств, которые мы ищем. Я предполагаю, что имена свойств хранятся где-то еще, но в данном случае не совсем уместны. Когда свойств мало, V8 будет хранить значения сразу после своих метаданных (например, HiddenClass, Property, Element), как и в нашем случае. Когда свойства продолжают добавляться, V8 выделяет массив, подобный объекту, для хранения этих дополнительных свойств.

Теперь мы немного понимаем, как V8 находит свойства, давайте рассмотрим следующее: когда к свойству объекта обращаются много раз, наверняка V8 что-то сделает с этим, чтобы ускорить выполнение, верно? Здесь на помощь приходит Inline Cache (IC).

IC кэширует часто используемые свойства, чтобы ускорить работу, записи в IC не будут проходить через обычный процесс поиска свойства, вместо этого IC сохраняет смещение свойства относительно объекта доступ. В V8 есть 3 состояния IC:

- Мономорфный IC: когда только один тип объекта обладает свойством
- Полиморфный IC: когда несколько типов объектов имеют одно и то же свойство
- Мегаморфный IC: когда слишком много типов объектов имеют одно и то же свойство

Продолжим наш пример кода, предположим, я добавляю эту функцию:

1685610764712.png


для мономорфных ИС, когда я вызываю только foo с одним типом, и аналогично, я вызываю foo(obj1) и foo(obj2)с obj1 и obj2 будучи разными типами объектов в полиморфных ИС, то же самое относится и к мегаморфным ИС.

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

Чтобы дать краткий пример, в нашем примере с объектом а, когда a.b оно попадает в IC, оно находит ьадрес объекта а и получает любое значение по его адресу address(a) + 0xcс смещению.

На этом этапе вы можете увидеть, как это происходит, если мы каким-то образом заставим IC думать, что свойство объекта, к которому мы обращаемся, является другим объектом, он перейдет к смещению этого другого объекта и получит значение. Но прежде чем мы углубимся в основную причину этой ошибки и в то, как использовать путаницу типов, нам нужно знать о receiver, lookup_start_object и holder в V8.

Теперь мы добавим больше классов в наш пример кода:

1685610775453.png


Допустим, мы вызываем bar. В данном случае — receiver это объект, который инициирует поиск свойства, а в нашем случае c — это объект receiver, поскольку доступ к свойству осуществляется из c. — это объект, с которого начинается поиск, и в нашем случае поначалу тоже lookup_start_object будет с, но поскольку C на самом деле свойство не C.c объявлено в его конструкторе, V8 продолжает подниматься по иерархии. B является родительским классом С , и он будет lookup_start_object следующим значением, так как B имеет свойство B.c, поиск остановится здесь, и он присваивается holder B, так как он владеет свойством с именем c. Если есть другая функция, пытающаяся получить доступ объекта obj.b, процесс будет аналогичен, но holder будет равен A, и lookup_start_object начнется с C, так и B и в конце концов A.

Анализ причин

Вот скриншот патча :

1685610786603.png


Для тех, кто мало знает о внутренностях V8, например как я, я действительно понятия не имею, в чем проблема. Но, пробежавшись по нему, он, вероятно, как-то связан с импортом или экспортом модуля. К счастью для меня, vngkv123 поделился PoC, поэтому также ознакомьтесь с его/ее анализом этой ошибки:

1685610802645.png


PoC импортирует некоторую переменную из модуля и в функции trigger() устанавливает прототип класса C, создает новый экземпляр класса Cи устанавливает несколько его свойств и, наконец, возвращает значение c.m(), которое извлекает значение super.y.

Теперь вернемся к V8 и прочитаем какой-нибудь источник. В ic.cc функция ComputeHandler находит обработчик объекта поиска. Я рекомендую прочитать весь его код, но вот что важно для этого CVE:

1685610812513.png


В операторе switch он проверяет состояние объекта поиска. Его состояния определены в lookup.h:

1685610822889.png


Я не смог найти много информации о том, что ACCESSOR представляет собой, поэтому я обратился за помощью к ChatGPT. По крайней мере, согласно ему, ACCESSOR используется для доступа к свойству, когда есть методы получения и установки. Хватит, продолжим путь. Когда владелец свойства находится в JSModuleNamespace, он находит запись в модуле exports, затем находит индекс значения и, наконец, возвращает LoadModuleExport(isolate(), index).

Давайте проследим и прочитаем код для LoadModuleExportin handler-configuration-inl.h:

1685610841458.png


Не уверен, что он на самом деле делает, но я предполагаю, что он берет индекс значения и загружает его.

На данный момент у нас есть общее представление о том, как возвращается значение свойства:

1685610852130.png


В accessor-assembler.cc, это тоже пропатченный файл, у нас есть фрагмент:

1685610861634.png


Он связывается с module_export и делает следующее:

- загружает объект module p->receiver() из объекта JSModuleNamespace
- загружает объект exports из объекта module на основе смещения
- получает cell по определенному индексу от exports
- загружает value в cell
- возвращает value
Но подождите минутку, если мы внимательно прочитаем ic.cc:

1685610870663.png


мы видим cast(holder)->module().exports(), и в accessor-assembler.cc:

1685610878761.png


Сначала мы передаем holder как lookup_start_object, а позже мы обращаемся к свойству receiver на основе смещений. И это путаница типов между holderи receiver. Когда lookup_start_object(holder) и receiver различаются, возникает путаница типов.

Напомним PoC ранее, в функции trigger() мы сначала устанавливаем прототип C к zz, а zz прототип к переменной module, которая является объектом из JSModuleNamespace. Теперь, когда вызывается c.m(), она пытается найти super.y, которое является свойством, принадлежащим его родительскому классу. Поскольку zz нет этого свойства, он поднимается по цепочке прототипов и находит module в качестве возможного holder. В данном случае у receiver есть C и у holder есть module. И в IC, когда IC пытается получить доступ к свойству, он ищет смещение JSModuleNamespace->modulefirst, что не удастся, поскольку JSModuleNamespace теперь есть C. И будет продолжать идти по этому пути.

Когда мы попытаемся выполнить этот PoC в d8, он выдаст ошибку доступа к памяти:

1685610895457.png


мы видим, что к значению C.x0 обращаются как к указателю, и, поскольку этот адрес ничего не имеет, происходит SEGV_ACCERR.

Давайте воспользуемся gdb для отладки и посмотрим, как IC найдет это свойство. Вот отладочная информация о JSModuleNamespace:

1685610903670.png


мы видим module, что он находится по смещению 0x081d3535 , и проверяем его пространство в памяти:

это по смещению JSModuleNamespace+0xc. Следующим должно быть module->exports:

1685610913691.png

и экспорт находится в 0x08049e8d:

1685610924392.png


его смещение равно module+0x4 байтам. Затем нам нужно найти ячейку экспортируемого значения в хеш-карте:

1685610933048.png


и значение ячейки находится в exports+0x28 байтах смещения. Поскольку это хэш-карта, значение постоянно меняется, но вы поняли идею. Мы также видим на скриншоте выше, что значение ячейки хранится по адресу 0x0804a095:

1685610943090.png

по смещению cell+0x4 адреса байтов. И это будет окончательное значение, возвращаемое IC. Полная цепочка выглядит так:

1685610949612.png


В моем примере я назвал свой JSModuleNamespace как foo, а экспортированное поле как bar, следовательно, foo.bar это узел.

Теперь, если мы посмотрим на эту диаграмму пути поиска, мы можем заметить, что когда мы назначаем дополнительные свойства для C, поскольку их количество невелико, эти значения будут храниться как свойство внутри объекта. И в C+0xc байтах смещения, то есть после HiddenClass(4 bytes), Property(4 bytes), Elements(4 bytes), значение Property1, следовательно, значение C.x0 = 0x40404040 / 2. Но зачем нам делить значение пополам? Это связано с тем, как V8 обрабатывает SM со всеми целыми числами (SMI) и указателями. Это объяснение ChatGPT4 кажется законным, поэтому я добавлю его сюда:

В V8 малые целые числа (SMI) и указатели обрабатываются по-разному с использованием механизма тегов. Младший значащий бит (LSB) значения используется, чтобы различать их. Вот пример, иллюстрирующий, как V8 представляет SMI и указатели:

Допустим, у нас есть 32-битная система. Младший значащий бит используется как тег:

1. Для SMI:
- Младший значащий бит устанавливается равным 0.
- Остальные 31 бит хранят фактическое целочисленное значение, сдвинутое влево на 1 бит.

Пример: Целое число 42 (в двоичном формате: 101010) будет представлено в виде SMI следующим образом:

1685610960269.png


Таким образом, SMI-представление числа 42 будет 1010100 или 0x54.

2. Для указателей (объектов или значений, выделенных кучей):
- Младший значащий бит устанавливается равным 1.
- Остальные 31 бит хранят адрес памяти.

Пример: Предположим, у нас есть указатель на адрес памяти 0x12345678. В V8 указатель будет представлен как:

1685610971727.png


Таким образом, представление указателя V8 будет 0x12345679.

Этот механизм тегирования позволяет V8 быстро различать SMI и указатели, проверяя младший значащий бит. Если LSB равен 0, V8 знает, что имеет дело с SMI; если LSB равен 1, V8 знает, что это указатель.

Обратите внимание, что приведенный выше пример относится к 32-битной системе. В 64-битной системе SMI используют аналогичный механизм тегирования, но сохраняют 32-битные целые значения в младших 32 битах 64-битного слова.

Эксплуатация

Теперь мы знаем, как происходит путаница типов и как IC может получить доступ к запутанному значению свойства. Нам нужно разработать план использования этого в RCE, если не сказать больше. В прошлый раз, когда я использовал CVE-2020-6570, это было вызвано чтением и записью OOB, поэтому с ограниченными возможностями чтения и записи я смог создавать addrOf и fakeObj, а в конечном итоге и более мощные функции чтения и записи. На этот раз у нас нет возможности чтения и записи. Из-за путаницы типов мы можем сначала попытаться создать поддельный объект, а затем addrOf, но для этого нам нужно знать адрес нашего поддельного объекта, информацию о карте и тому подобное.

Heap Spray?

Могу ли я представить вам технику heap spray в V8. Я оставил вопросительный знак в заголовке раздела, потому что я не очень понимаю, то, что я собираюсь сделать, имеет какое-либо отношение к 'Heap Spray', который я знал раньше. Хкамаэль объяснил это очень хорошо, рекомендую 10/10, хотя его блог на китайском языке.

Если мы вернемся к gdb и воспользуемся командой vmmap, мы увидим все сегменты памяти, используемые V8, и мы увидим один из них:

1685610985711.png

Содержимое 0x081c0000:

1685610993052.png


Мы видим, что каждый блок кучи имеет длину 0x40000 байт и начинается со смещения 0x2118. А в пустом блоке кучи останется 0x3dee8 байт свободного места. V8 выделяет новые блоки кучи, когда текущего недостаточно, поэтому, если бы мы создали большой массив размером почти в 0x3dee8 байта, он должен заставить V8 выделить новый блок кучи. И адреса этих блоков кучи фиксированы, поэтому, если мы создадим наш поддельный объект в этом большом массиве, мы сразу узнаем его адрес.

Давайте проверим это с некоторым объявлением массива:

1685610999919.png


Я решил создать двойной массив, числа с плавающей запятой в V8 занимают 8 байт места, а 0x7bd0 ~ (0x3dee8 / 8). Это должно заполнить весь блок, и должен появиться новый блок.

1685611015225.png


Теперь мы видим, что сегмент кучи заканчивается на 0x08280000, а его размер теперь равен 0xc0000. Если мы посмотрим на содержимое памяти по смещению 0x2118 этого нового блока, мы увидим:

1685611024442.png


карту, размер элемента массива и все остальное, если оно заполнено числами с плавающей запятой. Следующий шаг — использовать знания, которые у нас уже есть, для создания поддельного объекта. Поддельный объект должен иметь карту, свойство и указатель элемента. Нам также нужно убедиться, что смещения правильные, чтобы при срабатывании путаницы типов IC возвращал наш поддельный объект в качестве значения.

Поддельный объект

Опять же, Hcalmael придумал разумный способ настройки цепочек поиска, присваивая C.x0 объекту множество свойств:

1685611036231.png


если мы подумаем о расположении памяти JSObject в памяти, первое смещение, к которому обращается IC, будет JSModuleNamespace->module, которое находится по смещению +0x4. Это будет адрес массива свойств JSObject. Если объект создан со многими свойствами, V8 поместит первые несколько свойств после указателя элемента как свойство внутри объекта. Остальные будут храниться в массиве свойств.

Таким образом, это смещение +0x4 будет обращаться к массиву свойств объекта, и, чтобы продолжить, IC ищет смещение module->exports, которое рандомизировано в хэш-карте. Вот почему нам нужен цикл для заполнения свойств одним и тем же значением, чтобы убедиться, что независимо от того, какое смещение принимает IC, он всегда будет приземляться на один и тот же адрес.

С этой настройкой мы закончили с JSModuleNamespace->module->exports, и текущий указатель IC находится в ячейке экспортируемого значения, IC получает доступ к значению ячейки, получая содержимое со смещением +0x4. То есть, если мы поместим наш фальшивый объект по индексу i, то IC найдет адрес по адресу arr+0x4, который является младшими 4 байтами arr[i+1].

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

1685611078219.png


Произвольное чтение и запись

И теперь, когда создается поддельный объект, мы можем создать произвольную функцию чтения и записи:

1685611087498.png


1685611094247.png


и написать:

1685611103745.png


1685611110440.png


которая полностью работает, так что это хорошо знать.

addrOf

Последний шаг перед RCE — написать функцию addrOf. Чтобы сделать это, вместо этого нам нужен массив объектов, так как нам нужно назначать объекты, которые мы хотим прочитать в массиве, массивы объектов хранят объекты по их указателю, тогда мы можем прочитать его как двойное значение. Нам также нужно знать адрес, но мы можем использовать аналогичную идею при создании большого массива double. Поскольку массив объектов хранит указатели, размер его одного элемента будет 4 байта вместо 8 байтов, как в двойном массиве.

Если мы создадим новый большой массив объектов с размером, достаточным для заполнения одного блока кучи, он будет иметь адрес 0x40000 после нашего двойного массива. Мы можем проверить это:

1685611120614.png


1685611127574.png


И продолжим:

1685611134874.png


1685611143986.png


1685611162504.png

Остальное

Остальное почти то же самое, находит сегмент памяти RWX и записывает в него шеллкод.

1685611176429.png


Что может быть лучше…

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

Ссылки


Я делюсь своими заметками при разработке этого эксплойта, не стесняйтесь читать их.

Переведено специально для xss.pro
Автор перевода: yashechka
Источник: https://y4y.space/2023/05/06/cve-20...inline-cache-and-exploitating-type-confusion/
 
Последнее редактирование:


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