Кости и Баги (Dice and Bug)
Кости и Баги (Dice and Bug)
Platform: bug-makers | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-05-27 | Status: Solved Techniques: docker_version_matched_marshal_loading, nested_exec_deobfuscation, pyc_decompilation_with_pycdc, reversed_base64_zlib_unwrapping, xor_decryption_with_static_key
Summary
Task: Python 3.9 .pyc file containing a Farkle dice game with obfuscated flag logic. Solution: decompile with pycdc, unwrap 64 nested exec(zlib+base64) layers to recover XOR key and ciphertext, decrypt flag statically.
Кости и Баги (Dice and Bug) — Bug-Makers CTF
Description
Только элита достойна флага. Попробуй выиграть в кости и получить флаг.
English summary: A Python 3.9 compiled bytecode file (game-python3.9.pyc) implements a Farkle dice game. The challenge implies you need to win the game to get the flag, but the flag is actually statically embedded in deeply obfuscated code — no game interaction required.
Files provided: dice_and_bug.zip containing game-python3.9.pyc (Python 3.9 bytecode) and start.py (simple launcher that executes the .pyc).
Analysis
Decompilation challenges
The .pyc file targets Python 3.9. Standard decompilers (uncompyle6, decompyle3) failed when run from Python 3.14 due to bytecode version mismatch. Built pycdc (Decompyle++) from source, which provided partial but sufficient decompilation.
For proper bytecode inspection, used Docker with python:3.9-slim to run marshal.load() on the .pyc file with the correct interpreter version.
Code structure
The decompiled code reveals a fully functional Farkle dice game with scoring rules, player turns, and AI opponent. However, three functions contain the flag logic, each wrapped in identical obfuscation:
python read-only • copy alloweddef _k(): _ = lambda __: __import__('zlib').decompress(__import__('base64').b64decode(__[::-1])) exec(_(b'=oRsik+P73P//TZpB3Iq...')) def _get_real_flag(data=None, key=None): _ = lambda __: __import__('zlib').decompress(__import__('base64').b64decode(__[::-1])) exec(_(b'=4v4Bv8A//77z7frbxMH...')) def _get_flag(): _ = lambda __: __import__('zlib').decompress(__import__('base64').b64decode(__[::-1])) exec(_(b'==wJUYUkf8/fffu/adLm...'))
The obfuscation pattern: the base64 blob is reversed before decoding (__[::-1]), then zlib-decompressed. The result is another exec() call with another blob — nested 63-64 layers deep.
Deobfuscation
Wrote a recursive decoder to strip all layers:
python read-only • copy allowedimport zlib, base64, re def decode_blob(blob_bytes): return zlib.decompress(base64.b64decode(blob_bytes[::-1])).decode() def fully_decode(code_str, max_depth=200): current = code_str for depth in range(max_depth): blobs = re.findall(r"b'([A-Za-z0-9+/=]{100,})'", current) if not blobs: return current, depth decoded = None for blob in blobs: try: decoded = decode_blob(blob.encode()) break except: continue if decoded is None: return current, depth current = decoded return current, max_depth
Recovered functions
After full deobfuscation:
_k() (63 layers) — Returns the XOR key:
python read-only • copy allowedreturn bytes([0x68,0x65,0x68,0x65,0x68,0x65,0x5f,0x66,0x61,0x6b,0x65,0x5f,0x66,0x6c,0x61,0x67]) # b"hehehe_fake_flag"
_get_real_flag(data, key) (64 layers) — Simple XOR decryption:
python read-only • copy allowedklen = len(key) return bytes(d ^ key[i % klen] for i, d in enumerate(data))
_get_flag() (64 layers) — Decrypts hardcoded ciphertext:
python read-only • copy allowedct_hex = "2a100f263c232452140f5631125f12380e551a111d0b6b39501e136b1211" ct = bytes.fromhex(ct_hex) pt = _get_real_flag(ct, _k()) print("\nFlag " + pt.decode("utf-8", "ignore") + "\n")
Solution
python read-only • copy allowed#!/usr/bin/env python3 """Solve dice_and_bug: XOR decrypt the flag from deobfuscated constants""" # Key recovered from _k() after 63 layers of deobfuscation key = b"hehehe_fake_flag" # Ciphertext recovered from _get_flag() after 64 layers of deobfuscation ct = bytes.fromhex("2a100f263c232452140f5631125f12380e551a111d0b6b39501e136b1211") # XOR decrypt (from _get_real_flag) flag = bytes(d ^ key[i % len(key)] for i, d in enumerate(ct)) print(flag.decode()) # BugCTF{REDACTED}