Пожалуйста, обратите внимание, что пользователь заблокирован
fuzzilli – это фаззер для javascript-движков от команды googleprojectzero. Его отличительная черта – это FuzzIL, промежуточный язык, который можно мутировать и затем транслировать в js. Этот язык обеспечивает мутированному js семантическую валидность: даже после нескольких раундов изменений в коде остается логика, с которой движок будет работать.
С помощью fuzzilli уже найдено много интересных багов типа OOB. Разберемся, как он устроен.
Мутировать js-код можно на следующих уровнях:
Движок просто отбросит все предлагаемые варианты.
Способ с деревьями лучше: мутатор производит синтаксически правильный js-код. Например, вот такой:
Но, несмотря на это, в логике ошибка все-таки есть: переменная y здесь используется до своего объявления. Это вызовет исключение, а возможный баг type confusion останется неотловленным.
FuzzIL создан для того, чтобы добавить третий уровень: мутирование промежуточного представления. Оно должно обеспечить не только синтаксическую валидность, но и семантическую. Мутаторы FuzzIL не генерируют один только валидный код, но очевидных ошибок не допускают.
Пример программы на FuzzIL:
У FuzzIL нет синтаксиса, но представлять программу как-то надо, поэтому этот пример написан на некотором псевдокоде.
Транслятор у FuzzIL есть, и этот же пример на js будет выглядеть так:
Или:
fuzzilli умеет транслировать и так и так.
Чтобы описать мутаторы, нужно немного поговорить о реализации FuzzIL.
Проиллюстрировать классы, которые реализуют программу на FuzzIL, поможет диаграмма:
Типом данных, отвечающим за итоговый js -скрипт, является class Program.
Программа – это набор операций вместе с их входными и выходными переменными.
Например, операция LoadInteger:
LoadProperty:
Посмотреть все операции можно в Operations.swift
За создание программы отвечают ProgramBuilder и CodeGenerators. Каждый генератор производит свой специфический фрагмент FuzzIL кода. Например, IntegerGenerator создает инструкцию LoadInteger со случайным параметром, а IfElseGenerator – условный блок:
Таких генераторов десятки. Некоторые из них вызывают другие генераторы, как IfElseGenerator, чтобы код внутри блока получился интереснее.
Когда новая программа создана, она подвергается анализу через:
Проверяются блоки: если есть операция BeginWhileLoop, то должен быть и EndWhileLoop. И наоборот: если есть конец блока EndWhileLoop, то должно быть и начало BeginWhileLoop.
Типы: Unknown, Integer, Float, String, Boolean, Object, Function. Проверка типов не допустит, например, такой код:
Не допускается dead код, чтобы предотвратить бессмысленное разрастание тест-кейсов.
После всех проверок код транслируется в текст. За трансляцию отвечает class JavaScriptLifter. В этот класс заложены правила того, как представлять инструкции в текстовом виде.
Учитывается область видимости; переменная v12 из того же скоупа, что и все остальные.
Если невозможно найти замену, то кандидат вернется на свое место.
У объектов операций помимо входных и выходных переменных есть еще и параметры. Например, у LoadInteger это value, у LoadProperty – propertyName, а у CallMethod – methodName.
Покрытие – это битмап, фаззеру интересен только факт посещения базового блока, а сколько раз поток прошел через этот блок уже неинтересно.
В каждом базовом блоке размещается вызов sanitizer_cov_trace_pc_guard:
Здесь guard – указатель на индекс блока, __shmem->edges – массив, с которым работают, как с битмапом.
Как уже упоминалось, часть движков поддерживает инструментацию fuzzilli. Чтобы инструментировать, например, v8, нужно указывать флаг v8_fuzzilli=true в процессе сборки v8. К счастью, сборка всех движков автоматизирована и помнить о флагах не нужно – в составе есть скрипты для этого. А если инструментация не поддерживается, то там же есть и патчи.
Но производительность вроде бы за REPRL.
Авторы fuzzilli провели измерения для движка JavaScriptCore и оказалось, что REPRL опережает форк-сервер на 6 мс за одну итерацию. Тестом был скрипт, который просто ждет 1 секунду: результат REPRL – 1,001 c, а форк-сервер – 1,007 c. Тест кажется простым, но тем не менее был выбран REPRL.
Как и инструментация, реализация REPRL включена в состав конкретного движка, если он поддерживает работу с fuzzilli.
IR-представление функции v7 будет выглядеть примерно так (взято из отчета):
А вот как JIT-компилятор оптимизирует код:
Поэтому можно читать и писать за пределы массива.
Эксплуатация багов в js-движках – это отдельный жанр. Баг выше было легко описать, поэтому он здесь. Другие находки fuzzilli тоже интересны, но они слишком заковыристые, и их не уместить в пару абзацев.
Еще больше деталей есть в трудах:
Источник: https://dsec.ru/blog/posts/fazzing-js-dvizhkov-s-pomoshhyu-fuzzilli/
Движки, которые можно фаззить
В этом списке V8, Spidermonkey, XS и duktape уже поддерживают работу с fuzzilli. А остальные движки можно пропатчить и начать фаззинг, патчи включены в состав инструмента.С помощью fuzzilli уже найдено много интересных багов типа OOB. Разберемся, как он устроен.
FuzzIL
FuzzIL – это промежуточное представление js, и самое интересное в нем – это мутаторы для FuzzIL. Как они генерируют валидный js-код?Мутировать js-код можно на следующих уровнях:
- текст скрипта (изменение случайных байт)
- синтаксическое дерево AST (вырезать/вставлять случайное поддерево)
Код:
$ ./js_shell < /dev/urandom
Движок просто отбросит все предлагаемые варианты.
Способ с деревьями лучше: мутатор производит синтаксически правильный js-код. Например, вот такой:
JavaScript:
if(y > 0){
...
/* type confusion */
...
}
let y = 0;
Но, несмотря на это, в логике ошибка все-таки есть: переменная y здесь используется до своего объявления. Это вызовет исключение, а возможный баг type confusion останется неотловленным.
FuzzIL создан для того, чтобы добавить третий уровень: мутирование промежуточного представления. Оно должно обеспечить не только синтаксическую валидность, но и семантическую. Мутаторы FuzzIL не генерируют один только валидный код, но очевидных ошибок не допускают.
Пример программы на FuzzIL:
Код:
v0 <− LoadInt ’ 0’
v1 <− LoadInt ’10’
v2 <− LoadInt ’ 1’
v3 <− Phi v0
BeginFor v0, ’<’, v1, ’+’, v2 −> v4
v6 <− BinaryOperation v3, ’+’, v4
Copy v3, v6
EndFor
v7 <− LoadString ’Result :’
v8 <− BinaryOperation v7, ’+’, v3
v9 <− LoadGlobal ’console’
v10 <− CallMethod v9, ’log’, [v8]
У FuzzIL нет синтаксиса, но представлять программу как-то надо, поэтому этот пример написан на некотором псевдокоде.
Транслятор у FuzzIL есть, и этот же пример на js будет выглядеть так:
JavaScript:
const v0 = 0;
const v1 = 10;
const v2 = 1;
let v3 = v0;
for (let v4 = v0; v4 < v1; v4 = v4 + v2) {
const v6 = v3 + v4;
v3 = v6;
}
const v7 = ”Result: ”;
const v8 = v7 + v3;
const v9 = console;
const v10 = v9.log(v8);
Или:
JavaScript:
let v3 = 0;
for (let v4 = 0; v4 < 10; v4++) {
v3 = v3 + v4;
}
console.log(”Result: ” + v3);
fuzzilli умеет транслировать и так и так.
Чтобы описать мутаторы, нужно немного поговорить о реализации FuzzIL.
Реализация
fuzzilli и FuzzIL написаны на языке Swift.Проиллюстрировать классы, которые реализуют программу на FuzzIL, поможет диаграмма:
Типом данных, отвечающим за итоговый js -скрипт, является class Program.
Программа – это набор операций вместе с их входными и выходными переменными.
Например, операция LoadInteger:
Swift:
class LoadInteger: Operation {
let value: Int64
init(value: Int64) {
self.value = value
super.init(numInputs: 0, numOutputs: 1, attributes: [.isPure, .isMutable])
}
}
/*
v0 <− LoadInt ’ 0’
*/
LoadProperty:
Swift:
class LoadProperty: Operation {
let propertyName: String
init(propertyName: String) {
self.propertyName = propertyName
super.init(numInputs: 1, numOutputs: 1, attributes: [.isMutable])
}
}
/*
v1 <- LoadProperty v0, '__proto__'
*/
Посмотреть все операции можно в Operations.swift
За создание программы отвечают ProgramBuilder и CodeGenerators. Каждый генератор производит свой специфический фрагмент FuzzIL кода. Например, IntegerGenerator создает инструкцию LoadInteger со случайным параметром, а IfElseGenerator – условный блок:
Swift:
CodeGenerator("IntegerGenerator") { b in
b.loadInt(b.genInt())
}
/*...*/
CodeGenerator("IfElseGenerator", input: .boolean) { b, cond in
b.buildIfElse(cond, ifBody: {
b.generateRecursive()
}, elseBody: {
b.generateRecursive()
})
}
Таких генераторов десятки. Некоторые из них вызывают другие генераторы, как IfElseGenerator, чтобы код внутри блока получился интереснее.
Когда новая программа создана, она подвергается анализу через:
- VariableAnalyzer
- ScopeAnalyzer
- ContextAnalyzer
- DeadCodeAnalyzer
JavaScript:
let v2 = 10;
for(let v4 = v2; v4 < 100; v4++){...}
Проверяются блоки: если есть операция BeginWhileLoop, то должен быть и EndWhileLoop. И наоборот: если есть конец блока EndWhileLoop, то должно быть и начало BeginWhileLoop.
Типы: Unknown, Integer, Float, String, Boolean, Object, Function. Проверка типов не допустит, например, такой код:
JavaScript:
let v0 = 1;
v0.slice(1); // Exception: TypeError: v0.slice is not a function
Не допускается dead код, чтобы предотвратить бессмысленное разрастание тест-кейсов.
После всех проверок код транслируется в текст. За трансляцию отвечает class JavaScriptLifter. В этот класс заложены правила того, как представлять инструкции в текстовом виде.
Swift:
switch instr.op {
case let op as LoadInteger:
/*...*/
case let op as LoadBigInt:
/*...*/
case let op as LoadProperty:
/*...*/
case let op as BeginForLoop:
/*...*/
/*...*/
}
Мутаторы
- input mutator
- operation mutator
- combine mutator
- splice mutator
Input mutator
Мутирует входные аргументы-переменные.
Код:
// Before Mutation
. . .
v16 <− CallFunction v1, v6, v9, v3
// After Mutation
. . .
v16 <− CallFunction v1, v12, v9, v3
Учитывается область видимости; переменная v12 из того же скоупа, что и все остальные.
Если невозможно найти замену, то кандидат вернется на свое место.
Operation Mutator
Мутирует параметр операций, если он у них есть.У объектов операций помимо входных и выходных переменных есть еще и параметры. Например, у LoadInteger это value, у LoadProperty – propertyName, а у CallMethod – methodName.
Combine Mutator
Код:
// Program 1
v0 <− LoadString ’bar’
v1 <− CreateObject ’foo’, v0
// Program 2
v0 <− LoadInt ’ 1337 ’
v1 <− LoadGlobal ’console’
v2 <− CallMethod v1, ’log’, [v0]
// Combined program
v0 <− LoadInt ’ 1337 ’
v1 <− LoadString ’bar’
v2 <− CreateObject ’foo’, v1
v3 <− LoadGlobal ’console ’
v4 <− CallMethod v3, ’log’, [v0]
Splice Mutator
Вставка куска кода из одного тест-кейса в другой. Учитывается, что все переменные в коде должны быть определены. Алгоритм такой:- выбрать рандомную инструкцию;
- найти все инструкции, чьи выходные значения используются для этой рандомной; инструкции;
- скопировать, вставить.
Инструментация
Между фаззером и процессом движка создается shm область памяти, в которой хранится информация о покрытии.Покрытие – это битмап, фаззеру интересен только факт посещения базового блока, а сколько раз поток прошел через этот блок уже неинтересно.
В каждом базовом блоке размещается вызов sanitizer_cov_trace_pc_guard:
C:
struct shmem_data {
uint32_t num_edges;
unsigned char edges[];
};
struct shmem_data* __shmem;
/*...*/
extern "C" void __sanitizer_cov_trace_pc_guard(uint32_t *guard) {
uint32_t index = *guard;
if (!index) return;
__shmem->edges[index / 8] |= 1 << (index % 8);
*guard = 0;
}
Здесь guard – указатель на индекс блока, __shmem->edges – массив, с которым работают, как с битмапом.
Как уже упоминалось, часть движков поддерживает инструментацию fuzzilli. Чтобы инструментировать, например, v8, нужно указывать флаг v8_fuzzilli=true в процессе сборки v8. К счастью, сборка всех движков автоматизирована и помнить о флагах не нужно – в составе есть скрипты для этого. А если инструментация не поддерживается, то там же есть и патчи.
Исполнение скрипта
Для увеличения производительности фаззинга необходимо сокращать затраты на создание и запуск процесса, в котором будет исполняться исследуемый код. Для этого существует два механизма:- форк-сервер, как в AFL: размещается в процессе таргета, получает очередную порцию мусора от мастера и делает форк, в котором идет обработка. В этом случае переиспользуется все, что до форк-сервера.
- REPRL(read-eval-print-reset-loop), как в libfuzzer: в бесконечном цикле исследуемый код ждет и получает данные, обрабатывает, потом сбрасывает состояние на начальное, и по-новой. Здесь переиспользуется все, что до цикла.
Но производительность вроде бы за REPRL.
Авторы fuzzilli провели измерения для движка JavaScriptCore и оказалось, что REPRL опережает форк-сервер на 6 мс за одну итерацию. Тестом был скрипт, который просто ждет 1 секунду: результат REPRL – 1,001 c, а форк-сервер – 1,007 c. Тест кажется простым, но тем не менее был выбран REPRL.
Как и инструментация, реализация REPRL включена в состав конкретного движка, если он поддерживает работу с fuzzilli.
CVE-2019-8518
Это OOB—баг, найденный в JavaScriptCore/webkit:
JavaScript:
const v3 = [1337,1337,1337,1337];
const v6 = [1337,1337];
function v7(v8) {
for (let v9 in v8) {
v8.a = 42;
const v10 = v8[-698666199];
}
}
while (true) {
const v14 = v7(v6);
const v15 = v7(1337);
}
IR-представление функции v7 будет выглядеть примерно так (взято из отчета):
Код:
// Loop header
len = LoadArrayLength v8
// Do other loop header stuff
// Loop body
CheckStructure v8, expected_structure_id
StoreProperty v8, 'a', 42
CheckBounds -698666199, len // Bails out if index is OOB (always in this case...)
GetByVal v8, -698666199 // Loads the element from the backing storage without performing additional checks
// Jump back to beginning of loop
А вот как JIT-компилятор оптимизирует код:
- нода CheckStructure изначально проверяет тип массива в теле цикла, однако ее можно вынести, потому что тип массива один и тот же;
- CheckBounds – остается, потому что есть зависимость от длины массива;
- GetByVal – выносится, так как не зависит от переменных, определенных внутри цикла.
Код:
StructureCheck v8, expected_structure_id
GetByVal v8, -698666199
// Loop header
len = LoadArrayLength v8
// Do other loop header stuff
// Loop body
StoreProperty v8, 'a', 42
CheckBounds -698666199, len
// Jump back to beginning of loop
Поэтому можно читать и писать за пределы массива.
Эксплуатация багов в js-движках – это отдельный жанр. Баг выше было легко описать, поэтому он здесь. Другие находки fuzzilli тоже интересны, но они слишком заковыристые, и их не уместить в пару абзацев.
Вывод
fuzzilli – это фаззер для js-движков, который обеспечивает большее покрытие кода, потому что генерирует и синтаксически и семантически валидный код, то есть настоящие js-скрипты. В этой статье мы разобрали, как оно работает.Еще больше деталей есть в трудах:
- FuzzIL: Coverage Guided Fuzzing for JavaScript Engines
- The hunt for Chromium issue 1072171
- Fuzzing JavaScript Engines with Fuzzilli
Источник: https://dsec.ru/blog/posts/fazzing-js-dvizhkov-s-pomoshhyu-fuzzilli/