PBCTF: Switching it Up

October 17th 2021

Uh yes, I like python, surely reversing it will be no problem - me

Challenge

I upgraded our flag checker to Python 3.10.0b1. Can you figure it out?

Solve

First we need to get the disassembled code, for that we can just use python3.10, so let's compile that and then just do

import marshal
import dis

with open("challenge.cpython-310.pyc", "rb") as f:
  d = f.read()

dis.dis(marshal.loads(d[16:]))

and we get nicely decompiled source code, quickly copy the code into a text-editor and make the variables with zero-width spaces as names into more expressive variable names, like a and b (lol).

(Bytecode is usually in this form: line_number? byte_num instr_name op_arg (decoded_op_args)?)

It first does an import to use dataclasses

  1           0 LOAD_NAME                0 (__import__)
              2 LOAD_CONST               0 ('dataclasses')
              4 CALL_FUNCTION            1
              6 LOAD_ATTR                1 (dataclass)

==> import dataclasses

Then it creates a class with an int variable x and a y variable which is a string.

  3           8 LOAD_BUILD_CLASS
             10 LOAD_CONST               1 (<code object ᅠ at 0x7f3d7d65b920, file "challenge.py", line 3>)
             12 LOAD_CONST               2 ('ᅠ')
             14 MAKE_FUNCTION            0
             16 LOAD_CONST               2 ('ᅠ')
             18 CALL_FUNCTION            2
             20 CALL_FUNCTION            1
             22 STORE_NAME               2 (ᅠ)

together with the line 3 code object:

Disassembly of <code object ᅠ at 0x7f3d7d65b920, file "challenge.py", line 3>:
  3           0 LOAD_NAME                0 (__name__)
              2 STORE_NAME               1 (__module__)
              4 LOAD_CONST               0 ('ᅠ')
              6 STORE_NAME               2 (__qualname__)
              8 SETUP_ANNOTATIONS
             10 LOAD_NAME                3 (int)
             12 LOAD_NAME                4 (__annotations__)
             14 LOAD_CONST               1 ('x')
             16 STORE_SUBSCR

  5          18 LOAD_NAME                5 (str)
             20 LOAD_NAME                4 (__annotations__)
             22 LOAD_CONST               2 ('y')
             24 STORE_SUBSCR
             26 LOAD_CONST               3 (None)
             28 RETURN_VALUE

Then it decodes a tuple by xoring every element with 1337 (In a lambda further below) converting the output string to bytes, which gives us b"PBCTF"

  4          24 LOAD_NAME                3 (bytes)
             26 LOAD_CONST               3 (<code object <genexpr> at 0x7f3d7d65b9d0, file "challenge.py", line 8>)
             28 LOAD_CONST               4 ('<genexpr>')
             30 MAKE_FUNCTION            0
             32 LOAD_CONST               5 ((1385, 1403, 1402, 1389, 1407))
             34 GET_ITER
             36 CALL_FUNCTION            1
             38 CALL_FUNCTION            1
             40 STORE_NAME               4 (ᅠᅠᅠ)
Disassembly of <code object <genexpr> at 0x7fe4c44ab310, file "challenge.py", line 8>:
              0 GEN_START                0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                 7 (to 20)

  8           6 STORE_FAST               1 (e)
              8 LOAD_FAST                1 (e)
             10 LOAD_CONST               0 (1337)
             12 BINARY_XOR
             14 YIELD_VALUE
             16 POP_TOP
             18 JUMP_ABSOLUTE            2 (to 4)
        >>   20 LOAD_CONST               1 (None)
             22 RETURN_VALUE

Then it does a,b,c,d,e,f,*R,g = input("flag?") to read the input, checks the a, b, c, d, e, f, g variables to match the flag format and that the length of R is 32.

10          56 LOAD_NAME                7 (input)
             58 LOAD_CONST               8 ('flag? ')
             60 CALL_FUNCTION            1
             62 STORE_NAME               8 (I)
 12          72 LOAD_NAME               10 (list)
             74 LOAD_NAME                8 (I)
             76 CALL_FUNCTION            1
             78 BUILD_LIST               1
             80 STORE_NAME              11 (ᅠᅠᅠᅠᅠ)
 20     >>   94 LOAD_NAME               11 (ᅠᅠᅠᅠᅠ)
             96 LOAD_METHOD             14 (pop)
             98 LOAD_CONST              12 (0)
            100 CALL_METHOD              1

 21         102 DUP_TOP
            104 MATCH_SEQUENCE
            106 POP_JUMP_IF_FALSE      122 (to 244)
            108 GET_LEN
            110 LOAD_CONST              13 (7)
            112 COMPARE_OP               5 (>=)
            114 POP_JUMP_IF_FALSE      122 (to 244)
            116 EXTENDED_ARG             1
            118 UNPACK_EX              262
            120 LOAD_CONST              14 ('p')
            122 COMPARE_OP               2 (==)
            124 POP_JUMP_IF_FALSE      116 (to 232)
            126 LOAD_CONST              15 ('b')
            128 COMPARE_OP               2 (==)
            130 POP_JUMP_IF_FALSE      117 (to 234)
            132 LOAD_CONST              16 ('c')
            134 COMPARE_OP               2 (==)
            136 POP_JUMP_IF_FALSE      118 (to 236)
            138 LOAD_CONST              17 ('t')
            140 COMPARE_OP               2 (==)
            142 POP_JUMP_IF_FALSE      119 (to 238)
            144 LOAD_CONST              18 ('f')
            146 COMPARE_OP               2 (==)
            148 POP_JUMP_IF_FALSE      120 (to 240)
            150 LOAD_CONST              19 ('{')
            152 COMPARE_OP               2 (==)
            154 POP_JUMP_IF_FALSE      121 (to 242)
            156 ROT_TWO
            158 LOAD_CONST              20 ('}')
            160 COMPARE_OP               2 (==)
            162 POP_JUMP_IF_FALSE      122 (to 244)
            164 STORE_NAME              15 (R)
            166 POP_TOP
 22         168 LOAD_NAME               12 (ᅠᅠ)
            170 LOAD_NAME               13 (ᅠᅠᅠᅠᅠᅠᅠ)
            172 LOAD_CONST              21 (1)
            174 BINARY_ADD
            176 DUP_TOP
            178 STORE_NAME              13 (ᅠᅠᅠᅠᅠᅠᅠ)
            180 POP_JUMP_IF_TRUE        97 (to 194)
            182 LOAD_NAME               16 (len)
            184 LOAD_NAME               15 (R)
            186 CALL_FUNCTION            1
            188 LOAD_CONST              22 (32)
            190 COMPARE_OP               3 (!=)
            192 POP_JUMP_IF_FALSE      101 (to 202)
        >>  194 LOAD_CONST              12 (0)
            196 LOAD_CONST              23 (112)
            198 BUILD_MAP                1
            200 JUMP_FORWARD             1 (to 204)
        >>  202 BUILD_MAP                0
        >>  204 INPLACE_OR
            206 STORE_NAME              12 (ᅠᅠ)

Then it enters a while loop with condition that R is not empty. In every iteration it pops one element from R and compares it to the xth char of md5(b"PBCTF"*x).hexdigest()

 25         276 LOAD_NAME               12 (ᅠᅠ)
            278 LOAD_NAME               13 (ᅠᅠᅠᅠᅠᅠᅠ)
            280 LOAD_CONST              21 (1)
            282 BINARY_ADD
            284 DUP_TOP
            286 STORE_NAME              13 (ᅠᅠᅠᅠᅠᅠᅠ)
            288 LOAD_NAME               18 (x)
            290 LOAD_CONST              21 (1)
            292 BINARY_ADD
            294 COMPARE_OP               3 (!=)
            296 POP_JUMP_IF_TRUE       159 (to 318)
            298 LOAD_NAME                9 (ᅠᅠᅠᅠ)
            300 LOAD_NAME                4 (ᅠᅠᅠ)
            302 LOAD_NAME               18 (x)
            304 BINARY_MULTIPLY
            306 CALL_FUNCTION            1
            308 LOAD_NAME               18 (x)
            310 BINARY_SUBSCR
            312 LOAD_NAME               19 (y)
            314 COMPARE_OP               3 (!=)
            316 POP_JUMP_IF_FALSE      163 (to 326)
        >>  318 LOAD_NAME               18 (x)
            320 LOAD_NAME               19 (y)
            322 BUILD_MAP                1
            324 JUMP_FORWARD             1 (to 328)
        >>  326 BUILD_MAP                0
        >>  328 INPLACE_OR
            330 STORE_NAME              12 (ᅠᅠ)
            332 JUMP_FORWARD             8 (to 350)
        >>  334 POP_TOP
Disassembly of <code object <lambda> at 0x7fe4c44ab520, file "challenge.py", line 11>:
 11           0 LOAD_GLOBAL              0 (ᅠᅠᅠᅠᅠᅠ)
              2 LOAD_FAST                0 (x)
              4 CALL_FUNCTION            1
              6 LOAD_METHOD              1 (hexdigest)
              8 CALL_METHOD              0
             10 RETURN_VALUE

Here x is the current index of the char being compared and y the char being compared (Because it's a for x,y in enumerate(R):). If the characters match we or a dict with {} (Leaving it the way it was), else we or it with {x:y} which means it's non-empty and would evaluate to true...

After the loop we do if global_dict: to check if anything's inside, and print wrong if so.

30     >>  354 LOAD_NAME               20 (print)
            356 LOAD_NAME               12 (ᅠᅠ)
            358 POP_JUMP_IF_TRUE       185 (to 370)
            360 LOAD_CONST              27 ('Correct')
            362 CALL_FUNCTION            1
            364 POP_TOP
            366 LOAD_CONST              29 (None)
            368 RETURN_VALUE
        >>  370 LOAD_CONST              28 ('Nope')
            372 CALL_FUNCTION            1
            374 POP_TOP
            376 LOAD_CONST              29 (None)
            378 RETURN_VALUE

So to generate the characters inside the flag, one can just do

import hashlib
a=""
for i in range(32):
    a+=hashlib.md5(b"PBCTF"*i).hexdigest()[i]
print(a)

and we get the flag pbctf{dece0227383ca2ac793545ee989ce386}


< PBCTF: NP - Not Password | PolyRing >