Разработка Эксплойтов: Эксплуатация Браузера на 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. По умолчанию члены классов являются закрытыми, то есть к членам могут получить доступ только класс и унаследованные классы. Мы поговорим об этих концепциях через секунду. Приведем небольшой пример кода.
Приведенный выше код создает три класса: один "основной" или "базовый" класс (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:
Как вы знаете, первым членом этой структуры является int var1. То же самое и с классами C++. Функция, которая определена в классе, также является его членом, отсюда и термин "член функци".
Причина, по которой существуют виртуальные функции, заключается в том, что они позволяют разработчику создавать прототип функции в основном классе, но позволяют разработчику переопределить функцию в производном классе. Это работает, потому что производный класс может наследовать все переменные, функции и т.д. из своего "родительского" класса. Это можно увидеть в приведенном выше фрагменте кода, помещенном здесь для краткости: classOne* c1 = new classTwo;. Он берет производный класс classOne, которым является classTwo, и указывает объект classOne(c1) на производный класс.
Это гарантирует, что всякий раз, когда объект (например,c1) вызывает функцию, это правильно определенная функция для этого класса. Так что в основном думайте об этом как о функции, которая объявлена в основном классе, наследуется подклассом, и каждому подклассу, который наследует ее, разрешено изменять то, что делает функция. Затем, когда объект класса вызывает виртуальную функцию, вызывается соответствующее определение функции, соответствующее вызывающему ее объекту класса.
Запустив программу, мы видим, что получаем ожидаемый результат:
Теперь, когда мы вооружились базовым пониманием некоторых ключевых концепций, в основном конструкторов, деструкторов и виртуальных функций, давайте посмотрим на ассемблерный код того, как выбирается виртуальная функция.
Обратите внимание, что нет необходимости повторять эти шаги, если вы следуете им.Однако, если вы хотите следовать пошаговым инструкциям, имя этого .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.
После этого следуйте этим инструкциям от 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().
Достигнув начала вызова виртуальной функции, давайте установим точки останова на следующих трех инструкциях после инструкции в RIP. Для этого используйте bp 00007ff7b67c1703 и т. д.
Переходя к следующей инструкции, мы видим, что значение, на которое указывает RAX, будет перемещено в RAX. Это значение, согласно WinDbg, - это virtualfunctions!ClassTwo::vftable.
Как мы видим, этот адрес является указателем на "vftable" (указатель таблицы виртуальных функций, или vptr). Vftable - это таблица виртуальных функций, которая по сути представляет собой структуру указателей на различные виртуальные функции. Вспомните, как мы говорили ранее: "когда класс вызывает виртуальную функцию, программа будет знать, какая функция соответствует каждому объекту класса". Вот этот процесс в действии. Давайте посмотрим на текущую инструкцию и две следующие.
Возможно, вы не сможете сказать это сейчас, но такая процедура (например, mov reg, [ptr] + call [ptr]) указывает на то, что конкретная виртуальная функция извлекается из таблицы виртуальных функций. Давайте пройдемся сейчас, чтобы увидеть, как это работает. При вызове, vptr (который является указателем на таблицу) загружается в RAX. Давайте теперь взглянем на эту таблицу.
Хотя эти символы немного сбивают с толку, обратите внимание, что у нас здесь два указателя - один - "sharedFunctionclassTwo", а другой — "sharedFunction1classTwo". На самом деле это указатели на две виртуальные функции в classTwo!
Если мы перейдем к вызову, мы увидим, что это вызов, который перенаправляет на переход к виртуальной функции sharedFunction, определенной в classTwo!
Затем продолжайте переходить к инструкциям в отладчике, пока мы не дойдем до инструкции c1-> sharedFunction1(). Обратите внимание, что по мере продвижения вы в конечном итоге увидите процедуру того же типа, которая выполняется с sharedFunction внутри classThree.
Опять же, мы можем наблюдать тот же тип поведения, только на этот раз инструкция вызова - call qword ptr [rax+0x8]. Это связано с тем, как виртуальные функции выбираются из таблицы. Грамотно составленная диаграмма Microsoft Paint ниже показывает, как программа индексирует таблицу при наличии нескольких виртуальных функций, как в нашей программе.
Как мы помним из нескольких изображений которые были раньше, где мы сдампили таблицу и увидели два адреса наших виртуальных функций. Мы видим, что на этот раз выполнение программы будет вызывать эту таблицу со смещением 0x8, которое на этот раз является указателем на sharedFunction1, а не sharedFunction!
Выполняя инструкции, мы переходим на sharedFunction1.
После выполнения всех виртуальных функций будет вызван наш деструктор. Поскольку мы создали только два объекта classOne и удаляем только эти два объекта, мы знаем, что будет вызван только деструктор classOne, что очевидно при поиске термина "деструктор" в IDA. Мы видим, что будет вызвана функция j_operator_delete, которая представляет собой просто длинный и затянутый переход к функции UCRTBASED Windows API _free_dbg, чтобы уничтожить объект. Обратите внимание, что обычно это был бы бесплатный вызов функции C Runtime, но поскольку мы создали эту программу в режиме отладки, по умолчанию используется отладочная версия.
Круто! Теперь мы знаем, как классы C++ индексируют таблицы виртуальных функций для извлечения виртуальных функций, связанных с данным объектом класса. Почему это важно? Напомним, это будет эксплойт браузера, а браузеры написаны на C++! Эти объекты класса, которые почти наверняка будут использовать виртуальные функции, размещены в куче! Это нам очень пригодится.
Прежде чем мы перейдем к нашему пути эксплуатации, давайте потратим всего несколько дополнительных минут, чтобы показать, как потенциально может выглядеть UAF с программной точки зрения. Добавим в основную функцию следующий фрагмент кода:
Пересоберите решение. После перестройки давайте сделаем WinDbg нашим отладчиком по умолчанию. Откройте сеанс cmd.exe от имени администратора и измените текущий рабочий каталог на установку WinDbg. Затем введите windbg.exe -I.
Эта команда настроила 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 должен запуститься.
После включения PageHeap давайте запустим уязвимый код. (Обратите внимание, что вам может потребоваться щелкнуть правой кнопкой мыши изображение ниже и открыть его в новой вкладке)
Очень интересно, мы видим, что произошел сбой! Обратите внимание на инструкцию 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). Эта структура используется для хранения важной информации для управления свободными фрагментами, но делает гораздо больше.
Далее, вот как выглядит структура HeapBase-> BlocksIndex (_HEAP_LIST_LOOKUP).
Первый член этой структуры является указателем на следующую структуру _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.
Например, если для 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, я пошел дальше и сократил код до "кода триггера", что вызывает сбой.
Возвращаясь к уязвимости и читая описание, эта уязвимость возникает, когда 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 обнаруживается сбой, как и ожидалось.
Теперь, когда мы знаем, что наш POC приведет к сбою браузера, давайте сделаем WinDbg нашим отладчиком по умолчанию, точно так же, как мы делали это раньше, чтобы определить почему произошел сбой.
Снова запустив POC, мы видим, что наш сбой зарегистрирован в WinDbg, но это кажется бессмысленным.
Мы знаем, в соответствии с рекомендациями, что это условие UAF. Мы также знаем, что это результат выборки виртуальной функции из объекта, который больше не существует. Зная это, мы должны ожидать разыменования некоторой памяти, которая больше не существует. Однако это не так, и мы просто видим ссылку на недопустимую память. Вспомните, когда мы включали PageHeap! Здесь нам нужно сделать то же самое и включить PageHeap для Internet Explorer. Воспользуйтесь той же командой, что и ранее, но на этот раз укажите iexplore.exe.
После включения PageHeap давайте повторно запустим POC.
Интересно! Инструкция, по которой подает программа, взята из класса CElement. Обратите внимание на инструкцию, по которой происходит сбой: mov reg, dword ptr [eax + 70h]. Если мы дизассемблируем текущий указатель инструкции, мы увидим нечто, очень напоминающее наши инструкции ассемблирования, которые мы показали ранее для выборки виртуальной функции.
Вспомните, как в прошлый раз в нашей 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.
Обратите внимание, как мы получаем тот же результат, что и наша простая программа ранее, хотя инструкции по ассемблированию немного отличаются? Поиск этих типов подпрограмм очень указывает на выборку виртуальной функции!
Прежде чем двигаться дальше, вспомним прежнюю картинку.
Обратите внимание на состояние EAX при сбое функции (прямо под оператором Access Violation). Вроде есть своего рода шаблон f0f0f0f0. Это шаблон gflags.exe для "освобожденного выделения", означающий, что значение в EAX находится в свободном состоянии. Это имеет смысл, поскольку мы пытаемся проиндексировать объект, которого просто больше не существует!
Перезапустите POC, и когда произойдет сбой, давайте выполним следующую команду !heap -p -a ecx.
Почему 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 и найти любые функции, содержащие слово деструктор.
Как видим, мы нашли деструктор! Согласно предыдущей трассировке стека, этот деструктор должен вызвать HeapFree, который фактически выполняет освобождение. Мы видим, что это так после дизассемблирования функции в IDA.
Запрашивая документацию Microsoft по HeapFree(https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapfree), мы видим, что он принимает три аргумента: 1. Дескриптор кучи, в которой будет освобождена часть памяти, 2. Флажки для освобождения и 3. Указатель на фактический фрагмент памяти, который нужно освободить.
На этом этапе вы можете спросить: "Ни один из этих параметров не является размером". Это верно! Однако теперь мы видим, что адрес блока, который будет освобожден, будет третьим параметром, передаваемым вызову HeapFree. Обратите внимание, что, поскольку мы находимся в 32-битной системе, аргументы функций будут передаваться через соглашение о вызовах __stdcall, что означает, что стек используется для передачи аргументов в вызов функции.
Еще раз взгляните на прототип предыдущего образа. Обратите внимание, что деструктор принимает аргумент для объекта типа CanchorElement. Это имеет смысл, поскольку это деструктор для объекта, созданного из класса CanchorElement. Это также означает, однако, что должен быть конструктор, способный также создавать указанный объект! И когда деструктор вызывает HeapFree, конструктор, скорее всего, вызовет либо malloc, либо HeapAlloc! Мы знаем, что последний аргумент для вызова HeapFree в деструкторе - это адрес фактического фрагмента, который нужно освободить. Это означает, что в первую очередь необходимо выделить кусок. При повторном поиске функций в IDA в классе CAnchorElement есть функция под названием CreateElement, которая очень характерна для конструктора объекта CAnchorElement! Давайте посмотрим на это в IDA.
Отлично, мы видим, что на самом деле есть вызов HeapAlloc. Обратимся к документации Microsoft для этой функции (https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapalloc).
Первый параметр - это снова дескриптор существующей кучи. Во-вторых, это любые флаги, которые вы хотите установить для выделения кучи. Третье и самое важное для нас - это фактический размер кучи. Это говорит нам о том, что при создании объекта CAnchorElement он будет иметь размер 0x68 байт. Если мы снова откроем наш POC в Internet Explorer, позволив отладчику снова взять на себя ответственность, мы фактически увидим, что размер свободного от уязвимости фрагмента кучи размером 0x68 байт, точно так же, как и наш реверс инжиниринг CAnchorElement::CreateElement показывает функцию.
Это доказывает нашу гипотезу, и теперь мы можем приступить к редактированию нашего скрипта, чтобы увидеть, не можем ли мы контролировать это распределение. Прежде чем продолжить, давайте отключим PageHeap для IE8.
Теперь, когда это сделано, давайте обновим наш POC следующим кодом.
Вышеупомянутый 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 в качестве посмертного отладчика.
Интересно! На этот раз мы пытаемся разыменовать адрес 0x41414141 вместо того, чтобы получить произвольный сбой, как это было в начале этой статьи, путем запуска исходного POC без включенного PageHeap! Однако причина этого сбоя совсем другая! Напомним, что фрагмент кучи, вызывающий проблему, находится в ECX, как мы видели ранее. Однако на этот раз вместо того, чтобы видеть освобожденную память, мы действительно можем видеть, что наши данные, контролируемые пользователем, теперь выделяют кусок кучи!
Теперь, когда мы, наконец, выяснили, как мы можем контролировать данные в ранее освобожденном фрагменте, мы можем довести все, что описано в этом руководстве, до полного круга. Давайте посмотрим на текущее выполнение программы.
Мы знаем, что это процедура для выборки виртуальной функции из таблицы виртуальных функций. Первая инструкция mov eax, dword ptr [ecx] берет указатель таблицы виртуальных функций, также известный как vptr, и загружает его в регистр EAX. Затем оттуда снова разыменовывается этот vptr, который указывает на таблицу виртуальных функций, и вызывается с указанным смещением. Обратите внимание, как в настоящее время мы контролируем регистр ECX, который используется для хранения vptr.
Давайте также посмотрим на этот фрагмент в контексте структуры HeapBase.
Как мы видим, в куче наш чанк является частью, 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. (Возможно, вам потребуется открыть первое изображение в новой вкладке)
Опять же, в блоге Exodus будут подробно описаны детали, но, по сути, здесь происходит то, что мы можем использовать SMIL (язык синхронизированной интеграции мультимедиа), чтобы вместо простого создания 0x68 байтов данных для заполнения кучи создавать указатели на 0x68 байтов, который гораздо более полезен и позволит нам создать фальшивую таблицу виртуальных функций.
Обратите внимание, что хип распыление - это альтернатива, хотя она относительно тщательно изучается. Смысл этого эксплойта состоит в том, чтобы задокументировать уязвимости, связанные с использованием после освобождения, и то, как определить размер освобожденного выделения и как его правильно заполнить. Эта специфическая техника сегодня тоже не применима. Однако это начало моего изучения эксплуатации браузеров, и я ожидаю, что начну с основ.
Давайте теперь снова запустим POC и посмотрим, что произойдет.
Отличная новость, мы управляем указателем инструкции! Давайте посмотрим, как мы сюда попали. Напомним, что мы выполняем код в рамках той же процедуры в CElement :: Doc, где мы были, где мы извлекаем виртуальную функцию из vftable. Взгляните на изображение ниже.
Начнем с самого верха. Как мы видим, EIP теперь настроен на наши данные, контролируемые пользователем. Значение в ECX, как это было верно на протяжении всей этой процедуры, содержит адрес блока кучи, который был виновником уязвимости.
Теперь мы контролировали этот освобожденный фрагмент с помощью предоставленного пользователем фрагмента байта 0x68.
Как мы знаем, этот кусок кучи в ECX при разыменовании содержит vptr, или, в нашем случае, поддельный vptr. Обратите внимание, что первое значение в ECX и все последующие значения - 004 .… Это массив указателей, возвращенных методом HTML + TIME! Если мы разыменуем первый член, это будет указатель на наш поддельный vftable! Это замечательно, поскольку значение в ECX разыменовывается для получения нашего поддельного vptr (один из указателей из метода HTML + TIME). Затем это указывает на нашу фальшивую таблицу виртуальных функций, и мы установили 70-й член равным 42424242, чтобы подтвердить контроль над указателем инструкции. Чтобы повторить еще раз, помните, что код для получения виртуальной функции выглядит следующим образом:
Итак, здесь произошло то, что мы загрузили наш кусок кучи, который заменил освобожденный фрагмент, в ECX. Значение в ECX указывает на наш кусок кучи. Наш кусок кучи имеет размер 0x68 байт и состоит только из указателей либо на поддельную таблицу виртуальных функций (1-й указатель), либо на указатель на строку vftable (2-й указатель и т.д.). Это можно увидеть на изображении ниже (в WinDbg poi() разыменует то, что находится в круглых скобках, и отобразит это).
Это значение в ECX, которое является указателем на нашу поддельную vtable, также помещается в EAX.
Значение EAX со смещением 0x70 затем помещается в регистр EDX. Затем вызывается это значение.
Как мы видим, это 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, он нам уже знаком!
Для начала мы знаем, что наша фальшивая таблица виртуальных функций находится в EAX. Здесь мы не ограничены определенным размером, так как на эту таблицу указывает первый из 26 DWORDS (всего 0x68 или 104 байта), который заполняет освобожденный кусок кучи. Благодаря этому мы можем обменять регистр EAX (которым мы управляем) с регистром ESP. Это даст нам контроль над стеком и позволит начать формирование цепочки ROP.
Анализируя вывод ROP-гаджета из rp++, мы видим, что существует хороший ROP-гаджет.
Давайте обновим наш POC этим гаджетом ROP вместо прежнего DWORD 42424242, который используется вместо нашей фальшивой виртуальной функции.
Давайте (пока) оставим WinDbg настроенным как наш отладчик и посмотрим, что произойдет. Запустив POC, мы видим, что происходит сбой, и указатель инструкции указывает на 41414141.
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) вызывает функцию, это правильно определенная функция для этого класса. Так что в основном думайте об этом как о функции, которая объявлена в основном классе, наследуется подклассом, и каждому подклассу, который наследует ее, разрешено изменять то, что делает функция. Затем, когда объект класса вызывает виртуальную функцию, вызывается соответствующее определение функции, соответствующее вызывающему ее объекту класса.
Запустив программу, мы видим, что получаем ожидаемый результат:
Теперь, когда мы вооружились базовым пониманием некоторых ключевых концепций, в основном конструкторов, деструкторов и виртуальных функций, давайте посмотрим на ассемблерный код того, как выбирается виртуальная функция.
Обратите внимание, что нет необходимости повторять эти шаги, если вы следуете им.Однако, если вы хотите следовать пошаговым инструкциям, имя этого .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.
После этого следуйте этим инструкциям от 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().
Достигнув начала вызова виртуальной функции, давайте установим точки останова на следующих трех инструкциях после инструкции в RIP. Для этого используйте bp 00007ff7b67c1703 и т. д.
Переходя к следующей инструкции, мы видим, что значение, на которое указывает RAX, будет перемещено в RAX. Это значение, согласно WinDbg, - это virtualfunctions!ClassTwo::vftable.
Как мы видим, этот адрес является указателем на "vftable" (указатель таблицы виртуальных функций, или vptr). Vftable - это таблица виртуальных функций, которая по сути представляет собой структуру указателей на различные виртуальные функции. Вспомните, как мы говорили ранее: "когда класс вызывает виртуальную функцию, программа будет знать, какая функция соответствует каждому объекту класса". Вот этот процесс в действии. Давайте посмотрим на текущую инструкцию и две следующие.
Возможно, вы не сможете сказать это сейчас, но такая процедура (например, mov reg, [ptr] + call [ptr]) указывает на то, что конкретная виртуальная функция извлекается из таблицы виртуальных функций. Давайте пройдемся сейчас, чтобы увидеть, как это работает. При вызове, vptr (который является указателем на таблицу) загружается в RAX. Давайте теперь взглянем на эту таблицу.
Хотя эти символы немного сбивают с толку, обратите внимание, что у нас здесь два указателя - один - "sharedFunctionclassTwo", а другой — "sharedFunction1classTwo". На самом деле это указатели на две виртуальные функции в classTwo!
Если мы перейдем к вызову, мы увидим, что это вызов, который перенаправляет на переход к виртуальной функции sharedFunction, определенной в classTwo!
Затем продолжайте переходить к инструкциям в отладчике, пока мы не дойдем до инструкции c1-> sharedFunction1(). Обратите внимание, что по мере продвижения вы в конечном итоге увидите процедуру того же типа, которая выполняется с sharedFunction внутри classThree.
Опять же, мы можем наблюдать тот же тип поведения, только на этот раз инструкция вызова - call qword ptr [rax+0x8]. Это связано с тем, как виртуальные функции выбираются из таблицы. Грамотно составленная диаграмма Microsoft Paint ниже показывает, как программа индексирует таблицу при наличии нескольких виртуальных функций, как в нашей программе.
Как мы помним из нескольких изображений которые были раньше, где мы сдампили таблицу и увидели два адреса наших виртуальных функций. Мы видим, что на этот раз выполнение программы будет вызывать эту таблицу со смещением 0x8, которое на этот раз является указателем на sharedFunction1, а не sharedFunction!
Выполняя инструкции, мы переходим на sharedFunction1.
После выполнения всех виртуальных функций будет вызван наш деструктор. Поскольку мы создали только два объекта classOne и удаляем только эти два объекта, мы знаем, что будет вызван только деструктор classOne, что очевидно при поиске термина "деструктор" в IDA. Мы видим, что будет вызвана функция j_operator_delete, которая представляет собой просто длинный и затянутый переход к функции UCRTBASED Windows API _free_dbg, чтобы уничтожить объект. Обратите внимание, что обычно это был бы бесплатный вызов функции C Runtime, но поскольку мы создали эту программу в режиме отладки, по умолчанию используется отладочная версия.
Круто! Теперь мы знаем, как классы 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.
Эта команда настроила 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 должен запуститься.
После включения PageHeap давайте запустим уязвимый код. (Обратите внимание, что вам может потребоваться щелкнуть правой кнопкой мыши изображение ниже и открыть его в новой вкладке)
Очень интересно, мы видим, что произошел сбой! Обратите внимание на инструкцию 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). Эта структура используется для хранения важной информации для управления свободными фрагментами, но делает гораздо больше.
Далее, вот как выглядит структура HeapBase-> BlocksIndex (_HEAP_LIST_LOOKUP).
Первый член этой структуры является указателем на следующую структуру _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.
Например, если для 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, я пошел дальше и сократил код до "кода триггера", что вызывает сбой.
Возвращаясь к уязвимости и читая описание, эта уязвимость возникает, когда 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 обнаруживается сбой, как и ожидалось.
Теперь, когда мы знаем, что наш POC приведет к сбою браузера, давайте сделаем WinDbg нашим отладчиком по умолчанию, точно так же, как мы делали это раньше, чтобы определить почему произошел сбой.
Снова запустив POC, мы видим, что наш сбой зарегистрирован в WinDbg, но это кажется бессмысленным.
Мы знаем, в соответствии с рекомендациями, что это условие UAF. Мы также знаем, что это результат выборки виртуальной функции из объекта, который больше не существует. Зная это, мы должны ожидать разыменования некоторой памяти, которая больше не существует. Однако это не так, и мы просто видим ссылку на недопустимую память. Вспомните, когда мы включали PageHeap! Здесь нам нужно сделать то же самое и включить PageHeap для Internet Explorer. Воспользуйтесь той же командой, что и ранее, но на этот раз укажите iexplore.exe.
После включения PageHeap давайте повторно запустим POC.
Интересно! Инструкция, по которой подает программа, взята из класса CElement. Обратите внимание на инструкцию, по которой происходит сбой: mov reg, dword ptr [eax + 70h]. Если мы дизассемблируем текущий указатель инструкции, мы увидим нечто, очень напоминающее наши инструкции ассемблирования, которые мы показали ранее для выборки виртуальной функции.
Вспомните, как в прошлый раз в нашей 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.
Обратите внимание, как мы получаем тот же результат, что и наша простая программа ранее, хотя инструкции по ассемблированию немного отличаются? Поиск этих типов подпрограмм очень указывает на выборку виртуальной функции!
Прежде чем двигаться дальше, вспомним прежнюю картинку.
Обратите внимание на состояние EAX при сбое функции (прямо под оператором Access Violation). Вроде есть своего рода шаблон f0f0f0f0. Это шаблон gflags.exe для "освобожденного выделения", означающий, что значение в EAX находится в свободном состоянии. Это имеет смысл, поскольку мы пытаемся проиндексировать объект, которого просто больше не существует!
Перезапустите POC, и когда произойдет сбой, давайте выполним следующую команду !heap -p -a ecx.
Почему 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 и найти любые функции, содержащие слово деструктор.
Как видим, мы нашли деструктор! Согласно предыдущей трассировке стека, этот деструктор должен вызвать HeapFree, который фактически выполняет освобождение. Мы видим, что это так после дизассемблирования функции в IDA.
Запрашивая документацию Microsoft по HeapFree(https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapfree), мы видим, что он принимает три аргумента: 1. Дескриптор кучи, в которой будет освобождена часть памяти, 2. Флажки для освобождения и 3. Указатель на фактический фрагмент памяти, который нужно освободить.
На этом этапе вы можете спросить: "Ни один из этих параметров не является размером". Это верно! Однако теперь мы видим, что адрес блока, который будет освобожден, будет третьим параметром, передаваемым вызову HeapFree. Обратите внимание, что, поскольку мы находимся в 32-битной системе, аргументы функций будут передаваться через соглашение о вызовах __stdcall, что означает, что стек используется для передачи аргументов в вызов функции.
Еще раз взгляните на прототип предыдущего образа. Обратите внимание, что деструктор принимает аргумент для объекта типа CanchorElement. Это имеет смысл, поскольку это деструктор для объекта, созданного из класса CanchorElement. Это также означает, однако, что должен быть конструктор, способный также создавать указанный объект! И когда деструктор вызывает HeapFree, конструктор, скорее всего, вызовет либо malloc, либо HeapAlloc! Мы знаем, что последний аргумент для вызова HeapFree в деструкторе - это адрес фактического фрагмента, который нужно освободить. Это означает, что в первую очередь необходимо выделить кусок. При повторном поиске функций в IDA в классе CAnchorElement есть функция под названием CreateElement, которая очень характерна для конструктора объекта CAnchorElement! Давайте посмотрим на это в IDA.
Отлично, мы видим, что на самом деле есть вызов HeapAlloc. Обратимся к документации Microsoft для этой функции (https://docs.microsoft.com/en-us/windows/win32/api/heapapi/nf-heapapi-heapalloc).
Первый параметр - это снова дескриптор существующей кучи. Во-вторых, это любые флаги, которые вы хотите установить для выделения кучи. Третье и самое важное для нас - это фактический размер кучи. Это говорит нам о том, что при создании объекта CAnchorElement он будет иметь размер 0x68 байт. Если мы снова откроем наш POC в Internet Explorer, позволив отладчику снова взять на себя ответственность, мы фактически увидим, что размер свободного от уязвимости фрагмента кучи размером 0x68 байт, точно так же, как и наш реверс инжиниринг CAnchorElement::CreateElement показывает функцию.
Это доказывает нашу гипотезу, и теперь мы можем приступить к редактированию нашего скрипта, чтобы увидеть, не можем ли мы контролировать это распределение. Прежде чем продолжить, давайте отключим PageHeap для IE8.
Теперь, когда это сделано, давайте обновим наш POC следующим кодом.
Вышеупомянутый 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 в качестве посмертного отладчика.
Интересно! На этот раз мы пытаемся разыменовать адрес 0x41414141 вместо того, чтобы получить произвольный сбой, как это было в начале этой статьи, путем запуска исходного POC без включенного PageHeap! Однако причина этого сбоя совсем другая! Напомним, что фрагмент кучи, вызывающий проблему, находится в ECX, как мы видели ранее. Однако на этот раз вместо того, чтобы видеть освобожденную память, мы действительно можем видеть, что наши данные, контролируемые пользователем, теперь выделяют кусок кучи!
Теперь, когда мы, наконец, выяснили, как мы можем контролировать данные в ранее освобожденном фрагменте, мы можем довести все, что описано в этом руководстве, до полного круга. Давайте посмотрим на текущее выполнение программы.
Мы знаем, что это процедура для выборки виртуальной функции из таблицы виртуальных функций. Первая инструкция mov eax, dword ptr [ecx] берет указатель таблицы виртуальных функций, также известный как vptr, и загружает его в регистр EAX. Затем оттуда снова разыменовывается этот vptr, который указывает на таблицу виртуальных функций, и вызывается с указанным смещением. Обратите внимание, как в настоящее время мы контролируем регистр ECX, который используется для хранения vptr.
Давайте также посмотрим на этот фрагмент в контексте структуры HeapBase.
Как мы видим, в куче наш чанк является частью, 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. (Возможно, вам потребуется открыть первое изображение в новой вкладке)
Опять же, в блоге Exodus будут подробно описаны детали, но, по сути, здесь происходит то, что мы можем использовать SMIL (язык синхронизированной интеграции мультимедиа), чтобы вместо простого создания 0x68 байтов данных для заполнения кучи создавать указатели на 0x68 байтов, который гораздо более полезен и позволит нам создать фальшивую таблицу виртуальных функций.
Обратите внимание, что хип распыление - это альтернатива, хотя она относительно тщательно изучается. Смысл этого эксплойта состоит в том, чтобы задокументировать уязвимости, связанные с использованием после освобождения, и то, как определить размер освобожденного выделения и как его правильно заполнить. Эта специфическая техника сегодня тоже не применима. Однако это начало моего изучения эксплуатации браузеров, и я ожидаю, что начну с основ.
Давайте теперь снова запустим POC и посмотрим, что произойдет.
Отличная новость, мы управляем указателем инструкции! Давайте посмотрим, как мы сюда попали. Напомним, что мы выполняем код в рамках той же процедуры в CElement :: Doc, где мы были, где мы извлекаем виртуальную функцию из vftable. Взгляните на изображение ниже.
Начнем с самого верха. Как мы видим, 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() разыменует то, что находится в круглых скобках, и отобразит это).
Это значение в ECX, которое является указателем на нашу поддельную vtable, также помещается в EAX.
Значение EAX со смещением 0x70 затем помещается в регистр EDX. Затем вызывается это значение.
Как мы видим, это 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, он нам уже знаком!
Для начала мы знаем, что наша фальшивая таблица виртуальных функций находится в EAX. Здесь мы не ограничены определенным размером, так как на эту таблицу указывает первый из 26 DWORDS (всего 0x68 или 104 байта), который заполняет освобожденный кусок кучи. Благодаря этому мы можем обменять регистр EAX (которым мы управляем) с регистром ESP. Это даст нам контроль над стеком и позволит начать формирование цепочки ROP.
Анализируя вывод ROP-гаджета из rp++, мы видим, что существует хороший ROP-гаджет.
Давайте обновим наш 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.
Последнее редактирование: