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' ','