ScriptCTF 2025 Writeups Series – emoji

Category: Misc

scriptCTF is a 48-hour jeopardy style CTF for hackers to test their skills against creative and innovative CTF challenges. We have mostly beginner friendly challenges, with a few hard ones. scriptCTF makes CTFs fun and approachable for all skill levels! This is hosted by ScriptSorcerers and some members of n00bzUnit3d (as a replacement of n00bzCTF).

When we opened the out.txt file given in the challenge, we found something quite meaningless.

🁳🁣🁲🁩🁰🁴🁃🁔🁆🁻🀳🁭🀰🁪🀱🁟🀳🁮🁣🀰🁤🀱🁮🁧🁟🀱🁳🁟🁷🀳🀱🁲🁤🁟🀴🁮🁤🁟🁦🁵🁮🀡🀱🁥🀴🀶🁤🁽

I decided to Save Page As. So, the dowloaded file was a bit of short but looked similar. In other words, it’s an image created by “incorrect decoding of UTF-8 emoji bytes“. For example; “🁳” actually corresponds to a Mahjong tile/emoji like “🁳”. This means out.txt was opened with the wrong encoding.

Solution Hypothesis

These Mahjong tiles actually have different Unicode codepoints, but they all appear in a consecutive block (U+1F000 – U+1F02F). So, each tile is a number. We need to convert these back to ASCII.

Steps:

  1. Get the Unicode code of the characters in the file.
  2. Subtract the Mahjong base (0x1F000), and the remainder is a number.
  3. Try converting these numbers into a binary or ASCII map.

Conclusion

This code wasvals → Converts Mahjong tiles to numbers.

Three different methods are being tried:

  • Direct chr() → maybe there’s already an ASCII equivalent..
  • Hex string → maybe flag hex encoded
  • Binary parity → 0/1 map → ASCII.
def decode_tiles(text):
    codes = [ord(ch) for ch in text if ch.strip()]
    base = 0x1F000
    values = [c - base for c in codes]
    return values

with open("out.txt", "r", encoding="utf-8") as f:
    data = f.read().strip()

vals = decode_tiles(data)
print("[+] Unicode offsets:", vals)

try:
    ascii_str = "".join(chr(v) for v in vals)
    print("[ASCII] ", ascii_str)
except:
    pass

hex_str = "".join(f"{v:02x}" for v in vals)
print("[HEX] ", hex_str)

try:
    print("[HEX->ASCII]", bytes.fromhex(hex_str).decode())
except:
    pass

bin_str = "".join("1" if v % 2 else "0" for v in vals)
print("[BINARY] ", bin_str)
try:
    out = int(bin_str, 2).to_bytes((len(bin_str)+7)//8, 'big')
    print("[BINARY->ASCII]", out)
except:
    pass
[+] Unicode offsets: [115, 99, 114, 105, 112, 116, 67, 84, 70, 123, 51, 109, 48, 106, 49, 95, 51, 110, 99, 48, 100, 49, 110, 103, 95, 49, 115, 95, 119, 51, 49, 114, 100, 95, 52, 110, 100, 95, 102, 117, 110, 33, 49, 101, 52, 54, 100, 125]
[ASCII]  scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}
[HEX]  7363726970744354467b336d306a315f336e633064316e675f31735f77333172645f346e645f66756e2131653436647d
[HEX->ASCII] scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}
[BINARY]  110100100111001110100101111111100100010101110001
[BINARY->ASCII] b'\xd2s\xa5\xfeEq'

FLAG: scriptCTF{3m0j1_3nc0d1ng_1s_w31rd_4nd_fun!1e46d}