CipherCraft
CipherCraft
Platform: CUSTOM | Category: Crypto | Type: Challenge | Difficulty: Medium | OS: N/A | **Author: XESXOR Techniques: direct_world_parsing, sqlite_map_extraction, luanti_mapblock_parsing, baconian_decoding, clue_chaining, tap_code_decoding, signal_grouping
Summary
Walkthrough of the CUSTOM Crypto CipherCraft. Task: a Mineclonia world archive hides a chain of classical ciphers inside world data rather than a normal text artifact. Solution: parse map.sqlite directly to recover sign and book clues, follow the
Description
Provided artifact: a Mineclonia / Minetest world archive.
English summary: the challenge gives a game world rather than a normal ciphertext. The goal is to inspect the world, recover the embedded clue chain, and decode the final message into the required DawgCTF{...} format.
Recon
The important solving decision was not to launch the game. Instead, I extracted the world archive and worked directly with the world databases, especially map.sqlite.
That approach was faster and more reproducible because the hidden data was stored in normal world content:
- mapblocks in
map.sqlite; - node metadata for signs, containers, and books;
- text strings embedded in decompressed Luanti block payloads.
I wrote a helper parser, analyze_world.py, to inspect the world offline. Its job was to:
- open the SQLite databases;
- decompress Luanti mapblock blobs;
- parse enough metadata to recover signs, chest contents, and written books;
- print coordinates alongside discovered text.
This immediately turned the challenge from "explore a world manually" into "extract a structured clue graph from a database."
Analysis
Once the world text was extracted, the puzzle resolved into a sequence of ciphers and coordinate jumps.
Cipher chain recovered from books and signs
-
CIPHER #1 decoded to coordinates near
(1312, 48, 92). -
CIPHER #2 was a Baconian cipher and pointed to
(-432, -28999, 68). -
CIPHER #3 explicitly indicated that the colors mattered.
-
CIPHER #4 used those colored constructions to send us onward to the final area.
-
The final room explained the answer format and gave the real decoding hints:
DIFFERENTIATE THE GROUPSHINT: THE PITCH OF THE NOTEHINT: THERE IS NO KTAP TAP TAP!
The first four stages were mostly there to guide navigation and establish that the world contained multiple classical encodings. The final stage was the only part where the exact interpretation mattered.
Why the final machine was tricky
The note-block construction is easy to misread if treated as one giant stream. Doing that makes the notes look noisy and ambiguous.
The crucial observation was the hint DIFFERENTIATE THE GROUPS: the machine was intentionally split into separate tracks. After separating the note streams by their intended grouped lanes and then reading run lengths of equal-pitch notes, the output became stable.
The clue THERE IS NO K strongly suggests a 5x5 Polybius family alphabet, where I/J share a cell and K is omitted in the tap-code variant used by the puzzle. Combined with TAP TAP TAP!, the intended decoding is tap code.
So the final interpretation is:
- split the machine into its intended groups;
- for each group, count repeated notes of the same pitch;
- convert the counts into tap-code row/column pairs;
- decode with a 5x5 alphabet that omits
K.
That produced two plaintext segments:
- right group →
SEEMSYOU - middle + left continuation →
REDACTED
Combining them gives:
text read-only • copy allowedREDACTED
and therefore the final flag:
text read-only • copy allowedDawgCTF{REDACTED}
Solution
Step 1: parse the world directly
Instead of opening Mineclonia and searching by hand, extract the world and inspect map.sqlite. A lightweight offline parser is enough to recover the clue text.
Step 2: follow the intermediate ciphers
- Decode CIPHER #1 to reach the first coordinate target.
- Decode CIPHER #2 as Baconian to reach
(-432, -28999, 68). - Follow the "colors matter" hint for CIPHER #3 and CIPHER #4.
- Continue until reaching the final note-block installation.
Step 3: split the final note machine into tracks
Do not read all notes as one stream. Separate the construction into the intended grouped tracks first.
Step 4: decode note runs as tap code
Treat consecutive equal-pitch notes as row/column counts in tap code. Use the 5x5 alphabet variant indicated by the clue that says there is no K.
The grouped tracks decode to:
text read-only • copy allowedright -> SEEMSYOU middle -> REMINING... left -> ...AWAYAGAIN
which yields REDACTED.
python read-only • copy allowed#!/usr/bin/env python3 import re import sqlite3 import string import zlib PRINTABLE = re.compile(rb"[ -~]{4,}") def printable_strings(blob: bytes): return [m.decode("utf-8", errors="ignore") for m in PRINTABLE.findall(blob)] def dump_interesting_strings(db_path: str): conn = sqlite3.connect(db_path) cur = conn.cursor() rows = cur.execute("SELECT pos, data FROM blocks") hits = [] for pos, data in rows: try: raw = zlib.decompress(data) except Exception: raw = data strings = printable_strings(raw) interesting = [s for s in strings if any(k in s.lower() for k in ( "cipher", "hint", "tap", "k", "color", "book", "answer" ))] if interesting: hits.append((pos, interesting)) conn.close() return hits def bacon_decode(bits: str) -> str: alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" out = [] for i in range(0, len(bits), 5): chunk = bits[i:i + 5] if len(chunk) < 5: break value = int(chunk.replace("A", "0").replace("B", "1"), 2) if value < 26: out.append(alphabet[value]) return "".join(out) def tap_grid(): letters = "ABCDEFGHILMNOPQRSTUVWXYZ" # no K grid = {} idx = 0 for row in range(1, 6): for col in range(1, 6): grid[(row, col)] = letters[idx] idx += 1 return grid def tap_decode(pairs): grid = tap_grid() return "".join(grid[p] for p in pairs) if __name__ == "__main__": # 1) Inspect map.sqlite offline and recover clue text. for pos, strings in dump_interesting_strings("map.sqlite"): print(f"block={pos}") for s in strings: print(f" {s}") # 2) Final note-block machine, after splitting into intended groups and # converting equal-pitch runs into tap-code row/column counts. right_pairs = [(4, 3), (1, 5), (1, 5), (3, 4), (4, 3), (5, 4), (3, 5), (4, 5)] rest_pairs = [ (4, 2), (1, 5), (3, 1), (2, 3), (3, 5), (2, 4), (3, 5), (1, 3), (1, 3), (3, 1), (5, 1), (1, 5), (1, 1), (4, 1), (1, 5), (1, 2), (1, 3), (1, 2), ] plaintext = tap_decode(right_pairs) + tap_decode(rest_pairs) print(plaintext) print(f"DawgCTF{{{plaintext}}}")
Key Takeaways / Lessons
- Mastering direct_world_parsing, sqlite_map_extraction, luanti_mapblock_parsing, baconian_decoding, clue_chaining, tap_code_decoding, signal_grouping was key to this challenge.
- Always check for common misconfigurations and verify assumptions.
Crafted by XESXOR Research • 2026-04-10 • CUSTOM • CipherCraft • Difficulty: Medium