← Back to Writeups
CUSTOMMediumReversing

Black Box Hunter

XESXOR9/10/20263 min read
#reversing#custom#medium#bytecode_reverse_engineering#deterministic_optimization#pickle_deserialization#pyc_disassembly#linux

Black Box Hunter

Platform: miptctf | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-03-14 | Status: Solved Techniques: bytecode_reverse_engineering, deterministic_optimization, pickle_deserialization, pyc_disassembly

Summary

Task: Reverse engineer Python 3.12 .pyc and pickle files to understand black box reward function. Solution: Extract optima and state sequence from pickle, reconstruct Gaussian reward formula from bytecode, generate optimal parameters for each step.

Black Box Hunter — MIPT CTF

Description

"Нравятся черные ящики? Засунь туда что-нибудь и получи награду." (Like black boxes? Shove something in and get a reward.)

Given archive source.tar.gz with files:

  • baseline.py — baseline hunter implementation
  • black_box.pkl — pickle with SecretBlackBox object
  • black_box_core.pyc — compiled Python 3.12 bytecode

Server accepts CSV with parameters, calculates total reward over 500 steps. Threshold: reward > 1500.

Analysis

1. Examining baseline.py

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

python
def load_black_box():
    # Loads pickle and pyc
    with open('black_box.pkl', 'rb') as f:
        box = pickle.load(f)
    # Loads black_box_core from .pyc
    ...

Interface: black box accepts 3 parameters in range [-5, 5], returns reward.

2. Analyzing pickle file

Loading with stub class to extract attributes:

python
class SecretBlackBox:
    pass

with open('black_box.pkl', 'rb') as f:
    box = pickle.load(f)

print(box.__dict__)

Extracted parameters:

  • n_states = 5 — number of states
  • change_interval = 37 — state change interval
  • noise_level = 0.1 — noise level
  • optima — 5x3 array with optimal coordinates for each state
  • sigmas — 5x3 array with variances
  • seed — seed for deterministic noise
  • state_sequence — array of 1000 elements with state sequence ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Optima:

State 0: [3.035, 3.777, -0.672]
State 1: [-2.600, -1.448, 3.385]
State 2: [-0.619, -1.718, -3.066]
State 3: [-1.279, -3.460, 2.111]
State 4: [-3.382, 3.386, 1.431]

3. Reverse Engineering .pyc (Python 3.12)

uncompyle6 doesn't support Python 3.12. Used Docker:

bash
docker run -it --rm -v $(pwd):/work python:3.12 python3 -c "
import dis, marshal
with open('/work/black_box_core.pyc', 'rb') as f:
    f.read(16)  # skip header
    code = marshal.load(f)
dis.dis(code)
"

Reconstructed code: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

python
def black_box_core(params, hidden_state):
    params = np.asarray(params).flatten()
    n_states, change_interval, noise_level, optima, sigmas, call_count, seed, state_sequence = hidden_state
    
    # Determine current state
    if call_count < len(state_sequence):
        current_state = state_sequence[call_count]
    else:
        current_state = 0
    
    # Gaussian reward: maximum 10.0 when params == optima[current_state]
    diff = params - optima[current_state]
    weighted = np.sum((diff / sigmas[current_state]) ** 2)
    result = 10.0 * np.exp(-0.5 * weighted)
    
    # Deterministic noise
    rng = np.random.RandomState((seed + call_count + 1000) % 4294967296)
    noisy = result + rng.normal(0, noise_level)
    
    call_count += 1
    new_state = (n_states, change_interval, noise_level, optima, sigmas, call_count, seed, state_sequence)
    return (noisy, new_state)

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

Key insight: Reward is a Gaussian function with maximum 10.0 at point optima[current_state]. State is determined by state_sequence[call_count].

Solution

1. Generating optimal CSV

Knowing state_sequence and optima, for each step we select the exact optimum:

python
#!/usr/bin/env python3
import pickle
import numpy as np

class SecretBlackBox:
    pass

with open('black_box.pkl', 'rb') as f:
    box = pickle.load(f)

optima = box.optima
state_sequence = box.state_sequence

# Generate CSV for 500 steps
with open('submission_optimal.csv', 'w') as f:
    f.write('step,param1,param2,param3\n')
    for step in range(500):
        state = state_sequence[step]
        opt = optima[state]
        f.write(f'{step},{opt[0]},{opt[1]},{opt[2]}\n')

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

2. Local verification

bash
docker run -it --rm -v $(pwd):/work python:3.12 python3 /work/verify.py
# Total reward: 4995.83

3. Submitting to server

bash
curl -X POST http://HOST:PORT/check -F "file=@submission_optimal.csv"

Response: MIPT{REDACTED} ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍