← Back to Writeups
CUSTOMEasyReversing

Кости и Баги (Dice and Bug)

XESXOR9/2/20263 min read
#reversing#custom#easy#docker_version_matched_marshal#nested_exec_deobfuscation#pyc_decompilation_with_pycdc#reversed_base64_zlib_unwrappin#xor_decryption_with_static_key

Кости и Баги (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
def _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
import 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
return 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
klen = len(key)
return bytes(d ^ key[i % klen] for i, d in enumerate(data))

_get_flag() (64 layers) — Decrypts hardcoded ciphertext:

python
ct_hex = "2a100f263c232452140f5631125f12380e551a111d0b6b39501e136b1211"
ct = bytes.fromhex(ct_hex)
pt = _get_real_flag(ct, _k())
print("\nFlag " + pt.decode("utf-8", "ignore") + "\n")

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

Solution

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

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