💯 (unicode magic)
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 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 muy 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 siguen 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 esta.
U+FE00..U+FE0Fse convertía en0..15U+E0100..U+E01EFse convertía en16..255
Una vez convertido cada selector a su valor numérico, ya solo quedaba concatenar esos bytes para obtener el texto oculto.
No había cifrado ni compresión. El contenido estaba en los caracteres invisibles que acompañaban al emoji visible.
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}