Writeup StarHack CTF: Desafío - Chaos

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:

Terminal window
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 stripped

El 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:

Terminal window
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.0

Vemos 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.

Terminal window
$ strings chaos | grep flag
flag.txt
Error: Could not read flag. Contact admin.
print_flag

Análisis con Ghidra#

Luego uso ghidra para analizar el binario más a fondo.

NOTE

Ya 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í:

cedev-1
/
Nix-CTF
Waiting for api.github.com...
00K
0K
0K
Waiting...

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.

Primera cosa interesante, vemos que hay una función print_banner() que se muestra al inicio del programa.

banner

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ácter va a la posición 3 del bytecode descifrado
  • el segundo carácter a la posición 10
  • el tercer carácter a 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:

  • x para examinar la memoria
  • /23 para leer 23 bytes
  • xb para mostrar en hexadecimal (byte por byte)
  • &encrypted_stage2 es la dirección de la variable.

gdb-encrypted-stage2

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/bash
encrypted="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))
done
echo

xor-decrypt

Análisis del bytecode descifrado#

Mirando este bytecode descifrado notamos un patrón claro:

Terminal window
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#

Terminal window
echo "B3p" | nc 15.237.107.187 1029

flag

¡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.

Writeup StarHack CTF: Desafío - Chaos
https://blog.ce-dev.eu/posts/es/writeup-challenge-chaos-ctf/
Author
Cedev
Published at
2025-10-17
License
CC BY-NC-SA 4.0