Skip to content

Instantly share code, notes, and snippets.

@pgy
Last active June 30, 2019 19:35
Show Gist options
  • Save pgy/13fd7b188890e6c420e479a2d210ab0e to your computer and use it in GitHub Desktop.
Save pgy/13fd7b188890e6c420e479a2d210ab0e to your computer and use it in GitHub Desktop.
0ctf2017 py writeup

0ctf2017 - py

We permutate the opcode of python2.7, and use it to encrypt the flag.. Try to recover it!

The provided pyc file could be parsed with the marshal module, yielding a code object representing a python module. Examining its co_{name,argcount,...} attributes showed that it had 3 names ('rotor', 'encrypt', 'decrypt') and four constants (-1, None, and two code objects). The module's bytecode could not be disassembled of course. Originally I thought by 'permutate' they meant 'rearrange' but later we realized it was 'substitute' instead.

co_name         '<module>'
co_flags        64
co_firstlineno  1
co_argcount     0
co_nlocals      0
co_stacksize    2
co_names        ('rotor', 'encrypt', 'decrypt')
co_varnames     ()
co_consts       (-1, None, <code object encrypt>, <code object decrypt>)
co_code         9900009901008600009100009902008800009101009903008...

The two inner code objects were functions called encrypt and decrypt. The only difference between them were the strings "encrypt" and "decrypt" and their co_firstlineno arguments. Their co_code attributes were the same too, thus we could focus only on decrypt.

co_name         'decrypt'
co_flags        67
co_firstlineno  10
co_argcount     1
co_nlocals      6
co_stacksize    3
co_names        ('rotor', 'newrotor', 'decrypt')
co_varnames     ('data', 'key_a', 'key_b', 'key_c', 'secret', 'rot')
co_consts       (None, '!@#$%^&*', 'abcdefgh', '<>{}:"', 4, '|', 2, 'EOF')
co_code         99010068010099020068020099030068030061010099040046...

Using these attributes as reference I managed to create a module skeleton that had the same properties as the one in the pyc file:

 1| import rotor
 2| def encrypt(data):
 3|     pass
 4|
 5|
 6|
 7|
 8|
 9|
10| def decrypt(data):
11|     pass

We compared this module's bytecode to the one in the pyc file and realized that permutation was in fact 'substitution' and that only the opcodes were replaced, the arguments -- usually the 2nd and 3rd byte in an instruction -- remained the same. We came up with this substitution table to fix some of the instructions:

SHOULD_BE = {
    0x68: 0x7d,     # BUILD_SET should be STORE_FAST
    0x86: 0x6c,     # ...
    0x88: 0x84,
    0x91: 0x5a,
    0x99: 0x64,
}

The python compiler inserts objects into the co_names, co_varnames, and co_consts attributes in the same order as it encounters them while walking the ast of the compiled function. Using this property of the compiler, the partially fixed bytecode, and the source code of the rotor module found on the Internet we had an incomplete idea about the structure of the decrypt function:

10| def decrypt(data):
11|     key_a = '!@#$%^&*'
12|     key_b = 'abcdefgh'
13|     key_c = 'abcdefgh'
14|     secret = ?????????
15|     rot = rotor.newrotor(secret)
16|     return rot.decrypt(data)

At very simple hand-rolled disassembler was used to examine the bytecode of the original decrypt function. Some unknown opcodes still remained, e.g. PRINT_EXPR (only used in the interactive repl), <39> (invalid opcode), and STORE_GLOBAL. PRINT_EXPR and <39> were definitely single-byte instructions, as they had valid three-byte instructions around them. We guessed that STORE_GLOBAL was LOAD_FAST, and DELETE_ATTR was LOAD_ATTR.

As the constants used by the functions were strings or numbers it became apparent that PRINT_EXPR was BINARY_MULTIPY and that <39> was BINARY_ADD.

SHOUD_BE = {
    0x68: 0x7d, # BUILD_SET should be STORE_FAST
    0x86: 0x6c, # ...
    0x88: 0x84,
    0x91: 0x5a,
    0x99: 0x64,
    0x61: 0x7c, # 
    0x27: 23,   # ADD
    0x46: 20,   # MULTIPLY
    0x9b: 116,  # LOAD_GLOBAL
    0x3c: 106,  # LOAD_ATTR
}

The 'secret' could be reconstructed from the fixed disassembly:

secret = key_a*4 + "|" + (key_b+key_a+key_c) * 2 + "|" + key_b*2 + "EOF"

The flag in the encrypted_flag file was: flag{Gue55_opcode_G@@@me}

The fixed disassembly of decrypt:

64 01 00    LOAD_CONST          was: 0x99 = <153>
7d 01 00    STORE_FAST          was: 0x68 = BUILD_SET
64 02 00    LOAD_CONST          was: 0x99 = <153>
7d 02 00    STORE_FAST          was: 0x68 = BUILD_SET
64 03 00    LOAD_CONST          was: 0x99 = <153>
7d 03 00    STORE_FAST          was: 0x68 = BUILD_SET
7c 01 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
64 04 00    LOAD_CONST          was: 0x99 = <153>
14          BINARY_MULTIPLY     was: 0x46 = PRINT_EXPR
64 05 00    LOAD_CONST          was: 0x99 = <153>
17          BINARY_ADD          was: 0x27 = <39>
7c 02 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
7c 01 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
17          BINARY_ADD          was: 0x27 = <39>
7c 03 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
17          BINARY_ADD          was: 0x27 = <39>
64 06 00    LOAD_CONST          was: 0x99 = <153>
14          BINARY_MULTIPLY     was: 0x46 = PRINT_EXPR
17          BINARY_ADD          was: 0x27 = <39>
64 05 00    LOAD_CONST          was: 0x99 = <153>
17          BINARY_ADD          was: 0x27 = <39>
7c 02 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
64 06 00    LOAD_CONST          was: 0x99 = <153>
14          BINARY_MULTIPLY     was: 0x46 = PRINT_EXPR
17          BINARY_ADD          was: 0x27 = <39>
64 07 00    LOAD_CONST          was: 0x99 = <153>
17          BINARY_ADD          was: 0x27 = <39>
7d 04 00    STORE_FAST          was: 0x68 = BUILD_SET
74 00 00    LOAD_GLOBAL         was: 0x9b = <155>
6a 01 00    LOAD_ATTR           was: 0x60 = DELETE_ATTR
7c 04 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
83 01 00    CALL_FUNCTION       TODO
7d 05 00    STORE_FAST          was: 0x68 = BUILD_SET
7c 05 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
6a 02 00    LOAD_ATTR           was: 0x60 = DELETE_ATTR
7c 00 00    LOAD_FAST           was: 0x61 = STORE_GLOBAL
83 01 00    CALL_FUNCTION       TODO
53          RETURN_VALUE        TODO
import dis, marshal, types, time, struct, io, sys
from collections import *
import subprocess
import binascii
from itertools import *
def flatten(code, path=[0]):
yield code, path
if type(code) != types.CodeType:
raise StopIteration()
for i, const in enumerate(code.co_consts):
for result in flatten(const, path + [i]):
yield result
def print_code_info(code):
print ""
print "=========== %r ===========" % (code.co_name, )
print "co_flags %s" % (code.co_flags, )
print "co_firstlineno %s" % (code.co_firstlineno, )
print "co_argcount %d" % (code.co_argcount, )
print "co_nlocals %d" % (code.co_nlocals, )
print "co_stacksize %d" % (code.co_stacksize, )
print "co_names %r" % (code.co_names, )
print "co_varnames %r" % (code.co_varnames, )
print "co_consts %r" % (code.co_consts, )
print "co_opcode bytes %r" % (binascii.hexlify(code.co_code), )
print
def loadmodule(filename):
with open(filename, "rb") as f:
f.read(8)
return marshal.load(f)
def loadfunc(module, name):
for const in module.co_consts:
if type(const) == types.CodeType and const.co_name == name:
return const
cryptmodule = loadmodule("crypt.pyc")
print(cryptmodule)
subprocess.call(["rm", "-f", "mycrypt.pyc"])
subprocess.call(["python", "-m", "py_compile", "mycrypt.py"])
mycryptmodule = loadmodule("mycrypt.pyc")
decrypt_0 = loadfunc(cryptmodule, "decrypt")
decrypt_1 = loadfunc(mycryptmodule, "decrypt")
TABLE = {
0x68: 0x7d, # BUILD_SET should be STORE_FAST
0x86: 0x6c, # ...
0x88: 0x84,
0x91: 0x5a,
0x99: 0x64,
0x61: 0x7c, #
0x27: 23, # ADD
0x46: 20, # MULTIPLY
0x9b: 116, # LOAD_GLOBAL
0x3c: 106, # LOAD_ATTR
}
hex = "{:02x}".format
def disasm(co):
print " ========= {} =========".format(co.co_name)
bs = list(bytearray(co.co_code))
while bs:
op = bs.pop(0)
r = " "
old = ""
if op in TABLE:
old = "was: 0x{} = {}".format(hex(op), dis.opname[op])
r = "ok"
op = TABLE[op]
arg0 = " "
arg1 = " "
if op >= dis.HAVE_ARGUMENT:
arg0 = hex(bs.pop(0))
arg1 = hex(bs.pop(0))
print "{}\t{} {} {} \t{:15} \t{}".format(r, hex(op), arg0, arg1, dis.opname[op], old)
print_code_info(cryptmodule)
disasm(cryptmodule)
print_code_info(decrypt_0)
disasm(decrypt_0)
import rotor
flag = open("encrypted_flag", "rb").read()
key_a = '!@#$%^&*'
key_b = 'abcdefgh'
key_c = '<>{}:"'
secret = key_a * 4 + '|' + (key_b + key_a + key_c) * 2 + '|' + key_b * 2 + "EOF"
rot = rotor.newrotor(secret)
print rot.decrypt(flag)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment