The challenge input consists of a single .pyc file, which is a bytecode for a program run with python 3.8.
There are existing tools to decompile .pyc back to .py such as decompyle3 or uncompyle6, but unfortunately both of these tools and some others we tested fail to decompile the given pyc file. The two mentioned ones print the JIT opcodes in human readable format and print Parse error at or near 'None' instruction at offset -1
We started off trying to fix the pyc file, maybe something was modified manually? But in the end we just took a look at the JIT opcodes manually. It contains a lot code blocks looking similar to this:
4 LOAD_GLOBAL sum
6 LOAD_FAST 'password'
8 LOAD_CONST 0
10 LOAD_CONST 3
12 BUILD_SLICE_2 2
14 BINARY_SUBSCR
16 CALL_FUNCTION_1 1 ''
18 LOAD_CONST 222
L. 3 20 COMPARE_OP !=
22 POP_JUMP_IF_FALSE 28 'to 28'
24 LOAD_GLOBAL exit
26 LOAD_CONST -1
28_0 COME_FROM 22 '22'
L. 4 28 CALL_FUNCTION_1 1 ''
30 POP_TOP
32 LOAD_FAST 'password'
34 LOAD_CONST 0
36 BINARY_SUBSCR
38 LOAD_FAST 'password'
40 LOAD_CONST 1
42 BINARY_SUBSCR
44 BINARY_XOR
46 LOAD_FAST 'password'
48 LOAD_CONST 2
50 BINARY_SUBSCR
52 BINARY_XOR
54 LOAD_CONST 94
L. 5 56 COMPARE_OP !=
58 POP_JUMP_IF_FALSE 64 'to 64'
60 LOAD_GLOBAL exit
62 LOAD_CONST -1
64_0 COME_FROM 58 '58'
L. 6 64 CALL_FUNCTION_1 1 ''
As we can see there seems to be a local variable password
which is probably a string or list, maybe even the flag. First we get characters 0-3
from it and calculate their sum. This sum is required to be 222
or the function will call exit(-1)
. Afterwards the first three characters are XORed and the result needs to be 94
or the function will call exit(-1)
.
In python this logic would look something like this:
if sum(password[0:3]) != 222:
exit(-1)
if (password[0] ^ password[1] ^ password[2]) != 94:
exit(-1)
After the code above the pyc file repeats this process, always summing and xoring three, comparing the results to a constant.
The start and end are always incremented by one such that in the second step sum(password[1:4])
is calculated and so on.
At this point we just assumed the correct password is the flag we are looking for and we can confirm this is probably the case using:
password = [ord(x) for x in "HTB"]
if sum(password[0:3]) != 222:
exit(-1)
if (password[0] ^ password[1] ^ password[2]) != 94:
exit(-1)
print("the first three seem to be correct!")
Running above program prints the first three seem to be correct!
. This means we know the password we are looking for is porbably our flag as flags have the format HTB{...}
.
Given the first three characters as HTB
we can calculate the remaining letters one by one. After the sum(password[0:3]) != 222
check the pyc file checks sum(password[1:4]) != 273
. Thus we can calculate the forth character using chr(273 - ord("B") - ord("T"))
. Then the fifth character using the next sum check and so on.
In total there are 46 characters i.e. 46 sum(password[i : i + 3]) == z
checks. We used the following bash/grep line to extract all constants z
:
uncompyle6 bamboozled.pyc | sed '/^[[:space:]]*$/d' | grep -E -B 2 'COMPARE_OP\s+!=' | grep -E -A 1 'CALL' | grep -E 'LOAD_CONST' | grep -Eo '\s[0-9]+$' | tr '\n' ','
Which prints
# file bamboozled.pyc
# Deparsing stopped due to parse error
222, 273, 301, 356, 349, 341, 268, 262, 253, 305, 244, 202, 155, 158, 158, 156, 213, 258, 315, 257, 273, 218, 262, 245, 241, 256, 275, 321, 264, 196, 196, 247, 251, 270, 266, 309, 311, 259, 259, 241, 307, 263, 217, 210, 192, 265,
Using this data one can trivially calculate the full flag:
sum_results = [ 222, 273, 301, 356, 349, 341, 268, 262, 253, 305, 244, 202, 155, 158, 158, 156, 213, 258, 315, 257, 273, 218, 262, 245, 241, 256, 275, 321, 264, 196, 196, 247, 251, 270, 266, 309, 311, 259, 259, 241, 307, 263, 217, 210, 192, 265]
result = "HTB"
for i in range(1, len(sum_results)):
c = chr(sum_results[i] - ord(result[i]) - ord(result[i + 1]))
result += c
print("flag:", result)
Which prints:
flag: HTB{pyth0n_d155453mbl3r5_a1nt_50_h4rd_t0_br34k!}
We do not really need the constants from the XOR checks, but one can extract them using:
uncompyle6 bamboozled.pyc | sed '/^[[:space:]]*$/d' | grep -E -B 2 'COMPARE_OP\s+!=' | grep -E -A 1 'BINARY_XOR' | grep -E 'LOAD_CONST' | grep -Eo '\s[0-9]+$' | tr '\n' ','