XUT_ / MACHINES / CTF

0101

The Rust Dance

CTFMACHINENONEHARD
CTFTalent ArenaWindowsLinuxPythonNetworkReverse EngineeringRust

Introduction

Rust-Dance: The Virus That Dances

The Story

A new threat has emerged from the depths of the network. They call it Rust-Dance - a virus that doesn't just infect systems, it makes them dance.

Every file, every process, every byte... now moves to its rhythm.

Your mission: Find the unlock code that stops the dance. Reverse engineer the virus, understand its choreography, and break free.

The flag is definitely not "unlockcode123456789", so don't even try.


How to Run

Requirements

You may use one of the following:

  • Windows Subsystem for Linux (WSL)
  • Linux x86_64 system (native or VM)

Execution

bash
# Make the binary executable (if needed)
chmod +x rust-dance

# Run the challenge
./rust-dance

# The virus will prompt you for the unlock code
# Your goal: find the correct code to reveal the flag

Rules

  1. The flag format is: MWC{...}
  2. You must find the unlock code through reverse engineering
  3. Standard reversing tools are allowed (IDA, Ghidra, radare2, GDB, etc.)
  4. Have fun and learn!

Flag Submission

Once you find the flag, submit it on the CTF platform in the format:

text
MWC{hash}

Good luck, reverser. Stop the dance.

---------------------------------------------------------## Rust-Dance: El Virus Que Baila

La Historia

Una nueva amenaza ha surgido de las profundidades de la red. Lo llaman Rust-Dance: un virus que no solo infecta sistemas, sino que los hace bailar.

Cada archivo, cada proceso, cada byte... ahora se mueve a su ritmo.

Tu misión: encontrar el código de desbloqueo que detiene el baile. Aplica ingeniería inversa al virus, comprende su coreografía y libérate.

El indicador definitivamente no es "unlockcode123456789", así que ni lo intentes.


Cómo ejecutar

Requisitos

Puedes usar uno de los siguientes:

  • Subsistema de Windows para Linux (WSL)
  • Sistema Linux x86_64 (nativo o VM)

Ejecución

bash
# Crea el binario ejecutable (si es necesario)
chmod +x rust-dance

# Ejecuta el desafío
./rust-dance

# El virus te pedirá el código de desbloqueo
# Tu objetivo: encontrar el código correcto para revelar la bandera

Reglas

  1. El formato de la bandera es: MWC{...}
  2. Debes encontrar el código de desbloqueo mediante ingeniería inversa
  3. Se permiten herramientas de reversión estándar (IDA, Ghidra, radare2, GDB, etc.)
  4. ¡Diviértete y aprende!

Envío de la bandera

Una vez que encuentres la bandera, envíala a la plataforma CTF con el siguiente formato:

text
MWC{hash}

¡Buena suerte, reversor! ¡Para el bailar!

The Rust Dance

  1. Empezamos comprobando el tipo de archivo que tenemos en frente. reverse-engineering
  2. Ahora vamos a ejecutar el código una vez para ver su comportamiento. reverse-engineering Parece que debemos de introducir el código correcto para que el programa nos devuelva la flag.
  3. Vamos a hacer una serie de comprobaciones para ver qué strings hay dentro del código.
    1. Empezamos investigando las strings con strings ./rust-dance. reverse-engineering Pero hay demasiadas. reverse-engineering
    2. Así que seremos un poco más selectivos. reverse-engineering Aquí vemos algunas coincidencias, pero (aunque hay más) no vemos nada muy interesante.
    3. Vamos a observar trazas dinámicas, para ello usamos strace y filtramos después. reverse-engineering reverse-engineering Como strace no revela comparaciones en libc, el siguiente paso es usar ltrace para ver si memcmp/strcmp/bcmp existen. Sin embargo, nos ayuda a conocer el punto en el que introducir un breakpoint más adelante.
    4. Usamos ltrace. reverse-engineering Parece que no hay ninguna llamada típica por lo que parece que NO hay una comprobación directa entre dos strings. Lo más probable es que la validación la haga en el código propio sin acceder a más valores de memoria.
  4. Vamos a descompilar el binario con ghidra.
    1. Abrimos el archivo en ghidra y lo analizamos. reverse-engineering
    2. Buscamos la string de Unlock code. reverse-engineering reverse-engineering Esto nos lleva a la función que lo imprime.
    3. Si hacemos doble click sobre la referencia de función que lo imprime: reverse-engineering Nos lleva aquí: reverse-engineering Donde hay otras dos referencias: reverse-engineering Hacemos doble click en la primera
    4. Se nos cargará la función en el Decompile (a la derecha) y después de buscar un rato encontramos el bucle que realiza operaciones contra nuestro string. Primero se valida si el input tiene 19 (0x13) caracteres exactamente. Si es así comienza las transformaciones. reverse-engineering reverse-engineering
  5. Son más de 700 lineas de operaciones, vueltas y comprobaciones, es demasiado para un humano con poco tiempo, por lo que se lo pasaremos a nuestro mejor amigo: ChatGPT.
  6. Le pasamos todo el contenido de .rodata a ChatGPT. reverse-engineering
  7. Con el contenido de .rodata y el bucle de comprobación, la IA nos devuelve un código que al ejecutar puede obtener el "Unlock code".
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# -------------------------
# Helpers (rotaciones)
# -------------------------
def rol8(x, r):
    r &= 7
    return ((x << r) | (x >> (8 - r))) & 0xFF

def ror8(x, r):
    r &= 7
    return ((x >> r) | (x << (8 - r))) & 0xFF

def rol4(x):
    # swap nibbles (su propio inverso)
    return ((x << 4) | (x >> 4)) & 0xFF

# -------------------------
# Constantes (sacadas de .rodata, según tu dump)
# -------------------------
KEY_00103CF0 = [
    0x87, 0x9e, 0xb5, 0xcc, 0xe3, 0xfa, 0x11, 0x28,
    0x3f, 0x56, 0x6d, 0x84, 0x9b, 0xb2, 0xc9, 0xe0
]

ADD_00103D10 = [
    0x33, 0xCC, 0x35, 0xCA, 0x37, 0xC8, 0x39, 0xC6,
    0x3B, 0xC4, 0x3D, 0xC2, 0x3F, 0xC0, 0x41, 0xBE
]

# DWords objetivo tras el nibble-swap (porque el OR da 0 => cada XOR debe ser 0)
# buf[0:4]  == 0x4694ac0e
# buf[4:8]  == 0x72be6fc6
# buf[8:12] == 0xee795db7
# buf[12:16]== 0xa2edd0e2
TARGET16_AFTER_NIBBLE = [
    0x0e, 0xac, 0x94, 0x46,
    0xc6, 0x6f, 0xbe, 0x72,
    0xb7, 0x5d, 0x79, 0xee,
    0xe2, 0xd0, 0xed, 0xa2
]

# Bytes finales tras rol4 (lo que compara el if)
POST16_18_ROL4 = [0xC6, 0x28, 0x8A]

# -------------------------
# Shift exacto del bucle (según el ensamblado)
# -------------------------
def shift_even(i, c):
    # ECX = floor(i/7)
    q7 = i // 7
    # EDX = ((((i - q7) >> 1) + q7) >> 2)
    edx = (((i - q7) >> 1) + q7) >> 2
    # R9D = edx*7
    r9 = edx * 7
    # CL = c - r9  (y luego rotate)
    return (c - r9) & 7

def shift_odd(i, c):
    # en ensamblado: (floor(i/5) * 5) y luego c - eso
    q5 = i // 5
    edx = q5 * 5
    return (c - edx) & 7

def invert_rotation(buf19_after):
    """
    Invierte el bucle que rota 19 bytes (i=0..18, c=1..19).
    """
    b = buf19_after[:]
    for i in range(18, -1, -1):
        c = i + 1
        sh = shift_even(i, c) if (i & 1) == 0 else shift_odd(i, c)
        if (i & 1) == 0:
            # forward: ROL -> inverse: ROR
            b[i] = ror8(b[i], sh)
        else:
            # forward: ROR -> inverse: ROL
            b[i] = rol8(b[i], sh)
    return b

# -------------------------
# Inversión total
# -------------------------
def recover_unlock():
    # (1) Invertir nibble-swap de los 16 primeros bytes
    after_padd = [rol4(x) for x in TARGET16_AFTER_NIBBLE]

    # (2) Invertir PADDB (restar vector ADD_00103D10)
    after_rotate_0_15 = [(b - k) & 0xFF for b, k in zip(after_padd, ADD_00103D10)]

    # (3) Invertir bytes 16..18:
    #     forward: b += [0x43,0xBC,0x45] ; rol4(b)
    #     inverse: ror4(b) ; b -= [0x43,0xBC,0x45]
    pre_add = [rol4(x) for x in POST16_18_ROL4]  # inverse rol4 == rol4
    b16 = (pre_add[0] - 0x43) & 0xFF
    b17 = (pre_add[1] - 0xBC) & 0xFF
    b18 = (pre_add[2] - 0x45) & 0xFF

    buf_after_rotation = after_rotate_0_15 + [b16, b17, b18]

    # (4) Invertir el bucle de rotaciones para obtener el buffer justo después del shuffle inicial
    buf_pre_loop = invert_rotation(buf_after_rotation)

    # (5) Invertir el “shuffle SSE”:
    #     En ensamblado realmente es: out[0..15] = reverse( (in[3..18] ^ KEY) )
    out0_15 = buf_pre_loop[:16]
    in_3_18_xored = list(reversed(out0_15))
    in_3_18 = [b ^ k for b, k in zip(in_3_18_xored, KEY_00103CF0)]

    # (6) Recuperar in[0..2] desde buf[16..18]:
    #     buf[16] = in[2]^0x70, buf[17] = in[1]^0x59, buf[18]=in[0]^0x42
    in2 = buf_pre_loop[16] ^ 0x70
    in1 = buf_pre_loop[17] ^ 0x59
    in0 = buf_pre_loop[18] ^ 0x42

    unlock = bytes([in0, in1, in2] + in_3_18)
    return unlock

if __name__ == "__main__":
    code = recover_unlock()
    print(code.decode("latin1"))
  1. Si ahora ejecutamos dicho código: reverse-engineering
  2. Introducimos dicho código en el programa. reverse-engineering