bytecode-me
Solved by: grb
This is what the challenge looks like, there's a text file attached with it.

Dumping the file, it looks like that we get a python bytecode dump.
0 RESUME 0
1 LOAD_CONST 0 ('13171b180e20251c1d141f210e1b1849211b191322221e3347372f55242b3b234f3f3f2e2e552627404f402b39483f3d41484c4f3647')
STORE_NAME 0 (flag)
2 LOAD_CONST 1 ('pytecode')
STORE_NAME 1 (key)
4 LOAD_CONST 2 ('')
STORE_NAME 2 (user_input)
5 LOAD_CONST 2 ('')
STORE_NAME 3 (enc)
7 LOAD_NAME 4 (input)
PUSH_NULL
LOAD_CONST 3 ('enter flag: ')
CALL 1
STORE_NAME 2 (user_input)
9 LOAD_NAME 5 (range)
PUSH_NULL
LOAD_NAME 6 (len)
PUSH_NULL
LOAD_NAME 2 (user_input)
CALL 1
CALL 1
GET_ITER
L1: FOR_ITER 54 (to L2)
STORE_NAME 7 (i)
10 LOAD_NAME 8 (ord)
PUSH_NULL
LOAD_NAME 2 (user_input)
LOAD_NAME 7 (i)
BINARY_SUBSCR
CALL 1
LOAD_NAME 8 (ord)
PUSH_NULL
LOAD_NAME 1 (key)
LOAD_NAME 7 (i)
LOAD_NAME 6 (len)
PUSH_NULL
LOAD_NAME 1 (key)
CALL 1
BINARY_OP 6 (%)
BINARY_SUBSCR
CALL 1
BINARY_OP 12 (^)
STORE_NAME 9 (x)
11 LOAD_NAME 9 (x)
LOAD_NAME 7 (i)
BINARY_OP 0 (+)
LOAD_CONST 4 (256)
BINARY_OP 6 (%)
STORE_NAME 9 (x)
12 LOAD_NAME 3 (enc)
LOAD_NAME 10 (chr)
PUSH_NULL
LOAD_NAME 9 (x)
CALL 1
BINARY_OP 13 (+=)
STORE_NAME 3 (enc)
JUMP_BACKWARD 56 (to L1)
9 L2: END_FOR
POP_TOP
14 LOAD_NAME 3 (enc)
LOAD_ATTR 23 (encode + NULL|self)
CALL 0
LOAD_ATTR 25 (hex + NULL|self)
CALL 0
LOAD_NAME 0 (flag)
COMPARE_OP 88 (bool(==))
POP_JUMP_IF_FALSE 9 (to L3)
15 LOAD_NAME 13 (print)
PUSH_NULL
LOAD_CONST 5 ('👍')
CALL 1
POP_TOP
RETURN_CONST 7 (None)
17 L3: LOAD_NAME 13 (print)
PUSH_NULL
LOAD_CONST 6 ('jangan dikasih tau 🤫')
CALL 1
POP_TOP
RETURN_CONST 7 (None)
You might ask, "Yo grb, tf is python bytecode bruh", well, python bytecode is a low-level set of instructions that the python interpreter executes, specifically, the CPython virtual machine (the default Python implementation). So, bytecode is essentially a platform-independent representation of your program, a "Python assembly language." if you will.
So, lets try to translate these bytecodes into normal python source code.
Lines 1-5: constants and variable setup
1 LOAD_CONST 0 ('13171b180e20251c1d141f210e1b1849211b191322221e3347372f55242b3b234f3f3f2e2e552627404f402b39483f3d41484c4f3647')
STORE_NAME 0 (flag)
2 LOAD_CONST 1 ('pytecode')
STORE_NAME 1 (key)
4 LOAD_CONST 2 ('')
STORE_NAME 2 (user_input)
5 LOAD_CONST 2 ('')
STORE_NAME 3 (enc)
LOAD_CONST pushes a constant from the constant table onto the evaluation stack. STORE_NAME pops that value from the stack and assigns it to the name referenced by the given index (the name is shown in parentheses). So these four pairs create:
flag = "1317..."
key = "pytecode"
user_input = ""
enc = ""
Line 7: getting input
7 LOAD_NAME 4 (input)
PUSH_NULL
LOAD_CONST 3 ('enter flag: ')
CALL 1
STORE_NAME 2 (user_input)
LOAD_NAME 4 (input) loads the built-in input function object.LOAD_CONST 3 ('enter flag: ') loads the prompt string. CALL 1 calls the function on the stack with 1 positional argument (the prompt), returning the string the user typed. STORE_NAME 2 (user_input) stores that result into user_input. This corresponds to:
user_input = input("enter flag: ")
Lines 9-L1..L2: building the loop (range + iteration)
9 LOAD_NAME 5 (range)
PUSH_NULL
LOAD_NAME 6 (len)
PUSH_NULL
LOAD_NAME 2 (user_input)
CALL 1
CALL 1
GET_ITER
L1: FOR_ITER 54 (to L2)
STORE_NAME 7 (i)
...
JUMP_BACKWARD 56 (to L1)
9 L2: END_FOR
POP_TOP
LOAD_NAME 5 (range) loads range. Then it prepares the single argument len(user_input) by:
LOAD_NAME 6 (len)loads len.LOAD_NAME 2 (user_input)loads the string.CALL 1callslen(user_input).CALL 1then callsrange(len(user_input)).GET_ITERmakes an iterator from that range object.FOR_ITERis the loop driver: it yields next values from the iterator, or jumps to L2 when exhausted.STORE_NAME 7 (i)stores each yielded value intoi. So this is:
for i in range(len(user_input)):
...
Lines 10-12: the per-iteration computation
10 LOAD_NAME 8 (ord)
PUSH_NULL
LOAD_NAME 2 (user_input)
LOAD_NAME 7 (i)
BINARY_SUBSCR
CALL 1
LOAD_NAME 8 (ord)
PUSH_NULL
LOAD_NAME 1 (key)
LOAD_NAME 7 (i)
LOAD_NAME 6 (len)
PUSH_NULL
LOAD_NAME 1 (key)
CALL 1
BINARY_OP 6 (%)
BINARY_SUBSCR
CALL 1
BINARY_OP 12 (^)
STORE_NAME 9 (x)
11 LOAD_NAME 9 (x)
LOAD_NAME 7 (i)
BINARY_OP 0 (+)
LOAD_CONST 4 (256)
BINARY_OP 6 (%)
STORE_NAME 9 (x)
12 LOAD_NAME 3 (enc)
LOAD_NAME 10 (chr)
PUSH_NULL
LOAD_NAME 9 (x)
CALL 1
BINARY_OP 13 (+=)
STORE_NAME 3 (enc)
JUMP_BACKWARD 56 (to L1)
This chunk is the meat. Let's unpack it expression-by-expression, reconstructing the stack behaviour into readable code.
Getting ord(user_input[i])
LOAD_NAME 8 (ord)pushes ord function.LOAD_NAME 2 (user_input)pushes the string.LOAD_NAME 7 (i)pushes the index.BINARY_SUBSCRperformsuser_input[i].CALL 1callsord(user_input[i]).- => this yields
ord(user_input[i]).
Getting ord(key[i % len(key)]) :
LOAD_NAME 8 (ord)pushesord.LOAD_NAME 1 (key)pusheskey.LOAD_NAME 7 (i)andLOAD_NAME 6 (len)andLOAD_NAME 1 (key)plus aCALL 1combine to computelen(key)(similar pattern as before).BINARY_OP 6 (%)computesi % len(key).BINARY_SUBSCRthen computeskey[i % len(key)].CALL 1callsord(...)on that character.- => yields
ord(key[i % len(key)]).
Note: the dump shows two LOAD_NAME 8 (ord) calls because ord is used twice (once for input character, once for key character).
XOR and store x
BINARY_OP 12 (^)does bitwise XOR betweenord(user_input[i])andord(key[i % len(key)]).STORE_NAME 9 (x)stores result intox.
So after line 10 we have:
x = ord(user_input[i]) ^ ord(key[i % len(key)])
Add index and mod 256. Line 11:
LOAD_NAME 9 (x)pushes current x.LOAD_NAME 7 (i)pushes i.BINARY_OP 0 (+)computes x + i.LOAD_CONST 4 (256)pushes 256.BINARY_OP 6 (%)computes (x + i) % 256.STORE_NAME 9 (x)stores back to x.- => yields
x = (x + i) % 256
Append chr(x) to enc. Line 12:
LOAD_NAME 3 (enc)loads current enc string.LOAD_NAME 10 (chr)loads chr.LOAD_NAME 9 (x)loads computed numeric x.CALL 1callschr(x)to get a one-character string.BINARY_OP 13 (+=)performs string concatenation assignmentenc += chr(x).STORE_NAME 3 (enc)stores the updated string back.- => yields
enc += chr(x)ThenJUMP_BACKWARDreturns control toFOR_ITER(loop top) until iterator is exhausted.
Lines 14-17: final compare and prints
14 LOAD_NAME 3 (enc)
LOAD_ATTR 23 (encode + NULL|self)
CALL 0
LOAD_ATTR 25 (hex + NULL|self)
CALL 0
LOAD_NAME 0 (flag)
COMPARE_OP 88 (bool(==))
POP_JUMP_IF_FALSE 9 (to L3)
15 LOAD_NAME 13 (print)
PUSH_NULL
LOAD_CONST 5 ('👍')
CALL 1
POP_TOP
RETURN_CONST 7 (None)
17 L3: LOAD_NAME 13 (print)
PUSH_NULL
LOAD_CONST 6 ('jangan dikasih tau 🤫')
CALL 1
POP_TOP
RETURN_CONST 7 (None)
LOAD_NAME 3 (enc)pushes the built string enc.LOAD_ATTR 23 (encode)resolves the encode attribute ->enc.encode.CALL 0calls it with no args:enc.encode().LOAD_ATTR 25 (hex)resolves.hexmethod on the bytes result.CALL 0calls.hex()producing a hex string of the bytes.LOAD_NAME 0 (flag)pushes the stored flag constant.COMPARE_OP 88 (bool(==))compares whetherenc.encode().hex() == flag.POP_JUMP_IF_FALSE 9 (to L3)jumps to failure printing if comparison is false. If true: print 👍 and return None. If false: print jangan dikasih tau 🤫 and return None.
So that yields:
if enc.encode().hex() == flag:
print("👍")
else:
print("jangan dikasih tau 🤫")
Combining it all together
From the bytecode we reconstruct:
flag = "13171b180e20251c1d141f210e1b1849211b191322221e3347372f55242b3b234f3f3f2e2e552627404f402b39483f3d41484c4f3647"
key = "pytecode"
user_input = input("enter flag: ")
enc = ""
for i in range(len(user_input)):
x = (ord(user_input[i]) ^ ord(key[i % len(key)]))
x = (x + i) % 256
enc += chr(x)
if enc.encode().hex() == flag:
print("👍")
else:
print("jangan dikasih tau 🤫")
And to reverse the encryption, we just need to modify the code we reconstruct to look like this
flag_hex = "13171b180e20251c1d141f210e1b1849211b191322221e3347372f55242b3b234f3f3f2e2e552627404f402b39483f3d41484c4f3647"
key = "pytecode"
enc = bytes.fromhex(flag_hex)
out = ""
for i in range(len(enc)):
x = (enc[i] - i) % 256
c = x ^ ord(key[i % len(key)])
out += chr(c)
print(out)
And...

PO- PO- PO- PWNED!!!