Введение в эксплуатацию двоичных файлов x64 Linux (часть 1)
Введение в бинарную эксплуатацию x64 Linux (часть 2)
Heap exploitation, Overflows (часть 3)
Heap exploitation, Use After Free & Double free (Часть 4)
Heap Exploitation, FastBin Dup to Stack (часть 4.1)
Heap Exploitation, FastBin Dup Consolidate (часть 4.2)
The unlink macro
Мы видели из предыдущего сообщения, что во время процесса освобождения чанков и при возникновении определенных условий, аллокатор объединяет соседние чанки в более крупные для более эффективного распределения памяти. Проще говоря, предположим, что у вас есть чанки A, B, C и B освобождается, тогда согласно текущей реализации free будет проверять, используются ли A или C, и если нет, то попытается создать более крупный чанк, объединив их в B . Реализация этой логики показана в приведенном ниже фрагменте кода:
После макроса unlink в строках 3977 и 3986 мы приходим к следующему определению:
Этот код изменит (см. строки 1350-1351) указатели fd и bk заголовка чанка (см. рисунок ниже) в контексте перестановки двойного списка:
Этот процесс аналогичен удалению узла из двойного списка:
Но прежде чем что-то произойдет, выполняется проверка на валидность (в строке 1347):
Сначала заметим, что FD = P→fd и BK = P→bk , поэтому FD должен указывать на следующий соседний кусок, а BK - на предыдущий соседний кусок:
Таким образом, чтобы двойной связный список был действительным, BK→fd и FD→bk должны указывать на P :
После проверки этого условия мы получили следующие назначения FD->bk = BK и BK->fd = FD, так что наши чанки теперь будут выглядеть следующим образом:
Exploitation plan
Как мы уже упоминали в начале статьи, мы полностью контролируем содержимое чанка и благодаря ошибке переполнения можем изменять метаданные соседнего чанка. Поэтому на пути к успешной эксплуатации мы должны пройти проверку на отвязку:
Для этого мы сделаем следующее:
Создадим поддельный чанк внутри управляемого чанка.
Мы собираемся вставить определенные значения в определенные адреса памяти, чтобы сформировать действительную структуру чанка внутри сектора данных контролируемого чанка:
Поскольку мы хотим пройти проверку на несвязанность, значения, которые мы собираемся вставить в качестве fd и bk в поддельный чанк, должны указывать на структуры, соответствующие указатели fd и bk которых будут указывать обратно на наш поддельный чанк! Чтобы представить себе эту концепцию, давайте посмотрим на пару рисунков:
Представьте, что таблица Global_Var формирует chunk struct, тогда по адресу памяти 0x6020b0 мы будем иметь размер предыдущего чанка, по адресу 0x6020b8 - размер текущего чанка, по адресу 0x6020c0 - указатель fd и по адресу 0x6020c8 - указатель bk. Таким образом, у нас есть fake_chunk.fd→bk = 0x1967030 и fake_chunk.bk →fd=0x1967030, что пройдет проверку на валидность отвязки.
Следующий шаг:
Измените заголовок следующего чанка, чтобы показать фальшивый чанк как свободный
Помните: из-за переполнения кучи мы можем писать за границы КОНТРОЛИРУЕМОГО ЧАЙНА, поэтому мы можем модифицировать заголовок следующего ЧАЙНА:
Что касается типа модификации, напомню, что заголовок выделенного чанка состоит из текущего размера и размера предыдущего чанка, если и только если предыдущий чанк свободен. Я размещаю структуру чанка еще раз, чтобы вам не пришлось прокручивать страницу вверх:
Помните также, что размер mchunk_size включает в себя 3 флага, на которые указывают последние 3 бита значения. Так, если размер равен 0x10 и предыдущий чанк используется, mchunk_size будет выглядеть следующим образом:
Остальные флаги нас сейчас не волнуют, поскольку нам нужно перевернуть только последний бит, чтобы указать, что предыдущий чанк не используется. Это запустит процесс обратной консолидации, который, в свою очередь, запустит макрос unlink.
И последнее, но не менее важное: размер mchunk_prev_size должен соответствовать размеру поддельного чанка, чтобы обойти остальные проверки безопасности. Если все будет так, как должно быть, то при создании NEXT CHUNK, FAKE CHUNK будет консолидирован, а указатели fd, bk FAKE CHUNK будут перезаписаны в двух последующих шагах:
Assume the following C program:
Давайте разберем это построчно, чтобы понять суть. В строке 6 мы определяем указатель на функцию, которая возвращает void и не принимает никаких параметров. В строке 8 мы определяем указатель на беззнаковое целое число, а в строках с 10 по 17 мы определяем две функции, doNothing, которая не делает абсолютно ничего, и shell, которая открывает оболочку. В строке 26 мы имеем первый malloc размером 0x420 байт, поэтому после этого оператора у нас будут следующие куски:
Таким образом, chunk0_ptr указывает на 0x555555555592a0 (часть данных чанка), в то время как заголовок того же чанка начинается на 0x10 байт раньше, по адресу 0x555555559290 . Наконец, адрес chunk0_ptr находится по адресу 0x55555555558018 , поэтому, чтобы продолжить, после первого malloc мы имеем следующее:
В строке 27 мы имеем второй вызов malloc, и после этого наши куски будут выглядеть следующим образом:
Creating a fake chunk
Строка 29 установит размер поддельного чанка в 0x421, поскольку chunk0_ptr[-1] равен 0x431
А в строках 30-31 мы устанавливаем указатели fd/bk поддельного чанка:
chunk0_ptr[2] = 0x55555555558018 - 3 * 8 = 0x555555558000
chunk0_ptr[2] = 0x55555555558018 - 3 * 8 = 0x555555558008
Таким образом, наш управляемый набор чанков будет выглядеть следующим образом:
И это пройдет проверку на развязку, поскольку, как вы помните из проверки на развязку, у нас будет следующее:
FD = P->fd => FD = 0x0000555555558000
BK = P->bk => BK = 0x0000555555558008
Впоследствии FD→bk переместит указатель FD на 3 позиции вперед (так как это позиция bk в заголовке чанка => 0x0000555555558000 + 0x18 = 0x0000555555558018), а FD→fd переместит указатель BK на 2 позиции вперед (так как это позиция fd в заголовке чанка => 0x0000555555558000 + 0x10 = 0x00005555558018).
Подводя итог, можно сказать, что на данный момент мы работаем следующим образом:
“Fixing” the next chunk’s header
Это самая простая для понимания часть: поскольку мы можем писать за пределами контролируемого чанка, будет тривиально перезаписать соседний. Именно это и демонстрируют строки 33-35:
chunk1_hdr[0] будет указывать на размер mchunk_prev_size, а chunk1_hdr[1] - на текущий размер. Строка 35 перевернет последний бит, чтобы фальшивый чанк отображался как неиспользуемый:
Теперь мы готовы звонить бесплатно:
After free
Давайте теперь посмотрим, как действует функция free. Обратите внимание, что перед ее вызовом мы имеем следующее:*0x555555558018 = 0x00005555555592a0
Во время свободного выполнения будут выполняться следующие утверждения:
И немедленно:
Write anything anywhere
Вспомните из нашей программы С следующие строки сразу после бесплатного звонка:
Переменная d указывает на функцию doNothing, но поскольку мы контролируем содержимое chunk0_ptr, мы можем изменить значение chunk0_ptr[3], таким образом, после строки 41 мы получим следующее:
Поэтому это будет &chunk0_ptr = 0x00007fffffffe338, который содержит адрес doNothing. Итак, chunk0_ptr[0] указывает сюда:
Таким образом, строка 42 перезапишет содержимое этого адреса памяти адресом функции shell:
Это завершает последний этап эксплуатации:
The toddler’s introduction to Heap Exploitation, Unsafe Unlink(Part 4.3)
Введение в бинарную эксплуатацию x64 Linux (часть 2)
Heap exploitation, Overflows (часть 3)
Heap exploitation, Use After Free & Double free (Часть 4)
Heap Exploitation, FastBin Dup to Stack (часть 4.1)
Heap Exploitation, FastBin Dup Consolidate (часть 4.2)
The unlink macro
Мы видели из предыдущего сообщения, что во время процесса освобождения чанков и при возникновении определенных условий, аллокатор объединяет соседние чанки в более крупные для более эффективного распределения памяти. Проще говоря, предположим, что у вас есть чанки A, B, C и B освобождается, тогда согласно текущей реализации free будет проверять, используются ли A или C, и если нет, то попытается создать более крупный чанк, объединив их в B . Реализация этой логики показана в приведенном ниже фрагменте кода:
После макроса unlink в строках 3977 и 3986 мы приходим к следующему определению:
Этот код изменит (см. строки 1350-1351) указатели fd и bk заголовка чанка (см. рисунок ниже) в контексте перестановки двойного списка:
Этот процесс аналогичен удалению узла из двойного списка:
Но прежде чем что-то произойдет, выполняется проверка на валидность (в строке 1347):
Сначала заметим, что FD = P→fd и BK = P→bk , поэтому FD должен указывать на следующий соседний кусок, а BK - на предыдущий соседний кусок:
Таким образом, чтобы двойной связный список был действительным, BK→fd и FD→bk должны указывать на P :
После проверки этого условия мы получили следующие назначения FD->bk = BK и BK->fd = FD, так что наши чанки теперь будут выглядеть следующим образом:
Exploitation plan
Как мы уже упоминали в начале статьи, мы полностью контролируем содержимое чанка и благодаря ошибке переполнения можем изменять метаданные соседнего чанка. Поэтому на пути к успешной эксплуатации мы должны пройти проверку на отвязку:
Для этого мы сделаем следующее:
Создадим поддельный чанк внутри управляемого чанка.
Мы собираемся вставить определенные значения в определенные адреса памяти, чтобы сформировать действительную структуру чанка внутри сектора данных контролируемого чанка:
Поскольку мы хотим пройти проверку на несвязанность, значения, которые мы собираемся вставить в качестве fd и bk в поддельный чанк, должны указывать на структуры, соответствующие указатели fd и bk которых будут указывать обратно на наш поддельный чанк! Чтобы представить себе эту концепцию, давайте посмотрим на пару рисунков:
Представьте, что таблица Global_Var формирует chunk struct, тогда по адресу памяти 0x6020b0 мы будем иметь размер предыдущего чанка, по адресу 0x6020b8 - размер текущего чанка, по адресу 0x6020c0 - указатель fd и по адресу 0x6020c8 - указатель bk. Таким образом, у нас есть fake_chunk.fd→bk = 0x1967030 и fake_chunk.bk →fd=0x1967030, что пройдет проверку на валидность отвязки.
Следующий шаг:
Измените заголовок следующего чанка, чтобы показать фальшивый чанк как свободный
Помните: из-за переполнения кучи мы можем писать за границы КОНТРОЛИРУЕМОГО ЧАЙНА, поэтому мы можем модифицировать заголовок следующего ЧАЙНА:
Что касается типа модификации, напомню, что заголовок выделенного чанка состоит из текущего размера и размера предыдущего чанка, если и только если предыдущий чанк свободен. Я размещаю структуру чанка еще раз, чтобы вам не пришлось прокручивать страницу вверх:
Помните также, что размер mchunk_size включает в себя 3 флага, на которые указывают последние 3 бита значения. Так, если размер равен 0x10 и предыдущий чанк используется, mchunk_size будет выглядеть следующим образом:
Остальные флаги нас сейчас не волнуют, поскольку нам нужно перевернуть только последний бит, чтобы указать, что предыдущий чанк не используется. Это запустит процесс обратной консолидации, который, в свою очередь, запустит макрос unlink.
И последнее, но не менее важное: размер mchunk_prev_size должен соответствовать размеру поддельного чанка, чтобы обойти остальные проверки безопасности. Если все будет так, как должно быть, то при создании NEXT CHUNK, FAKE CHUNK будет консолидирован, а указатели fd, bk FAKE CHUNK будут перезаписаны в двух последующих шагах:
Assume the following C program:
C:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef void (*dn)();
uint64_t *chunk0_ptr;
void doNothing()
{
printf("nothing\n");
}
void shell()
{
system("/bin/sh");
}
int main()
{
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin
int header_size = 2;
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0
uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1
chunk0_ptr[1] = chunk0_ptr[-1] - 0x10;
chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3);
chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2);
uint64_t *chunk1_hdr = chunk1_ptr - header_size;
chunk1_hdr[0] = malloc_size;
chunk1_hdr[1] &= ~1;
free(chunk1_ptr);
dn d = doNothing;
chunk0_ptr[3] = (uint64_t) &d;
chunk0_ptr[0] = (uint64_t) &shell;
(*d)();
}
Давайте разберем это построчно, чтобы понять суть. В строке 6 мы определяем указатель на функцию, которая возвращает void и не принимает никаких параметров. В строке 8 мы определяем указатель на беззнаковое целое число, а в строках с 10 по 17 мы определяем две функции, doNothing, которая не делает абсолютно ничего, и shell, которая открывает оболочку. В строке 26 мы имеем первый malloc размером 0x420 байт, поэтому после этого оператора у нас будут следующие куски:
Таким образом, chunk0_ptr указывает на 0x555555555592a0 (часть данных чанка), в то время как заголовок того же чанка начинается на 0x10 байт раньше, по адресу 0x555555559290 . Наконец, адрес chunk0_ptr находится по адресу 0x55555555558018 , поэтому, чтобы продолжить, после первого malloc мы имеем следующее:
В строке 27 мы имеем второй вызов malloc, и после этого наши куски будут выглядеть следующим образом:
Creating a fake chunk
Строка 29 установит размер поддельного чанка в 0x421, поскольку chunk0_ptr[-1] равен 0x431
А в строках 30-31 мы устанавливаем указатели fd/bk поддельного чанка:
chunk0_ptr[2] = 0x55555555558018 - 3 * 8 = 0x555555558000
chunk0_ptr[2] = 0x55555555558018 - 3 * 8 = 0x555555558008
Таким образом, наш управляемый набор чанков будет выглядеть следующим образом:
И это пройдет проверку на развязку, поскольку, как вы помните из проверки на развязку, у нас будет следующее:
FD = P->fd => FD = 0x0000555555558000
BK = P->bk => BK = 0x0000555555558008
Впоследствии FD→bk переместит указатель FD на 3 позиции вперед (так как это позиция bk в заголовке чанка => 0x0000555555558000 + 0x18 = 0x0000555555558018), а FD→fd переместит указатель BK на 2 позиции вперед (так как это позиция fd в заголовке чанка => 0x0000555555558000 + 0x10 = 0x00005555558018).
Подводя итог, можно сказать, что на данный момент мы работаем следующим образом:
“Fixing” the next chunk’s header
Это самая простая для понимания часть: поскольку мы можем писать за пределами контролируемого чанка, будет тривиально перезаписать соседний. Именно это и демонстрируют строки 33-35:
chunk1_hdr[0] будет указывать на размер mchunk_prev_size, а chunk1_hdr[1] - на текущий размер. Строка 35 перевернет последний бит, чтобы фальшивый чанк отображался как неиспользуемый:
Теперь мы готовы звонить бесплатно:
After free
Давайте теперь посмотрим, как действует функция free. Обратите внимание, что перед ее вызовом мы имеем следующее:*0x555555558018 = 0x00005555555592a0
Во время свободного выполнения будут выполняться следующие утверждения:
И немедленно:
Write anything anywhere
Вспомните из нашей программы С следующие строки сразу после бесплатного звонка:
Переменная d указывает на функцию doNothing, но поскольку мы контролируем содержимое chunk0_ptr, мы можем изменить значение chunk0_ptr[3], таким образом, после строки 41 мы получим следующее:
Поэтому это будет &chunk0_ptr = 0x00007fffffffe338, который содержит адрес doNothing. Итак, chunk0_ptr[0] указывает сюда:
Таким образом, строка 42 перезапишет содержимое этого адреса памяти адресом функции shell:
Это завершает последний этап эксплуатации:
The toddler’s introduction to Heap Exploitation, Unsafe Unlink(Part 4.3)