Skip to content

Instantly share code, notes, and snippets.

@Aplet123
Last active April 9, 2024 14:12
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Aplet123/2cdb1656b47ccb8273c832d8f7299c3c to your computer and use it in GitHub Desktop.
Save Aplet123/2cdb1656b47ccb8273c832d8f7299c3c to your computer and use it in GitHub Desktop.
GoogleCTF 2022 Mixed Writeup: Creating Python Bytecode Tooling from Scratch

GoogleCTF 2022 Mixed Writeup: Creating Python Bytecode Tooling from Scratch

Foreword

I recently played GoogleCTF 2022 with DiceGang, and we managed to get 2nd place! We also got first blood on this challenge, which was a fun little bonus :)

I'd like to thank the GoogleCTF organizers for maintaining such a high quality throughout the years and making such fun challenges. I'd also like to thank my teammates, Ailuropoda Melanoleuca, clam, and cppio for working on this challenge with me.

The Challenge

A company I interview for gave me a test to solve. I'd decompile it and get the answer that way, but they seem to use some custom Python version... At least I know it's based on commit 64113a4ba801126028505c50a7383f3e9df29573.

Attached is a very long patch file that essentially randomly shuffles opcodes 0-90 and 90-200 along with the pyc file that constitutes the challenge.

Extracting the Bytecode

The first step to analyzing bytecode is, well, actually getting the bytecode. Luckily, this is pretty easy. pyc files are currently 16 bytes of metadata followed by a marshaled code object, so we can just trim out the metadata and use the marshal module to extract it:

import marshal

with open("x.pyc", "rb") as f:
    c = marshal.loads(f.read()[16:])

print(c)

Which prints out:

Traceback (most recent call last):
  File "dump.py", line 4, in <module>
    c = marshal.loads(f.read()[16:])
ValueError: bad marshal data (unknown type code)

Oh... right. Marshal is very version-dependent and my native python 3.10 interpreter can't load it. At this point you could build cpython from the commit hash given, but that takes precious time and it turns out the python 3.11 docker image is good enough. Running it again with the right version this time:

<code object <module> at 0x7f654aa6cc60, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 1>

We got a code object! Now, what can we do with this code object? If we run help on the code constructor, we can see a list of properties:

co_argcount
co_posonlyargcount
co_kwonlyargcount
co_nlocals
co_stacksize
co_flags
co_firstlineno
co_code
co_consts
co_names
co_varnames
co_freevars
co_cellvars
co_filename
co_name
co_qualname
co_linetable
co_exceptiontable

Most of these aren't actually useful for our reverse engineering purposes. Here are the ones we'll use:

  • co_code - the actual bytecode
  • co_consts - the constant pool referenced by the function
  • co_names - the globals referenced by the function
  • co_varnames - the locals referenced by the function
  • co_name - the name of the function

Now that that's out of the way, let's check up on our bytecode!

>>> len(c.co_code)
72

...wait that's way too short. Where's the rest of the bytecode?

>>> c.co_consts
(0, None, <code object ks at 0x7f10c822e670, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 5>, <code object cry at 0x7f10c816cb70, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 11>, <code object fail at 0x7f10c8160030, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 18>, <code object game1 at 0x558afe95cca0, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 23>, <code object game2 at 0x558afe4c9b00, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 68>, <code object game3 at 0x558afe64ee00, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 96>, <code object main at 0x558afe59a660, file "/usr/local/google/home/xxxxxxxxx/yyyy/zzzzzzzzzzzzzzzz/xxxxxxxxxxx.py", line 123>)

Oh. When you define a function, python loads the code object as a constant and makes a function from that. So, if we want to dump all the bytecode, we'll have to recursively look through the constants for code objects to dump as well. Here's a script that does that:

import marshal
import pickle

# read the bytecode
with open("x.pyc", "rb") as f:
    c = marshal.loads(f.read()[16:])

# get the code type for use in isinstance
code = type((lambda: 0).__code__)

# properties we're interested in
stuff = ["co_code", "co_consts", "co_names", "co_varnames"]

funcs = {}

def dump(f):
    # make a dict of just the important properties
    ser = {s: getattr(f, s) for s in stuff}
    for (i, x) in enumerate(f.co_consts):
        if isinstance(x, code):
            # replace code object with a string so we can pickle it
            ser["co_consts"] = list(ser["co_consts"])
            ser["co_consts"][i] = f"<code object {x.co_name}>"
            ser["co_consts"] = tuple(ser["co_consts"])
            # recursively dump the inner code object
            dump(x)
    funcs[f.co_name] = ser

dump(c)

print(list(funcs.keys()))
with open("dump.pickle", "wb") as f:
    pickle.dump(funcs, f)

Running this, we get our functions as well as a pickle dump with the bytecode:

['ks', 'cry', 'fail', 'w', 'game1', 'game2', 'game3', 'main', '<module>']

Mission success!

Let's check up on our bytecode just to make sure:

>>> funcs["<module>"]["co_code"]
b'a\x00`\x00`\x01\xab\x00\x00\x00\x00\x00\x00\x00\x00\x00\x97\x01`\x00`\x01\xab\x02\x00\x00\x00\x00\x00\x00\x00\x00`\x03c\x00\x97\x04`\x04c\x00\x97\x05`\x05c\x00\x97\x06`\x06c\x00\x97\x07`\x07c\x00\x97\x08`\x08c\x00\x97\t`\x011\x00'

...wait a second. What's that long string of null bytes doing? Is that supposed to be there?

>>> funcs["<module>"]["co_code"] in open("x.pyc", "rb").read()
False

Uh oh. Marshal is corrupting the bytecode for some reason. That's not good. It turns out that, starting in python 3.11, code object bytecode is actually validated and invalid opcodes are just nulled out. Note that, this isn't the case in python 3.10; you can stick whatever garbage you want into a code object and get it back out unscathed.

During the actual competition, we hackily wrote a script that extracted bytecode from the original pyc that was most similar to the one given by marshal. However, it was a huge mess that required a lot of finnicky human intervention to get right. Later, we found out about a completely undocumented _co_code_adapative property that just magically has the raw bytecode! How convenient! I'm definitely not crying my eyes out right now! Let's just quickly patch our dump:

ser["co_code"] = f._co_code_adaptive

...and one last check just to be sure:

>>> funcs["<module>"]["co_code"]
b'a\x00`\x00`\x01\xab\x00\x96\x00`\x00`\x01\xab\x01\x96\x01`\x00`\x01\xab\x02\x96\x02`\x02c\x00\x96\x03`\x03c\x00\x96\x04`\x04c\x00\x96\x05`\x05c\x00\x96\x06`\x06c\x00\x96\x07`\x07c\x00\x96\x08`\x08c\x00\x96\t`\x011\x00'
>>> funcs["<module>"]["co_code"] in open("x.pyc", "rb").read()
True

Much better. Mission success! (for real this time)

Crafting Interpreters Disassemblers

Now that we have the bytecode, we have to disassemble it. In a normal bytecode reversing challenge, I'd just use the builtin dis module, but that doesn't work because the opcodes are all messed up. So, we need a flexible disassembler that can remap known opcodes, but can handle unknown opcodes. How do we get a hold of one of these? We make it ourselves, of course!

From the implementation of dis, we can see that opcodes are always 2 bytes, where the first byte is the opcode and the second is the argument. So, let's jump right into it:

import fakeopcode as opcode
import pickle

with open("dump.pickle", "rb") as f:
    dump = pickle.load(f)

# where we'll put our known opcodes
lookup = {}

# make sure we didn't make any typos
assert all(x in opcode.opmap for x in lookup.values())

# figure out padding to make it look nicer :D
if lookup:
    padding = max(max(len(x) for x in lookup.values()) + 1, 6)
else:
    padding = 6


def disas(func):
    f = dump[func]
    c = f["co_code"]
    for i in range(0, len(c), 2):
        # extract opcode and argument
        (o, a) = c[i : i + 2]
        # get the name if we know it otherwise just prepend OP_ to it
        oprs = lookup.get(o, f"OP_{o:02x}")
        # padding to make it prettier :D
        opr = oprs.ljust(padding)
        # opcodes less than HAVE_ARGUMENT take no argument so we don't need to worry about it
        if o < opcode.HAVE_ARGUMENT:
            argr = ""
        else:
            # always display the argument as a number
            argr = f"{a:02x}"
            # but also maybe include extra statistics
            deet = []
            # get the unscrambled opcode if it's known
            og = opcode.opmap.get(lookup.get(o))
            # if the opcode is unknown, or it takes a name as an argument, display the name if it exists
            if a < len(f["co_names"]) and (og is None or og in opcode.hasname):
                n = f["co_names"][a]
                deet.append(f"name: {n}")
            # if the opcode is unknown, or it takes a local as an argument, display the local if it exists
            if a < len(f["co_varnames"]) and (og is None or og in opcode.haslocal):
                n = f["co_varnames"][a]
                deet.append(f"local: {n}")
            # if the opcode is unknown, or it takes a const as an argument, display the const if it exists
            if a < len(f["co_consts"]) and (og is None or og in opcode.hasconst):
                n = f["co_consts"][a]
                deet.append(f"const: {n!r}")
            # special disassembly for certain instructions that don't conform to the pattern
            if oprs == "LOAD_GLOBAL":
                n = f["co_names"][a // 2]
                if a & 1:
                    n = f"NULL + {n}"
                deet = [f"global: {n}"]
            elif oprs == "BINARY_OP":
                n = opcode._nb_ops[a][1]
                deet = [f"op: {n}"]
            elif oprs == "COMPARE_OP":
                n = opcode.cmp_op[a]
                deet = [f"cmp: {n}"]
            elif oprs in {"POP_JUMP_FORWARD_IF_FALSE", "JUMP_FORWARD", "FOR_ITER"}:
                deet = [f"jmp: {i + 2 + a * 2:04x}"]
            elif oprs in {"POP_JUMP_BACKWARD_IF_TRUE", "JUMP_BACKWARD"}:
                deet = [f"jmp: {i + 2 - a * 2:04x}"]
            if deet:
                argr += f" ({', '.join(deet)})"
        # hide CACHE and NOP
        if oprs not in {"CACHE", "NOP"}:
            print(f"{i:04x} :: {opr}{argr}")


disas("<module>")

Since I wanted to not have to build the specific commit hash, I fetched just opcode.py from github and saved it as fakeopcode.py. Running it we get:

0000 :: OP_61 00 (name: sys, const: 0)
0002 :: OP_60 00 (name: sys, const: 0)
0004 :: OP_60 01 (name: random, const: None)
0006 :: OP_ab 00 (name: sys, const: 0)
0008 :: OP_96 00 (name: sys, const: 0)
000a :: OP_60 00 (name: sys, const: 0)
000c :: OP_60 01 (name: random, const: None)
000e :: OP_ab 01 (name: random, const: None)
0010 :: OP_96 01 (name: random, const: None)
0012 :: OP_60 00 (name: sys, const: 0)
0014 :: OP_60 01 (name: random, const: None)
0016 :: OP_ab 02 (name: time, const: '<code object ks>')
0018 :: OP_96 02 (name: time, const: '<code object ks>')
001a :: OP_60 02 (name: time, const: '<code object ks>')
001c :: OP_63 00 (name: sys, const: 0)
001e :: OP_96 03 (name: ks, const: '<code object cry>')
0020 :: OP_60 03 (name: ks, const: '<code object cry>')
0022 :: OP_63 00 (name: sys, const: 0)
0024 :: OP_96 04 (name: cry, const: '<code object fail>')
0026 :: OP_60 04 (name: cry, const: '<code object fail>')
0028 :: OP_63 00 (name: sys, const: 0)
002a :: OP_96 05 (name: fail, const: '<code object game1>')
002c :: OP_60 05 (name: fail, const: '<code object game1>')
002e :: OP_63 00 (name: sys, const: 0)
0030 :: OP_96 06 (name: game1, const: '<code object game2>')
0032 :: OP_60 06 (name: game1, const: '<code object game2>')
0034 :: OP_63 00 (name: sys, const: 0)
0036 :: OP_96 07 (name: game2, const: '<code object game3>')
0038 :: OP_60 07 (name: game2, const: '<code object game3>')
003a :: OP_63 00 (name: sys, const: 0)
003c :: OP_96 08 (name: game3, const: '<code object main>')
003e :: OP_60 08 (name: game3, const: '<code object main>')
0040 :: OP_63 00 (name: sys, const: 0)
0042 :: OP_96 09 (name: main)
0044 :: OP_60 01 (name: random, const: None)
0046 :: OP_31

Perfect.

Getting Some Low-Hanging Fruit

Just looking at the names, we can see a lot of references to random, sys, time, and then the various functions. Let's see what happens when we compile the following code and disassemble it:

import random
import sys
import time

def a():
    pass

def b():
    pass

def c():
    pass

Becomes:

              0 RESUME                   0

  1           2 LOAD_CONST               0 (0)
              4 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (random)
              8 STORE_NAME               0 (random)

  2          10 LOAD_CONST               0 (0)
             12 LOAD_CONST               1 (None)
             14 IMPORT_NAME              1 (sys)
             16 STORE_NAME               1 (sys)

  3          18 LOAD_CONST               0 (0)
             20 LOAD_CONST               1 (None)
             22 IMPORT_NAME              2 (time)
             24 STORE_NAME               2 (time)

  5          26 LOAD_CONST               2 (<code object a at 0x7fbef1b2a340, file "wtmoo.py", line 5>)
             28 MAKE_FUNCTION            0
             30 STORE_NAME               3 (a)

  8          32 LOAD_CONST               3 (<code object b at 0x7fbef1b2a3f0, file "wtmoo.py", line 8>)
             34 MAKE_FUNCTION            0
             36 STORE_NAME               4 (b)

 11          38 LOAD_CONST               4 (<code object c at 0x7fbef1b2a4a0, file "wtmoo.py", line 11>)
             40 MAKE_FUNCTION            0
             42 STORE_NAME               5 (c)
             44 LOAD_CONST               1 (None)
             46 RETURN_VALUE

Now, we can just do some basic pattern matching to get our first opcodes:

lookup = {
    0x61: "RESUME",
    0x60: "LOAD_CONST",
    0x63: "MAKE_FUNCTION",
    0x97: "STORE_NAME",
    0x31: "RETURN_VALUE",
    0xAB: "IMPORT_NAME",
    0x96: "STORE_NAME"
}

Rerunning the disassembler:

0000 :: RESUME        00
0002 :: LOAD_CONST    00 (const: 0)
0004 :: LOAD_CONST    01 (const: None)
0006 :: IMPORT_NAME   00 (name: sys)
0008 :: STORE_NAME    00 (name: sys)
000a :: LOAD_CONST    00 (const: 0)
000c :: LOAD_CONST    01 (const: None)
000e :: IMPORT_NAME   01 (name: random)
0010 :: STORE_NAME    01 (name: random)
0012 :: LOAD_CONST    00 (const: 0)
0014 :: LOAD_CONST    01 (const: None)
0016 :: IMPORT_NAME   02 (name: time)
0018 :: STORE_NAME    02 (name: time)
001a :: LOAD_CONST    02 (const: '<code object ks>')
001c :: MAKE_FUNCTION 00
001e :: STORE_NAME    03 (name: ks)
0020 :: LOAD_CONST    03 (const: '<code object cry>')
0022 :: MAKE_FUNCTION 00
0024 :: STORE_NAME    04 (name: cry)
0026 :: LOAD_CONST    04 (const: '<code object fail>')
0028 :: MAKE_FUNCTION 00
002a :: STORE_NAME    05 (name: fail)
002c :: LOAD_CONST    05 (const: '<code object game1>')
002e :: MAKE_FUNCTION 00
0030 :: STORE_NAME    06 (name: game1)
0032 :: LOAD_CONST    06 (const: '<code object game2>')
0034 :: MAKE_FUNCTION 00
0036 :: STORE_NAME    07 (name: game2)
0038 :: LOAD_CONST    07 (const: '<code object game3>')
003a :: MAKE_FUNCTION 00
003c :: STORE_NAME    08 (name: game3)
003e :: LOAD_CONST    08 (const: '<code object main>')
0040 :: MAKE_FUNCTION 00
0042 :: STORE_NAME    09 (name: main)
0044 :: LOAD_CONST    01 (const: None)
0046 :: RETURN_VALUE

Cool! We've successfully recovered the disassembly! Just 8 more functions to go, how bad can it be?

Recovering main

0000 :: RESUME        00
0002 :: OP_32
0004 :: OP_b9         00 (name: print, local: seed, const: None)
0006 :: OP_20
0008 :: OP_20
000a :: OP_20
000c :: OP_20
000e :: OP_20
0010 :: LOAD_CONST    01 (const: 'Pass 3 tests to prove your worth!')
0012 :: OP_bf         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
0014 :: OP_20
0016 :: OP_8e         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
0018 :: OP_20
001a :: OP_20
001c :: OP_20
001e :: OP_20
0020 :: OP_01
... output trimmed

That's a lot of OP_20. Is the bytecode still corrupted somehow? Well, no. OP_20 is actually CACHE, which appears so often that dis actually hides it from disassembly. What does it do? Who knows. It might be related to this PEP but I don't care enough. Anyway, let's add it to the opcode list and move on:

0x20: "CACHE",

Disassembling again:

0000 :: RESUME        00
0002 :: OP_32
0004 :: OP_b9         00 (name: print, local: seed, const: None)
0010 :: LOAD_CONST    01 (const: 'Pass 3 tests to prove your worth!')
0012 :: OP_bf         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
0016 :: OP_8e         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
0020 :: OP_01
0022 :: LOAD_CONST    02 (const: 'seed:')
0024 :: OP_8d         00 (name: print, local: seed, const: None)
0026 :: OP_62         00 (name: print, local: seed, const: None)
0028 :: OP_32
002a :: OP_b9         02 (name: game2, const: 'seed:')
0036 :: OP_bf         00 (name: print, local: seed, const: None)
003a :: OP_8e         00 (name: print, local: seed, const: None)
0044 :: LOAD_CONST    03 (const: ':')
0046 :: OP_c6         00 (name: print, local: seed, const: None)
004a :: OP_c6         0d
004e :: OP_8d         00 (name: print, local: seed, const: None)
0050 :: OP_32
0052 :: OP_b9         00 (name: print, local: seed, const: None)
005e :: OP_62         00 (name: print, local: seed, const: None)
0060 :: OP_bf         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
0064 :: OP_8e         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
006e :: OP_01
0070 :: OP_62         00 (name: print, local: seed, const: None)
0072 :: OP_32
0074 :: OP_b9         04 (name: cry, const: "You can drive to work, know some maths and can type fast. You're hired!")
0080 :: OP_bf         00 (name: print, local: seed, const: None)
0084 :: OP_8e         00 (name: print, local: seed, const: None)
008e :: LOAD_CONST    03 (const: ':')
0090 :: OP_c6         00 (name: print, local: seed, const: None)
0094 :: OP_c6         0d
0098 :: OP_8d         00 (name: print, local: seed, const: None)
009a :: OP_32
009c :: OP_b9         00 (name: print, local: seed, const: None)
00a8 :: OP_62         00 (name: print, local: seed, const: None)
00aa :: OP_bf         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
00ae :: OP_8e         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
00b8 :: OP_01
00ba :: OP_62         00 (name: print, local: seed, const: None)
00bc :: OP_32
00be :: OP_b9         06 (const: b'\xa0?n\xa5\x7f)\x1f6Jvh\x95\xcc!\x1e\x95\x996a\x11\xf6OV\x88\xc1\x9f\xde\xb50\x9d\xae\x14\xde\x18YHI\xd8\xd5\x90\x8a\x181l\xb0\x16^O;]')
00ca :: OP_bf         00 (name: print, local: seed, const: None)
00ce :: OP_8e         00 (name: print, local: seed, const: None)
00d8 :: OP_c6         0d
00dc :: OP_8d         00 (name: print, local: seed, const: None)
00de :: OP_32
00e0 :: OP_b9         00 (name: print, local: seed, const: None)
00ec :: OP_62         00 (name: print, local: seed, const: None)
00ee :: OP_bf         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
00f2 :: OP_8e         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
00fc :: OP_01
00fe :: OP_32
0100 :: OP_b9         00 (name: print, local: seed, const: None)
010c :: OP_bf         00 (name: print, local: seed, const: None)
0110 :: OP_8e         00 (name: print, local: seed, const: None)
011a :: OP_01
011c :: OP_32
011e :: OP_b9         00 (name: print, local: seed, const: None)
012a :: LOAD_CONST    04 (const: "You can drive to work, know some maths and can type fast. You're hired!")
012c :: OP_bf         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
0130 :: OP_8e         01 (name: game1, const: 'Pass 3 tests to prove your worth!')
013a :: OP_01
013c :: OP_32
013e :: OP_b9         00 (name: print, local: seed, const: None)
014a :: LOAD_CONST    05 (const: 'Your sign-on bonus:')
014c :: OP_32
014e :: OP_b9         08
015a :: LOAD_CONST    06 (const: b'\xa0?n\xa5\x7f)\x1f6Jvh\x95\xcc!\x1e\x95\x996a\x11\xf6OV\x88\xc1\x9f\xde\xb50\x9d\xae\x14\xde\x18YHI\xd8\xd5\x90\x8a\x181l\xb0\x16^O;]')
015c :: OP_62         00 (name: print, local: seed, const: None)
015e :: OP_bf         02 (name: game2, const: 'seed:')
0162 :: OP_8e         02 (name: game2, const: 'seed:')
016c :: OP_92         05 (name: decode, const: 'Your sign-on bonus:')
0182 :: OP_bf         00 (name: print, local: seed, const: None)
0186 :: OP_8e         00 (name: print, local: seed, const: None)
0190 :: OP_bf         02 (name: game2, const: 'seed:')
0194 :: OP_8e         02 (name: game2, const: 'seed:')
019e :: OP_01
01a0 :: LOAD_CONST    00 (const: None)
01a2 :: RETURN_VALUE

It looks like it starts with a print of some kind. Let's compile print("x") to see what that looks like:

 2           2 LOAD_GLOBAL              1 (NULL + print)
             14 LOAD_CONST               1 ('x')
             16 PRECALL                  1
             20 CALL                     1
             30 POP_TOP

Pattern matching again:

0xB9: "LOAD_GLOBAL",
0xBF: "PRECALL",
0x8E: "CALL",
0x01: "POP_TOP",

I still don't know what OP_32 is, but if I had to guess it's probably PUSH_NULL. It only ever shows up right before LOAD_GLOBAL though, so I'll put it down as NOP so it stops showing up:

0x32: "NOP",

Next, looking at some more unknown opcodes:

0022 :: LOAD_CONST    02 (const: 'seed:')
0024 :: OP_8d         00 (name: print, local: seed, const: None)
0026 :: OP_62         00 (name: print, local: seed, const: None)
002a :: LOAD_GLOBAL   02 (global: game1)
0036 :: PRECALL       00
003a :: CALL          00
0044 :: LOAD_CONST    03 (const: ':')
0046 :: OP_c6         00 (name: print, local: seed, const: None)
004a :: OP_c6         0d
004e :: OP_8d         00 (name: print, local: seed, const: None)
0052 :: LOAD_GLOBAL   00 (global: print)
005e :: OP_62         00 (name: print, local: seed, const: None)
0060 :: PRECALL       01
0064 :: CALL          01
006e :: POP_TOP

There's some definite string manipulation going on here. OP_c6 is called with both 0 and 0xd as an argument, and after scrolling through the opcode list, BINARY_OP fits perfectly:

0022 :: LOAD_CONST    02 (const: 'seed:')
0024 :: OP_8d         00 (name: print, local: seed, const: None)
0026 :: OP_62         00 (name: print, local: seed, const: None)
002a :: LOAD_GLOBAL   02 (global: game1)
0036 :: PRECALL       00
003a :: CALL          00
0044 :: LOAD_CONST    03 (const: ':')
0046 :: BINARY_OP     00 (op: +)
004a :: BINARY_OP     0d (op: +=)
004e :: OP_8d         00 (name: print, local: seed, const: None)
0052 :: LOAD_GLOBAL   00 (global: print)
005e :: OP_62         00 (name: print, local: seed, const: None)
0060 :: PRECALL       01
0064 :: CALL          01
006e :: POP_TOP

By squinting hard enough, this kind of looks like:

seed = "seed:"
seed += game1() + ":"

Let's compile that and see if our hypothesis is correct:

  2           2 LOAD_CONST               1 ('seed:')
              4 STORE_FAST               0 (seed)
              
  3           6 LOAD_FAST                0 (seed)
              8 LOAD_GLOBAL              1 (NULL + game1)
             20 PRECALL                  0
             24 CALL                     0
             34 LOAD_CONST               2 (':')
             36 BINARY_OP                0 (+)
             40 BINARY_OP               13 (+=)
             44 STORE_FAST               0 (seed)

It was indeed. A bit more pattern matching gives us some more opcodes:

0xC6: "BINARY_OP",
0x8D: "STORE_FAST",
0x62: "LOAD_FAST",

One last unknown opcode:

013e :: LOAD_GLOBAL   00 (global: print)
014a :: LOAD_CONST    05 (const: 'Your sign-on bonus:')
014e :: LOAD_GLOBAL   08 (global: cry)
015a :: LOAD_CONST    06 (const: b'\xa0?n\xa5\x7f)\x1f6Jvh\x95\xcc!\x1e\x95\x996a\x11\xf6OV\x88\xc1\x9f\xde\xb50\x9d\xae\x14\xde\x18YHI\xd8\xd5\x90\x8a\x181l\xb0\x16^O;]')
015c :: LOAD_FAST     00 (local: seed)
015e :: PRECALL       02
0162 :: CALL          02
016c :: OP_92         05 (name: decode, const: 'Your sign-on bonus:')
0182 :: PRECALL       00
0186 :: CALL          00
0190 :: PRECALL       02
0194 :: CALL          02
019e :: POP_TOP

This definitely looks like it's trying to call something.decode(). Let's see what x.decode() compiles to:

  2           2 LOAD_GLOBAL              0 (x)
             14 LOAD_METHOD              1 (decode)
             36 PRECALL                  0
             40 CALL

So, OP_92 is LOAD_METHOD.

0x92: "LOAD_METHOD",

With that, we're done with main and have a full disassembly:

0000 :: RESUME        00
0004 :: LOAD_GLOBAL   00 (global: print)
0010 :: LOAD_CONST    01 (const: 'Pass 3 tests to prove your worth!')
0012 :: PRECALL       01
0016 :: CALL          01
0020 :: POP_TOP
0022 :: LOAD_CONST    02 (const: 'seed:')
0024 :: STORE_FAST    00 (local: seed)
0026 :: LOAD_FAST     00 (local: seed)
002a :: LOAD_GLOBAL   02 (global: game1)
0036 :: PRECALL       00
003a :: CALL          00
0044 :: LOAD_CONST    03 (const: ':')
0046 :: BINARY_OP     00 (op: +)
004a :: BINARY_OP     0d (op: +=)
004e :: STORE_FAST    00 (local: seed)
0052 :: LOAD_GLOBAL   00 (global: print)
005e :: LOAD_FAST     00 (local: seed)
0060 :: PRECALL       01
0064 :: CALL          01
006e :: POP_TOP
0070 :: LOAD_FAST     00 (local: seed)
0074 :: LOAD_GLOBAL   04 (global: game2)
0080 :: PRECALL       00
0084 :: CALL          00
008e :: LOAD_CONST    03 (const: ':')
0090 :: BINARY_OP     00 (op: +)
0094 :: BINARY_OP     0d (op: +=)
0098 :: STORE_FAST    00 (local: seed)
009c :: LOAD_GLOBAL   00 (global: print)
00a8 :: LOAD_FAST     00 (local: seed)
00aa :: PRECALL       01
00ae :: CALL          01
00b8 :: POP_TOP
00ba :: LOAD_FAST     00 (local: seed)
00be :: LOAD_GLOBAL   06 (global: game3)
00ca :: PRECALL       00
00ce :: CALL          00
00d8 :: BINARY_OP     0d (op: +=)
00dc :: STORE_FAST    00 (local: seed)
00e0 :: LOAD_GLOBAL   00 (global: print)
00ec :: LOAD_FAST     00 (local: seed)
00ee :: PRECALL       01
00f2 :: CALL          01
00fc :: POP_TOP
0100 :: LOAD_GLOBAL   00 (global: print)
010c :: PRECALL       00
0110 :: CALL          00
011a :: POP_TOP
011e :: LOAD_GLOBAL   00 (global: print)
012a :: LOAD_CONST    04 (const: "You can drive to work, know some maths and can type fast. You're hired!")
012c :: PRECALL       01
0130 :: CALL          01
013a :: POP_TOP
013e :: LOAD_GLOBAL   00 (global: print)
014a :: LOAD_CONST    05 (const: 'Your sign-on bonus:')
014e :: LOAD_GLOBAL   08 (global: cry)
015a :: LOAD_CONST    06 (const: b'\xa0?n\xa5\x7f)\x1f6Jvh\x95\xcc!\x1e\x95\x996a\x11\xf6OV\x88\xc1\x9f\xde\xb50\x9d\xae\x14\xde\x18YHI\xd8\xd5\x90\x8a\x181l\xb0\x16^O;]')
015c :: LOAD_FAST     00 (local: seed)
015e :: PRECALL       02
0162 :: CALL          02
016c :: LOAD_METHOD   05 (name: decode)
0182 :: PRECALL       00
0186 :: CALL          00
0190 :: PRECALL       02
0194 :: CALL          02
019e :: POP_TOP
01a0 :: LOAD_CONST    00 (const: None)
01a2 :: RETURN_VALUE

With this, we can write out main:

def main():
    print("Pass 3 tests to prove your worth!")
    seed = "seed:"
    seed += game1() + ":"
    print(seed)
    seed += game2() + ":"
    print(seed)
    seed += game3()
    print(seed)
    print()
    print("You can drive to work, know some maths and can type fast. You're hired!")
    print("Your sign-on bonus:", cry(b'\xa0?n\xa5\x7f)\x1f6Jvh\x95\xcc!\x1e\x95\x996a\x11\xf6OV\x88\xc1\x9f\xde\xb50\x9d\xae\x14\xde\x18YHI\xd8\xd5\x90\x8a\x181l\xb0\x16^O;]', seed).decode())

We also get a bit of a feel for how the program works: we have to win 3 games, and our inputs will be used to decrypt the flag.

Let the Games Begin

For this part, I'll just briefly go over new opcodes that appear and what they are. The process is the exact same: guess what the code might be, then compile it and pattern match the opcodes.

game3:

0024 :: LOAD_GLOBAL   02 (global: time)
0030 :: OP_bc         01 (name: time, local: text, const: 'Speed typing game.')
003a :: PRECALL       00
003e :: CALL          00

OP_bc is LOAD_ATTR (this is time.time())

007e :: LOAD_FAST     03 (local: it)
0082 :: LOAD_GLOBAL   06 (global: len)
008e :: LOAD_FAST     02 (local: words)
0090 :: PRECALL       01
0094 :: CALL          01
009e :: OP_68         03 (name: len, local: it, const: 1)
00a4 :: OP_ba         f2

OP_68 is COMPARE_OP and OP_ba is POP_JUMP_FORWARD_IF_FALSE (this is while it != len(words))

0124 :: LOAD_FAST                 02 (local: words)
0126 :: LOAD_CONST                00 (const: None)
0128 :: LOAD_FAST                 03 (local: it)
012a :: OP_88                     02 (name: split, local: words, const: '\n  Text: Because of its performance advantage, today many language implementations\n  execute a program in two phases, first compiling the source code into bytecode,\n  and then passing the bytecode to the virtual machine.\n  ')
012c :: OP_52

OP_88 is BUILD_SLICE and OP_52 is BINARY_SUBSCR (this is words[:it])

0144 :: OP_89                     01 (name: time, local: text, const: 'Speed typing game.')
0146 :: LOAD_CONST                09 (const: '\x1b[39m ')
0148 :: LOAD_FAST                 02 (local: words)
014a :: LOAD_FAST                 03 (local: it)
014c :: BINARY_SUBSCR
0156 :: OP_89                     01 (name: time, local: text, const: 'Speed typing game.')
0158 :: OP_8a                     04 (name: join, local: log, const: '_')

OP_89 is FORMAT_VALUE and OP_8a is BUILD_STRING (these are chunks of an f-string)

0240 :: OP_5e                     10
0244 :: LOAD_GLOBAL               0c (global: fail)
0250 :: LOAD_CONST                0b (const: 'You made a mistake!')
0252 :: PRECALL                   01
0256 :: CALL                      01
0260 :: POP_TOP

You have to do a bit of guessing here, but there was previously a jump to the fail call, so this is probably the the other branch of an if/else, meaning OP_89 is JUMP_FORWARD.

0262 :: LOAD_FAST                 03 (local: it)
0266 :: LOAD_GLOBAL               06 (global: len)
0272 :: LOAD_FAST                 02 (local: words)
0274 :: PRECALL                   01
0278 :: CALL                      01
0282 :: COMPARE_OP                03 (cmp: !=)
0288 :: OP_af                     f2

This is the end of the while loop, so OP_af is POP_JUMP_BACKWARD_IF_TRUE.

All the new opcodes:

0xBC: "LOAD_ATTR",
0x68: "COMPARE_OP",
0xBA: "POP_JUMP_FORWARD_IF_FALSE",
0x88: "BUILD_SLICE",
0x52: "BINARY_SUBSCR",
0x89: "FORMAT_VALUE",
0x8A: "BUILD_STRING",
0x5E: "JUMP_FORWARD",
0xAF: "POP_JUMP_BACKWARD_IF_TRUE",

Manually decompiled code:

def game3():
    print("Speed typing game.")
    t = time.time()
    text = '\n  Text: Because of its performance advantage, today many language implementations\n  execute a program in two phases, first compiling the source code into bytecode,\n  and then passing the bytecode to the virtual machine.\n  '
    words = text.split()
    it = 1
    log = "_"
    while it != len(words):
        print("%0.2f seconds left." % (20 - (time.time() - t)))
        print(f"\x1b[32m{' '.join(words[:it])}\x1b[39m {words[it]}")
        inp = input()
        if time.time() > t + 20:
            fail("Too slow!")
        if inp == words[it]:
            log += words[it].upper() + "_"
            it += 1
        else:
            fail("You made a mistake!")
    print("Nice!")
    return log

game2:

0022 :: OP_74                     00 (name: print, local: qs, const: None)
0024 :: LOAD_CONST                02 (const: (('sum', 12, 5), ('difference', 45, 14), ('product', 8, 9), ('ratio', 18, 6), ('remainder from division', 23, 7)))
0026 :: OP_5d                     01 (name: fail, local: log, const: 'Math quiz time!')

Again, this takes a bit of guessing, but list literals in python actually build an empty list, load a tuple, then extend the list, so OP_74 is BUILD_LIST and OP_5d is LIST_EXTEND.

002e :: LOAD_FAST                 00 (local: qs)
0030 :: OP_1f
0032 :: OP_b6                     c1
0034 :: STORE_FAST                02 (local: q)

OP_1f is GET_ITER and OP_b6 is FOR_ITER (this is for q in qs:)

005c :: LOAD_FAST                 02 (local: q)
005e :: OP_8f                     03 (name: input, local: x, const: '_')
0062 :: STORE_FAST                03 (local: x)
0064 :: STORE_FAST                04 (local: a)
0066 :: STORE_FAST                05 (local: b)

OP_8f is UNPACK_SEQUENCE (this is (x, a, b) = q)

0192 :: OP_7b                     b1
0196 :: LOAD_GLOBAL               02 (global: fail)
01a2 :: LOAD_CONST                0c (const: 'Wrong!')
01a4 :: PRECALL                   01
01a8 :: CALL                      01
01b2 :: POP_TOP
01b4 :: OP_7b                     c2
01b6 :: LOAD_FAST                 01 (local: log)
01b8 :: RETURN_VALUE

These are at the end of the for loops, so OP_7b is JUMP_BACKWARD.

All the new opcodes:

0x74: "BUILD_LIST",
0x5D: "LIST_EXTEND",
0x1F: "GET_ITER",
0xB6: "FOR_ITER",
0x8F: "UNPACK_SEQUENCE",
0x7B: "JUMP_BACKWARD",

Manually decompiled code:

def game2():
    print("Math quiz time!")
    qs = [('sum', 12, 5), ('difference', 45, 14), ('product', 8, 9), ('ratio', 18, 6), ('remainder from division', 23, 7)]
    log = "_"
    for q in qs:
        print("What is the %s of %d and %d?" % q)
        x, a, b = q
        if x == "sum":
            r = a + b
        elif x == "difference":
            r = a - b
        elif x == "product":
            r = a * b
        elif x == "ratio":
            r = a // b
        elif x == "remainder from division":
            r = a % b
        else:
            fail("What?")
        inp = int(input())
        if inp == r:
            print("Correct!")
            log += str(inp) + "_"
        else:
            fail("Wrong!")
    return log

game1:

003e :: LOAD_CONST                07 (const: '')
0040 :: STORE_FAST                06 (local: log)
0042 :: OP_1b
0046 :: LOAD_GLOBAL               02 (global: print)
0052 :: LOAD_CONST                09 (const: 'Fuel:')
0054 :: LOAD_FAST                 02 (local: fuel)
0056 :: PRECALL                   02
005a :: CALL                      02
0064 :: POP_TOP

I'm honestly not sure what OP_1b is. Based on the challenge source released after the competition, it's apparently LOAD_GLOBAL_BUILTIN? Not sure what that's about but it didn't end up mattering.

00d8 :: LOAD_FAST                 09 (local: j)
00da :: LOAD_FAST                 07 (local: i)
00dc :: OP_82                     02 (name: range, local: fuel, const: 1267034045110727999721745963007)
00de :: LOAD_FAST                 03 (local: x)
00e0 :: LOAD_FAST                 04 (local: y)
00e2 :: OP_82                     02 (name: range, local: fuel, const: 1267034045110727999721745963007)
00e4 :: COMPARE_OP                02 (cmp: ==)

OP_82 is BUILD_TUPLE (this is (j, i) == (x, y))

0114 :: LOAD_FAST                 09 (local: j)
0116 :: LOAD_FAST                 07 (local: i)
0118 :: BUILD_TUPLE               02
011a :: LOAD_FAST                 05 (local: stops)
011c :: OP_75                     00 (name: set, local: w, const: None)
011e :: POP_JUMP_FORWARD_IF_FALSE 06 (jmp: 012c)

This takes a bit of guessing, but OP_75 is clearly a conditional since it's before a POP_JUMP_FORWARD_IF_FALSE, and stops is a set, so you can guess that this is (j, i) in stops, meaning that OP_75 is CONTAINS_OP.

0202 :: LOAD_CONST                15 (const: (0, -1))
0204 :: LOAD_CONST                16 (const: (0, 1))
0206 :: LOAD_CONST                17 (const: (-1, 0))
0208 :: LOAD_CONST                18 (const: (1, 0))
020a :: LOAD_CONST                19 (const: ('w', 's', 'a', 'd'))
020c :: OP_b1                     04 (name: strip, local: y, const: (1, 1))
020e :: LOAD_FAST                 0b (local: c)
0210 :: BINARY_SUBSCR

OP_b1 is BUILD_CONST_KEY_MAP (this is {"w": (0, -1), "s": (0, 1), "a": (-1, 0), "d": (1, 0)}[c])

02ee :: LOAD_FAST                 06 (local: log)
02f0 :: OP_95                     02 (name: range, local: fuel, const: 1267034045110727999721745963007)
02f2 :: POP_TOP
02f4 :: RETURN_VALUE

Again, I had no clue what OP_95 was and it didn't end up mattering. Apparently it's COPY_FREE_VARS.

All the new opcodes:

0x82: "BUILD_TUPLE",
0x75: "CONTAINS_OP",
0xB1: "BUILD_CONST_KEY_MAP",

Manually decompiled code (the nested w function had no new opcodes):

def game1():
    def w(m, i, j):
        return (m >> (i * 10 + j)) & 1
    m = 1267034045110727999721745963007
    fuel = 8
    x, y = 1, 1
    stops = set([(5, 4), (3, 3)])
    log = ""
    print("Fuel:", fuel)
    for i in range(10):
        s = ""
        for j in range(10):
            if w(m, i, j):
                s += '🧱'
            elif (j, i) == (x, y):
                s += '🚓'
            elif (j, i) == (8, 8):
                s += '🏁'
            elif (j, i) in stops:
                s += '⛽️'
            else:
                s += '  '
        print(s)
    inp = input().strip()
    for c in inp:
        log += c
        if c not in "wasd":
            fail("Nope!")
        if fuel == 0:
            fail("Empty...")
        dx, dy = {"w": (0, -1), "s": (0, 1), "a": (-1, 0), "d": (1, 0)}[c]
        x += dx
        y += dy
        if w(m, y, x):
            fail("Crash!")
        fuel -= 1
        if (x, y) in stops:
            stops.remove((x, y))
            fuel += 15
        if (x, y) == (8, 8):
            print("Nice!")
            return log

fail and cry have no new opcodes so here they are:

def fail(s):
    print(s)
    print("Thanks for playing!")
    sys.exit(0)
    
def cry(s, seed):
    r = []
    for x, y in zip(ks(seed), s):
        r.append(x ^ y)
    return bytes(r)

ks:

0000 :: OP_03
0002 :: POP_TOP
0004 :: RESUME                    00
0008 :: LOAD_GLOBAL               00 (global: random)
0014 :: LOAD_ATTR                 01 (name: seed)
001e :: LOAD_FAST                 00 (local: seed)
0020 :: PRECALL                   01
0024 :: CALL                      01
002e :: POP_TOP
0030 :: OP_1b
0034 :: LOAD_GLOBAL               00 (global: random)
0040 :: LOAD_ATTR                 02 (name: randint)
004a :: LOAD_CONST                02 (const: 0)
004c :: LOAD_CONST                03 (const: 255)
004e :: PRECALL                   02
0052 :: CALL                      02
005c :: LOAD_CONST                04 (const: 13)
005e :: BINARY_OP                 05 (op: *)
0062 :: LOAD_CONST                05 (const: 17)
0064 :: BINARY_OP                 00 (op: +)
0068 :: LOAD_CONST                06 (const: 256)
006a :: BINARY_OP                 06 (op: %)
006e :: OP_45
0070 :: RESUME                    01
0072 :: POP_TOP
0074 :: JUMP_BACKWARD             22 (jmp: 0032)

This one's a bit special. It doesn't start with RESUME and has no RETURN_VALUE at the end. If you look at its usage in cry you can see why: it's actually a generator. Here's while True: yield 1:

  1           0 RETURN_GENERATOR
              2 POP_TOP
              4 RESUME                   0

  2           6 NOP

  3     >>    8 LOAD_CONST               2 (1)
             10 YIELD_VALUE
             12 RESUME                   1
             14 POP_TOP

  2          16 JUMP_BACKWARD            5 (to 8)

You can see that OP_03 is RETURN_GENERATOR and OP_45 is YIELD_VALUE.

All the new opcodes:

0x03: "RETURN_GENERATOR",
0x45: "YIELD_VALUE",

Manually decompiled code:

def ks(seed):
    random.seed(seed)
    while True:
        yield (random.randint(0, 255) * 13 + 17) % 256

Reaping the Fruits of our Labor

All of the hard work is done, and now we can kick back, relax, and get the flag. I recommend you do this part by yourself because it's pretty relaxing after all that tough reversing :)

game1 is a maze where you're driving a car using wasd controls to get to the flag and need to stop by both fuel stops along the way to make it:

Fuel: 8
🧱🧱🧱🧱🧱🧱🧱🧱🧱🧱
🧱🚓🧱  🧱        🧱
🧱  🧱      🧱  🧱🧱
🧱  🧱⛽️🧱🧱🧱  🧱🧱
🧱      🧱⛽️🧱    🧱
🧱🧱🧱🧱🧱  🧱🧱  🧱
🧱                🧱
🧱  🧱🧱🧱🧱🧱🧱🧱🧱
🧱              🏁🧱
🧱🧱🧱🧱🧱🧱🧱🧱🧱🧱

Here's a picture for people without monospace emoji fonts:

The solution is: sssddwwddwddsssdssaaawwssaaaassddddddd

game2 is a pretty simple math quiz that surrounds your answers with underscores.

The solution is: _17_31_72_3_2_

game3 is a speed typing game that surrounds the words with underscores and uppercases them.

The solution is: _BECAUSE_OF_ITS_PERFORMANCE_ADVANTAGE,_TODAY_MANY_LANGUAGE_IMPLEMENTATIONS_EXECUTE_A_PROGRAM_IN_TWO_PHASES,_FIRST_COMPILING_THE_SOURCE_CODE_INTO_BYTECODE,_AND_THEN_PASSING_THE_BYTECODE_TO_THE_VIRTUAL_MACHINE._

Combining all of these, we get the final seed: seed:sssddwwddwddsssdssaaawwssaaaassddddddd:_17_31_72_3_2_:_BECAUSE_OF_ITS_PERFORMANCE_ADVANTAGE,_TODAY_MANY_LANGUAGE_IMPLEMENTATIONS_EXECUTE_A_PROGRAM_IN_TWO_PHASES,_FIRST_COMPILING_THE_SOURCE_CODE_INTO_BYTECODE,_AND_THEN_PASSING_THE_BYTECODE_TO_THE_VIRTUAL_MACHINE._

Passing it into cry, we get the coveted flag: CTF{4t_l3ast_1t_w4s_n0t_4n_x86_opc0d3_p3rmut4tion}

Addendum

Here's the final opcode table:

lookup = {
    0x61: "RESUME",
    0x60: "LOAD_CONST",
    0x63: "MAKE_FUNCTION",
    0x97: "STORE_NAME",
    0x31: "RETURN_VALUE",
    0xAB: "IMPORT_NAME",
    0x96: "STORE_NAME",
    0x20: "CACHE",
    0xB9: "LOAD_GLOBAL",
    0xBF: "PRECALL",
    0x8E: "CALL",
    0x01: "POP_TOP",
    0x32: "NOP",
    0xC6: "BINARY_OP",
    0x8D: "STORE_FAST",
    0x62: "LOAD_FAST",
    0x92: "LOAD_METHOD",
    0xBC: "LOAD_ATTR",
    0x68: "COMPARE_OP",
    0xBA: "POP_JUMP_FORWARD_IF_FALSE",
    0x88: "BUILD_SLICE",
    0x52: "BINARY_SUBSCR",
    0x89: "FORMAT_VALUE",
    0x8A: "BUILD_STRING",
    0x5E: "JUMP_FORWARD",
    0xAF: "POP_JUMP_BACKWARD_IF_TRUE",
    0x74: "BUILD_LIST",
    0x5D: "LIST_EXTEND",
    0x1F: "GET_ITER",
    0xB6: "FOR_ITER",
    0x8F: "UNPACK_SEQUENCE",
    0x7B: "JUMP_BACKWARD",
    0x82: "BUILD_TUPLE",
    0x75: "CONTAINS_OP",
    0xB1: "BUILD_CONST_KEY_MAP",
    0x03: "RETURN_GENERATOR",
    0x45: "YIELD_VALUE",
}

Here's the reversed program, in full:

import sys
import random
import time

def ks(seed):
    random.seed(seed)
    while True:
        yield (random.randint(0, 255) * 13 + 17) % 256
        
def cry(s, seed):
    r = []
    for x, y in zip(ks(seed), s):
        r.append(x ^ y)
    return bytes(r)
        
def fail(s):
    print(s)
    print("Thanks for playing!")
    sys.exit(0)
    
def game1():
    def w(m, i, j):
        return (m >> (i * 10 + j)) & 1
    m = 1267034045110727999721745963007
    fuel = 8
    x, y = 1, 1
    stops = set([(5, 4), (3, 3)])
    log = ""
    print("Fuel:", fuel)
    for i in range(10):
        s = ""
        for j in range(10):
            if w(m, i, j):
                s += '🧱'
            elif (j, i) == (x, y):
                s += '🚓'
            elif (j, i) == (8, 8):
                s += '🏁'
            elif (j, i) in stops:
                s += '⛽️'
            else:
                s += '  '
        print(s)
    inp = input().strip()
    for c in inp:
        log += c
        if c not in "wasd":
            fail("Nope!")
        if fuel == 0:
            fail("Empty...")
        dx, dy = {"w": (0, -1), "s": (0, 1), "a": (-1, 0), "d": (1, 0)}[c]
        x += dx
        y += dy
        if w(m, y, x):
            fail("Crash!")
        fuel -= 1
        if (x, y) in stops:
            stops.remove((x, y))
            fuel += 15
        if (x, y) == (8, 8):
            print("Nice!")
            return log

def game2():
    print("Math quiz time!")
    qs = [('sum', 12, 5), ('difference', 45, 14), ('product', 8, 9), ('ratio', 18, 6), ('remainder from division', 23, 7)]
    log = "_"
    for q in qs:
        print("What is the %s of %d and %d?" % q)
        x, a, b = q
        if x == "sum":
            r = a + b
        elif x == "difference":
            r = a - b
        elif x == "product":
            r = a * b
        elif x == "ratio":
            r = a // b
        elif x == "remainder from division":
            r = a % b
        else:
            fail("What?")
        inp = int(input())
        if inp == r:
            print("Correct!")
            log += str(inp) + "_"
        else:
            fail("Wrong!")
    return log

def game3():
    print("Speed typing game.")
    t = time.time()
    text = '\n  Text: Because of its performance advantage, today many language implementations\n  execute a program in two phases, first compiling the source code into bytecode,\n  and then passing the bytecode to the virtual machine.\n  '
    words = text.split()
    it = 1
    log = "_"
    while it != len(words):
        print("%0.2f seconds left." % (20 - (time.time() - t)))
        print(f"\x1b[32m{' '.join(words[:it])}\x1b[39m {words[it]}")
        inp = input()
        if time.time() > t + 20:
            fail("Too slow!")
        if inp == words[it]:
            log += words[it].upper() + "_"
            it += 1
        else:
            fail("You made a mistake!")
    print("Nice!")
    return log

def main():
    print("Pass 3 tests to prove your worth!")
    seed = "seed:"
    seed += game1() + ":"
    print(seed)
    seed += game2() + ":"
    print(seed)
    seed += game3()
    print(seed)
    print()
    print("You can drive to work, know some maths and can type fast. You're hired!")
    print("Your sign-on bonus:", cry(b'\xa0?n\xa5\x7f)\x1f6Jvh\x95\xcc!\x1e\x95\x996a\x11\xf6OV\x88\xc1\x9f\xde\xb50\x9d\xae\x14\xde\x18YHI\xd8\xd5\x90\x8a\x181l\xb0\x16^O;]', seed).decode())
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment