malloc & sysmalloc

Aprende y practica Hacking en AWS: HackTricks Training AWS Red Team Expert (ARTE) Aprende y practica Hacking en GCP: HackTricks Training GCP Red Team Expert (GRTE)

Apoya a HackTricks

Resumen del Orden de Asignación

(No se explican todas las comprobaciones en este resumen y algunos casos se han omitido por brevedad)

  1. __libc_malloc intenta obtener un fragmento de la tcache, si no lo consigue llama a _int_malloc

  2. _int_malloc:

    1. Intenta generar el área si no existe ninguna

    2. Si hay algún fragmento de fast bin del tamaño correcto, lo utiliza

      1. Llena la tcache con otros fragmentos rápidos

    3. Si hay algún fragmento de small bin del tamaño correcto, lo utiliza

      1. Llena la tcache con otros fragmentos de ese tamaño

    4. Si el tamaño solicitado no es para small bins, consolida el fast bin en unsorted bin

    5. Comprueba el unsorted bin, utiliza el primer fragmento con suficiente espacio

      1. Si el fragmento encontrado es más grande, divídelo para devolver una parte y añadir el resto de nuevo al unsorted bin

      2. Si un fragmento es del mismo tamaño que el solicitado, úsalo para llenar la tcache en lugar de devolverlo (hasta que la tcache esté llena, luego devuelve el siguiente)

      3. Por cada fragmento de tamaño más pequeño comprobado, colócalo en su respectivo small o large bin

    6. Comprueba el large bin en el índice del tamaño solicitado

      1. Comienza a buscar desde el primer fragmento que sea más grande que el tamaño solicitado, si se encuentra alguno, devuélvelo y añade los restos al small bin

    7. Comprueba los large bins desde los índices siguientes hasta el final

      1. Desde el siguiente índice más grande, busca cualquier fragmento, divide el primer fragmento encontrado para usarlo en el tamaño solicitado y añade el resto al unsorted bin

    8. Si no se encuentra nada en los bins anteriores, obtén un fragmento del chunk superior

    9. Si el chunk superior no era lo suficientemente grande, agrándalo con sysmalloc

__libc_malloc

La función malloc en realidad llama a __libc_malloc. Esta función comprobará la tcache para ver si hay algún fragmento disponible del tamaño deseado. Si lo hay, lo utilizará y si no, comprobará si es un solo hilo y en ese caso llamará a _int_malloc en la arena principal, y si no, llamará a _int_malloc en la arena del hilo.

Código de __libc_malloc

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

#if IS_IN (libc) void * __libc_malloc (size_t bytes) { mstate ar_ptr; void *victim;

_Static_assert (PTRDIFF_MAX <= SIZE_MAX / 2, "PTRDIFF_MAX is not more than half of SIZE_MAX");

if (!__malloc_initialized) ptmalloc_init (); #if USE_TCACHE /* int_free also calls request2size, be careful to not pad twice. */ size_t tbytes = checked_request2size (bytes); if (tbytes == 0) { __set_errno (ENOMEM); return NULL; } size_t tc_idx = csize2tidx (tbytes);

MAYBE_INIT_TCACHE ();

DIAG_PUSH_NEEDS_COMMENT; if (tc_idx < mp_.tcache_bins && tcache != NULL && tcache->counts[tc_idx] > 0) { victim = tcache_get (tc_idx); return tag_new_usable (victim); } DIAG_POP_NEEDS_COMMENT; #endif

if (SINGLE_THREAD_P) { victim = tag_new_usable (_int_malloc (&main_arena, bytes)); assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || &main_arena == arena_for_chunk (mem2chunk (victim))); return victim; }

arena_get (ar_ptr, bytes);

victim = _int_malloc (ar_ptr, bytes); /* Retry with another arena only if we were able to find a usable arena before. */ if (!victim && ar_ptr != NULL) { LIBC_PROBE (memory_malloc_retry, 1, bytes); ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); }

if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex);

victim = tag_new_usable (victim);

assert (!victim || chunk_is_mmapped (mem2chunk (victim)) || ar_ptr == arena_for_chunk (mem2chunk (victim))); return victim; }

</details>

Observa cómo siempre etiquetará el puntero devuelto con `tag_new_usable`, según el código:
```c
void *tag_new_usable (void *ptr)

Allocate a new random color and use it to color the user region of
a chunk; this may include data from the subsequent chunk's header
if tagging is sufficiently fine grained.  Returns PTR suitably
recolored for accessing the memory there.

_int_malloc

Esta es la función que asigna memoria utilizando los otros bins y el fragmento superior.

  • Comienzo

Comienza definiendo algunas variables y obteniendo el tamaño real que necesita tener el espacio de memoria solicitado:

Fast Bin

Si el tamaño necesario está dentro de los tamaños de Fast Bins, intenta usar un fragmento de la fast bin. Básicamente, basado en el tamaño, encontrará el índice de fast bin donde deberían estar ubicados los fragmentos válidos, y si hay alguno, devolverá uno de esos. Además, si tcache está habilitado, llenará la tcache bin de ese tamaño con fast bins.

Mientras se realizan estas acciones, se ejecutan algunas comprobaciones de seguridad aquí:

  • Si el fragmento no está alineado: malloc(): fragmento fastbin no alineado detectado 2

  • Si el fragmento siguiente no está alineado: malloc(): fragmento fastbin no alineado detectado

  • Si el fragmento devuelto tiene un tamaño incorrecto debido a su índice en la fast bin: malloc(): corrupción de memoria (fast)

  • Si algún fragmento utilizado para llenar la tcache no está alineado: malloc(): fragmento fastbin no alineado detectado 3

malloc_consolidate

Si no era un fragmento pequeño, es un fragmento grande, y en este caso se llama a malloc_consolidate para evitar la fragmentación de memoria.

Bin desordenado

Es hora de revisar el bin desordenado en busca de un fragmento válido para usar.

Inicio

Esto comienza con un gran bucle que recorrerá el bin desordenado en la dirección bk hasta que llegue al final (la estructura de arena) con while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))

Además, se realizan algunas comprobaciones de seguridad cada vez que se considera un nuevo fragmento:

  • Si el tamaño del fragmento es extraño (demasiado pequeño o demasiado grande): malloc(): tamaño no válido (desordenado)

  • Si el tamaño del fragmento siguiente es extraño (demasiado pequeño o demasiado grande): malloc(): tamaño siguiente no válido (desordenado)

  • Si el tamaño previo indicado por el fragmento siguiente difiere del tamaño del fragmento: malloc(): tamaño previo no coincidente (desordenado)

  • Si no victim->bck->fd == victim o no victim->fd == av (arena): malloc(): lista doble enlazada desordenada

  • Como siempre estamos revisando el último, su fd debería apuntar siempre a la estructura de arena.

  • Si el fragmento siguiente no indica que el anterior está en uso: malloc(): previo no válido->prev_inuse (desordenado)

Si esto fue exitoso, devuelve el fragmento y se acabó, si no, continúa ejecutando la función...

si es de igual tamaño

Continúa eliminando el fragmento del bin, en caso de que el tamaño solicitado sea exactamente el del fragmento:

  • Si el tcache no está lleno, agréguelo al tcache y continúe indicando que hay un fragmento de tcache que podría ser utilizado

  • Si el tcache está lleno, simplemente úsalo devolviéndolo

Límites de _int_malloc

En este punto, si algún fragmento estaba almacenado en la tcache y se puede usar y se alcanza el límite, simplemente devuelve un fragmento de la tcache.

Además, si se alcanza MAX_ITERS, sal del bucle y obtén un fragmento de una manera diferente (fragmento superior).

Si return_cached estaba configurado, simplemente devuelve un fragmento de la tcache para evitar búsquedas más largas.

Si no se encuentra un fragmento adecuado para esto, continúa

Contenedor grande (siguiente más grande)

Si en el contenedor grande exacto no hay ningún fragmento que se pueda usar, comienza a recorrer todos los contenedores grandes siguientes (comenzando por el inmediatamente más grande) hasta que se encuentre uno (si hay alguno).

El recordatorio del fragmento dividido se agrega en el contenedor no ordenado, last_reminder se actualiza y se realiza la misma verificación de seguridad:

  • bck->fd-> bk != bck: malloc(): corrupted unsorted chunks2

sysmalloc

Inicio de sysmalloc

Si la arena es nula o el tamaño solicitado es demasiado grande (y aún quedan mmaps permitidos), utiliza sysmalloc_mmap para asignar espacio y devolverlo.

// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L2531

/*
sysmalloc handles malloc cases requiring more memory from the system.
On entry, it is assumed that av->top does not have enough
space to service request for nb bytes, thus requiring that av->top
be extended or replaced.
*/

static void *
sysmalloc (INTERNAL_SIZE_T nb, mstate av)
{
mchunkptr old_top;              /* incoming value of av->top */
INTERNAL_SIZE_T old_size;       /* its size */
char *old_end;                  /* its end address */

long size;                      /* arg to first MORECORE or mmap call */
char *brk;                      /* return value from MORECORE */

long correction;                /* arg to 2nd MORECORE call */
char *snd_brk;                  /* 2nd return val */

INTERNAL_SIZE_T front_misalign; /* unusable bytes at front of new space */
INTERNAL_SIZE_T end_misalign;   /* partial page left at end of new space */
char *aligned_brk;              /* aligned offset into brk */

mchunkptr p;                    /* the allocated/returned chunk */
mchunkptr remainder;            /* remainder from allocation */
unsigned long remainder_size;   /* its size */


size_t pagesize = GLRO (dl_pagesize);
bool tried_mmap = false;


/*
If have mmap, and the request size meets the mmap threshold, and
the system supports mmap, and there are few enough currently
allocated mmapped regions, try to directly map this request
rather than expanding top.
*/

if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm;
if (mp_.hp_pagesize > 0 && nb >= mp_.hp_pagesize)
{
/* There is no need to issue the THP madvise call if Huge Pages are
used directly.  */
mm = sysmalloc_mmap (nb, mp_.hp_pagesize, mp_.hp_flags, av);
if (mm != MAP_FAILED)
return mm;
}
mm = sysmalloc_mmap (nb, pagesize, 0, av);
if (mm != MAP_FAILED)
return mm;
tried_mmap = true;
}

/* There are no usable arenas and mmap also failed.  */
if (av == NULL)
return 0;

Verificaciones de sysmalloc

Comienza obteniendo información del fragmento superior antiguo y verificando que algunas de las siguientes condiciones sean verdaderas:

  • El tamaño del montón antiguo es 0 (nuevo montón)

  • El tamaño del montón anterior es mayor que MINSIZE y el fragmento superior antiguo está en uso

  • El montón está alineado al tamaño de página (0x1000, por lo que los 12 bits inferiores deben ser 0)

Luego también verifica que:

  • El tamaño antiguo no tiene suficiente espacio para crear un fragmento del tamaño solicitado

Verificaciones de sysmalloc

```c /* Record incoming configuration of top */

old_top = av->top; old_size = chunksize (old_top); old_end = (char *) (chunk_at_offset (old_top, old_size));

brk = snd_brk = (char *) (MORECORE_FAILURE);

/* If not the first time through, we require old_size to be at least MINSIZE and to have prev_inuse set. */

assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0));

/* Precondition: not enough current space to satisfy nb request */ assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

</details>

### sysmalloc no en la arena principal

Primero intentará **extender** el montón anterior para este montón. Si no es posible, intentará **asignar un nuevo montón** y actualizar los punteros para poder usarlo.\
Finalmente, si eso no funcionó, intentará llamar a **`sysmalloc_mmap`**.&#x20;

<details>

<summary>sysmalloc no en la arena principal</summary>
```c
if (av != &main_arena)
{
heap_info *old_heap, *heap;
size_t old_heap_size;

/* First try to extend the current heap. */
old_heap = heap_for_ptr (old_top);
old_heap_size = old_heap->size;
if ((long) (MINSIZE + nb - old_size) > 0
&& grow_heap (old_heap, MINSIZE + nb - old_size) == 0)
{
av->system_mem += old_heap->size - old_heap_size;
set_head (old_top, (((char *) old_heap + old_heap->size) - (char *) old_top)
| PREV_INUSE);
}
else if ((heap = new_heap (nb + (MINSIZE + sizeof (*heap)), mp_.top_pad)))
{
/* Use a newly allocated heap.  */
heap->ar_ptr = av;
heap->prev = old_heap;
av->system_mem += heap->size;
/* Set up the new top.  */
top (av) = chunk_at_offset (heap, sizeof (*heap));
set_head (top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);

/* Setup fencepost and free the old top chunk with a multiple of
MALLOC_ALIGNMENT in size. */
/* The fencepost takes at least MINSIZE bytes, because it might
become the top chunk again later.  Note that a footer is set
up, too, although the chunk is marked in use. */
old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
set_head (chunk_at_offset (old_top, old_size + CHUNK_HDR_SZ),
0 | PREV_INUSE);
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size),
CHUNK_HDR_SZ | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), CHUNK_HDR_SZ);
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);
}
else
{
set_head (old_top, (old_size + CHUNK_HDR_SZ) | PREV_INUSE);
set_foot (old_top, (old_size + CHUNK_HDR_SZ));
}
}
else if (!tried_mmap)
{
/* We can at least try to use to mmap memory.  If new_heap fails
it is unlikely that trying to allocate huge pages will
succeed.  */
char *mm = sysmalloc_mmap (nb, pagesize, 0, av);
if (mm != MAP_FAILED)
return mm;
}
}

sysmalloc main arena

Comienza calculando la cantidad de memoria necesaria. Comenzará solicitando memoria contigua para poder utilizar la memoria antigua no utilizada. También se realizan algunas operaciones de alineación.

sysmalloc main arena

```c // From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L2665C1-L2713C10

else /* av == main_arena */

{ /* Request enough space for nb + pad + overhead */ size = nb + mp_.top_pad + MINSIZE;

/* If contiguous, we can subtract out existing space that we hope to combine with new space. We add it back later only if we don't actually get contiguous space. */

if (contiguous (av)) size -= old_size;

/* Round to a multiple of page size or huge page size. If MORECORE is not contiguous, this ensures that we only call it with whole-page arguments. And if MORECORE is contiguous and this is not first time through, this preserves page-alignment of previous calls. Otherwise, we correct to page-align below. */

#ifdef MADV_HUGEPAGE /* Defined in brk.c. */ extern void *__curbrk; if (_glibc_unlikely (mp.thp_pagesize != 0)) { uintptr_t top = ALIGN_UP ((uintptr_t) _curbrk + size, mp.thp_pagesize); size = top - (uintptr_t) __curbrk; } else #endif size = ALIGN_UP (size, GLRO(dl_pagesize));

/* Don't try to call MORECORE if argument is so big as to appear negative. Note that since mmap takes size_t arg, it may succeed below even if we cannot call MORECORE. */

if (size > 0) { brk = (char *) (MORECORE (size)); if (brk != (char *) (MORECORE_FAILURE)) madvise_thp (brk, size); LIBC_PROBE (memory_sbrk_more, 2, brk, size); }

### Error anterior en la arena principal de `sysmalloc` 1

Si el error anterior devolvió `MORECORE_FAILURE`, intenta nuevamente asignar memoria usando `sysmalloc_mmap_fallback`
```c
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L2715C7-L2740C10

if (brk == (char *) (MORECORE_FAILURE))
{
/*
If have mmap, try using it as a backup when MORECORE fails or
cannot be used. This is worth doing on systems that have "holes" in
address space, so sbrk cannot extend to give contiguous space, but
space is available elsewhere.  Note that we ignore mmap max count
and threshold limits, since the space will not be used as a
segregated mmap region.
*/

char *mbrk = MAP_FAILED;
if (mp_.hp_pagesize > 0)
mbrk = sysmalloc_mmap_fallback (&size, nb, old_size,
mp_.hp_pagesize, mp_.hp_pagesize,
mp_.hp_flags, av);
if (mbrk == MAP_FAILED)
mbrk = sysmalloc_mmap_fallback (&size, nb, old_size, MMAP_AS_MORECORE_SIZE,
pagesize, 0, av);
if (mbrk != MAP_FAILED)
{
/* We do not need, and cannot use, another sbrk call to find end */
brk = mbrk;
snd_brk = brk + size;
}
}

Continuar con la arena principal de sysmalloc

Si lo anterior no devolvió MORECORE_FAILURE, si funcionó, crear algunos alineamientos:

Error anterior de la arena principal de sysmalloc 2

```c // From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L2742

if (brk != (char *) (MORECORE_FAILURE)) { if (mp_.sbrk_base == 0) mp_.sbrk_base = brk; av->system_mem += size;

/* If MORECORE extends previous space, we can likewise extend top size. */

if (brk == old_end && snd_brk == (char *) (MORECORE_FAILURE)) set_head (old_top, (size + old_size) | PREV_INUSE);

else if (contiguous (av) && old_size && brk < old_end) /* Oops! Someone else killed our space.. Can't touch anything. */ malloc_printerr ("break adjusted to free malloc space");

/* Otherwise, make adjustments:

  • If the first time through or noncontiguous, we need to call sbrk just to find out where the end of memory lies.

  • We need to ensure that all returned chunks from malloc will meet MALLOC_ALIGNMENT

  • If there was an intervening foreign sbrk, we need to adjust sbrk request size to account for fact that we will not be able to combine new space with existing space in old_top.

  • Almost all systems internally allocate whole pages at a time, in which case we might as well use the whole last page of request. So we allocate enough more memory to hit a page boundary now, which in turn causes future contiguous calls to page-align. */

else { front_misalign = 0; end_misalign = 0; correction = 0; aligned_brk = brk;

/* handle contiguous cases / if (contiguous (av)) { / Count foreign sbrk as system_mem. */ if (old_size) av->system_mem += brk - old_end;

/* Guarantee alignment of first new chunk made from this space */

front_misalign = (INTERNAL_SIZE_T) chunk2mem (brk) & MALLOC_ALIGN_MASK; if (front_misalign > 0) { /* Skip over some bytes to arrive at an aligned position. We don't need to specially mark these wasted front bytes. They will never be accessed anyway because prev_inuse of av->top (and any chunk created from its start) is always true after initialization. */

correction = MALLOC_ALIGNMENT - front_misalign; aligned_brk += correction; }

/* If this isn't adjacent to existing space, then we will not be able to merge with old_top space, so must add to 2nd request. */

correction += old_size;

/* Extend the end address to hit a page boundary */ end_misalign = (INTERNAL_SIZE_T) (brk + size + correction); correction += (ALIGN_UP (end_misalign, pagesize)) - end_misalign;

assert (correction >= 0); snd_brk = (char *) (MORECORE (correction));

/* If can't allocate correction, try to at least find out current brk. It might be enough to proceed without failing.

Note that if second sbrk did NOT fail, we assume that space is contiguous with first sbrk. This is a safe assumption unless program is multithreaded but doesn't use locks and a foreign sbrk occurred between our first and second calls. */

if (snd_brk == (char *) (MORECORE_FAILURE)) { correction = 0; snd_brk = (char *) (MORECORE (0)); } else madvise_thp (snd_brk, correction); }

/* handle non-contiguous cases / else { if (MALLOC_ALIGNMENT == CHUNK_HDR_SZ) / MORECORE/mmap must correctly align / assert (((unsigned long) chunk2mem (brk) & MALLOC_ALIGN_MASK) == 0); else { front_misalign = (INTERNAL_SIZE_T) chunk2mem (brk) & MALLOC_ALIGN_MASK; if (front_misalign > 0) { / Skip over some bytes to arrive at an aligned position. We don't need to specially mark these wasted front bytes. They will never be accessed anyway because prev_inuse of av->top (and any chunk created from its start) is always true after initialization. */

aligned_brk += MALLOC_ALIGNMENT - front_misalign; } }

/* Find out current end of memory */ if (snd_brk == (char *) (MORECORE_FAILURE)) { snd_brk = (char *) (MORECORE (0)); } }

/* Adjust top based on results of second sbrk */ if (snd_brk != (char *) (MORECORE_FAILURE)) { av->top = (mchunkptr) aligned_brk; set_head (av->top, (snd_brk - aligned_brk + correction) | PREV_INUSE); av->system_mem += correction;

/* If not the first time through, we either have a gap due to foreign sbrk or a non-contiguous region. Insert a double fencepost at old_top to prevent consolidation with space we don't own. These fenceposts are artificial chunks that are marked as inuse and are in any case too small to use. We need two to make sizes and alignments work out. */

if (old_size != 0) { /* Shrink old_top to insert fenceposts, keeping size a multiple of MALLOC_ALIGNMENT. We know there is at least enough space in old_top to do this. */ old_size = (old_size - 2 * CHUNK_HDR_SZ) & ~MALLOC_ALIGN_MASK; set_head (old_top, old_size | PREV_INUSE);

/* Note that the following assignments completely overwrite old_top when old_size was previously MINSIZE. This is intentional. We need the fencepost, even if old_top otherwise gets lost. */ set_head (chunk_at_offset (old_top, old_size), CHUNK_HDR_SZ | PREV_INUSE); set_head (chunk_at_offset (old_top, old_size + CHUNK_HDR_SZ), CHUNK_HDR_SZ | PREV_INUSE);

/* If possible, release the rest. / if (old_size >= MINSIZE) { _int_free (av, old_top, 1); } } } } } } / if (av != &main_arena) */

</details>

### sysmalloc final

Finaliza la asignación actualizando la información del área.
```c
// From https://github.com/bminor/glibc/blob/f942a732d37a96217ef828116ebe64a644db18d7/malloc/malloc.c#L2921C3-L2943C12

if ((unsigned long) av->system_mem > (unsigned long) (av->max_system_mem))
av->max_system_mem = av->system_mem;
check_malloc_state (av);

/* finally, do the allocation */
p = av->top;
size = chunksize (p);

/* check that one of the above allocation paths succeeded */
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (p, nb);
av->top = remainder;
set_head (p, nb | PREV_INUSE | (av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, p, nb);
return chunk2mem (p);
}

/* catch all failure paths */
__set_errno (ENOMEM);
return 0;

sysmalloc_mmap

Last updated