← Back to Writeups
CUSTOMEasyMisc

SignCraft

XESXOR9/2/20265 min read
#misc#custom#easy#minecraft-region-file-(.mca)-p#nbt-data-extraction-with-zlib-#sign-text-extraction-via-regex#fake-flag-filtering#linux

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
$ 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
{"text":"CODEBY{...}"}

Or in the old format as simple strings in Text1, Text2, Text3, Text4 fields.

Solution

Parsing Region Files

python
#!/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:

  1. Multiple fake flags: CODEBY{fake_flag} — a distraction
  2. Real flag split into two parts:
    • Sign 1: CODEBY{fl4g_
    • Sign 2: 4m0ng_f4k3s}

Assembling the Flag

python
part1 = "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

  1. NBTExplorer — GUI for viewing NBT data
  2. mcaselector — visual region editor
  3. Amulet Editor — world editor with support for all versions
  4. Python nbt library — pip install nbt for programmatic access ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Alternative Method: strings + grep

bash
# 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

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  1. Signs — text on 4 lines
  2. Written Books — multi-page text
  3. Chests — item names
  4. Command Blocks — commands and output
  5. Mob Names — custom name tags
  6. Maps — images on maps
  7. Structures — block patterns (QR codes)
  8. level.dat — world name, seed

Security

This task demonstrates:

  1. Steganography in games — data can be hidden in game objects
  2. Obfuscation through volume — real data among many fakes
  3. Split secrets — splitting a secret into parts complicates the search
  4. Binary format knowledge — understanding file formats is essential for forensics ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍