Libc Heap

Concetti di Heap

L'heap è essenzialmente il luogo in cui un programma sarà in grado di memorizzare dati quando richiede dati chiamando funzioni come malloc, calloc... Inoltre, quando questa memoria non è più necessaria, viene resa disponibile chiamando la funzione free.

Come mostrato, si trova subito dopo il caricamento del binario in memoria (controlla la sezione [heap]):

Allocazione di Chunk di Base

Quando viene richiesto di memorizzare alcuni dati nell'heap, viene allocato uno spazio dell'heap per essi. Questo spazio apparterrà a un bin e solo i dati richiesti + lo spazio degli header del bin + l'offset della dimensione minima del bin verranno riservati per il chunk. L'obiettivo è riservare solo la memoria minima possibile senza rendere complicato trovare dove si trova ciascun chunk. Per questo, le informazioni sui metadati del chunk vengono utilizzate per sapere dove si trova ciascun chunk utilizzato/libero.

Ci sono diversi modi per riservare lo spazio principalmente a seconda del bin utilizzato, ma una metodologia generale è la seguente:

  • Il programma inizia richiedendo una certa quantità di memoria.

  • Se nella lista dei chunk c'è qualcuno abbastanza grande da soddisfare la richiesta, verrà utilizzato

  • Questo potrebbe anche significare che parte del chunk disponibile verrà utilizzata per questa richiesta e il resto verrà aggiunto alla lista dei chunk

  • Se non c'è alcun chunk disponibile nella lista ma c'è ancora spazio nella memoria dell'heap allocata, il gestore dell'heap crea un nuovo chunk

  • Se non c'è abbastanza spazio nell'heap per allocare il nuovo chunk, il gestore dell'heap chiede al kernel di espandere la memoria allocata all'heap e quindi utilizza questa memoria per generare il nuovo chunk

  • Se tutto fallisce, malloc restituisce null.

Nota che se la memoria richiesta supera una soglia, verrà utilizzato mmap per mappare la memoria richiesta.

Aree

Nelle applicazioni multithread, il gestore dell'heap deve prevenire condizioni di gara che potrebbero portare a crash. Inizialmente, ciò veniva fatto utilizzando un mutex globale per garantire che solo un thread potesse accedere all'heap alla volta, ma ciò causava problemi di prestazioni a causa del collo di bottiglia indotto dal mutex.

Per affrontare questo problema, l'allocatore dell'heap ptmalloc2 ha introdotto "arene", dove ogni arena funge da heap separato con le sue proprie strutture dati e mutex, consentendo a più thread di eseguire operazioni sull'heap senza interferire l'uno con l'altro, purché utilizzino arene diverse.

L'arena "principale" predefinita gestisce le operazioni sull'heap per le applicazioni single-threaded. Quando vengono aggiunti nuovi thread, il gestore dell'heap assegna loro arene secondarie per ridurre la contesa. Prima tenta di collegare ciascun nuovo thread a un'arena inutilizzata, creandone di nuove se necessario, fino a un limite di 2 volte il numero di core CPU per sistemi a 32 bit e 8 volte per sistemi a 64 bit. Una volta raggiunto il limite, i thread devono condividere le arene, portando a una potenziale contesa.

A differenza dell'arena principale, che si espande utilizzando la chiamata di sistema brk, le arene secondarie creano "subheap" utilizzando mmap e mprotect per simulare il comportamento dell'heap, consentendo flessibilità nella gestione della memoria per operazioni multithread.

Subheap

I subheap fungono da riserve di memoria per le arene secondarie nelle applicazioni multithread, consentendo loro di crescere e gestire le proprie regioni di heap separatamente dall'heap principale. Ecco come i subheap differiscono dall'heap iniziale e come operano:

  1. Heap Iniziale vs. Subheap:

  • L'heap iniziale si trova direttamente dopo il binario del programma in memoria e si espande utilizzando la chiamata di sistema sbrk.

  • I subheap, utilizzati dalle arene secondarie, vengono creati tramite mmap, una chiamata di sistema che mappa una regione di memoria specificata.

  1. Riserva di Memoria con mmap:

  • Quando il gestore dell'heap crea un subheap, riserva un grande blocco di memoria tramite mmap. Questa riserva non alloca immediatamente memoria; semplicemente designa una regione che altri processi di sistema o allocazioni non dovrebbero utilizzare.

  • Per impostazione predefinita, la dimensione riservata per un subheap è di 1 MB per processi a 32 bit e di 64 MB per processi a 64 bit.

  1. Espansione Graduale con mprotect:

  • La regione di memoria riservata è inizialmente contrassegnata come PROT_NONE, indicando che il kernel non ha bisogno di allocare memoria fisica per questo spazio ancora.

  • Per "far crescere" il subheap, il gestore dell'heap utilizza mprotect per cambiare le autorizzazioni delle pagine da PROT_NONE a PROT_READ | PROT_WRITE, spingendo il kernel ad allocare memoria fisica agli indirizzi precedentemente riservati. Questo approccio step-by-step consente al subheap di espandersi secondo necessità.

  • Una volta esaurito l'intero subheap, il gestore dell'heap crea un nuovo subheap per continuare l'allocazione.

heap_info

Questa struttura alloca le informazioni rilevanti dell'heap. Inoltre, la memoria dell'heap potrebbe non essere continua dopo ulteriori allocazioni, questa struttura memorizzerà anche tali informazioni.

// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/malloc/arena.c#L837

typedef struct _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 mprotected
PROT_READ|PROT_WRITE.  */
size_t pagesize; /* Page size used when allocating the arena.  */
/* Make sure the following data is properly aligned, particularly
that sizeof (heap_info) + 2 * SIZE_SZ is a multiple of
MALLOC_ALIGNMENT. */
char pad[-3 * SIZE_SZ & MALLOC_ALIGN_MASK];
} heap_info;

malloc_state

Ogni heap (arena principale o arene di altri thread) ha una struttura malloc_state. È importante notare che la struttura malloc_state dell'arena principale è una variabile globale nella libc (quindi situata nello spazio di memoria della libc). Nel caso delle strutture malloc_state degli heap dei thread, sono situate all'interno del "heap" del thread stesso.

Ci sono alcune cose interessanti da notare da questa struttura (vedi codice C sotto):

  • __libc_lock_define (, mutex); Serve per assicurare che questa struttura dell'heap sia accessibile da 1 thread alla volta

  • Flags:

#define NONCONTIGUOUS_BIT (2U)

#define contiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) == 0) #define noncontiguous(M) (((M)->flags & NONCONTIGUOUS_BIT) != 0) #define set_noncontiguous(M) ((M)->flags |= NONCONTIGUOUS_BIT) #define set_contiguous(M) ((M)->flags &= ~NONCONTIGUOUS_BIT)

* Il `mchunkptr bins[NBINS * 2 - 2];` contiene **puntatori** ai **primi e ultimi chunk** dei **blocchi** piccoli, grandi e non ordinati (il -2 è perché l'indice 0 non viene utilizzato)
* Pertanto, il **primo chunk** di questi blocchi avrà un **puntatore all'indietro a questa struttura** e l'**ultimo chunk** di questi blocchi avrà un **puntatore in avanti** a questa struttura. Ciò significa fondamentalmente che se riesci a **rilevare questi indirizzi nell'arena principale** avrai un puntatore alla struttura nella **libc**.
* Le strutture `struct malloc_state *next;` e `struct malloc_state *next_free;` sono liste collegate di arene
* Il chunk `top` è l'ultimo "chunk", che è fondamentalmente **tutto lo spazio rimanente dell'heap**. Una volta che il chunk top è "vuoto", l'heap è completamente utilizzato e ha bisogno di richiedere più spazio.
* Il chunk `last reminder` proviene dai casi in cui non è disponibile un chunk di dimensioni esatte e quindi viene diviso un chunk più grande, una parte rimanente del puntatore viene posizionata qui.
```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

Questa struttura rappresenta un particolare blocco di memoria. I vari campi hanno significati diversi per i blocchi allocati e non allocati.

// https://github.com/bminor/glibc/blob/master/malloc/malloc.c
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;

Come già commentato in precedenza, questi chunk hanno anche alcuni metadati, molto ben rappresentati in questa immagine:

Di solito i metadati sono 0x08B, indicando la dimensione corrente del chunk utilizzando gli ultimi 3 bit per indicare:

  • A: Se è 1 proviene da un subheap, se è 0 è nell'arena principale

  • M: Se è 1, questo chunk fa parte di uno spazio allocato con mmap e non fa parte di un heap

  • P: Se è 1, il chunk precedente è in uso

Successivamente, lo spazio per i dati dell'utente e infine 0x08B per indicare la dimensione del chunk precedente quando il chunk è disponibile (o per memorizzare i dati dell'utente quando è allocato).

Inoltre, quando disponibili, i dati dell'utente vengono utilizzati per contenere anche alcuni dati:

  • fd: Puntatore al prossimo chunk

  • bk: Puntatore al chunk precedente

  • fd_nextsize: Puntatore al primo chunk nella lista più piccolo di se stesso

  • bk_nextsize: Puntatore al primo chunk nella lista più grande di se stesso

Nota come organizzare la lista in questo modo evita la necessità di avere un array in cui ogni singolo chunk viene registrato.

Puntatori dei Chunk

Quando viene utilizzato malloc, viene restituito un puntatore al contenuto che può essere scritto (subito dopo gli header), tuttavia, quando si gestiscono i chunk, è necessario un puntatore all'inizio degli header (metadati). Per queste conversioni vengono utilizzate queste funzioni:

// https://github.com/bminor/glibc/blob/master/malloc/malloc.c

/* Convert a chunk address to a user mem pointer without correcting the tag.  */
#define chunk2mem(p) ((void*)((char*)(p) + CHUNK_HDR_SZ))

/* Convert a user mem pointer to a chunk address and extract the right tag.  */
#define mem2chunk(mem) ((mchunkptr)tag_at (((char*)(mem) - CHUNK_HDR_SZ)))

/* The smallest possible chunk */
#define MIN_CHUNK_SIZE        (offsetof(struct malloc_chunk, fd_nextsize))

/* The smallest size we can malloc is an aligned minimal chunk */

#define MINSIZE  \
(unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))

Allineamento e dimensione minima

Il puntatore al chunk e 0x0f devono essere entrambi 0.

// From https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/sysdeps/generic/malloc-size.h#L61
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)

// https://github.com/bminor/glibc/blob/a07e000e82cb71238259e674529c37c12dc7d423/sysdeps/i386/malloc-alignment.h
#define MALLOC_ALIGNMENT 16


// https://github.com/bminor/glibc/blob/master/malloc/malloc.c
/* Check if m has acceptable alignment */
#define aligned_OK(m)  (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)

#define misaligned_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 constant
if passed a literal constant.  */
#define request2size(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 resulting
value is less than PTRDIFF_T.  Returns the requested size or
MINSIZE in case the value is less than MINSIZE, or 0 if any of the
previous checks fail.  */
static inline size_t
checked_request2size (size_t req) __nonnull (1)
{
if (__glibc_unlikely (req > PTRDIFF_MAX))
return 0;

/* When using tagged memory, we cannot share the end of the user
block with the header for the next chunk, so ensure that we
allocate blocks that are rounded up to the granule size.  Take
care not to overflow from close to MAX_SIZE_T to a small
number.  Ideally, this would be part of request2size(), but that
must be a macro that produces a compile time constant if passed
a 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);
}

return request2size (req);
}

Ottenere i dati del Chunk e modificare i metadati

Queste funzioni funzionano ricevendo un puntatore a un chunk e sono utili per controllare/impostare i metadati:

  • Controllare i flag del chunk

// 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 */
#define PREV_INUSE 0x1

/* extract inuse bit of previous chunk */
#define prev_inuse(p)       ((p)->mchunk_size & PREV_INUSE)


/* size field is or'ed with IS_MMAPPED if the chunk was obtained with mmap() */
#define IS_MMAPPED 0x2

/* check for mmap()'ed chunk */
#define chunk_is_mmapped(p) ((p)->mchunk_size & IS_MMAPPED)


/* size field is or'ed with NON_MAIN_ARENA if the chunk was obtained
from a non-main arena.  This is only set immediately before handing
the chunk to the user, if necessary.  */
#define NON_MAIN_ARENA 0x4

/* Check for chunk from main arena.  */
#define chunk_main_arena(p) (((p)->mchunk_size & NON_MAIN_ARENA) == 0)

/* Mark a chunk as not being on the main arena.  */
#define set_non_main_arena(p) ((p)->mchunk_size |= NON_MAIN_ARENA)
  • Dimensioni e puntatori ad altri chunk

/*
Bits to mask off when extracting size

Note: IS_MMAPPED is intentionally not masked off from size field in
macros for which mmapped chunks should never be seen. This should
cause helpful core dumps to occur if it is tried by accident by
people extending or adapting this malloc.
*/
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)

/* Get size, ignoring use bits */
#define chunksize(p) (chunksize_nomask (p) & ~(SIZE_BITS))

/* Like chunksize, but do not mask SIZE_BITS.  */
#define chunksize_nomask(p)         ((p)->mchunk_size)

/* Ptr to next physical malloc_chunk. */
#define next_chunk(p) ((mchunkptr) (((char *) (p)) + chunksize (p)))

/* Size of the chunk below P.  Only valid if !prev_inuse (P).  */
#define prev_size(p) ((p)->mchunk_prev_size)

/* Set the size of the chunk below P.  Only valid if !prev_inuse (P).  */
#define set_prev_size(p, sz) ((p)->mchunk_prev_size = (sz))

/* Ptr to previous physical malloc_chunk.  Only valid if !prev_inuse (P).  */
#define prev_chunk(p) ((mchunkptr) (((char *) (p)) - prev_size (p)))

/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s)  ((mchunkptr) (((char *) (p)) + (s)))
  • Iniettare bit

/* extract p's inuse bit */
#define inuse(p)							      \
((((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size) & PREV_INUSE)

/* set/clear chunk as being inuse without otherwise disturbing */
#define set_inuse(p)							      \
((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size |= PREV_INUSE

#define clear_inuse(p)							      \
((mchunkptr) (((char *) (p)) + chunksize (p)))->mchunk_size &= ~(PREV_INUSE)


/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s)					      \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size & PREV_INUSE)

#define set_inuse_bit_at_offset(p, s)					      \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size |= PREV_INUSE)

#define clear_inuse_bit_at_offset(p, s)					      \
(((mchunkptr) (((char *) (p)) + (s)))->mchunk_size &= ~(PREV_INUSE))
  • Imposta l'intestazione e il piè di pagina (quando i numeri di chunk sono in uso)

/* Set size at head, without disturbing its use bit */
#define set_head_size(p, s)  ((p)->mchunk_size = (((p)->mchunk_size & SIZE_BITS) | (s)))

/* Set size/use field */
#define set_head(p, s)       ((p)->mchunk_size = (s))

/* Set size at footer (only when chunk is not in use) */
#define set_foot(p, s)       (((mchunkptr) ((char *) (p) + (s)))->mchunk_prev_size = (s))
  • Ottenere la dimensione dei dati effettivamente utilizzabili all'interno del chunk

#pragma GCC poison mchunk_size
#pragma GCC poison mchunk_prev_size

/* This is the size of the real usable data in the chunk.  Not valid for
dumped heap chunks.  */
#define memsize(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 granule
size, 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;
}

Esempi

Esempio Veloce di Heap

Esempio veloce di heap da https://guyinatuxedo.github.io/25-heap/index.html ma in arm64:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void main(void)
{
char *ptr;
ptr = malloc(0x10);
strcpy(ptr, "panda");
}

Imposta un breakpoint alla fine della funzione principale e scopriamo dove le informazioni sono state memorizzate:

È possibile vedere che la stringa panda è stata memorizzata a 0xaaaaaaac12a0 (che è l'indirizzo restituito da malloc all'interno di x0). Controllando 0x10 byte prima, è possibile vedere che il 0x0 rappresenta che il chunk precedente non è in uso (lunghezza 0) e che la lunghezza di questo chunk è 0x21.

Gli spazi extra riservati (0x21-0x10=0x11) provengono dagli header aggiunti (0x10) e 0x1 non significa che sono stati riservati 0x21 byte ma gli ultimi 3 bit della lunghezza dell'header corrente hanno alcuni significati speciali. Poiché la lunghezza è sempre allineata su 16 byte (nelle macchine a 64 bit), questi bit non verranno mai utilizzati dal numero di lunghezza.

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

Esempio di Multithreading

Multithread

```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 1\n"); getchar(); ret = pthread_create(&t1, NULL, threadFuncMalloc, NULL); getchar();

printf("Before creating thread 2\n"); ret = pthread_create(&t1, NULL, threadFuncNoMalloc, NULL);

printf("Before exit\n"); getchar();

return 0; }

</details>

Debuggando l'esempio precedente è possibile vedere come all'inizio ci sia solo 1 arena:

<figure><img src="../../.gitbook/assets/image (1).png" alt=""><figcaption></figcaption></figure>

Poi, dopo aver chiamato il primo thread, quello che chiama malloc, viene creata una nuova arena:

<figure><img src="../../.gitbook/assets/image (1) (1).png" alt=""><figcaption></figcaption></figure>

e al suo interno è possibile trovare alcuni chunk:

<figure><img src="../../.gitbook/assets/image (2).png" alt=""><figcaption></figcaption></figure>

## Bins & Assegnazioni/Liberazioni di Memoria

Controlla quali sono i bins e come sono organizzati e come la memoria viene assegnata e liberata in:

<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>

## Controlli di Sicurezza delle Funzioni di Heap

Le funzioni coinvolte nell'heap eseguiranno determinati controlli prima di eseguire le loro azioni per cercare di assicurarsi che l'heap non sia stato corrotto:

<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>

## Riferimenti

* [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/)

Last updated