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:
cookleerecipe.asm.- Interpreta instrucciones custom.
- Pide un input.
- Transforma ese input.
- 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:
- Carga un byte desde un offset concreto.
- Lo escribe en
mem[CARBO]. - Incrementa
CARBO. - 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}
