SignCraft
SignCraft
Platform: hackerlab.pro | Category: Misc | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2025-12-25 | Status: Solved Techniques: Minecraft region file (.mca) parsing, NBT data extraction with zlib decompression, Sign text extraction via regex, Fake flag filtering
Summary
The archive SignCraft.zip contains a Minecraft world (version 1.18.2 with Forge and WorldEdit mods).
SignCraft - hackerlab.pro
Description
"Ищи и обязательно найдешь!" (Search and you will definitely find!)
The archive SignCraft.zip contains a Minecraft world (version 1.18.2 with Forge and WorldEdit mods).
Analysis
Archive Investigation
bash read-only • copy allowed$ unzip SignCraft.zip $ ls -la task/ drwxr-xr-x region/ -rw-r--r-- level.dat ... $ ls task/region/ r.0.0.mca r.0.-1.mca ...
The task name "SignCraft" hints at signs in Minecraft. The flag is most likely written on signs in the game world.
Minecraft Region Files Format
.mca (Minecraft Anvil) files contain world chunk data:
- Each region file stores 32x32 chunks
- Chunk data is compressed with zlib
- Inside — NBT (Named Binary Tag) structures
- Signs are stored as block entities with text fields
Sign Data Structure
In Minecraft 1.18+ sign text is stored in JSON format:
json read-only • copy allowed{"text":"CODEBY{...}"}
Or in the old format as simple strings in Text1, Text2, Text3, Text4 fields.
Solution
Parsing Region Files
python read-only • copy allowed#!/usr/bin/env python3 import zlib import re import os from pathlib import Path def extract_signs_from_mca(filepath): """Extract sign text from Minecraft .mca region file""" signs = [] with open(filepath, 'rb') as f: data = f.read() # Region file header: 4KB locations + 4KB timestamps # Each chunk location is 4 bytes (3 bytes offset + 1 byte size) for i in range(1024): # 32x32 = 1024 chunks per region offset_bytes = data[i*4:(i*4)+3] size_byte = data[i*4+3] if offset_bytes == b'\x00\x00\x00' or size_byte == 0: continue # Offset is in 4KB sectors offset = int.from_bytes(offset_bytes, 'big') * 4096 if offset >= len(data): continue # Chunk data: 4 bytes length + 1 byte compression type + compressed data chunk_length = int.from_bytes(data[offset:offset+4], 'big') compression = data[offset+4] if compression != 2: # 2 = zlib continue compressed = data[offset+5:offset+4+chunk_length] try: decompressed = zlib.decompress(compressed) # Search for sign text patterns in decompressed NBT data signs.extend(find_sign_text(decompressed)) except: pass return signs def find_sign_text(nbt_data): """Extract text from sign NBT data""" texts = [] data_str = nbt_data.decode('latin-1', errors='ignore') # Pattern for JSON text format: {"text":"..."} json_pattern = r'\{"text":"([^"]+)"\}' matches = re.findall(json_pattern, data_str) texts.extend(matches) # Pattern for raw text after Text1/Text2/Text3/Text4 tags # NBT string format: 2-byte length + string data for match in re.finditer(rb'Text[1-4]', nbt_data): pos = match.end() if pos + 2 < len(nbt_data): str_len = int.from_bytes(nbt_data[pos:pos+2], 'big') if str_len > 0 and str_len < 1000: text = nbt_data[pos+2:pos+2+str_len].decode('utf-8', errors='ignore') if text and text != '{"text":""}': texts.append(text) return texts def main(): region_dir = Path('task/region') all_signs = [] for mca_file in region_dir.glob('*.mca'): print(f"Processing {mca_file}...") signs = extract_signs_from_mca(mca_file) all_signs.extend(signs) # Filter and display unique texts unique_texts = set() for text in all_signs: # Clean up JSON wrapper if present if text.startswith('{"text":"') and text.endswith('"}'): text = text[9:-2] if text and text not in unique_texts: unique_texts.add(text) # Highlight potential flags if 'CODEBY' in text or 'flag' in text.lower(): print(f"[FLAG?] {text}") elif text.strip(): print(f"[TEXT] {text}") if __name__ == '__main__': main()
Parsing Results
Processing task/region/r.0.0.mca...
[FLAG?] CODEBY{fake_flag}
[FLAG?] CODEBY{fake_flag}
[FLAG?] CODEBY{fake_flag}
... (many fake_flag entries)
[FLAG?] CODEBY{fl4g_
[FLAG?] 4m0ng_f4k3s}
Results Analysis
Found:
- Multiple fake flags:
CODEBY{fake_flag}— a distraction - Real flag split into two parts:
- Sign 1:
CODEBY{fl4g_ - Sign 2:
4m0ng_f4k3s}
- Sign 1:
Assembling the Flag
python read-only • copy allowedpart1 = "CODEBY{fl4g_" part2 = "4m0ng_f4k3s}" flag = part1 + part2 print(flag) # CODEBY{REDACTED}
Task Hints
- "SignCraft" — direct hint at signs in Minecraft
- "Ищи и обязательно найдешь" (Search and you will definitely find) — indicates searching among multiple objects
- Flag "REDACTED" (flag among fakes) — confirms the task concept
Minecraft World Forensics
Minecraft World Structure
world/
├── level.dat # World metadata (NBT)
├── region/ # Overworld chunks
│ └── r.X.Z.mca # Region files (32x32 chunks each)
├── DIM-1/region/ # Nether
├── DIM1/region/ # End
├── playerdata/ # Player data
└── data/ # Maps, structures
Region File Format (.mca)
Offset 0x0000-0x0FFF: Location table (1024 entries × 4 bytes)
Offset 0x1000-0x1FFF: Timestamp table (1024 entries × 4 bytes)
Offset 0x2000+: Chunk data (compressed NBT)
Location entry: 3 bytes offset (in 4KB sectors) + 1 byte size
Chunk data: 4 bytes length + 1 byte compression + zlib data
Useful Tools
- NBTExplorer — GUI for viewing NBT data
- mcaselector — visual region editor
- Amulet Editor — world editor with support for all versions
- Python nbt library —
pip install nbtfor programmatic access
Alternative Method: strings + grep
bash read-only • copy allowed# Quick search in compressed data for f in task/region/*.mca; do zlib-flate -uncompress < "$f" 2>/dev/null | strings | grep -i "CODEBY" done # Or via Python python3 -c " import zlib, glob for f in glob.glob('task/region/*.mca'): data = open(f,'rb').read() for i in range(0, len(data)-100, 4096): try: d = zlib.decompress(data[i:i+65536], -15) if b'CODEBY' in d: print(d) except: pass "
Places to Search for Flags in Minecraft
- Signs — text on 4 lines
- Written Books — multi-page text
- Chests — item names
- Command Blocks — commands and output
- Mob Names — custom name tags
- Maps — images on maps
- Structures — block patterns (QR codes)
- level.dat — world name, seed
Security
This task demonstrates:
- Steganography in games — data can be hidden in game objects
- Obfuscation through volume — real data among many fakes
- Split secrets — splitting a secret into parts complicates the search
- Binary format knowledge — understanding file formats is essential for forensics