Photo by Florian Olivo / Unsplash

Writeup: Minecraft Enterprise Edition - upCTF (Reverse)

Feri Harjulianto

Challenge Description

My company recently acquired a limited number of Minecraft Enterprise Edition keys to reward top-performing employees. Sadly, I didn't make the cut. I managed to get my hands on the internal activation program they use to validate these licenses. Will you help me go around management and generate myself a valid key?

Initial Analysis

$ file minecraft-enterprise
ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, stripped

Statically linked, stripped binary. Searching for strings reveals the key format and validation flow:

Enter Key (format: XXXXX-XXXXX-XXXXX-XXXXX):
Key parsing error.
Invalid Key.
flag.txt
Flag: %s
@@IMNOTTHEKEY
ABCDEFGHIJKLMNOPQRSTUVWXYZ234567   <-- Base32 alphabet

Reverse Engineering the Validation

The main function lives at 0x402cd0. The validation pipeline has 5 stages:

Stage 1: Parse Key (0x4031d0)

  • Verifies input length is exactly 23 characters
  • Checks dashes at positions 6, 12, 18 (1-indexed)
  • Validates each non-dash character is alphanumeric
  • Applies toupper() to each character (confirmed via GDB - chars stay uppercase)
  • Outputs 20 uppercase alphanumeric characters: p[0..19]

Stage 2: Fixed Seed Computation (lines 0x402d87-0x402dd4)

A fixed computation using the AES S-box table at 0x7e24c0:

seed = 0x12345678
for i in range(64):
    a = ROL32(seed, i)
    a ^= seed
    a = (a >> (i & 7)) & 0xFF
    a = SBOX[a]
    seed = a + seed

Result: seed = 0xde43e410 (constant, independent of input).

Stage 3: Transform Key (0x403170)

Two byte-shuffling operations on the 20 parsed characters:

  1. Swap halveschars[0:10] <-> chars[10:20]
  2. Swap adjacent pairs: For each pair (i, i+1), swap the two bytes

Final mapping (original position -> transformed position):

transformed = [p11,p10,p13,p12,p15,p14,p17,p16,p19,p18, p1,p0,p3,p2,p5,p4,p7,p6,p9,p8]
              |__________ first_10 __________________|  |_________ last_10 ______________|

Stage 4: HMAC-SHA256 + Base32 (0x4030a0)

The first 10 characters of the transformed key are hashed:

digest = HMAC-SHA256(key="IMNOTTHEKEY", data=first_10_transformed)

The hash function was identified as SHA256 by:

  1. 0x4037a0 returns an EVP_MD pointer at 0x88d340
  2. Brute-forcing all common hash functions against a known input/output pair from GDB

The first 7 bytes of the HMAC digest are converted to a 10-character Base32 string:

val = int.from_bytes(digest[:7], 'big') >> 6   # 50 bits
for i in range(10):  # 10 chars x 5 bits = 50 bits
    char = BASE32_ALPHA[val & 0x1f]
    val >>= 5
# Written right-to-left (big-endian base32)

Stage 5: Comparison (0x402e54)

The Base32-encoded HMAC result must equal the last 10 characters of the transformed key:

HMAC_base32(first_10_transformed) == last_10_transformed

Key Generation

To forge a valid key:

  1. Choose any 10 uppercase alphanumeric characters for first_10_transformed
  2. Compute HMAC-SHA256("IMNOTTHEKEY", first_10) and Base32-encode to get last_10
  3. Reverse the byte shuffling to recover p[0..19]
  4. Format as XXXXX-XXXXX-XXXXX-XXXXX
import hmac, hashlib

BASE32_ALPHA = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"

def hmac_to_base32_10(data):
    h = hmac.new(b"IMNOTTHEKEY", data, hashlib.sha256).digest()
    val = int.from_bytes(h[:7], 'big') >> 6
    result = []
    for _ in range(10):
        result.append(BASE32_ALPHA[val & 0x1f])
        val >>= 5
    result.reverse()
    return ''.join(result)

first_10 = "AAAAAAAAAA"
last_10 = hmac_to_base32_10(first_10.encode())  # -> "3CX463HTMX"

t, h = list(first_10), list(last_10)
p = [''] * 20
p[11],p[10],p[13],p[12],p[15],p[14],p[17],p[16],p[19],p[18] = t
p[1],p[0],p[3],p[2],p[5],p[4],p[7],p[6],p[9],p[8] = h

key = '-'.join([''.join(p[i:i+5]) for i in range(0, 20, 5)])
print(f"Key: {key}")  # C34X3-6THXM-AAAAA-AAAAA

Solution

$ echo 'C34X3-6THXM-AAAAA-AAAAA' | nc <host> <port>
Enter Key (format: XXXXX-XXXXX-XXXXX-XXXXX): Flag: UPCTF{...}

Key Debugging Moment

Initial attempt used HMAC-SHA1 (misidentified from nearby "SHA1" strings in the EVP_MD struct). Using QEMU + GDB to inspect the actual HMAC output at the comparison breakpoint (0x402e54) revealed the mismatch, and testing all common hash functions against the known pair confirmed SHA256.

The binary also uses x32 mode switching (visible via addr32 instruction prefixes and strace showing alternating x32/64-bit mode), which prevented native GDB debugging - QEMU user-mode emulation with gdb-multiarch was required.

Flag

upCTF{m1n3cr4ft_0n_th3_b4nks-7jWw48VRc83b39fd}

CTFReverseWriteup