Skip to main content
  1. Posts/

hack.lu CTF 2021 Writeups

Tenbagger #

Challenge description:

I think I took it too far and made some trades and lost everything. My only chance to fix my account balance is a tenbagger.

We are given a pcap and open in Wireshark. It contains a lot of what looks like normal web browsing. But somewhere in there are a few FIX messages. FIX is the Financial Information eXchange protocol. First, I thought that we need to get the credentials from the FIX login, but there are no such packages. It is much simpler than that. In the text field, spread over multiple packages, there is the flag in plain text:

flag{t0_th3_m00n_4nd_b4ck}

Touchy Logger #

Challenge description:

Manfred always has the best insides for stock investments, but he hates to share it with his beloved brother. Can you help me out?

We are given a libinput log file. As we would expect from the challenge title, it consists of touch events – both swiping and tapping.

A teammate (who knows how html canvases work) and I (who has no idea about fancy web stuff) solved the challenge together.

Visualizing the input on an HMTL canvas, gives us the following image:

(PNG Image, 520 × 350 pixels).png

The dots at the bottom are interesting, because they are looking like typing on a screen keyboard. Drawing boxes around it looks like:

Untitled

Because of personal familiarity with it, we guessed that this must be the Ubuntu Gnome onscreen keyboard. And as we have the temporal order in the log, we can get the keys. Actually, there was a lot of tweaking the boxes and the annoyance of changing the keyboard layers involved until we got the correct text:

yfirefox
reddit.com r stocks
Twitter.com
r#StocksToBuy
hevolution
wolfgang47@stocks.iveThanks!!1!!11!!&Hi Wolfgang,Hi Wolfgang,

Count me in??? sounds like a great investment (:
Returnr rate of + -50% souns ds liketo good to be true.
I will invest 1337.69$

Hope Arnold did not mess with my Laptop again to steal my shit (-.-')
BTW using a Tablet now. This so called "keylogger" cannot work if there is no keyboard ;)

Best Regards,
Manfredhttps:  investment24.flu.xxx
ffluxmanfredOiVyi)=wi$?;Ezq-lZx#

Because of the random password, the detection must be good. Visiting the website with the credentials, gives us the flag. Here is the visualization code:

script.js

(function () {
  const draw = document.getElementById('draw')
  const ctx = draw.getContext('2d')
  const log = document.getElementById('input').value

  const parsed = document.getElementById('parsed')
  parsed.value = ''

  let location = { x: 0, y: 0 }

  const width = 13
  const hideCtrl = true

  const keyBoxes = []
  const mkKey = (k, x, y, w = null, h = null) => {
    w = w || width
    h = h || width;
    [x, y, w, h] = [x, y, w, h].map(e => e * 2)

    keyBoxes.push(
      { sx: x, sy: y, ex: x + w, ey: y + h, k }
    )
  }

  const findKey = (px, py) => {
    const key = keyBoxes.find(({ sx, sy, ex, ey }) => {
      return sx < px && sy < py && ex > px && ey > py
    })

    return key ? key.k : null
  }

  const logKey = (px, py) => {
    let key = findKey(px, py)

    if (key === null) {
      return
    }

    if (lMap[mode].hasOwnProperty(key)) {
      key = lMap[mode][key]
    }

    if (!key.includes('<') && hideCtrl) {
      parsed.value += shift ? key.toUpperCase() : key
      shift = false
      return
    }

    shift = key === '<shiftl>' || key === '<shiftr>';

    if (key === '<l2>') {
      mode = 'l2'
    }

    if (key === '<l3>') {
      mode = 'l3'
    }

    if (key === '<l1>') {
      mode = 'l1'
    }
  }

  const l1 = ',qwertyuiopasdfghjklzxcvbnm'.split('')
  const l2 = `_1234567890@#$%&-+()*"':;!?`.split('')
  const l3 = ``

  let shift = false

  const l2Map = Object.fromEntries(l1.map((k, i) => [k, l2[i]]))
  //const l3Map = Object.fromEntries(l2.map((k, i) => [k, l3[i]]))
  const l3Map = {}

  l2Map['<l2>'] = '<l1>'
  l2Map['<shiftl>'] = '<l3>'
  l2Map['<shiftr>'] = '<l3>'

  l3Map['<l2>'] = '<l1>'
  l3Map['<shiftl>'] = '<l2>'
  l3Map['<shiftr>'] = '<l2>'
  l3Map['j'] = '='

  let mode = 'l1'

  const lMap = {
    l1: {},
    l2: l2Map,
    l3: l3Map
  }

  console.log(lMap)

  'qwertyuiop'.split('').forEach((k, i) => {
    mkKey(k, 53 + i * width, 120)
  })

  mkKey('<bak>', 183, 120, 25)

  'asdfghjkl'.split('').forEach((k, i) => {
    mkKey(k, 60 + width * i, 133)
  })

  mkKey('\n', 177, 133, 30)

  mkKey('<shiftl>', 45, 146, 25)

  'zxcvbnm'.split('').forEach((k, i) => {
    mkKey(k, 70 + i * width, 146)
  })

  mkKey('<shiftr>', 161, 146, 40)

  mkKey('<l2>', 45, 159, 25)
  mkKey(',', 70, 159)
  mkKey(' ', 83, 159, 65)
  mkKey('.', 148, 159, 25)

  const match = l => l.match(/([0-9.]+)\/ ?([0-9.]+)mm/)

  let startLocation

  log.split('\n').map(l => l.trim()).forEach(l => {
    if (l.includes('TOUCH_DOWN')) {
      const cord = match(l)

      if (cord === null) {
        console.log(l)
        return
      }

      location = { x: cord[1], y: cord[2] }
      startLocation = { x: cord[1], y: cord[2] }

    } else if (l.includes('TOUCH_MOTION')) {
      const cord = match(l)

      if (cord === null) {
        console.log(l)
        return
      }

      location = { x: cord[1], y: cord[2] }

    } else if (l.includes('TOUCH_UP')) {
      const endKey = findKey(location.x * 2, location.y * 2)
      const startKey = findKey(startLocation.x * 2, startLocation.y * 2)

      if (startKey && endKey && startKey === endKey) {
        ctx.beginPath()
        ctx.moveTo(startLocation.x * 2, startLocation.y * 2)
        ctx.lineTo(location.x * 2, location.y * 2)
        ctx.stroke()
      }
      logKey(location.x * 2, location.y * 2)
      //location = { x: 0, y: 0 }
    }

  })

  console.log(keyBoxes)

  ctx.font = '10px serif';
  ctx.strokeStyle = 'red'

  keyBoxes.forEach(({ sx, sy, ex, ey, k }) => {
    ctx.fillText(k, sx + 5, sy + 8)
    ctx.strokeRect(sx, sy, ex - sx, ey - sy)
  })
})()

visual.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>

<textarea id="parsed" style="width: 800px; height: 600px"></textarea>
<br>

<canvas width="520" height="350" id="draw"></canvas>

<textarea id="input" style="display: none">
[...long log omitted...]
</textarea>
<script src="script.js"></script>
</body>
</html>

PYCOIN #

Challenge description:

A friend gave me this and he says he can not reverse this… but this is just python?

We are given a .pyc file. We can decompile it with uncompyle6 pycoin.pyc and get

import marshal
marshalled = b'\xe3\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\x00\x00\x00@\x00\x00\x00\xf3\xf0\x01\x00\x00n\x02tcd\x00d\x01l\x00m\x01Z\x01\x01\x00e\x02e\x03d\x02\x83\x01\x83\x01\xa0\x04\xa1\x00Z\x05e\x06e\x05\x83\x01d\x03k\x02\x90\x01o\xcee\x05d\x00\x19\x00d\x04k\x02\x90\x01o\xcee\x05d\x05\x19\x00e\x05d\x00\x19\x00d\x06\x17\x00k\x02\x90\x01o\xcee\x05d\x07\x19\x00e\x05d\x05\x19\x00e\x05d\x00\x19\x00\x18\x00d\x08n\n]\x02n\x02n\x02r\x04q\x04\x17\x00k\x02\x90\x01o\xcee\x05d\t\x19\x00d\nk\x02\x90\x01o\xcee\x05d\x0b\x19\x00e\x05d\x0c\x19\x00d\t\x14\x00d\r\x18\x00k\x02\x90\x01o\xcee\x05d\x0e\x19\x00e\x07e\x05\x83\x01d\x0f\x18\x00k\x02\x90\x01o\xcee\x05d\x06\x19\x00e\x05d\x10\x19\x00\x17\x00e\x05d\x11\x19\x00\x17\x00d\x12k\x02\x90\x01o\xcee\x08e\te\x05d\x10\x19\x00\x83\x01d\x07\x14\x00\x83\x01d\x05\x17\x00e\x05d\x13\x19\x00k\x02\x90\x01o\xcee\x05d\x14\x19\x00d\x15\x16\x00d\x03k\x02\x90\x01o\xcee\x05d\x13\x19\x00e\x05d\x14\x19\x00d\x07\x14\x00k\x02\x90\x01o\xcee\x01e\x05d\x11\x19\x00d\x16\x14\x00\x83\x01\xa0\n\xa1\x00d\x00\x19\x00d\x05\x18\x00e\x05d\t\x19\x00k\x02\x02\x00\x02\x00\x90\x01o\xcee\x05d\x0c\x19\x00d\x17k\x02\x90\x01o\xcee\x05d\x18\x19\x00e\x05d\x19\x19\x00d\x07\x1b\x00d\x07\x18\x00k\x02\x90\x01o\xcee\x05d\x1a\x19\x00e\x05d\x11\x19\x00e\x05d\x14\x19\x00\x14\x00d\x1b\x16\x00d\x07\x14\x00d\x05\x18\x00k\x02\x90\x01o\xcee\x05d\x19\x19\x00e\x05d\x18\x19\x00e\x05d\x13\x19\x00A\x00e\x05d\x1c\x19\x00A\x00d\t\x14\x00d\x1d\x18\x00k\x02\x90\x01o\xcee\x05d\x1c\x19\x00d\x1ek\x02Z\x0be\x0ce\x0b\x90\x01r\xe6d\x1fe\x05\xa0\r\xa1\x00\x9b\x00\x9d\x02n\x02d \x83\x01\x01\x00d!S\x00)"\xe9\x00\x00\x00\x00)\x01\xda\x03md5z\x1aplease supply a valid key:\xe9\x10\x00\x00\x00\xe9f\x00\x00\x00\xe9\x01\x00\x00\x00\xe9\x06\x00\x00\x00\xe9\x02\x00\x00\x00\xe9[\x00\x00\x00\xe9\x03\x00\x00\x00\xe9g\x00\x00\x00\xe9\x04\x00\x00\x00\xe9\x0b\x00\x00\x00\xe9*\x00\x00\x00\xe9\x05\x00\x00\x00i*\x05\x00\x00\xe9\x07\x00\x00\x00\xe9\n\x00\x00\x00i\x04\x01\x00\x00\xe9\t\x00\x00\x00\xe9\x08\x00\x00\x00\xe9\x11\x00\x00\x00\xf3\x01\x00\x00\x00a\xe97\x00\x00\x00\xe9\x0c\x00\x00\x00\xe9\x0e\x00\x00\x00\xe9\r\x00\x00\x00\xe9 \x00\x00\x00\xe9\x0f\x00\x00\x00\xe9\x17\x00\x00\x00\xe9}\x00\x00\x00z\x0bvalid key! z\x0einvalid key :(N)\x0eZ\x07hashlibr\x03\x00\x00\x00\xda\x03str\xda\x05input\xda\x06encode\xda\x01k\xda\x03len\xda\x03sum\xda\x03int\xda\x03chr\xda\x06digestZ\x07correct\xda\x05print\xda\x06decode\xa9\x00r)\x00\x00\x00r)\x00\x00\x00\xfa\r<disassembly>\xda\x08<module>\x01\x00\x00\x00sF\x00\x00\x00\x0c\x02\x10\x03\x0e\x01\n\xff\x04\x02\x12\xfe\x04\x03\x1a\xfd\x04\x04\n\xfc\x04\x05\x16\xfb\x04\x06\x12\xfa\x04\x07\x1a\xf9\x04\x08\x1e\xf8\x04\t\x0e\xf7\x04\n\x12\xf6\x04\x0b"\xf5\x04\x0c\n\xf4\x04\r\x16\xf3\x04\x0e"\xf2\x04\x0f&\xf1\x04\x10\n\xee\x02\x15'
exec(marshal.loads(marshalled))

Great! Python bytecode in python bytecode.

Prepending the pyc magic bytes and a date field to the marshalled bytes

head = b'\x55\x0d\x0d\x0a\x00\x00\x00\x00\x66\x39\x7d\x61\x15\x00\x00\x00'
l = marshal.loads(marshalled)
with open('co_code.pyc', 'wb+') as f:
    f.write(head + l.co_code)

allows us to save the program as as a working pyc file again.

Doing uncompyle6 co_code.pyc does not work. We found out the reason for this only after the CTF was over. They injected invalid instructions, so that the program would still work, but uncompyle6 and even pythons dis would not work. During the CTF, we solved it with manual reversing. After the CTF I managed to get dis to work, but this handcrafted bytecode could not be decompyled by any tool I tried.

marshalled = marshalled.replace(b'n\x02t', b'\x09\x09\x09', 1)
marshalled.replace(b'\x02\x02', b'\x09\x09', 1)
marshalled.replace(b'n\n]\x02n\x02', b'\x09' * 6, 1)

By the way, you can get opcodes from dis.opmap. E.g.,opmap['NOT'] = 9

Here is the disassembly with some of my comments

L.   1         0  JUMP_FORWARD          4  'to 4'
                2  LOAD_GLOBAL          99  99
              4_0  COME_FROM           114  '114'
              4_1  COME_FROM           112  '112'
              4_2  COME_FROM             0  '0'
                4  LOAD_CONST               0
                6  LOAD_CONST               ('md5',)
                8  IMPORT_NAME              hashlib
               10  IMPORT_FROM              md5

 L.   3        12  STORE_NAME               md5
               14  POP_TOP          
               16  LOAD_NAME                str
               18  LOAD_NAME                input
               20  LOAD_STR                 'please supply a valid key:'
               22  CALL_FUNCTION_1       1  ''
               24  CALL_FUNCTION_1       1  ''
               26  LOAD_METHOD              encode

 L.   6        28  CALL_METHOD_0         0  ''
               30  STORE_NAME               k
               32  LOAD_NAME                len
               34  LOAD_NAME                k
               36  CALL_FUNCTION_1       1  ''
               38  LOAD_CONST               16
               40  COMPARE_OP               ==

# here is the start of the 16 compares
# k[0] = 102 = ord('f')
 L.   7     42_44  JUMP_IF_FALSE_OR_POP   462  'to 462'
               46  LOAD_NAME                k
               48  LOAD_CONST               0
               50  BINARY_SUBSCR    

 L.   6        52  LOAD_CONST               102
               54  COMPARE_OP               ==

# k[1] = k[0] + 6 = 108 = ord('l')
 L.   8     56_58  JUMP_IF_FALSE_OR_POP   462  'to 462'
               60  LOAD_NAME                k
               62  LOAD_CONST               1
               64  BINARY_SUBSCR    
               66  LOAD_NAME                k
               68  LOAD_CONST               0
               70  BINARY_SUBSCR    
               72  LOAD_CONST               6

 L.   6        74  BINARY_ADD       
               76  COMPARE_OP               ==
 L.   9     78_80  JUMP_IF_FALSE_OR_POP   462  'to 462'

# assume k[2] = ord('a')
               82  LOAD_NAME                k
               84  LOAD_CONST               2
               86  BINARY_SUBSCR    
               88  LOAD_NAME                k
               90  LOAD_CONST               1
               92  BINARY_SUBSCR    
               94  LOAD_NAME                k
               96  LOAD_CONST               0
               98  BINARY_SUBSCR
              100  BINARY_SUBTRACT                      # first - second
              102  LOAD_CONST               91
 L.   6       104  JUMP_FORWARD        116  'to 116'
              106  FOR_ITER            110  'to 110'
 L.  10       108  JUMP_FORWARD        112  'to 112'
            110_0  COME_FROM           106  '106'
              110  BREAK_LOOP          114  'to 114'
            112_0  COME_FROM           108  '108'
              112  POP_JUMP_IF_FALSE_BACK     4  'to 4'
            114_0  COME_FROM           110  '110'
              114  JUMP_BACK             4  'to 4'
            116_0  COME_FROM           104  '104'
              116  BINARY_ADD       
 L.   6       118  COMPARE_OP               ==
          120_122  JUMP_IF_FALSE_OR_POP   462  'to 462'

#  k[3] = 103 = ord('g')
              124  LOAD_NAME                k
              126  LOAD_CONST               3
              128  BINARY_SUBSCR    
              130  LOAD_CONST               103
              132  COMPARE_OP               ==
          134_136  JUMP_IF_FALSE_OR_POP   462  'to 462'

#  assume k[4] = 123 = ord('{')
# k[4] = k[11] * k[3] - 42 => k[11] = 55
              138  LOAD_NAME                k
              140  LOAD_CONST               4
              142  BINARY_SUBSCR    
 L.   6       144  LOAD_NAME                k
              146  LOAD_CONST               11
 L.  12       148  BINARY_SUBSCR    
              150  LOAD_CONST               3
              152  BINARY_MULTIPLY  
              154  LOAD_CONST               42
              156  BINARY_SUBTRACT  
              158  COMPARE_OP               ==
          160_162  JUMP_IF_FALSE_OR_POP   462  'to 462'

# from hint we know k[5] = 53 = ord('5')
# sum(k) - 1322 = 53
              164  LOAD_NAME                k
 L.   6       166  LOAD_CONST               5
              168  BINARY_SUBSCR    
 L.  13       170  LOAD_NAME                sum
              172  LOAD_NAME                k
              174  CALL_FUNCTION_1       1  ''
              176  LOAD_CONST               1322
              178  BINARY_SUBTRACT  
              180  COMPARE_OP               ==
          182_184  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[6] + k[7] + k[10] = 260
              186  LOAD_NAME                k
              188  LOAD_CONST               6  
              190  BINARY_SUBSCR                #  push k[6]
              192  LOAD_NAME                k
              194  LOAD_CONST               7
 L.   6       196  BINARY_SUBSCR                #  push k[7]
              198  BINARY_ADD                   #  pop; pop; push k[6] + k[7]
 L.  14       200  LOAD_NAME                k
              202  LOAD_CONST               10
              204  BINARY_SUBSCR                # push k[10]
              206  BINARY_ADD
              208  LOAD_CONST               260
              210  COMPARE_OP               ==
          212_214  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[9] == int(chr(k[7]) * 2) + 1
# k[7] is a single digit number
# k[9] \in {12, 23, 34, 45, 56, 67, 78, 89, 100}

              216  LOAD_NAME                int
              218  LOAD_NAME                chr
              220  LOAD_NAME                k
              222  LOAD_CONST               7
              224  BINARY_SUBSCR                # k[7]
              226  CALL_FUNCTION_1       1  ''  # chr(k[7]) 
              228  LOAD_CONST               2   
 L.   6       230  BINARY_MULTIPLY              # chr(k[7]) * 2
              232  CALL_FUNCTION_1       1  ''  # int(chr(k[7]) * 2)
 L.  15       234  LOAD_CONST               1
              236  BINARY_ADD                   # int(chr(k[7]) * 2) + 1
              238  LOAD_NAME                k
              240  LOAD_CONST               9
              242  BINARY_SUBSCR    
              244  COMPARE_OP               ==
          246_248  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[8] % 17 = 16
# k[8] = 17n + 16 for one n
# Possible solutions
# 33 !
# 50 2
# 67 C
# 84 T
# 101 e
# 118 v
              250  LOAD_NAME                k
 L.  16       252  LOAD_CONST               8
              254  BINARY_SUBSCR                
              256  LOAD_CONST               17
              258  BINARY_MODULO    
              260  LOAD_CONST               16
              262  COMPARE_OP               ==
          264_266  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[8] = k[9] * 2
              268  LOAD_NAME                k
 L.   6       270  LOAD_CONST               9
              272  BINARY_SUBSCR                
 L.  17       274  LOAD_NAME                k
              276  LOAD_CONST               8
              278  BINARY_SUBSCR                
              280  LOAD_CONST               2
              282  BINARY_MULTIPLY 
              284  COMPARE_OP               ==
          286_288  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[10] = 101 = ord('e')
# 103 = k[3] = md5(k[10] * b'a').digest()[0] - 1 
# import hashlib
# for i in string.printable[:-7]:
#     if hashlib.md5(ord(i) * b'a').digest()[0] -1 == 103:
#         print(i)

              290  LOAD_NAME                md5
              292  LOAD_NAME                k
              294  LOAD_CONST               10
              296  BINARY_SUBSCR    
              298  LOAD_CONST               b'a'
              300  BINARY_MULTIPLY  
              302  CALL_FUNCTION_1       1  ''
              304  LOAD_METHOD              digest
              306  CALL_METHOD_0         0  ''
 L.   6       308  LOAD_CONST               0
              310  BINARY_SUBSCR    
 L.  18       312  LOAD_CONST               1
              314  BINARY_SUBTRACT  
              316  LOAD_NAME                k
              318  LOAD_CONST               3
              320  BINARY_SUBSCR    
 L.   6       322  COMPARE_OP               ==
              324  ROT_TWO                          
 L.  19       326  ROT_TWO                          # does nothing, swaps stack back and forth
          328_330  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[11] = 55
              332  LOAD_NAME                k
              334  LOAD_CONST               11
              336  BINARY_SUBSCR    
              338  LOAD_CONST               55
              340  COMPARE_OP               ==
          342_344  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[14] // 2 - 2 = k[12]
              346  LOAD_NAME                k
 L.   6       348  LOAD_CONST               12
              350  BINARY_SUBSCR    
 L.  20       352  LOAD_NAME                k
              354  LOAD_CONST               14
              356  BINARY_SUBSCR
              358  LOAD_CONST               2
              360  BINARY_TRUE_DIVIDE               
              362  LOAD_CONST               2
              364  BINARY_SUBTRACT
              366  COMPARE_OP               ==
          368_370  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[13] ((k[10] * k[8] % 32) * 2) - 1
              372  LOAD_NAME                k
              374  LOAD_CONST               13
              376  BINARY_SUBSCR    
              378  LOAD_NAME                k
              380  LOAD_CONST               10
              382  BINARY_SUBSCR    
              384  LOAD_NAME                k
 L.   6       386  LOAD_CONST               8
              388  BINARY_SUBSCR    
 L.  21       390  BINARY_MULTIPLY                      
              392  LOAD_CONST               32
              394  BINARY_MODULO    
              396  LOAD_CONST               2
              398  BINARY_MULTIPLY  
              400  LOAD_CONST               1
              402  BINARY_SUBTRACT  
              404  COMPARE_OP               ==
          406_408  JUMP_IF_FALSE_OR_POP   462  'to 462'

# k[14] = (k[12] XOR k[9] XOR k[15]) * 3 - 23
              410  LOAD_NAME                k
              412  LOAD_CONST               14
              414  BINARY_SUBSCR    
              416  LOAD_NAME                k
              418  LOAD_CONST               12
              420  BINARY_SUBSCR                    # k[12]
              422  LOAD_NAME                k
              424  LOAD_CONST               9
              426  BINARY_SUBSCR                    # k[9]
 L.   6       428  BINARY_XOR                       # k[12] XOR k[9]
              430  LOAD_NAME                k
 L.  22       432  LOAD_CONST               15
              434  BINARY_SUBSCR                    
              436  BINARY_XOR                       # k[12] XOR k[9] XOR k[15]
              438  LOAD_CONST               3
              440  BINARY_MULTIPLY  
 L.   4       442  LOAD_CONST               23
 L.  25       444  BINARY_SUBTRACT  
              446  COMPARE_OP               ==
          448_450  JUMP_IF_FALSE_OR_POP   462  'to 462'

k[15]  = 125 = ord('}')
              452  LOAD_NAME                k
              454  LOAD_CONST               15
              456  BINARY_SUBSCR    
              458  LOAD_CONST               125
              460  COMPARE_OP               ==

            462_0  COME_FROM           448  '448'
            462_1  COME_FROM           406  '406'
            462_2  COME_FROM           368  '368'
            462_3  COME_FROM           342  '342'
            462_4  COME_FROM           328  '328'
            462_5  COME_FROM           286  '286'
            462_6  COME_FROM           264  '264'
            462_7  COME_FROM           246  '246'
            462_8  COME_FROM           212  '212'
            462_9  COME_FROM           182  '182'
           462_10  COME_FROM           160  '160'
           462_11  COME_FROM           134  '134'
           462_12  COME_FROM           120  '120'
           462_13  COME_FROM            78  '78'
           462_14  COME_FROM            56  '56'
           462_15  COME_FROM            42  '42'
              462  STORE_NAME               correct
              464  LOAD_NAME                print
              466  LOAD_NAME                correct
          468_470  POP_JUMP_IF_FALSE   486  'to 486'
              472  LOAD_STR                 'valid key! '
              474  LOAD_NAME                k
              476  LOAD_METHOD              decode
              478  CALL_METHOD_0         0  ''
              480  FORMAT_VALUE          0  ''
              482  BUILD_STRING_2        2 
              484  JUMP_FORWARD        488  'to 488'
            486_0  COME_FROM           468  '468'
              486  LOAD_STR                 'invalid key :('
            488_0  COME_FROM           484  '484'
              488  CALL_FUNCTION_1       1  ''

During the CTF we solved the equations by hand, but here a z3 solution from after the CTF. This is not well-written and next time I will definitely use lists:

from z3 import *

s = z3.Solver()

k0 = BitVec('k0', 16)
k1 = BitVec('k1', 16)
k2 = BitVec('k2', 16)
k3 = BitVec('k3', 16)
k4 = BitVec('k4', 16)
k5 = BitVec('k5', 16)
k6 = BitVec('k6', 16)
k7 = BitVec('k7', 16)
k8 = BitVec('k8', 16)
k9 = BitVec('k9', 16)
k10 = BitVec('k10', 16)
k11 = BitVec('k11', 16)
k12 = BitVec('k12', 16)
k13 = BitVec('k13', 16)
k14 = BitVec('k14', 16)
k15 = BitVec('k15', 16)

s.add(k0 == 102) # f
s.add(k1 == 108) # l
s.add(k2 == 97)  # a
s.add(k3 == 103) # g
s.add(k4 == 123) # {
s.add(k5 == 53)  # 5 (the hint that was released)
s.add(k0 + k1 + k2 + k3 + k4 + k5 + k6 + k7 + k8 + k9 + k10 + k11 + k12 + k13 + k14 + k15 - 1322 == 53)
s.add(k6 + k7 + k10 == 260)
s.add(k7 >= 49)  # single digit number
s.add(k7 <= 57)
s.add((k7 - 0x30) * 11  + 1 == k9)  # k9k9
s.add(k9 == 100)
s.add(k8 == 50)
s.add(k14 / 2 -2 == k12)
s.add(k10 == 101)
s.add(k13 == ((((k10 * k8) % 32) * 2) - 1))
s.add(k14 == (k12 ^ k9 ^ k15) * 3 - 23)
s.add(k11 == 55)
s.add(k15 == 125)

s.add(k0 >= ord('!'))
s.add(k0 <= ord('~'))
s.add(k1 >= ord('!'))
s.add(k1 <= ord('~'))
s.add(k2 >= ord('!'))
s.add(k2 <= ord('~'))
s.add(k3 >= ord('!'))
s.add(k3 <= ord('~'))
s.add(k4 >= ord('!'))
s.add(k4 <= ord('~'))
s.add(k5 >= ord('!'))
s.add(k5 <= ord('~'))
s.add(k6 >= ord('!'))
s.add(k6 <= ord('~'))
s.add(k8 >= ord('!'))
s.add(k8 <= ord('~'))
s.add(k9 >= ord('!'))
s.add(k9 <= ord('~'))
s.add(k10 >= ord('!'))
s.add(k10 <= ord('~'))
s.add(k11 >= ord('!'))
s.add(k11 <= ord('~'))
s.add(k12 >= ord('!'))
s.add(k12 <= ord('~'))
s.add(k13 >= ord('!'))
s.add(k13 <= ord('~'))
s.add(k14 >= ord('!'))
s.add(k14 <= ord('~'))
s.add(k15 >= ord('!'))
s.add(k15 <= ord('~'))

while s.check() == sat:
    k = [k0,  k1,  k2,  k3,  k4,  k5,  k6,  k7,  k8,  k9,  k10,  k11,  k12,  k13,  k14,  k15]
    print(''.join([chr(s.model()[i].as_long()) for i in k]))
    s.add(Or(*[i!=s.model()[i].as_long() for i in k]))

Thanks to the hint, we get an unique solution:

flag{5f92de703d}