Содержание статьи
Практиковаться мы будем на реализации кучи ptmalloc2, которая сейчас используется по умолчанию в glibc, поэтому нам понадобятся машина с Linux и необходимый софт. Установим отладчик GDB, GCC (можно сразу поставить весь пакет build-essential) и отладочную версию библиотеки libc, позволяющую видеть подробную информацию о куче. Также поставим pwngdb и его зависимость — peda, чтобы получить удобные команды vmmap
, hexdump
, heapinfo
.
sudo apt install gdb build-essential libc6-dbg
git clone https://github.com/scwuaptx/Pwngdb.git ~/Pwngdb
cp ~/Pwngdb/.gdbinit ~/
git clone https://github.com/longld/peda.git ~/peda
Основы GDB
Для изучения работы наших тестовых программ понадобится знание базовых команд GDB:
-
r[
— запустить файл;un] -
b[
— поставить точку останова на адресеreak] *0x1234 0x1234
; -
b[
— поставить точку останова на строкеreak] 123 123
текущего исходного файла; -
b[
— поставить точку останова на строкеreak] basic. c: 123 123
исходного файлаbasic.
;c -
c[
— продолжить выполнение;ontinue] -
s[
— выполнить одну ассемблерную инструкцию;tep] -
n[
— выполнить одну строчку исходного файла;ext] -
x/
— распечатать десять 8-байтных слов по адресу10xg 0x1234 0x1234
; -
p[
— распечатать значение переменнойrint] a a
; -
p[
— взять содержимое памяти по адресуrint] *(( mchunkptr) 0x555555756680) 0x555555756680
как типmchunkptr
, задереференсить его и распечатать; -
where
— показать, на какой строчке исходного кода находится выполнение программы.
Команды peda и pwngdb:
-
vmmap
— вывести карту памяти; -
hexdump
— показать содержимое памяти по адресу в видеhexdump
; -
heapinfo
— посмотреть информацию о куче.
Структура чанков
Когда программа запрашивает буфер для данных (например, размером в 10 байт) с помощью malloc
, на самом деле выделяется больше памяти, так как для хранения метаданных необходимо дополнительное пространство. Такой кусок памяти, содержащий метаданные, называют чанком (chunk).
Структура чанка, используемая в ptmalloc2, приведена ниже. Из нее можно понять, что перед указателем на выделенный буфер памяти, который возвращается пользователю (mem
), располагаются еще два поля: размер чанка и размер предыдущего чанка.
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... . . . . (malloc_usable_size() bytes) . . |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Сам чанк имеет такую структуру:
struct malloc_chunk { INTERNAL_SIZE_T mchunk_prev_size; /* Size of previous chunk, if it is free. */ INTERNAL_SIZE_T mchunk_size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links — used only if this chunk is free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links — used only if this chunk is free. */ struct malloc_chunk* bk_nextsize;};typedef struct malloc_chunk* mchunkptr;
Чтобы получить из указателя на чанк (служебную структуру) указатель на буфер памяти, который можно использовать, к первому прибавляют значение 2*SIZE_SZ
. SIZE_SZ
. Для архитектуры x64 оно равно 8, а для x86 — 4. То есть на x64 user_mem
. И наоборот, чтобы из указателя, который вернул malloc
, получить указатель на чанк, необходимо вычесть 2*SIZE_SZ
из него. За это отвечают следующие макросы:
#define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ))#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))
Важный момент: поле mchunk_prev_size
в следующем чанке используется для хранения пользовательских данных предыдущего чанка.
Арена
Старые менеджеры кучи использовали одну кучу на весь процесс и синхронизировали доступ к ней разных потоков с помощью мьютексов. Как несложно догадаться, положительно на производительности это не сказывалось. Ptmalloc2 использует арены — области памяти для того, чтобы каждый поток мог там хранить свою кучу.
Но поскольку на практике потоков в приложении может быть слишком много, максимальное количество создаваемых арен вычисляется по следующей формуле (n
— количество процессоров):
#define NARENAS_FROM_NCORES(n) ((n) * (**sizeof** (**long**) == 4 ? 2 : 8))
Пока количество арен меньше максимального, менеджер кучи создает новую арену на каждый новый поток. После этого, увы, нескольким потокам придется делить между собой одну арену.
Первая созданная менеджером кучи арена называется основной (main). Однопоточное приложение использует только основную арену.
Флаги
Остановимся подробнее на флагах чанка. Поле размера предыдущего чанка (mchunk_size
), кроме собственно размера, хранит три флага: A
, M
, P
. Это возможно за счет выравнивания размера чанка. Так как размер чанка всегда кратен либо 8, либо 16 байтам, последние 3 бита размера не несут смысловой нагрузки, и их можно использовать для хранения флагов.
-
A
(NON_MAIN_ARENA): 0 — чанк был выделен из основной арены и основной кучи; 1 — чанк принадлежит одной из второстепенных арен. Когда приложение создает дополнительные потоки, каждый из них получает свою арену (грубо говоря, свою кучу). В чанках, выделяемых на этих аренах, установлен битA
; -
M
(IS_MMAPPED): 1 — чанк получен с помощью вызоваmmap
. Остальные флаги игнорируются, потому что данные чанки не располагаются в арене и к ним не примыкают другие чанки; -
P
(PREV_INUSE): 0 — предыдущий чанк не используется. Перед полемmchunk_size
располагается значение размера предыдущего чанка; 1 — предыдущий чанк используется. Перед полемmchunk_size
располагаются пользовательские данные.
Bins
Для повышения быстродействия чанки используют повторно (и именно эту особенность учитывают при эксплуатации кучи). Ранее использованные и освобожденные чанки складывают в бины (bins). В нашей реализации кучи существует пять типов бинов:
- small (62 штуки);
- large (63 штуки);
- unsorted (1 штука);
- fast (10 штук);
- tcache (64 на поток).
Small bins
- Чанки в каждом из small bin хранятся в двусвязном списке.
- Вставка освобожденных чанков в этот список производится с начала (head), удаление — с конца (tail, FIFO). Для ведения этого списка используются указатели fd и bk (см. структуру чанка).
- Таких бинов 62 штуки. Каждый из small bins хранит чанки только одного размера: 16, 24, ..., 504 байт для x86 и 1008 байт для x64.
- Если в small bin попадают два соседних чанка, то они объединяются и отправляются в unsorted bin.
Large bins
- Чанки в каждом из large bin также хранятся в двусвязном списке.
- Чанки в каждом из бинов имеют диапазон размеров.
- Чанки сортируются следующим образом: самый большой чанк находится в head списка, а самый маленький — в tail. Для этого используются указатели
fd_nextsize
иbk_nextsize
. - Вставки и удаления происходят на любой позиции.
- Таких бинов 63 штуки, в них хранятся чанки размером от 512 байт для x86 и от 1024 байт для x64.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»