Si usas doble-free en C tu programa ya es hackeable XD.
George trabaja en explotar una vulnerabilidad crítica en font config, un programa que vive en todos los sistemas Linux y genera cachés de fuentes. El CVE específico permite crear ataques de double-free modificando archivos de caché corrupto. Lo fundamental es que los offsets en estos archivos no están validados adecuadamente, lo que permite pasar punteros arbitrarios. El ataque puede bypasear ASLR (Address Space Layout Randomization) porque si los datos están en un archivo mapeado en memoria, los offsets relativos se mantienen constantes incluso cuando los espacios de memoria son aleatorizados.
Mientras intenta construir un exploit, George reflexiona sobre cómo malloc usa diferentes estrategias: para asignaciones grandes usa mmap, pero para pequeñas usa brk. Esto es importante porque cambia cómo se puede explotar la memoria. El problema real de lograr el double-free es que necesita apuntar dos objetos a la misma estructura de valor en el caché, pero todo está mapeado en memoria, lo que complica el ataque. No está claro si puede obtener suficiente control en memoria heap para hacer lo que necesita, especialmente considerando mitigaciones modernas.
Un double-free (doble liberación) es una vulnerabilidad de seguridad y un error de programación que ocurre cuando un programa intenta liberar la misma dirección de memoria dinámica dos veces seguidas mediante la función free(), sin que haya habido una asignación (malloc, calloc, etc.) intermedia.
En el lenguaje C, la gestión de memoria es manual. Cuando liberas memoria, el gestor de la memoria dinámica (el heap) actualiza sus estructuras internas para saber qué bloques están vacíos y listos para ser reutilizados. Si le dices que libere el mismo bloque dos veces, rompes esas estructuras internas.
Este es el escenario de libro de texto en C:
#include <stdio.h>
#include <stdlib.h>
int main() {
// 1. Reservamos memoria para un puntero
int *ptr = (int *)malloc(sizeof(int) * 10);
if (ptr == NULL) return 1;
// ... Se usa la memoria aquí ...
// 2. Primera liberación (Correcto)
free(ptr);
// ... Pasan muchas líneas de código intermedio ...
// 3. Segunda liberación (¡ERROR - Double Free!)
// El programa intenta liberar 'ptr' de nuevo, pero esa memoria ya no le pertenece.
free(ptr);
return 0;
}
¿Por qué es peligroso? El mecanismo interno
Cuando el gestor de memoria (como glibc en Linux) procesa un free(), a menudo mete ese bloque de memoria en una lista enlazada interna (llamada fastbins o tcache en Linux) para poder entregarla rápido si el programa pide más memoria en el futuro.
Si haces un double-free, puedes lograr que el gestor de memoria añada el mismo bloque dos veces a la lista de disponibles.
Esto provoca un caos estructural en el heap:
Si el programa pide memoria de nuevo, recibirá el bloque A.
Si el programa (o un atacante) escribe datos en A, estará sobreescribiendo los punteros internos de la lista enlazada del propio gestor de memoria que aún apuntan a A.
La siguiente vez que el programa pida memoria, el gestor leerá el puntero manipulado y le entregará al programa una dirección de memoria arbitraria elegida por el atacante (por ejemplo, donde se guardan variables de control de flujo o direcciones de retorno).
A partir de ahí, el atacante puede lograr Ejecución Remota de Código (RCE).
Ejemplo en estructuras de datos (Más realista)
En programas reales, el double-free rara vez es tan obvio como dos free() seguidos. Suele ocurrir debido a malas lógicas en el manejo de errores o alias de punteros (dos variables apuntando al mismo sitio).
#include <stdlib.h>
#include <string.h>
typedef struct {
char *nombre;
int ID;
} Usuario;
void limpiar_usuario(Usuario *u) {
if (u->nombre != NULL) {
free(u->nombre); // Primera liberación
}
free(u);
}
int main() {
Usuario *usr1 = (Usuario *)malloc(sizeof(Usuario));
usr1->nombre = (char *)malloc(20);
strcpy(usr1->nombre, "George");
// Creas un alias: usr2 apunta exactamente a la misma memoria de usr1
Usuario *usr2 = usr1;
// Se limpia usr1 (libera usr1->nombre y usr1)
limpiar_usuario(usr1);
// ... Más adelante en el código, por error se intenta limpiar usr2 ...
// Esto causará un Double-Free sobre la memoria de 'nombre' y del struct
limpiar_usuario(usr2);
return 0;
}
¿Cómo se previene?
La regla de oro y defensa más simple en C es neutralizar el puntero inmediatamente después de liberarlo, asignándole NULL.
En C, la función free(NULL) está garantizada por el estándar para no hacer absolutamente nada (es una operación segura).
free(ptr);
ptr = NULL; // El puntero ya no apunta a nada peligroso
// ... Más adelante ...
free(ptr); // Esto es equivalente a free(NULL), es completamente seguro y no rompe el programa
En sistemas modernos de Linux, glibc incluye mecanismos de protección severos. Si el gestor de memoria detecta una anomalía obvia de double-free, abortará inmediatamente la ejecución del programa lanzando un error del tipo double free or corruption (fasttop) para evitar que la vulnerabilidad sea explotada.
Las reflexiones de George tocan uno de los pilares más complejos de la explotación del heap (memoria dinámica) en Linux: el comportamiento bifásico de malloc y cómo las mitigaciones modernas rompen las técnicas de ataque clásicas.
Aquí desglosamos técnicamente por qué la diferencia entre brk y mmap cambia por completo las reglas del juego para un exploit, y las barreras que imponen los sistemas actuales:
El gestor de memoria de la librería de C (glibc malloc) utiliza dos llamadas al sistema distintas para pedir memoria al kernel, dependiendo del tamaño solicitado (controlado por un umbral dinámico que por defecto suele rondar los 128 KB o 256 KB):
El área brk (Asignaciones Pequeñas/Medianas)
Para bloques pequeños, malloc hace crecer el heap principal moviendo el puntero de ruptura (brk/sbrk).
Estructura: Todos los bloques se colocan de forma contigua en una gran región de memoria.
Comportamiento: Cuando liberas un bloque aquí con free(), la memoria no se devuelve al sistema operativo de inmediato. Se guarda en listas internas de reciclaje (fastbins, tcache, unsorted bins) para ser reutilizada rápidamente.
Impacto en Explotación: Al ser memoria compartida y contigua, un desbordamiento o un double-free permite corromper los metadatos de los bloques vecinos o manipular las listas de reciclaje para engañar a malloc.
El área mmap (Asignaciones Grandes)
Para bloques que superan el umbral, malloc solicita páginas de memoria completamente independientes usando mmap.
Estructura: Cada asignación crea una región de memoria aislada, mapeada en una zona distinta del espacio de direcciones del proceso.
Comportamiento: Cuando haces free() de un bloque creado por mmap, glibc ejecuta inmediatamente munmap, devolviendo la memoria al sistema operativo. Las páginas desaparecen.
Impacto en Explotación: Si intentas hacer un double-free sobre un bloque manejado por mmap, el segundo free() resultará casi instantáneamente en un Crash de segmentación (SIGSEGV) porque el programa está intentando operar sobre una dirección de memoria que ya no existe en su mapa de páginas. No hay listas de reciclaje que corromper aquí.
En el escenario que describe George, si fontconfig mapea o procesa archivos grandes mediante mmap (o xmmap), los objetos quedan aislados en páginas específicas de memoria.
Para lograr que un double-free funcione y permita apuntar dos objetos a la misma estructura, el atacante se enfrenta a un dilema de control:
Falta de continuidad: No puedes usar un desbordamiento lineal simple para saltar de un bloque brk a una página mmap.
Sincronización del ciclo de vida: Para abusar de las estructuras de caché compartidas, se necesita que el programa libere un bloque, mantenga una referencia huérfana (Dangling Pointer), y luego permita que otro componente asigne un objeto idéntico en esa misma dirección. Si las páginas se desmapean constantemente debido a munmap, esa ventana de oportunidad se cierra.
Incluso si George logra forzar que las asignaciones caigan dentro del rango de brk para aprovechar las listas de reciclaje del heap, las versiones modernas de glibc (particularmente desde la introducción y endurecimiento de tcache) cuentan con protecciones severas:
Tcache Double-Free Detection: Las versiones recientes de glibc guardan una clave de verificación (key) en los bloques libres. Si intentas liberar un bloque que ya tiene esa clave, el sistema detecta el double-free inmediatamente y aborta el programa.
Safe Linking: Se introdujo una mitigación que aplica una operación XOR (ofuscación) a los punteros de las listas enlazadas del heap, utilizando un valor derivado de ASLR. Esto significa que un atacante ya no puede simplemente sobreescribir un puntero con una dirección arbitraria, ya que al ser desofuscado por malloc, resultará en una dirección inválida y el programa caerá.
Por esta razón, la explotación moderna del heap a menudo requiere una vulnerabilidad secundaria (como una fuga de información o Information Leak) que permita leer la memoria primero para calcular las direcciones aleatorizadas y las claves de ofuscación antes de poder armar el exploit funcional.