Post

HTB Hexecution

HTB Hexecution

Descripción

Hexecution es un challenge de Reversing de HackTheBox, dificultad Hard, centrado en el análisis de un intérprete custom y una pseudo-ISA escrita en un archivo de texto. A primera vista parece un reto de assembly por el nombre recipe.asm, pero realmente el binario cook actúa como una máquina virtual que parsea instrucciones inventadas con nombres como BOIL, AES256, SPELL, WINDOW y PEPEFROG.

El flujo del reto consiste en entender cómo esa VM inicializa memoria, pide el input del usuario, aplica varias permutaciones sobre los bytes introducidos y finalmente compara el resultado contra una constante generada por la propia receta. La dificultad está en no caer en las distracciones del naming: AES256 no implementa AES, recipe.asm no es ensamblador nativo y el binario ELF solo sirve como intérprete del lenguaje.

Durante el análisis se combinan varias técnicas típicas de reversing: inspección de strings, desensamblado del ELF, reconstrucción de semántica de instrucciones, seguimiento de memoria y modelado de transformaciones en Python para invertir el algoritmo. No hay explotación ni fuerza bruta; la solución sale de comprender exactamente qué bytes se mueven, en qué orden y contra qué buffer se comparan.

El reto entrega dos archivos:

1
file cook recipe.asm
1
2
cook:       ELF 64-bit LSB pie executable, x86-64, dynamically linked, stripped
recipe.asm: ASCII text

La idea principal es que recipe.asm no es assembly real de x86. Es un lenguaje propio, con instrucciones disfrazadas de cocina, interpretado por el binario cook.

El objetivo no es explotar memoria ni romper criptografía. Hay que entender la máquina virtual, reconstruir las transformaciones aplicadas al input y resolver la comparación final.


Reconocimiento inicial

Archivos del reto

1
ls -l
1
2
-rwxr-xr-x 1 vr0px vr0px 14496 cook
-rw-r--r-- 1 vr0px vr0px  4527 recipe.asm

Comprobamos el binario:

1
file cook
1
cook: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, stripped

El binario está stripped, por lo que no tenemos símbolos útiles. Aun así, las strings dan bastante contexto:

1
strings -a -tx cook
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2008 VEGETABLE
2012 FRUIT
2018 MEAT
201d DAIRY
2023 PROTEIN
202b CARBO
2073 BOIL
2078 ROAST
207e AES256
2085 QUICKMAFFS
2090 SPELL
209c GRIND
20ac GOODBYE
20b4 WINDOW
20bb LADDER
20c2 PEPEFROG
20d0 Nice! The flag is HTB{YOUR_INPUT} :)
20f5 CHAIR

Esto confirma que cook interpreta instrucciones del archivo recipe.asm.


Ejecución del programa

Al ejecutar el binario con el archivo de receta:

1
./cook ./recipe.asm

Vemos que pide una flag:

1
Enter the flag:

Probando cualquier valor falla:

1
printf 'test\n' | ./cook ./recipe.asm
1
Enter the flag: Wrong!

Por tanto el flujo es:

  1. cook lee recipe.asm.
  2. Interpreta instrucciones custom.
  3. Pide un input.
  4. Transforma ese input.
  5. Compara el resultado contra una constante interna generada por la receta.

Análisis de recipe.asm

El inicio del archivo contiene bytes ASCII escritos con la instrucción AES256:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BOIL CARBO, 0x0
AES256 0x45
AES256 0x6e
AES256 0x74
AES256 0x65
AES256 0x72
AES256 0x20
AES256 0x74
AES256 0x68
AES256 0x65
AES256 0x20
AES256 0x66
AES256 0x6c
AES256 0x61
AES256 0x67
AES256 0x3a
AES256 0x20

Pasando esos valores hexadecimales a ASCII:

1
45 6e 74 65 72 20 74 68 65 20 66 6c 61 67 3a 20

Obtenemos:

1
Enter the flag:

Esto ya nos da una primera pista: AES256 no cifra nada. Solo escribe bytes en memoria.

Más abajo aparece otra secuencia importante:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
BOIL CARBO, 0x40
AES256 0x35
AES256 0x6d
AES256 0x61
AES256 0x4e
AES256 0x63
AES256 0x49
AES256 0x34
AES256 0x5f
AES256 0x5f
AES256 0x55
AES256 0x31
AES256 0x30
AES256 0x5f
AES256 0x64
AES256 0x65
AES256 0x35
AES256 0x4c
AES256 0x31
AES256 0x33
AES256 0x5f
AES256 0x4d
AES256 0x6e
AES256 0x34
AES256 0x55
AES256 0x30
AES256 0x75
AES256 0x34
AES256 0x74
AES256 0x72
AES256 0x66
AES256 0x6e
AES256 0x5f

La constante en ASCII es:

1
5maNcI4__U10_de5L13_Mn4U0u4trfn_

Esta cadena no es la flag directa. Es el resultado esperado después de reordenar el input.


Semántica de la VM

Revirtiendo el intérprete con objdump, podemos resumir las instrucciones relevantes así:

Instrucción Comportamiento
BOIL REG, value Asigna un valor de 16 bits a un registro.
ROAST A, B Hace XOR entre registros. En la receta se usa para poner registros a cero.
AES256 value Escribe un byte en memoria usando CARBO + cursor y aumenta el cursor interno.
SPELL 1 Imprime bytes de memoria entre offsets controlados por registros.
SPELL 0 Lee el input del usuario y lo copia a memoria.
QUICKMAFFS offset Carga mem[offset] en el registro PROTEIN.
GOODBYE A, B Copia el valor del registro B al registro A.
WINDOW REG Escribe el byte bajo de REG en mem[CARBO].
LADDER REG Incrementa el registro.
GRIND A, B Compara dos registros. Si no coinciden, imprime Wrong!.
PEPEFROG A, B Compara 32 bytes desde mem[A] y mem[B].

Los registros disponibles son:

1
2
3
4
5
6
VEGETABLE
FRUIT
MEAT
DAIRY
PROTEIN
CARBO

Un detalle importante es que AES256 usa un cursor interno que no se reinicia cuando cambia CARBO. Esto explica por qué la constante escrita después del input acaba siendo comparada más adelante desde otra zona de memoria.


Restricción de longitud

Después de leer el input, la receta hace esto:

1
2
3
QUICKMAFFS 0x36
BOIL VEGETABLE, 0x20
GRIND PROTEIN, VEGETABLE

SPELL 0 guarda la longitud del input en mem[CARBO + 0x22].

En este punto CARBO vale 0x14, por lo que la longitud queda guardada en:

1
0x14 + 0x22 = 0x36

Luego:

1
QUICKMAFFS 0x36

Carga esa longitud en PROTEIN, y:

1
2
BOIL VEGETABLE, 0x20
GRIND PROTEIN, VEGETABLE

Comprueba que la longitud sea 0x20, es decir, 32 caracteres.

La flag final tendrá esta forma:

1
HTB{<32_caracteres>}

Primera transformación

La receta trabaja sobre el input almacenado desde mem[0x14] hasta mem[0x33].

Para cada bloque de 4 bytes aplica esta lógica:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
QUICKMAFFS 0x14
GOODBYE VEGETABLE, PROTEIN
QUICKMAFFS 0x15
GOODBYE FRUIT, PROTEIN
QUICKMAFFS 0x16
GOODBYE MEAT, PROTEIN
QUICKMAFFS 0x17
GOODBYE DAIRY, PROTEIN

BOIL CARBO, 0x14
WINDOW MEAT
BOIL CARBO, 0x15
WINDOW FRUIT
BOIL CARBO, 0x16
WINDOW DAIRY
BOIL CARBO, 0x17
WINDOW VEGETABLE

Si llamamos al bloque original:

1
[a, b, c, d]

La receta lo convierte en:

1
[c, b, d, a]

Esta transformación se repite para los 8 bloques de 4 bytes que forman los 32 caracteres del input.

En Python, el equivalente sería:

1
2
3
4
5
for g in range(0, 32, 4):
    transformed[g + 0] = original[g + 2]
    transformed[g + 1] = original[g + 1]
    transformed[g + 2] = original[g + 3]
    transformed[g + 3] = original[g + 0]

Segunda transformación

Después de la transformación por bloques, la receta crea otro buffer desde mem[0x42].

El patrón es:

1
2
3
4
5
6
7
8
9
10
BOIL CARBO, 0x42
QUICKMAFFS 0x14
WINDOW PROTEIN
LADDER CARBO
QUICKMAFFS 0x19
WINDOW PROTEIN
LADDER CARBO
QUICKMAFFS 0x1e
WINDOW PROTEIN
LADDER CARBO

Esto significa:

  1. Carga un byte desde un offset concreto.
  2. Lo escribe en mem[CARBO].
  3. Incrementa CARBO.
  4. Repite hasta construir 32 bytes.

La tabla de offsets usada por la receta es:

1
2
3
4
5
6
7
8
9
10
seq = [
    0x14, 0x19, 0x1e, 0x23,
    0x17, 0x1a, 0x1d, 0x20,
    0x18, 0x15, 0x16, 0x1b,
    0x1c, 0x21, 0x22, 0x1f,
    0x24, 0x29, 0x2e, 0x33,
    0x27, 0x2a, 0x2d, 0x30,
    0x28, 0x25, 0x26, 0x2b,
    0x2c, 0x31, 0x32, 0x2f,
]

Como el input transformado empieza en 0x14, podemos normalizar esos offsets restando 0x14:

1
pos = offset - 0x14

Comparación final

Al final de la receta aparece:

1
2
3
BOIL PROTEIN, 0x42
BOIL CARBO, 0x70
PEPEFROG PROTEIN, CARBO

PEPEFROG compara 32 bytes:

1
mem[0x42 : 0x42 + 32] == mem[0x70 : 0x70 + 32]

La zona 0x42 contiene nuestro input después de las dos permutaciones.

La zona 0x70 contiene la constante esperada:

1
5maNcI4__U10_de5L13_Mn4U0u4trfn_

Por tanto, el problema se reduce a invertir las dos permutaciones.


Solver

Este solver reconstruye el input correcto a partir de la constante esperada.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#!/usr/bin/env python3

expected = b"5maNcI4__U10_de5L13_Mn4U0u4trfn_"

seq = [
    0x14, 0x19, 0x1e, 0x23,
    0x17, 0x1a, 0x1d, 0x20,
    0x18, 0x15, 0x16, 0x1b,
    0x1c, 0x21, 0x22, 0x1f,
    0x24, 0x29, 0x2e, 0x33,
    0x27, 0x2a, 0x2d, 0x30,
    0x28, 0x25, 0x26, 0x2b,
    0x2c, 0x31, 0x32, 0x2f,
]

# Invertimos la segunda transformación:
# mem[0x42 + k] = transformed[seq[k] - 0x14]
transformed = [None] * 32
for k, off in enumerate(seq):
    transformed[off - 0x14] = expected[k]

# Invertimos la primera transformación por bloques.
# Forward: [a, b, c, d] -> [c, b, d, a]
original = [None] * 32
for g in range(0, 32, 4):
    original[g + 2] = transformed[g + 0]
    original[g + 1] = transformed[g + 1]
    original[g + 3] = transformed[g + 2]
    original[g + 0] = transformed[g + 3]

flag_input = bytes(original).decode()
print("HTB{" + flag_input + "}")

Ejecución:

1
python3 solve.py
1
HTB{cU510m_I54_aNd_eMuL4t10n_4r3_fUn}

Verificación

Probamos el input obtenido contra el binario real:

1
printf '%s\n' 'cU510m_I54_aNd_eMuL4t10n_4r3_fUn' | ./cook ./recipe.asm

Resultado:

1
Enter the flag: Nice! The flag is HTB{YOUR_INPUT} :)

El programa no imprime la flag completa, sino el placeholder HTB{YOUR_INPUT}. La verificación correcta es que el input pasa la comparación final y llega al mensaje Nice!.


Flag

1
HTB{cU510m_I54_aNd_eMuL4t10n_4r3_fUn}
This post is licensed under CC BY 4.0 by the author.