Datos del reto
| Campo | Valor |
|---|---|
| CTF | Nullcon CTF 2026 |
| Reto | 💯 (unicode magic) |
| Categoría | Misc |
| Flag | ENO{EM0J1S_UN1COD3_1S_MAG1C} |
Descripción del reto
El único archivo entregado era un README.md que, a simple vista, parecía contener solo un emoji: 💯. El truco del reto estaba en que ese emoji no venía solo; detrás llevaba caracteres Unicode invisibles que escondían el mensaje real.
Reconocimiento
Cuando un archivo "no tiene nada" pero claramente debería esconder algo, revisar los codepoints suele ser un buen primer paso. En este caso, al leer el contenido en UTF-8 y mostrar ord() de cada carácter aparecían valores en los rangos U+FE00..U+FE0F y U+E0100..U+E01EF.
Esos rangos corresponden a Variation Selectors, caracteres que normalmente no se ven al mostrar el texto pero que siguen estando presentes en el archivo. El emoji hacía de base y los selectores que venían después eran los que llevaban la información.
Análisis
El esquema del reto era directo: cada Variation Selector representaba un valor entre 0 y 255, y esos valores se interpretaban como bytes ASCII.
La conversión que hacía falta era:
U+FE00..U+FE0F->0..15U+E0100..U+E01EF->16..255
Una vez convertido cada selector a su valor numérico, solo quedaba concatenar esos bytes para obtener el texto oculto.
Resolución
Con un script corto en Python alcanzaba para ignorar el emoji inicial y decodificar los selectores:
def decode_variation_selectors(text: str) -> str:
payload = text[1:]
values = []
for ch in payload:
cp = ord(ch)
if 0xFE00 <= cp <= 0xFE0F:
values.append(cp - 0xFE00)
elif 0xE0100 <= cp <= 0xE01EF:
values.append(cp - 0xE0100 + 16)
return "".join(chr(v) for v in values)
with open("README.md", "r", encoding="utf-8") as f:
print(decode_variation_selectors(f.read().strip()))
Al ejecutarlo, la salida devolvía la flag en claro.
Flag
ENO{EM0J1S_UN1COD3_1S_MAG1C}