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 >