Skip to main content

bytecode-me

Solved by: grb

This is what the challenge looks like, there's a text file attached with it.

Challenge

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 1 calls len(user_input). CALL 1 then calls range(len(user_input)). GET_ITER makes an iterator from that range object.FOR_ITER is the loop driver: it yields next values from the iterator, or jumps to L2 when exhausted. STORE_NAME 7 (i) stores each yielded value into i. 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_SUBSCR performs user_input[i].
  • CALL 1 calls ord(user_input[i]).
  • => this yields ord(user_input[i]).

Getting ord(key[i % len(key)]) :

  • LOAD_NAME 8 (ord) pushes ord.
  • LOAD_NAME 1 (key) pushes key.
  • LOAD_NAME 7 (i) and LOAD_NAME 6 (len) and LOAD_NAME 1 (key) plus a CALL 1 combine to compute len(key) (similar pattern as before).
  • BINARY_OP 6 (%) computes i % len(key).
  • BINARY_SUBSCR then computes key[i % len(key)].
  • CALL 1 calls ord(...) 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 between ord(user_input[i]) and ord(key[i % len(key)]).
  • STORE_NAME 9 (x) stores result into x.

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 1 calls chr(x) to get a one-character string.
  • BINARY_OP 13 (+=) performs string concatenation assignment enc += chr(x).
  • STORE_NAME 3 (enc) stores the updated string back.
  • => yields enc += chr(x) Then JUMP_BACKWARD returns control to FOR_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 0 calls it with no args: enc.encode().
  • LOAD_ATTR 25 (hex) resolves .hex method on the bytes result.
  • CALL 0 calls .hex() producing a hex string of the bytes.
  • LOAD_NAME 0 (flag) pushes the stored flag constant.
  • COMPARE_OP 88 (bool(==)) compares whether enc.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...

PWNED!

PO- PO- PO- PWNED!!!