Analysis

In this Reverse Engineering challenge, we are provided with a Python bytecode file:

1
2
$ file chall.pyc
chall.pyc: python 2.7 byte-compiled

I decided to use uncompyle6 to decompile the Python bytecode:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ uncompyle6 chall.pyc
# uncompyle6 version 3.9.3
# Python bytecode version base 2.7 (62211)
# Decompiled from: Python 3.13.12
# Embedded file name: ./snake_obf.py
import marshal, types, time

ll = types.FunctionType(
    marshal.loads(
        ('YwEAAAABAAAABQAAAEMAAABzNAAAAHQAAGoBAHQCAGoDAHQEAGQBAIMBAGoFAHwAAGoGAGQCAIMB\n'
         'AIMBAIMBAHQHAIMAAIMCAFMoAwAAAE50BAAAAHpsaWJ0BgAAAGJhc2U2NCgIAAAAdAUAAAB0eXBl\n'
         'c3QMAAAARnVuY3Rpb25UeXBldAcAAABtYXJzaGFsdAUAAABsb2Fkc3QKAAAAX19pbXBvcnRfX3QK\n'
         'AAAAZGVjb21wcmVzc3QGAAAAZGVjb2RldAcAAABnbG9iYWxzKAEAAAB0AQAAAHMoAAAAACgAAAAA\n'
         'cwcAAAA8c3RkaW4+dAoAAABsb2FkTGFtYmRhAQAAAHQAAAAA\n').decode('base64')
    ),
    globals()
)

i0 = ll('eJxLZoACJiB2BuJiLiBRwsCQwsjQzMgQrAES9ythA5JFiXkp+bkajCB5kKL4+Mzcgvyikvh4DZAB\nCKKYHUjYFJekZObZlXCA2DmJuUkpiXaMEKMZGAC+nBJh\n')
i1 = ll('eJxLZoACJiB2BuJiLiBRwsCQwsjQzMgQrAES9ythA5LJpUXFqcUajCB5kKL4+Mzcgvyikvh4DZAB\nCKKYHUjYFJekZObZlXCA2DmJuUkpiXaMEKMZGADEORJ1\n')
f0 = ll('eJxLZmRgYABhJiB2BuJiXiBRw8CQxcCQwsjQzMgQrAGS8ssEEgwaIJUl7CAiMzc1v7QEIsAMJMoz\n8zTASkBEMUiJTXFJSmaeXQkHiJ2TmJuUkmgHVg5SAQBjWRD5\n')
# ... many more encoded strings ...

def snake(w):
    r = i0()
    c = i1()
    f0(w)
    d = (0, 1)
    p = [(5, 5)]
    pl = 1
    s = 0
    l = None
    while 1:
        p, d, pl, l, s, w, c, r = m2(p, d, pl, l, s, w, c, r)
        time.sleep(0.4)
    return

i1().wrapper(snake)
return

The decompiled code reveals a heavily obfuscated Python program. The important part is the loader function:

1
2
3
4
5
6
ll = types.FunctionType(
    marshal.loads(
        ('<base64 blob>').decode('base64')
    ),
    globals()
)

This function dynamically reconstructs Python functions from encoded data.

Every variable assigned through the loader, for example:

1
i0 = ll('eJxLZoACJiB2BuJiLiBR...')

goes through the following process:

1
2
3
4
5
6
7
8
9
10
11
Base64 decoding
        ↓
zlib decompression
        ↓
marshal.loads()
        ↓
Python code object
        ↓
types.FunctionType()
        ↓
Executable Python function

The challenge hides all important logic inside these dynamically generated functions.


Extraction

To extract the hidden functions, I modified the ll loader so that instead of creating functions, it writes the decoded bytecode to files:

1
2
3
4
5
6
7
import zlib
import base64

def ll(name, data):
    with open("out/" + name, "wb") as fp:
        fp.write(zlib.decompress(base64.b64decode(data)))

Then I replaced each function creation with a call to the modified loader:

1
2
3
i0 = ll("i0", 'eJxLZoACJiB2BuJiLiBR...')
i1 = ll("i1", 'eJxLZoACJiB2BuJiLiBR...')
# ... repeat for all encoded functions

After extracting all the hidden code objects, I converted them back into Python bytecode files and decompiled them:

1
$ for x in out/*; do python marshal-to-pyc.py $x; done

After reviewing the resulting files, one of the extracted functions (a2.py) contained the flag construction logic:

1
2
3
4
5
6
7
8
f = [
    'H', 'T', 'B', '{', 'S', 'u', 'P', '3',
    'r', '_', 'S', '3', 'C', 'R', 't', '_',
    'S', 'n', '4', 'k', '3', 'c', '0', 'd',
    '3', '}'
]

return ''.join(f)

The flag characters are stored directly inside the list. Joining them together reveals the final flag:

1
2
>>> ''.join(f)
'HTB{SuP3r_S3CRt_Sn4k3c0d3}'

Flag

1
HTB{SuP3r_S3CRt_Sn4k3c0d3}