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.
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.
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 bytecodeco_consts
- the constant pool referenced by the functionco_names
- the globals referenced by the functionco_varnames
- the locals referenced by the functionco_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)
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.
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?
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.
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
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}
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())