L'heap è fondamentalmente il luogo dove un programma può 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 dove il binario viene caricato in memoria (controlla la sezione [heap]):
Basic Chunk Allocation
Quando alcuni dati vengono richiesti per essere memorizzati nell'heap, viene allocato uno spazio nell'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 saranno riservati per il chunk. L'obiettivo è riservare la minima memoria possibile senza rendere complicato trovare dove si trova ciascun chunk. A questo scopo, vengono utilizzate le informazioni sui chunk di metadata 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 disponibile abbastanza grande da soddisfare la richiesta, verrà utilizzato.
Questo potrebbe anche significare che parte del chunk disponibile verrà utilizzato 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 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 poi utilizza questa memoria per generare il nuovo chunk.
Se tutto fallisce, malloc restituisce null.
Nota che se la memoria richiesta supera una soglia, mmap verrà utilizzato per mappare la memoria richiesta.
Arenas
Nelle applicazioni multithreaded, il gestore dell'heap deve prevenire race conditions che potrebbero portare a crash. Inizialmente, questo veniva fatto utilizzando un mutex globale per garantire che solo un thread potesse accedere all'heap alla volta, ma questo causava problemi di prestazioni a causa del collo di bottiglia indotto dal mutex.
Per affrontare questo problema, l'allocatore di heap ptmalloc2 ha introdotto "arenas", dove ogni arena funge da heap separato con le proprie strutture di dati e mutex, consentendo a più thread di eseguire operazioni sull'heap senza interferire tra loro, purché utilizzino arene diverse.
L'arena "principale" predefinita gestisce le operazioni sull'heap per applicazioni a thread singolo. 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 non utilizzata, creando nuove arene 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 potenziale contesa.
A differenza dell'arena principale, che si espande utilizzando la chiamata di sistema brk, le arene secondarie creano "subheaps" utilizzando mmap e mprotect per simulare il comportamento dell'heap, consentendo flessibilità nella gestione della memoria per operazioni multithreaded.
Subheaps
I subheaps servono come riserve di memoria per le arene secondarie nelle applicazioni multithreaded, consentendo loro di crescere e gestire le proprie regioni heap separatamente dall'heap principale. Ecco come i subheaps differiscono dall'heap iniziale e come operano:
Heap Iniziale vs. Subheaps:
L'heap iniziale si trova direttamente dopo il binario del programma in memoria e si espande utilizzando la chiamata di sistema sbrk.
I subheaps, utilizzati dalle arene secondarie, vengono creati tramite mmap, una chiamata di sistema che mappa una regione di memoria specificata.
Riservazione di Memoria con mmap:
Quando il gestore dell'heap crea un subheap, riserva un grande blocco di memoria tramite mmap. Questa riservazione non alloca immediatamente memoria; designa semplicemente 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 64 MB per processi a 64 bit.
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 "crescere" il subheap, il gestore dell'heap utilizza mprotect per cambiare i permessi delle pagine da PROT_NONE a PROT_READ | PROT_WRITE, invitando il kernel ad allocare memoria fisica agli indirizzi precedentemente riservati. Questo approccio graduale 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 struct alloca informazioni rilevanti dell'heap. Inoltre, la memoria heap potrebbe non essere continua dopo ulteriori allocazioni, questa struct memorizzerà anche tali informazioni.
// 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
Ogni heap (arena principale o altre arene dei thread) ha una struttura malloc_state.
È importante notare che la struttura malloc_state dell'arena principale è una variabile globale nella libc (quindi si trova nello spazio di memoria della libc).
Nel caso delle strutture malloc_state degli heap dei thread, esse si trovano all'interno del "heap" del proprio thread.
Ci sono alcune cose interessanti da notare da questa struttura (vedi il codice C qui sotto):
__libc_lock_define (, mutex); È presente per garantire che questa struttura dell'heap sia accessibile da 1 thread alla volta
* Il `mchunkptr bins[NBINS * 2 - 2];` contiene **puntatori** ai **primi e ultimi chunk** dei **bins** piccoli, grandi e non ordinati (il -2 è perché l'indice 0 non è utilizzato)
* Pertanto, il **primo chunk** di questi bins avrà un **puntatore all'indietro a questa struttura** e l'**ultimo chunk** di questi bins avrà un **puntatore in avanti** a questa struttura. Questo significa fondamentalmente che se puoi l**eakare 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 deve richiedere più spazio.
* Il chunk `last reminder` proviene da casi in cui un chunk di dimensione esatta non è disponibile e quindi un chunk più grande viene diviso, una parte rimanente del puntatore è 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.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;
Come commentato in precedenza, questi chunk hanno anche alcuni metadati, molto bene rappresentati in questa immagine:
I metadati sono solitamente 0x08B che indicano la dimensione attuale 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 è parte di uno spazio allocato con mmap e non parte di un heap
P: Se 1, il chunk precedente è in uso
Poi, 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 disponibile, i dati dell'utente vengono utilizzati per contenere anche alcuni dati:
fd: Puntatore al chunk successivo
bk: Puntatore al chunk precedente
fd_nextsize: Puntatore al primo chunk nella lista che è più piccolo di se stesso
bk_nextsize: Puntatore al primo chunk nella lista che è più grande di se stesso
Nota come collegare la lista in questo modo previene la necessità di avere un array dove ogni singolo chunk è registrato.
Puntatori Chunk
Quando viene utilizzato malloc, viene restituito un puntatore al contenuto che può essere scritto (subito dopo le intestazioni), tuttavia, quando si gestiscono i chunk, è necessario un puntatore all'inizio delle intestazioni (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. */#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))
Allineamento e dimensione minima
Il puntatore al chunk e 0x0f devono essere 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);}
Nota che per calcolare lo spazio totale necessario viene aggiunto SIZE_SZ solo 1 volta perché il campo prev_size può essere utilizzato per memorizzare dati, quindi è necessario solo l'intestazione iniziale.
Ottieni dati del chunk e altera i metadati
Queste funzioni funzionano ricevendo un puntatore a un chunk e sono utili per controllare/impostare i metadati:
Controlla 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 */#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)
Dimensioni e puntatori ad altri chunk
/*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)))
Imposta l'intestazione e il piè di pagina (quando i numeri dei chunk sono in uso)
/* 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))
Ottieni la dimensione dei dati utilizzabili reali all'interno del chunk
#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;}
Imposta un breakpoint alla fine della funzione principale e scopriamo dove sono state memorizzate le informazioni:
È possibile vedere che la stringa panda è stata memorizzata a 0xaaaaaaac12a0 (che era l'indirizzo fornito come risposta da malloc all'interno di x0). Controllando 0x10 byte prima, è possibile vedere che il 0x0 rappresenta che il chunk precedente non è utilizzato (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 è stato riservato 0x21B, ma gli ultimi 3 bit della lunghezza dell'attuale intestazione hanno alcuni significati speciali. Poiché la lunghezza è sempre allineata a 16 byte (in macchine a 64 bit), questi bit in realtà 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 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) (1) (1) (1) (1) (1) (1) (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) (1) (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
e al suo interno si possono trovare alcuni chunk:
<figure><img src="../../.gitbook/assets/image (2) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
## Bins & Allocazioni/Frees di Memoria
Controlla quali sono i bins e come sono organizzati e come la memoria viene allocata 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 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/)