2.project_chimera [FlareOn 2025]

rev
writeup by: PineBel

For the second challenge we receive a python file only. The file is pretty small:

# These are my encrypted instructions for the Sequencer.
encrypted_sequencer_data = b'x\x9cm\x96K\xcf\xe2\xe6\x15.....' # this is longer

print(f"Booting up {f"Project Chimera"} from Dr. Khem's journal...") # apperently this exists to indicate that a version of Python 3.12+ should be used, I didn't have this issue though

# Activate the Genetic Sequencer. From here, the process is automated.
sequencer_code = zlib.decompress(encrypted_sequencer_data)
exec(marshal.loads(sequencer_code)) # load bytecode and execute

So basically what we get is some bytecode which is then loaded and executed.

If we try to run it we get some errors that some libraries are missing. To have an idea what the code does we can just look at the dissasembled code with dis (dis.dis(marshal.loads(sequencer_code))).

If we look at the disassembled bytecodes we get:

  0           0 RESUME                   0

  2           2 LOAD_CONST               0 (0)
              4 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (base64)
              8 STORE_NAME               0 (base64)

  3          10 LOAD_CONST               0 (0)
             12 LOAD_CONST               1 (None)
             14 IMPORT_NAME              1 (zlib)
             16 STORE_NAME               1 (zlib)

  4          18 LOAD_CONST               0 (0)
             20 LOAD_CONST               1 (None)
             22 IMPORT_NAME              2 (marshal)
             24 STORE_NAME               2 (marshal)

  5          26 LOAD_CONST               0 (0)
             28 LOAD_CONST               1 (None)
             30 IMPORT_NAME              3 (types)
             32 STORE_NAME               3 (types)

  8          34 LOAD_CONST               2 (b'..') # again a lot of bytes
             36 STORE_NAME               4 (encoded_catalyst_strand)

 10          38 PUSH_NULL
             40 LOAD_NAME                5 (print)
             42 LOAD_CONST               3 ('--- Calibrating Genetic Sequencer ---')
             44 CALL                     1
             52 POP_TOP

 11          54 PUSH_NULL
             56 LOAD_NAME                5 (print)
             58 LOAD_CONST               4 ('Decoding catalyst DNA strand...')
             60 CALL                     1
             68 POP_TOP

 12          70 PUSH_NULL
             72 LOAD_NAME                0 (base64)
             74 LOAD_ATTR               12 (b85decode) # decoding
             94 LOAD_NAME                4 (encoded_catalyst_strand)
             96 CALL                     1
            104 STORE_NAME               7 (compressed_catalyst)

 13         106 PUSH_NULL
            108 LOAD_NAME                1 (zlib)
            110 LOAD_ATTR               16 (decompress)
            130 LOAD_NAME                7 (compressed_catalyst)
            132 CALL                     1
            140 STORE_NAME               9 (marshalled_genetic_code)

 14         142 PUSH_NULL
            144 LOAD_NAME                2 (marshal)
            146 LOAD_ATTR               20 (loads)
            166 LOAD_NAME                9 (marshalled_genetic_code)
            168 CALL                     1
            176 STORE_NAME              11 (catalyst_code_object)

 16         178 PUSH_NULL
            180 LOAD_NAME                5 (print)
            182 LOAD_CONST               5 ('Synthesizing Catalyst Serum...')
            184 CALL                     1
            192 POP_TOP

 19         194 PUSH_NULL
            196 LOAD_NAME                3 (types)
            198 LOAD_ATTR               24 (FunctionType)
            218 LOAD_NAME               11 (catalyst_code_object)
            220 PUSH_NULL
            222 LOAD_NAME               13 (globals)
            224 CALL                     0
            232 CALL                     2
            240 STORE_NAME              14 (catalyst_injection_function)

 22         242 PUSH_NULL
            244 LOAD_NAME               14 (catalyst_injection_function)
            246 CALL                     0
            254 POP_TOP
            256 RETURN_CONST             1 (None)

So we can see that the code contains another part where bytecode is loaded. We can just take the bytes from encoded_catalyst_strand and apply the same process as the original script, but we also need to decode the bytes with b85decode.

import base64, zlib, marshal, dis

second_bytecode = b'c$|e+O>7&-6`m!Rz....'
second_bytecode = base64.b85decode(second_bytecode)
second = zlib.decompress(second_bytecode)
print(dis.dis(marshal.loads(second)))

We now have access to the full disassembled bytecode. It’s pretty long so I won’t paste it in here.

This could also be done more elegantly (from the official writeup) like this:

import zlib, marshal, importlib

encrypted_sequencer_data = (
    b'x\x9cm\x96K\xcf\xe2\xe6\x15\xc7\xfd\xcedf\x92\xe6\xd2J\x93\xce...'
)
decompressed = zlib.decompress(encrypted_sequencer_data)
co = marshal.loads(decompressed)

# Convert marshal code object `co` to a .pyc file
# See: https://stackoverflow.com/a/73454818/6245337
pyc_data = importlib._bootstrap_external._code_to_timestamp_pyc(co)

# Write .pyc file to disk
with open("payload1.pyc", "wb") as f:
    f.write(pyc_data)

After that you can use a decompiler like pycdc to get a nicer, readable source.

We can see that it imports several non-standard modules (emoji, cowsay, etc.). After installing them, we find the challenge goal: log in as an admin.

16           6 LOAD_CONST               1 (b'm\x1b@I\x1dAoe@\x07ZF[BL\rN\n\x0cS')
              8 STORE_FAST               0 (LEAD_RESEARCHER_SIGNATURE)

 17          10 LOAD_CONST               2 (b'r2b-\r\x9e\xf2\x1fp\x185\x82\xcf\xfc\x90\x14\xf1O\xad#]\xf3\xe2\xc0L\xd0\xc1e\x0c\xea\xec\xae\x11b\xa7\x8c\xaa!\xa1\x9d\xc2\x90')
             12 STORE_FAST               1 (ENCRYPTED_CHIMERA_FORMULA)

    ...

 22          58 LOAD_GLOBAL              3 (NULL + os)
             68 LOAD_ATTR                4 (getlogin)
             88 CALL                     0
             96 LOAD_ATTR                7 (NULL|self + encode)
            116 CALL                     0
            124 STORE_FAST               2 (current_user)

 25         126 LOAD_GLOBAL              9 (NULL + bytes)
            136 LOAD_CONST               5 (<code object <genexpr> at 0x772c353ccc30, file "<catalyst_core>", line 25>)
            138 MAKE_FUNCTION            0
            140 LOAD_GLOBAL             11 (NULL + enumerate)
            150 LOAD_FAST                2 (current_user)
            152 CALL                     1
            160 GET_ITER
            162 CALL                     0
            170 CALL                     1
            178 STORE_FAST               3 (user_signature)


....
 32         254 LOAD_FAST                3 (user_signature)
            256 LOAD_FAST                0 (LEAD_RESEARCHER_SIGNATURE)
            258 COMPARE_OP              40 (==)
            262 POP_JUMP_IF_FALSE      112 (to 488)

 33         264 LOAD_GLOBAL             17 (NULL + art)
            274 LOAD_ATTR               18 (tprint)
            294 LOAD_CONST               8 ('AUTHENTICATION   SUCCESS') # the goal
            296 LOAD_CONST               9 ('small')
            298 KW_NAMES                10 (('font',))
            300 CALL                     2
            308 POP_TOP

...
 37         354 LOAD_GLOBAL             21 (NULL + ARC4)
            364 LOAD_FAST                2 (current_user)
            366 CALL                     1
            374 STORE_FAST               5 (arc4_decipher)

 38         376 LOAD_FAST                5 (arc4_decipher)
            378 LOAD_ATTR               23 (NULL|self + decrypt)
            398 LOAD_FAST                1 (ENCRYPTED_CHIMERA_FORMULA)
            400 CALL                     1
            408 LOAD_ATTR               25 (NULL|self + decode)
            428 CALL                     0
            436 STORE_FAST               6 (decrypted_formula)

So the idea would be to see how the current_user is transformed in the user_signature so that we can reverse that process. The transformation is done through a generator which is defined as:

Disassembly of <code object <genexpr> at 0x772c353ccc30, file "<catalyst_core>", line 25>:
 25           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0
              6 LOAD_FAST                0 (.0)
        >>    8 FOR_ITER                15 (to 42)
             12 UNPACK_SEQUENCE          2
             16 STORE_FAST               1 (i)
             18 STORE_FAST               2 (c)
             20 LOAD_FAST                2 (c)
             22 LOAD_FAST                1 (i)
             24 LOAD_CONST               0 (42)
             26 BINARY_OP                0 (+)
             30 BINARY_OP               12 (^)
             34 YIELD_VALUE              1
             36 RESUME                   1
             38 POP_TOP
             40 JUMP_BACKWARD           17 (to 8)

To not manually revert this, I gave it to GPT which did a pretty good job in converting the disassembled code into source code. After a couple of prompts I got the correct result of the generator expression:

user_signature = bytes( (c ^ (i + 42)) for i, c in enumerate(current_user))

Since we know that the user_signature should match LEAD_RESEARCHER_SIGNATURE, we just need to reverse the xor operation. Which is:

goal = bytes( (c ^ (i + 42)) for i, c in enumerate(LEAD_RESEARCHER_SIGNATURE)) 

We then get that the current user name should be:G0ld3n_Tr4nsmut4t10n.

We can then just replace the login name with the one computed earlier (os.getlogin = lambda: "G0ld3n_Tr4nsmut4t10n") and we get the flag.