← Back to Writeups
CUSTOMEasyReversing

Odna pika

XESXOR9/10/20263 min read
#reversing#custom#easy#bytecode_decompilation#headerless_pyc_recovery#interleaved_index_constraint_s#pyc_header_reconstruction#python27_marshal_identificatio

Odna pika

Platform: spbctf | Category: Reversing | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-07-05 | Status: Solved Techniques: bytecode_decompilation, headerless_pyc_recovery, interleaved_index_constraint_solve, pyc_header_reconstruction, python27_marshal_identification

Summary

Task: a 909-byte .pyc with erased header (magic+timestamp stripped), leaving raw marshal starting at 0x63. Solution: identify Python 2.7 via marshal code-object field layout and raw_input, rebuild the header, decompile with uncompyle6/xdis, and solve an interleaved even/odd index constraint check.

Odna pika — spbctf

Description

Привет от Тайной кошачьей! Стёрты заголовки. ("Hello from the Secret Cat! Headers erased.")

English summary: We are given a 909-byte file pyc_spbctf_1.pyc. file reports only "data". The description hints that the file's headers were erased. The goal is to recover the flag validation logic and compute the correct flag.

Analysis

Recon

$ file pyc_spbctf_1.pyc
pyc_spbctf_1.pyc: data

xxd shows the file begins immediately with byte 0x63 — that is the marshal TYPE_CODE ('c') for a code object. In a normal .pyc the marshal blob is preceded by an 8-byte header (4-byte magic number + 4-byte timestamp). Here that header is gone: this is exactly what "Стёрты заголовки" (headers erased) and the task name "Odna pika" (a lone .pyc) hint at. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

strings reveals the interesting content:

Enter your flag:
Noooooo!
Closer, but no
YAY!
raw_input

Version identification — Python 2.7

Two independent signals point to Python 2.7:

  • raw_input is a Python 2 builtin (removed in Python 3).

  • 2-byte-wide bytecode arguments (e.g. 64 00 00 = LOAD_CONST) — pre-3.6 wordcode.

  • The code-object marshal layout right after 0x63:

    00 00 00 00 | 00 00 00 00 | 09 00 00 00 | 40 00 00 00
    argcount=0    nlocals=0     stacksize=9   flags=0x40
    

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

That's 4 leading ints — the Python 2.7 code-object layout. Python 3.0–3.7 would carry 5 ints (an extra kwonlyargcount), so this is Python 2.7.

  • Running marshal.loads on modern Python fails with ValueError: bad marshal data (unknown type code), confirming the old format.

Solution

1. Reconstruct a valid Python 2.7 .pyc

Prepend the Python 2.7 magic \x03\xf3\x0d\x0a plus a 4-byte zero timestamp:

python
import struct
magic = b'\x03\xf3\x0d\x0a'
ts = struct.pack('<L', 0)
data = open('pyc_spbctf_1.pyc', 'rb').read()
open('fixed_27.pyc', 'wb').write(magic + ts + data)

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

2. Decompile / disassemble

Run these under a Python 3.9 host (they understand Python 2.7 bytecode):

  • uncompyle6 fixed_27.pyc recovers the source, BUT it misrenders single-character string constants as integers (their co_consts indices), producing misleading lists such as c = [4,5,6,...].

  • xdis shows the REAL constants:

    python3.9 -c "from xdis.disasm import disassemble_file; disassemble_file('fixed_27.pyc')"
    

The reconstructed logic:

python
a = raw_input('Enter your flag: ')
assert len(a) == 18
b = a[::2]                      # even indices 0,2,...,16
c = ['s','b','t','{','1','s','_','4','k']
if any(b[i] != c[i] for i in range(len(c))):
    print 'Noooooo!'
else:
    d = ['}','S','7','7','r','f','f','c','p']
    b = a[1::2]                 # odd indices 1,3,...,17
    if any(b[i] != d[::-1][i] for i in range(len(d))):
        print 'Closer, but no'
    else:
        print 'YAY!'

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

3. Solve the interleaved constraint

The 18-char flag interleaves two halves:

  • Even positions must equal ['s','b','t','{','1','s','_','4','k'].
  • Odd positions must equal d reversed = ['p','c','f','f','r','7','7','S','}'].
python
even = ['s','b','t','{','1','s','_','4','k']
odd  = ['}','S','7','7','r','f','f','c','p'][::-1]
flag = ''.join(even[i] + odd[i] for i in range(9))
print(flag)   # spbctf{REDACTED}

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