Пожалуйста, обратите внимание, что пользователь заблокирован
Анализ CVE-2019-5790 и то, как поиск неисследованной поверхности для атаки в V8 привел к ее раскрытию.
Введение
Если вы посмотрите на все недавние публикации, посвященные безопасности браузеров и в частности движка JavaScript, у вас может сложиться наивное впечатление, что единственным местом, где можно искать новые ошибки в современных реализациях JavaScript, являются JIT-компиляторы. , Огромная сложность этих движков, отток кода (500 коммитов только для V8 в течение последнего месяца [ 1 ]) и огромное количество репортов, казалось бы, никогда не прекращающийся поток открытых багов [ 2 ] справедливо указывают на то, что это вероятно область, которую стоит посмотреть как багхантеру.
Тем не менее, при взгляде на высококлассные цели, такие как веб-браузеры, есть вероятность, что внимание многих исследователей в основном определяется публично раскрытыми багами и публикациями. С одной стороны, этот подход может дать вам хороший быстрый обзор потенциально подверженных ошибкам областей кодовой базы при выборе новой цели. Также не вызывает сомнений, что некоторые области являются более сложными, чем другие, и поэтому заслуживают внимания (например, движки JIT). С другой стороны, люди слишком часто забывают, что другие части кодовой базы, которые в настоящее время не так популярны, могут также обеспечить некоторую интересную поверхность для атаки (и ошибки), и их не следует упускать из виду. Это особенно верно, если ваша цель состоит в том, чтобы находить уязвимости, срок жизни которых не превышает несколько недель или месяцев.
В этом сообщении в блоге мы описываем нашу успешную попытку найти уязвимость в V8 и то, как фокусировка на компоненте, который не выглядел так вкусно изначально, обеспечил огромную поверхность для атаки, позволил нам найти уязвимость высокой степени серьезности, за данный баг была присуждена награда в размере 7500 долларов, по программе вознаграждения за ошибки от Google.
II. JavaScript конвейер
Ниже приведено высокоуровневое описание различных этапов, задействованных в конвейере JavaScript, чтобы дать очень приблизительный обзор возможных поверхностей для атаки. Более подробное и настоятельно рекомендуемое введение можно найти в [ 3 ].
Следующие описания будут посвящены V8, но аналогичные концепции применимы и к другим движкам.
Первым шагом движка JavaScript является синтаксический анализ исходного кода JavaScript. Цель состоит в том, чтобы преобразовать исходный код в представление абстрактного синтаксического дерева (AST). Даже такая, казалось бы, простая задача, как сканирование текста на наличие известных токенов из потока символов, сильно оптимизирована по скорости и постоянно совершенствуется в современных движках JavaScript, таких как V8 [ 4 , 5 ]. Именно на этом первом этапе мы смогли определить уязвимость, которая будет описана ниже.
После создания AST он преобразуется в пользовательский байт-код, который затем используется интерпретатором или JIT-компилятором. V8 использует Ignition [ 6 ] в качестве интерпретатора. Байт-код либо выполняется непосредственно на машине регистрации, либо передается компилятору JIT. На этом этапе в конвейере JavaScript у нас уже есть несколько этапов оптимизации и, как следствие, потенциальная уязвимость.
После выполнения функции в определенном количестве раз в интерпретаторе она помечается как «горячая» и будет компилироваться в машинный код компилятором JIT. V8 использует TurboFan [ 7 ] в качестве JIT-компилятора. Не вдаваясь в подробности, этот этап является очень сложным процессом и уже являлся источником огромного числа уязвимостей [ 8 ] в прошлом.
Параллельно с конвейером JavaScript у нас есть сборщик мусора [ 9 , 10 ], который позволяет программисту не иметь явного управления памятью. Хотя это уменьшает большой класс ошибок, таких как утечки памяти, это может также привести к интересным уязвимостям [ 11 ].
III. Парсинг JavaScript
Реализация парсера в V8 подробно описана в [ 4 , 5 ]. Код, реализующий синтаксический анализатор, можно найти в src / parsing / в дереве исходного кода V8.
Первым шагом при разборе исходного кода JavaScript является сканирование текста на наличие токенов. Класс Scanner использует входные данные и генерирует объекты Token, которые используются синтаксическим анализатором. Класс UTF16CharacterStream используется в качестве абстракции для потока ввода текста, чтобы предоставлять токены в формате UTF-16 для сканера и абстрагироваться от различных возможных форматов кодирования JavaScript, полученных из сети. Класс Parser затем генерирует окончательный AST на основе потребляемых токенов.
IV. LiteralBuffer Integer Overflow (CVE-2019-5790)
Следующая ошибка была обнаружена нашим исследователем Дмитрием Фурни ( @DimitriFourny ) и сообщена в Google 13 декабря 2018 года. Она была исправлена в версии Chrome 73.0.3683.75. Соответствующую запись отслеживания ошибок можно найти в [ 12 ].
Метод Scanner :: Scan начинается с вызова Scanner :: ScanSingleToken, чтобы найти следующий токен без пробелов в потоке. В зависимости от обнаруженного токена, он реализует некоторые особые случаи для их правильной обработки. Например, одиночные символьные токены, такие как скобки, скобки или точки с запятой, просто возвращаются, тогда как другие токены вызывают потребление большего количества символов из потока.
Одним из таких примеров является токен TOKEN :: String, который, например, возвращается для символа кавычки. Если этот токен обнаружен, вызывается метод Scanner :: ScanString . Этот метод вызывает Scanner :: AddLiteralChar в цикле, пока не будет найден символ закрывающей кавычки.
Метод Scanner :: AddLiteralChar вызывает Scanner :: LiteralBuffer :: AddChar, который в конце вызывает Scanner :: LiteralBuffer :: AddTwoByteChar, если после начального символа кавычки следуют двухбайтовые символы.
Backing_store_ вектор байт буферизует уже отсканированную часть строки и динамически изменяется по требованию. Если метод Scanner :: LiteralBuffer :: AddTwoByteChar обнаруживает, что вектор должен расти, он вызывает Scanner :: LiteralBuffer :: ExpandBuffer, который выделяет больший буфер, а затем копирует байты из старого буфера в новый.
Метод Scanner :: LiteralBuffer :: NewCapacity используется для вычисления размера нового вектора.
Мы можем контролировать backing_store_.length () , изменяя количество символов после начального символа кавычки. Огромная строка JavaScript приводит к огромному значению capacity что может привести к переполнению выражения * kGrowthFactory , так что для new_capacity будет установлено меньшее значение, чем в предыдущий capacity . В результате при следующем вызове MemCopy в вектор будет записано больше байтов, чем было выделено ранее, что приведет к повреждению динамической памяти.
Следующее простое доказательство концепции может вызвать ошибку:
Ошибка выглядела достаточно очевидной при чтении кода, но, вероятно, ее было бы трудно обнаружить с помощью фаззинга, потому что для этого требуется около 20 ГБ памяти и достаточно много времени для ее воспроизведения на типичном настольном компьютере.
V. Вывод
При нацеливании на высокопрофильную цель имеет смысл углубиться в самые сложные и уже известные подверженные ошибкам области, потому что в этих местах есть много ошибок. Тем не менее огромные цели, такие как веб-браузеры, обеспечивают множество поверхностей для атаки, и быть успешным в проведении исследований уязвимости этих целей часто означает просто выявление новой поверхности для атаки, на которую раньше никто не смотрел.
Ссылки
[1] https://www.openhub.net/p/v8-js
[2] https://github.com/tunz/js-vuln-db
[3] https://saelo.github.io/presentations /blackhat_us_18_attacking_client_side_jit_compilers.pdf
[4] https://v8.dev/blog/scanner
[5] https://v8.dev/blog/preparser
[6] https://v8.dev/blog/ignition-interpreter
[7] https://v8.dev/docs/turbofan
[8] https://bugs.chromium.org/p/project-zero/issues/list?can=1&q=JIT
[9] https://v8.dev / blog / free-garbage-collection
[10] https://v8.dev/blog/concurrent-marking
[11] https://bugs.chromium.org/p/chromium/issues/detail?id=434136
[12] https://bugs.chromium.org/p/chromium/issues/detail?id=914736
Источник: https://labs.bluefrostsecurity.de/b...the-masses-bug-hunting-in-javascript-engines/
Введение
Если вы посмотрите на все недавние публикации, посвященные безопасности браузеров и в частности движка JavaScript, у вас может сложиться наивное впечатление, что единственным местом, где можно искать новые ошибки в современных реализациях JavaScript, являются JIT-компиляторы. , Огромная сложность этих движков, отток кода (500 коммитов только для V8 в течение последнего месяца [ 1 ]) и огромное количество репортов, казалось бы, никогда не прекращающийся поток открытых багов [ 2 ] справедливо указывают на то, что это вероятно область, которую стоит посмотреть как багхантеру.
Тем не менее, при взгляде на высококлассные цели, такие как веб-браузеры, есть вероятность, что внимание многих исследователей в основном определяется публично раскрытыми багами и публикациями. С одной стороны, этот подход может дать вам хороший быстрый обзор потенциально подверженных ошибкам областей кодовой базы при выборе новой цели. Также не вызывает сомнений, что некоторые области являются более сложными, чем другие, и поэтому заслуживают внимания (например, движки JIT). С другой стороны, люди слишком часто забывают, что другие части кодовой базы, которые в настоящее время не так популярны, могут также обеспечить некоторую интересную поверхность для атаки (и ошибки), и их не следует упускать из виду. Это особенно верно, если ваша цель состоит в том, чтобы находить уязвимости, срок жизни которых не превышает несколько недель или месяцев.
В этом сообщении в блоге мы описываем нашу успешную попытку найти уязвимость в V8 и то, как фокусировка на компоненте, который не выглядел так вкусно изначально, обеспечил огромную поверхность для атаки, позволил нам найти уязвимость высокой степени серьезности, за данный баг была присуждена награда в размере 7500 долларов, по программе вознаграждения за ошибки от Google.
II. JavaScript конвейер
Ниже приведено высокоуровневое описание различных этапов, задействованных в конвейере JavaScript, чтобы дать очень приблизительный обзор возможных поверхностей для атаки. Более подробное и настоятельно рекомендуемое введение можно найти в [ 3 ].
Код:
AST Bytecode
+-------------+ +--------+ +-------------+ +--------------+
| JavaScript |-->| Parser |--->| Interpreter |------->| JIT Compiler |----+
| source code | | | | (Ignition) | | (TurboFan) | |
+-------------+ +--------+ +-------------+ +--------------+ | Assembly
| | code
| +---------+ |
+-------------->| Runtime |<--------+
Bytecode +---------+
Следующие описания будут посвящены V8, но аналогичные концепции применимы и к другим движкам.
Первым шагом движка JavaScript является синтаксический анализ исходного кода JavaScript. Цель состоит в том, чтобы преобразовать исходный код в представление абстрактного синтаксического дерева (AST). Даже такая, казалось бы, простая задача, как сканирование текста на наличие известных токенов из потока символов, сильно оптимизирована по скорости и постоянно совершенствуется в современных движках JavaScript, таких как V8 [ 4 , 5 ]. Именно на этом первом этапе мы смогли определить уязвимость, которая будет описана ниже.
После создания AST он преобразуется в пользовательский байт-код, который затем используется интерпретатором или JIT-компилятором. V8 использует Ignition [ 6 ] в качестве интерпретатора. Байт-код либо выполняется непосредственно на машине регистрации, либо передается компилятору JIT. На этом этапе в конвейере JavaScript у нас уже есть несколько этапов оптимизации и, как следствие, потенциальная уязвимость.
После выполнения функции в определенном количестве раз в интерпретаторе она помечается как «горячая» и будет компилироваться в машинный код компилятором JIT. V8 использует TurboFan [ 7 ] в качестве JIT-компилятора. Не вдаваясь в подробности, этот этап является очень сложным процессом и уже являлся источником огромного числа уязвимостей [ 8 ] в прошлом.
Параллельно с конвейером JavaScript у нас есть сборщик мусора [ 9 , 10 ], который позволяет программисту не иметь явного управления памятью. Хотя это уменьшает большой класс ошибок, таких как утечки памяти, это может также привести к интересным уязвимостям [ 11 ].
III. Парсинг JavaScript
Реализация парсера в V8 подробно описана в [ 4 , 5 ]. Код, реализующий синтаксический анализатор, можно найти в src / parsing / в дереве исходного кода V8.
Код:
+-----------+
+---------->| PreParser |
| tokens +-----------+
| |
| v
+-------+ +--------+ +---------+ +--------+
| Blink |------>| Stream |------->| Scanner |------->| Parser |
+-------+ +--------+ +---------+ +--------+
ASCII UTF-16 tokens |
| AST
v
+----------+ +----------+
| TurboFan |<-----------| Ignition |
+----------+ +----------+
bytecode
Первым шагом при разборе исходного кода JavaScript является сканирование текста на наличие токенов. Класс Scanner использует входные данные и генерирует объекты Token, которые используются синтаксическим анализатором. Класс UTF16CharacterStream используется в качестве абстракции для потока ввода текста, чтобы предоставлять токены в формате UTF-16 для сканера и абстрагироваться от различных возможных форматов кодирования JavaScript, полученных из сети. Класс Parser затем генерирует окончательный AST на основе потребляемых токенов.
IV. LiteralBuffer Integer Overflow (CVE-2019-5790)
Следующая ошибка была обнаружена нашим исследователем Дмитрием Фурни ( @DimitriFourny ) и сообщена в Google 13 декабря 2018 года. Она была исправлена в версии Chrome 73.0.3683.75. Соответствующую запись отслеживания ошибок можно найти в [ 12 ].
Метод Scanner :: Scan начинается с вызова Scanner :: ScanSingleToken, чтобы найти следующий токен без пробелов в потоке. В зависимости от обнаруженного токена, он реализует некоторые особые случаи для их правильной обработки. Например, одиночные символьные токены, такие как скобки, скобки или точки с запятой, просто возвращаются, тогда как другие токены вызывают потребление большего количества символов из потока.
Одним из таких примеров является токен TOKEN :: String, который, например, возвращается для символа кавычки. Если этот токен обнаружен, вызывается метод Scanner :: ScanString . Этот метод вызывает Scanner :: AddLiteralChar в цикле, пока не будет найден символ закрывающей кавычки.
Метод Scanner :: AddLiteralChar вызывает Scanner :: LiteralBuffer :: AddChar, который в конце вызывает Scanner :: LiteralBuffer :: AddTwoByteChar, если после начального символа кавычки следуют двухбайтовые символы.
C++:
void Scanner::LiteralBuffer::AddTwoByteChar(uc32 code_unit) {
DCHECK(!is_one_byte());
if (position_ >= backing_store_.length()) ExpandBuffer();
if (code_unit <=
static_cast(unibrow::Utf16::kMaxNonSurrogateCharCode)) {
*reinterpret_cast<uint16_t*>(&backing_store_[position_]) = code_unit;
position_ += kUC16Size;
} else {
*reinterpret_cast<uint16_t*>(&backing_store_[position_]) =
unibrow::Utf16::LeadSurrogate(code_unit);
position_ += kUC16Size;
if (position_ >= backing_store_.length()) ExpandBuffer();
*reinterpret_cast<uint16_t*>(&backing_store_[position_]) =
unibrow::Utf16::TrailSurrogate(code_unit);
position_ += kUC16Size;
}
}
Backing_store_ вектор байт буферизует уже отсканированную часть строки и динамически изменяется по требованию. Если метод Scanner :: LiteralBuffer :: AddTwoByteChar обнаруживает, что вектор должен расти, он вызывает Scanner :: LiteralBuffer :: ExpandBuffer, который выделяет больший буфер, а затем копирует байты из старого буфера в новый.
C++:
void Scanner::LiteralBuffer::ExpandBuffer() {
Vector new_store = Vector::New(NewCapacity(kInitialCapacity));
MemCopy(new_store.start(), backing_store_.start(), position_);
backing_store_.Dispose();
backing_store_ = new_store;
}
Метод Scanner :: LiteralBuffer :: NewCapacity используется для вычисления размера нового вектора.
C++:
int Scanner::LiteralBuffer::NewCapacity(int min_capacity) {
int capacity = Max(min_capacity, backing_store_.length());
int new_capacity = Min(capacity * kGrowthFactory, capacity + kMaxGrowth);
return new_capacity;
}
Мы можем контролировать backing_store_.length () , изменяя количество символов после начального символа кавычки. Огромная строка JavaScript приводит к огромному значению capacity что может привести к переполнению выражения * kGrowthFactory , так что для new_capacity будет установлено меньшее значение, чем в предыдущий capacity . В результате при следующем вызове MemCopy в вектор будет записано больше байтов, чем было выделено ранее, что приведет к повреждению динамической памяти.
Следующее простое доказательство концепции может вызвать ошибку:
JavaScript:
let s = String.fromCharCode(0x4141).repeat(0x10000001) + "A";
s = "'"+s+"'";
eval(s);
Ошибка выглядела достаточно очевидной при чтении кода, но, вероятно, ее было бы трудно обнаружить с помощью фаззинга, потому что для этого требуется около 20 ГБ памяти и достаточно много времени для ее воспроизведения на типичном настольном компьютере.
V. Вывод
При нацеливании на высокопрофильную цель имеет смысл углубиться в самые сложные и уже известные подверженные ошибкам области, потому что в этих местах есть много ошибок. Тем не менее огромные цели, такие как веб-браузеры, обеспечивают множество поверхностей для атаки, и быть успешным в проведении исследований уязвимости этих целей часто означает просто выявление новой поверхности для атаки, на которую раньше никто не смотрел.
Ссылки
[1] https://www.openhub.net/p/v8-js
[2] https://github.com/tunz/js-vuln-db
[3] https://saelo.github.io/presentations /blackhat_us_18_attacking_client_side_jit_compilers.pdf
[4] https://v8.dev/blog/scanner
[5] https://v8.dev/blog/preparser
[6] https://v8.dev/blog/ignition-interpreter
[7] https://v8.dev/docs/turbofan
[8] https://bugs.chromium.org/p/project-zero/issues/list?can=1&q=JIT
[9] https://v8.dev / blog / free-garbage-collection
[10] https://v8.dev/blog/concurrent-marking
[11] https://bugs.chromium.org/p/chromium/issues/detail?id=434136
[12] https://bugs.chromium.org/p/chromium/issues/detail?id=914736
Источник: https://labs.bluefrostsecurity.de/b...the-masses-bug-hunting-in-javascript-engines/