.. | ||
heap-memory-functions | ||
use-after-free | ||
bins-and-memory-allocations.md | ||
double-free.md | ||
fast-bin-attack.md | ||
heap-overflow.md | ||
house-of-einherjar.md | ||
house-of-force.md | ||
house-of-lore.md | ||
house-of-orange.md | ||
house-of-rabbit.md | ||
house-of-roman.md | ||
house-of-spirit.md | ||
large-bin-attack.md | ||
off-by-one-overflow.md | ||
overwriting-a-freed-chunk.md | ||
README.md | ||
tcache-bin-attack.md | ||
unlink-attack.md | ||
unsorted-bin-attack.md |
Libc Heap
Heap Basics
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 daPROT_NONE
aPROT_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#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 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- 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 **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.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 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 principaleM
: Se 1, questo chunk è parte di uno spazio allocato con mmap e non parte di un heapP
: 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 successivobk
: Puntatore al chunk precedentefd_nextsize
: Puntatore al primo chunk nella lista che è più piccolo di se stessobk_nextsize
: Puntatore al primo chunk nella lista che è più grande di se stesso
{% hint style="info" %} Nota come collegare la lista in questo modo previene la necessità di avere un array dove ogni singolo chunk è registrato. {% endhint %}
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. */
#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 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);
}
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 */
#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)))
- Insue 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 dei 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))
- Ottieni la dimensione dei dati utilizzabili reali 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 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 <sys/types.h>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) (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:
{% content-ref url="bins-and-memory-allocations.md" %}
[bins-and-memory-allocations.md](bins-and-memory-allocations.md)
{% endcontent-ref %}
## 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:
{% content-ref url="heap-memory-functions/heap-functions-security-checks.md" %}
[heap-functions-security-checks.md](heap-memory-functions/heap-functions-security-checks.md)
{% endcontent-ref %}
## 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/)