Writeup StarHack CTF: Desafío - Chaos
Participé en StarHack CTF 2025, y aquí está el write-up del desafío “Chaos” que resolví.
Este desafío estaba en la categoría “Binary”; se nos pidió explotar un binario para obtener una flag en el formato flag{...}.
Teníamos acceso al binario Linux compilado y una sesión netcat para ejecutar el binario en los servidores del CTF.
Análisis del binario
Primer paso, miré la información básica sobre el binario con el comando file chaos para obtener alguna información:
chaos: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=e2937d93ac55bc47823fba02aa98e4b407ea01a3, for GNU/Linux 3.2.0, not strippedEl binario es un ELF de 64 bits para Linux (genial porque estoy en NixOS ❄️), no stripped, lo cual es una buena noticia porque podemos tener los nombres de las funciones.
Verifico rápidamente con el comando strings si hay cadenas interesantes:
Enter password:Error reading input.Password too short.Executing VM...Invalid password.···Welcome to the multi-stage VM challenge!Can you decrypt and execute the hidden bytecode?Patching bytecode with input...:*3$"UGCC: (Ubuntu 11.4.0-1ubuntu1~22.04.2) 11.4.0Vemos que es un desafío de Máquina Virtual (VM), donde debemos descifrar y ejecutar bytecode oculto. El programa solicita una contraseña, y hay mensajes de error para la lectura de la entrada y si la contraseña es demasiado corta.
Si busco referencias para “flag”, encontramos notablemente la función print_flag y el archivo flag.txt.
$ strings chaos | grep flagflag.txtError: Could not read flag. Contact admin.print_flagAnálisis con Ghidra
Luego uso ghidra para analizar el binario más a fondo.
NOTEYa no hago muchos CTF y no domino necesariamente todas las herramientas.
ghidra, junto con otras herramientas, son parte de lo esencial para los desafíos CTF, por eso creé un pequeño nix-shell con herramientas básicas para todas las categorías. Puedes encontrarlo aquí:
Como vimos anteriormente con el comando file, el binario no está stripped, así que tenemos los nombres de las funciones, lo cual es súper conveniente.
Encuentro directamente la función main en el menú Functions, que es el punto de entrada del programa con la lógica principal.
undefined8 main(void) { char *pcVar1; size_t sVar2; void *__ptr; long lVar3; undefined8 uVar4; char local_488 [64]; undefined1 local_448 [1024]; undefined4 local_48; void *local_40; undefined4 local_38; undefined1 *local_30; int local_28;
setbuf(stdout,(char *)0x0); setbuf(stdin,(char *)0x0); setbuf(stderr,(char *)0x0); print_banner(); __printf_chk(1,"Enter password: "); pcVar1 = fgets(local_488,0x40,stdin); if (pcVar1 == (char *)0x0) { puts("Error reading input."); uVar4 = 1; } else { sVar2 = strcspn(local_488,"\n"); local_488[sVar2] = '\0'; sVar2 = strlen(local_488); if (sVar2 < 3) { puts("Password too short."); uVar4 = 1; } else { __ptr = malloc(0x17); lVar3 = 0; do { *(byte *)((long)__ptr + lVar3) = (&encrypted_stage2)[lVar3] ^ 0xaa; lVar3 = lVar3 + 1; } while (lVar3 != 0x17);
puts("Patching bytecode with input...");
*(char *)((long)__ptr + 3) = local_488[0]; *(char *)((long)__ptr + 10) = local_488[1]; *(char *)((long)__ptr + 0x11) = local_488[2];
puts("Executing VM...\n");
local_48 = 0; local_38 = 0; local_28 = 0; local_40 = __ptr; local_30 = local_488;
vm_execute(local_448);
if (local_28 == 0) { puts("Invalid password."); } free(__ptr); uVar4 = 0; } } return uVar4;}Así que ahora intentaremos entender la lógica del programa y seguir el flujo de ejecución.
print_banner()
Primera cosa interesante, vemos que hay una función print_banner() que se muestra al inicio del programa.

Notamos alguna información:
- es una VM de múltiples etapas
- debemos descifrar y ejecutar el bytecode oculto
El código es relativamente simple:
void print_banner(void)
{ puts(&DAT_004020f8); puts(&DAT_004021a8); puts(&DAT_004021e0); putchar(10); puts("Welcome to the multi-stage VM challenge!"); puts("Can you decrypt and execute the hidden bytecode?"); putchar(10); return;}Contraseña
Así que inicio clásico, inicialización del búfer, visualización del banner, luego solicita una contraseña.
pcVar1 = fgets(local_488,0x40,stdin); if (pcVar1 == (char *)0x0) { puts("Error reading input."); uVar4 = 1; }La entrada se lee con fgets y se muestra un error si falla.
Luego se vuelve más interesante, verificamos la longitud de la contraseña:
sVar2 = strcspn(local_488,"\n"); local_488[sVar2] = '\0'; sVar2 = strlen(local_488); if (sVar2 < 3) { puts("Password too short."); uVar4 = 1; }Así que la contraseña debe tener al menos 3 caracteres (tendremos eso en cuenta por ahora).
Luego asignamos 23 bytes (0x17 en hexadecimal) — no estoy seguro de por qué aún.
else { __ptr = malloc(0x17);Y aquí llegamos a la parte interesante, hacemos un bucle sobre 23 (23 bytes) y para cada byte hacemos XOR con 0xaa de un array encrypted_stage2.
así que:
- hay datos cifrados en algún lugar del binario (encrypted_stage2)
- los desciframos con XOR 0xaa
- el resultado va a la memoria asignada
¡Aún más interesante!
puts("Patching bytecode with input...");
*(char *)((long)__ptr + 3) = local_488[0]; *(char *)((long)__ptr + 10) = local_488[1]; *(char *)((long)__ptr + 0x11) = local_488[2];- el
primer carácterva a la posición 3 del bytecode descifrado - el
segundo caráctera la posición 10 - el
tercer caráctera la posición 17 (0x11 en hexadecimal)
¡Así que mi contraseña modifica directamente el código que se ejecutará!
puts("Executing VM...\n");
local_48 = 0; local_38 = 0; local_28 = 0; local_40 = __ptr; local_30 = local_488;
vm_execute(local_448);Inicializamos algunas variables, luego llamamos a vm_execute que ejecutará el bytecode parcheado.
if (local_28 == 0) { puts("Invalid password."); }Otra cosa interesante, si local_28 es igual a 0 después de la ejecución del bytecode, mostramos “Invalid password”.
encrypted_stage2
Ahora que entiendo el mecanismo, necesito encontrar este encrypted_stage2.
encrypted_stage2 se define como una variable global.
Podemos usar gdb para extraer estos bytes directamente de la memoria del programa.
en gdb, podemos hacer:
x/23xb &encrypted_stage2
pequeñas explicaciones:
xpara examinar la memoria/23para leer 23 bytesxbpara mostrar en hexadecimal (byte por byte)&encrypted_stage2es la dirección de la variable.

Aquí está la salida, así que en la columna de la izquierda tenemos las direcciones de memoria y luego a la derecha los 23 bytes cifrados.
Ahora que extraje los datos cifrados, debo descifrarlos con XOR 0xAA como en el código.
Aquí tienes un pequeño script bash para hacer eso:
#!/bin/bashencrypted="ab e8 ab aa af ac a5 ab 99 ab aa af ac a2 ab da ab aa af ac ab ad 55"
echo "$encrypted" | tr ' ' '\n' | while read byte; do printf "%02x " $((0x$byte ^ 0xaa))doneecho
Análisis del bytecode descifrado
Mirando este bytecode descifrado notamos un patrón claro:
01 42 01 00 05 06 ...... 01 33 01 00 05 06 ...... 01 70 01 00 ...Las posiciones que el programa parchea (3, 10, y 17) son inicialmente 0x00.
Patrón repetitivo: vemos la secuencia 01 XX 01 00 05 06 que se repite 3 veces.
La lógica de la VM parece ser una comparación. Justo antes de cada 0x00, encontramos los bytes 0x42, 0x33 y 0x70.
- En la posición 1, tenemos 0x42. La VM probablemente espera este valor en la posición 3.
- En la posición 8, tenemos 0x33. La VM probablemente espera este valor en la posición 10.
- En la posición 15, tenemos 0x70. La VM probablemente espera este valor en la posición 17.
La contraseña correcta debe estar compuesta por los caracteres ASCII correspondientes a estos valores hexadecimales:
- 0x42 = ‘B’
- 0x33 = ‘3’
- 0x70 = ‘p’
La contraseña es B3p.
Prueba de la solución
echo "B3p" | nc 15.237.107.187 1029
¡Obtenemos la flag!
Conclusión
Aquí está el write-up para este desafío, encontré la flag. Para otros desafíos de StarHack CTF 2025 que resolví, podría publicarlos si tengo tiempo.
Terminé 24º en la clasificación general, ¡nada mal para un regreso al CTF!
Si quieres practicar en este desafío, la instancia pública ya no está disponible, ¡pero aún puedo proporcionar el binario!
Si tienes preguntas o sugerencias, no dudes en contactarme.