Купа - це, по суті, місце, де програма може зберігати дані, коли запитує дані, викликаючи функції, такі як malloc, calloc... Більше того, коли ця пам'ять більше не потрібна, вона стає доступною, викликаючи функцію free.
Як показано, це просто після того, як бінарний файл завантажується в пам'ять (перевірте розділ [heap]):
Basic Chunk Allocation
Коли запитується зберігання деяких даних у купі, для них виділяється певний обсяг пам'яті. Цей обсяг буде належати біну, і лише запитувані дані + простір заголовків бінів + мінімальний зсув розміру біна буде зарезервовано для частини. Мета полягає в тому, щоб зарезервувати якомога менше пам'яті, не ускладнюючи пошук, де знаходиться кожна частина. Для цього використовується інформація про метадані частини, щоб знати, де знаходиться кожна використана/вільна частина.
Існують різні способи резервування простору, головним чином залежно від використаного біна, але загальна методологія є такою:
Програма починає з запиту певної кількості пам'яті.
Якщо в списку частин є доступна, достатньо велика, щоб задовольнити запит, вона буде використана.
Це може навіть означати, що частина доступної частини буде використана для цього запиту, а решта буде додана до списку частин.
Якщо в списку немає доступної частини, але в виділеній пам'яті купи ще є місце, менеджер купи створює нову частину.
Якщо недостатньо місця в купі для виділення нової частини, менеджер купи запитує у ядра розширити пам'ять, виділену для купи, а потім використовує цю пам'ять для створення нової частини.
Якщо все не вдається, malloc повертає null.
Зверніть увагу, що якщо запитувана пам'ять перевищує поріг, mmap буде використано для відображення запитуваної пам'яті.
Arenas
У багатопотокових додатках менеджер купи повинен запобігати умовам гонки, які можуть призвести до збоїв. Спочатку це робилося за допомогою глобального м'ютекса, щоб забезпечити доступ до купи лише одного потоку в один момент часу, але це викликало проблеми з продуктивністю через вузьке місце, викликане м'ютексом.
Щоб вирішити цю проблему, аллокатор купи ptmalloc2 ввів "арени", де кожна арена діє як окрема купа зі своїми власними структурами даних та м'ютексом, що дозволяє кільком потокам виконувати операції з купою без перешкод один одному, якщо вони використовують різні арени.
За замовчуванням "основна" арена обробляє операції з купою для однопотокових додатків. Коли додаються нові потоки, менеджер купи призначає їм вторинні арени, щоб зменшити конкуренцію. Спочатку він намагається приєднати кожен новий потік до невикористаної арени, створюючи нові, якщо це необхідно, до межі 2 рази кількості ядер ЦП для 32-бітних систем і 8 разів для 64-бітних систем. Коли межа досягається, потоки повинні ділити арени, що може призвести до потенційної конкуренції.
На відміну від основної арени, яка розширюється за допомогою системного виклику brk, вторинні арени створюють "підкупи" за допомогою mmap та mprotect, щоб імітувати поведінку купи, що дозволяє гнучко управляти пам'яттю для багатопотокових операцій.
Subheaps
Підкупи служать резервами пам'яті для вторинних арен у багатопотокових додатках, дозволяючи їм рости та управляти своїми власними регіонами купи окремо від основної купи. Ось як підкупи відрізняються від початкової купи та як вони працюють:
Початкова купа vs. Підкупи:
Початкова купа розташована безпосередньо після бінарного файлу програми в пам'яті, і вона розширюється за допомогою системного виклику sbrk.
Підкупи, які використовуються вторинними аренами, створюються через mmap, системний виклик, який відображає вказану область пам'яті.
Резервування пам'яті з mmap:
Коли менеджер купи створює підкуп, він резервує великий блок пам'яті через mmap. Це резервування не виділяє пам'ять негайно; воно просто позначає область, яку інші системні процеси або алокації не повинні використовувати.
За замовчуванням зарезервований розмір для підкупу становить 1 МБ для 32-бітних процесів і 64 МБ для 64-бітних процесів.
Поступове розширення з mprotect:
Зарезервована область пам'яті спочатку позначена як PROT_NONE, що вказує на те, що ядро не повинно виділяти фізичну пам'ять для цього простору поки що.
Щоб "зрости" підкуп, менеджер купи використовує mprotect, щоб змінити дозволи сторінок з PROT_NONE на PROT_READ | PROT_WRITE, спонукаючи ядро виділити фізичну пам'ять для раніше зарезервованих адрес. Цей покроковий підхід дозволяє підкупу розширюватися за потреби.
Як тільки весь підкуп вичерпується, менеджер купи створює новий підкуп для продовження алокації.
heap_info
Ця структура виділяє відповідну інформацію про купу. Більше того, пам'ять купи може бути не безперервною після більше алокацій, ця структура також зберігатиме цю інформацію.
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/arena.c#L837typedefstruct _heap_info{mstate ar_ptr; /* Arena for this heap. */struct _heap_info *prev; /* Previous heap. */size_t size; /* Current size in bytes. */size_t mprotect_size; /* Size in bytes that has been mprotectedPROT_READ|PROT_WRITE. */size_t pagesize; /* Page size used when allocating the arena. *//* Make sure the following data is properly aligned, particularlythat sizeof (heap_info) + 2 * SIZE_SZ is a multiple ofMALLOC_ALIGNMENT. */char pad[-3* SIZE_SZ & MALLOC_ALIGN_MASK];} heap_info;
malloc_state
Кожен купа (основна арена або арени інших потоків) має структуру malloc_state.
Важливо зазначити, що структура malloc_state основної арени є глобальною змінною в libc (отже, розташована в пам'яті libc).
У випадку **структур malloc_state куп потоків, вони розташовані всередині власної "купи" потоку.
Є кілька цікавих моментів, які варто відзначити з цієї структури (див. код C нижче):
__libc_lock_define (, mutex); Призначено для забезпечення того, щоб до цієї структури з купи отримував доступ лише 1 потік одночасно
* `mchunkptr bins[NBINS * 2 - 2];` містить **вказівники** на **перший і останній шматки** малих, великих і неупорядкованих **бінів** (мінус 2, оскільки індекс 0 не використовується)
* Отже, **перший шматок** цих бінів матиме **зворотний вказівник на цю структуру**, а **останній шматок** цих бінів матиме **прямий вказівник** на цю структуру. Це в основному означає, що якщо ви зможете **викрити ці адреси в основній арені**, ви отримаєте вказівник на структуру в **libc**.
* Структури `struct malloc_state *next;` і `struct malloc_state *next_free;` є зв'язаними списками арен
* Шматок `top` є останнім "шматком", який в основному є **всією залишковою пам'яттю купи**. Як тільки верхній шматок "порожній", купа повністю використана, і потрібно запитати більше місця.
* Шматок `last reminder` виникає в тих випадках, коли точний шматок не доступний, і тому більший шматок розділяється, а вказівник на залишкову частину розміщується тут.
```c
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/malloc.c#L1812
struct malloc_state
{
/* Serialize access. */
__libc_lock_define (, mutex);
/* Flags (formerly in max_fast). */
int flags;
/* Set if the fastbin chunks contain recently inserted free blocks. */
/* Note this is a bool but not all targets support atomics on booleans. */
int have_fastchunks;
/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];
/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;
/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;
/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];
/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];
/* Linked list */
struct malloc_state *next;
/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;
/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;
/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};
malloc_chunk
Ця структура представляє собою певний фрагмент пам'яті. Різні поля мають різне значення для виділених та невиділених фрагментів.
// https://github.com/bminor/glibc/blob/master/malloc/malloc.cstruct 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;};typedefstruct malloc_chunk* mchunkptr;
Як було зазначено раніше, ці частини також мають деякі метадані, дуже добре представлені на цьому зображенні:
Метадані зазвичай 0x08B, що вказує на розмір поточної частини, використовуючи останні 3 біти для вказівки:
A: Якщо 1, це походить з підкучі, якщо 0 - в основній арені
M: Якщо 1, ця частина є частиною простору, виділеного за допомогою mmap, і не є частиною купи
P: Якщо 1, попередня частина використовується
Потім, простір для даних користувача, і нарешті 0x08B, щоб вказати розмір попередньої частини, коли частина доступна (або для зберігання даних користувача, коли вона виділена).
Більше того, коли доступно, дані користувача також використовуються для зберігання деяких даних:
fd: Вказівник на наступну частину
bk: Вказівник на попередню частину
fd_nextsize: Вказівник на першу частину в списку, яка менша за саму себе
bk_nextsize: Вказівник на першу частину в списку, яка більша за саму себе
Зверніть увагу, як зв'язування списку таким чином запобігає необхідності мати масив, в якому реєструється кожна окрема частина.
Вказівники на частини
Коли використовується malloc, повертається вказівник на вміст, який можна записати (безпосередньо після заголовків), однак, при управлінні частинами, потрібен вказівник на початок заголовків (метаданих).
Для цих перетворень використовуються ці функції:
// https://github.com/bminor/glibc/blob/master/malloc/malloc.c/* Convert a chunk address to a user mem pointer without correcting the tag. */#definechunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ))/* Convert a user mem pointer to a chunk address and extract the right tag. */#definemem2chunk(mem) ((mchunkptr)tag_at (((char*)(mem) - CHUNK_HDR_SZ)))/* The smallest possible chunk */#defineMIN_CHUNK_SIZE (offsetof(struct malloc_chunk, fd_nextsize))/* The smallest size we can malloc is an aligned minimal chunk */#defineMINSIZE \(unsignedlong)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) &~MALLOC_ALIGN_MASK))
Вирівнювання та мінімальний розмір
Вказівник на шматок і 0x0f повинні бути 0.
// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/sysdeps/generic/malloc-size.h#L61#defineMALLOC_ALIGN_MASK (MALLOC_ALIGNMENT -1)// https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/sysdeps/i386/malloc-alignment.h#defineMALLOC_ALIGNMENT16// https://github.com/bminor/glibc/blob/master/malloc/malloc.c/* Check if m has acceptable alignment */#definealigned_OK(m) (((unsignedlong)(m) & MALLOC_ALIGN_MASK) ==0)#definemisaligned_chunk(p) \((uintptr_t)(MALLOC_ALIGNMENT == CHUNK_HDR_SZ ? (p) :chunk2mem (p)) \& MALLOC_ALIGN_MASK)/* pad request bytes into a usable size -- internal version *//* Note: This must be a macro that evaluates to a compile time constantif passed a literal constant. */#definerequest2size(req) \(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) ? \MINSIZE : \((req) + SIZE_SZ + MALLOC_ALIGN_MASK) &~MALLOC_ALIGN_MASK)/* Check if REQ overflows when padded and aligned and if the resultingvalue is less than PTRDIFF_T. Returns the requested size orMINSIZE in case the value is less than MINSIZE, or 0 if any of theprevious checks fail. */staticinlinesize_tchecked_request2size (size_t req) __nonnull (1){if (__glibc_unlikely (req > PTRDIFF_MAX))return0;/* When using tagged memory, we cannot share the end of the userblock with the header for the next chunk, so ensure that weallocate blocks that are rounded up to the granule size. Takecare not to overflow from close to MAX_SIZE_T to a smallnumber. Ideally, this would be part of request2size(), but thatmust be a macro that produces a compile time constant if passeda constant literal. */if (__glibc_unlikely (mtag_enabled)){/* Ensure this is not evaluated if !mtag_enabled, see gcc PR 99551. */asm ("");req = (req + (__MTAG_GRANULE_SIZE -1)) &~(size_t)(__MTAG_GRANULE_SIZE -1);}returnrequest2size (req);}
Зверніть увагу, що для обчислення загального необхідного простору SIZE_SZ додається лише 1 раз, оскільки поле prev_size може використовуватися для зберігання даних, тому потрібен лише початковий заголовок.
Отримати дані про шматок і змінити метадані
Ці функції працюють, отримуючи вказівник на шматок, і корисні для перевірки/встановлення метаданих:
Перевірити прапори шматка
// From https://github.com/bminor/glibc/blob/master/malloc/malloc.c/* size field is or'ed with PREV_INUSE when previous adjacent chunk in use */#definePREV_INUSE0x1/* extract inuse bit of previous chunk */#defineprev_inuse(p) ((p)->mchunk_size & PREV_INUSE)/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */#defineIS_MMAPPED0x2/* check for mmap()'ed chunk */#definechunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtainedfrom a non-main arena. This is only set immediately before handingthe chunk to the user, if necessary. */#defineNON_MAIN_ARENA0x4/* Check for chunk from main arena. */#definechunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) ==0)/* Mark a chunk as not being on the main arena. */#defineset_non_main_arena(p) ((p)->mchunk_size |= NON_MAIN_ARENA)
Розміри та вказівники на інші шматки
/*Bits to mask off when extracting sizeNote: IS_MMAPPED is intentionally not masked off from size field inmacros for which mmapped chunks should never be seen. This shouldcause helpful core dumps to occur if it is tried by accident bypeople extending or adapting this malloc.*/#defineSIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)/* Get size, ignoring use bits */#definechunksize(p) (chunksize_nomask (p) &~(SIZE_BITS))/* Like chunksize, but do not mask SIZE_BITS. */#definechunksize_nomask(p) ((p)->mchunk_size)/* Ptr to next physical malloc_chunk. */#definenext_chunk(p) ((mchunkptr) (((char*) (p)) +chunksize (p)))/* Size of the chunk below P. Only valid if !prev_inuse (P). */#defineprev_size(p) ((p)->mchunk_prev_size)/* Set the size of the chunk below P. Only valid if !prev_inuse (P). */#defineset_prev_size(p, sz) ((p)->mchunk_prev_size = (sz))/* Ptr to previous physical malloc_chunk. Only valid if !prev_inuse (P). */#defineprev_chunk(p) ((mchunkptr) (((char*) (p)) -prev_size (p)))/* Treat space at ptr + offset as a chunk */#definechunk_at_offset(p, s) ((mchunkptr) (((char*) (p)) + (s)))
Встановіть заголовок і нижній колонтитул (коли використовуються номери частин)
/* Set size at head, without disturbing its use bit */#defineset_head_size(p, s) ((p)->mchunk_size = (((p)->mchunk_size & SIZE_BITS) | (s)))/* Set size/use field */#defineset_head(p, s) ((p)->mchunk_size = (s))/* Set size at footer (only when chunk is not in use) */#defineset_foot(p, s) (((mchunkptr) ((char*) (p) + (s)))->mchunk_prev_size = (s))
Отримати розмір реальних використовуваних даних всередині блоку
#pragmaGCCpoisonmchunk_size#pragmaGCCpoisonmchunk_prev_size/* This is the size of the real usable data in the chunk. Not valid fordumped heap chunks. */#definememsize(p) \(__MTAG_GRANULE_SIZE > SIZE_SZ &&__glibc_unlikely (mtag_enabled) ? \chunksize (p) - CHUNK_HDR_SZ : \chunksize (p) - CHUNK_HDR_SZ + (chunk_is_mmapped (p) ?0: SIZE_SZ))/* If memory tagging is enabled the layout changes to accommodate the granulesize, this is wasteful for small allocations so not done by default.Both the chunk header and user data has to be granule aligned. */_Static_assert (__MTAG_GRANULE_SIZE <= CHUNK_HDR_SZ,"memory tagging is not supported with large granule.");static __always_inline void*tag_new_usable (void*ptr){if (__glibc_unlikely (mtag_enabled)&& ptr){mchunkptr cp =mem2chunk(ptr);ptr =__libc_mtag_tag_region (__libc_mtag_new_tag (ptr), memsize (cp));}return ptr;}
Встановіть точку зупинки в кінці основної функції і давайте дізнаємося, де зберігалася інформація:
Можна побачити, що рядок panda був збережений за адресою 0xaaaaaaac12a0 (яка була адресою, наданою у відповіді malloc всередині x0). Перевіряючи 0x10 байт перед цим, можна побачити, що 0x0 представляє, що попередній шматок не використовується (довжина 0) і що довжина цього шматка становить 0x21.
Додаткові зарезервовані простори (0x21-0x10=0x11) походять від доданих заголовків (0x10), а 0x1 не означає, що було зарезервовано 0x21B, але останні 3 біти довжини поточного заголовка мають деякі спеціальні значення. Оскільки довжина завжди вирівняна на 16 байт (на 64-бітних машинах), ці біти насправді ніколи не будуть використані числом довжини.
0x1: Previous in Use - Specifies that the chunk before it in memory is in use
0x2: Is MMAPPED - Specifies that the chunk was obtained with mmap()
0x4: Non Main Arena - Specifies that the chunk was obtained from outside of the main arena
Приклад багатопоточності
Багатопоточність
```c #include #include #include #include #include
void* threadFuncMalloc(void* arg) { printf("Hello from thread 1\n"); char* addr = (char*) malloc(1000); printf("After malloc and before free in thread 1\n"); free(addr); printf("After free in thread 1\n"); }
void* threadFuncNoMalloc(void* arg) { printf("Hello from thread 2\n"); }
int main() { pthread_t t1; void* s; int ret; char* addr;
printf("Before creating thread 2\n"); ret = pthread_create(&t1, NULL, threadFuncNoMalloc, NULL);
printf("Before exit\n"); getchar();
return 0; }
</details>
Відлагоджуючи попередній приклад, можна побачити, що на початку є лише 1 арена:
<figure><img src="../../.gitbook/assets/image (1) (1) (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
Потім, після виклику першого потоку, того, що викликає malloc, створюється нова арена:
<figure><img src="../../.gitbook/assets/image (1) (1) (1) (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
і всередині неї можна знайти деякі шматки:
<figure><img src="../../.gitbook/assets/image (2) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
## Контейнери та виділення/звільнення пам'яті
Перевірте, що таке контейнери, як вони організовані та як пам'ять виділяється і звільняється в:
<div data-gb-custom-block data-tag="content-ref" data-url='bins-and-memory-allocations.md'>
[bins-and-memory-allocations.md](bins-and-memory-allocations.md)
</div>
## Перевірки безпеки функцій купи
Функції, пов'язані з купою, виконуватимуть певні перевірки перед виконанням своїх дій, щоб спробувати переконатися, що купа не була пошкоджена:
<div data-gb-custom-block data-tag="content-ref" data-url='heap-memory-functions/heap-functions-security-checks.md'>
[heap-functions-security-checks.md](heap-memory-functions/heap-functions-security-checks.md)
</div>
## Посилання
* [https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/](https://azeria-labs.com/heap-exploitation-part-1-understanding-the-glibc-heap-implementation/)
* [https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/](https://azeria-labs.com/heap-exploitation-part-2-glibc-heap-free-bins/)