SECCON 令和CTF の「零は？」問題サーバ

「[SECCON 令和CTF](https://2018.seccon.jp/2019/04/seccon-ctf.html)」に出題した問題「零は？」のサーバプログラムです。
サーバが停止するので、代わりに自分の環境で起動して、自作ソルバーの動作確認にご利用ください。

令和⇒れいわ⇒零は⇒0= という おっさん的発想で問題ネタができました。
ポート番号は「ZEROIS」の[Leet](https://ja.wikipedia.org/wiki/Leet)で23615。(ROは日本語Leetで6)
ちなみに、令和⇒れいわ⇒REIWA⇒bREInWAck という問題も出題しました。「新元号発表」問題も（山崎さんとの合作）。

問題サーバに接続すると、`0=`で始まる数式が表示され、式の中の1つの項目に`?`があり、その値を返送すると次の問題が表示され、100問正解するとフラグが表示されます。

```
$ nc localhost 23615
[1/100]
0=?-68
?=68
[2/100]
0=29-83+?
?=
```

最初は数字1個と`?`、1問につき数字が1個増え、100問目は100個の数字と`?`の演算式になります。
最後の3問は手を加えて難易度を上げました。

* 98問目： 最後の数値を?に選ぶ。最後の数値は演算結果を0にするための調整値で大きな値なので。
* 99問目： 乗算項目を探して右項を0にする。0乗算の逆算で0除算すると演算エラーになるので。
* 100問目： 乗算項目を探して左項を0にする。0乗算の逆算で0除算すると演算エラーになるので。

twitterで「最後の2問が0なのは零(0)輪(0)か？」のようなことが書かれていましたが、その意図はなかった・・・

以前出題したサーバを流用したのでPython2用のプログラムです。
socket通信はバイナリデータの送受信で、Python3で動かそうとすると文字列のencode/decode処理が必要になります。

```py:zerois.py
#!/usr/bin/env python2

import sys
import random
import socket
import SocketServer
from time import sleep
from datetime import datetime

SERVER = HOST, PORT = "", 23615     # 23615 is ZeroIs
TIMES = 100

FLAG = "The flag is SECCON{REIWA_is_not_ZERO_IS}.\n"


def makeNumsOps(n, NUMBERS=range(0, 100)):
    nums = [random.choice(NUMBERS) for _ in range(n)]
    ops = sum([random.sample("+-*", 3) for _ in range(n//3 + 1)], [])[:n-1]
    return nums, ops


def makeExp(nums, ops):
    exp = str(nums[0])
    for num, op in zip(nums[1:], ops):
        exp += op + str(num)
    return exp


def evalNumsOps(nums, ops):
    return eval(makeExp(nums, ops))


def addLastValue(nums, ops):
    last = -evalNumsOps(nums, ops)
    ops += ["+-"[last < 0]]
    nums += [abs(last)]


def setLastValue(nums, ops):
    last = -evalNumsOps(nums[:-1], ops[:-1])
    ops[-1] = "+-"[last < 0]
    nums[-1] = abs(last)


def printNumsOps(nums, ops):
    exp = makeExp(nums, ops)
    print(eval(exp), "=", exp)


def makeQuestion(n):
    nums, ops = makeNumsOps(n)
    addLastValue(nums, ops)
    if n == TIMES and '*' in ops:
        choice = len(ops) - ops[::-1].index('*')
        nums[choice - 1] = 0
        setLastValue(nums, ops)
    elif n == TIMES - 1 and '*' in ops:
        choice = len(ops) - ops[::-1].index('*') - 1
        nums[choice + 1] = 0
        setLastValue(nums, ops)
    elif n == TIMES - 2:
        choice = len(nums) - 1
    else:
        choice = random.randint(0, len(nums) - 1)
    answer = nums[choice]
    nums[choice] = "?"
    return makeExp(nums, ops), answer


def correct(question, answer):
    return answer.isdigit() and eval(question.replace('?', answer)) == 0


def timestamp():
    return datetime.now().strftime("%Y/%m/%d %H:%H:%S")


class ZeroIsHandler(SocketServer.BaseRequestHandler):
    def challenge(self, client):
        for i in range(1, TIMES + 1):
            question, expect = makeQuestion(i)
            client.sendall("[%d/%d]\n" % (i, TIMES))
            client.sendall("0=" + question + "\n?=")
            answer = client.recv(1024).decode().strip()
            sleep(1)
            if not correct(question, answer):
                print timestamp(), "NG", client.getpeername()[0], expect, answer[:20]
                client.sendall("Wrong!\n")
                return
        print timestamp(), "OK", client.getpeername()[0]
        client.sendall("Congratulations!\n")
        client.sendall(FLAG)

    def handle(self):
        client = self.request
        client.settimeout(20)
        try:
            self.challenge(client)
            client.sendall("(Enter RETURN key if this connection is not disconnected)\n")
        except socket.timeout as e:
            client.sendall(b"\nTimeout, bye.\n")
            client.sendall("(Enter RETURN key if this connection is not disconnected)\n")
        except socket.error:
            pass
        except Exception as e:
            print e


class ZeroIsServer(SocketServer.ThreadingTCPServer):
    request_queue_size = 50

    def server_bind(self):
        self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self.socket.bind(self.server_address)


if __name__ == "__main__":
    server = ZeroIsServer(SERVER, ZeroIsHandler)
    server.serve_forever()
```

みなさんのソルバーの方が優秀ですが、拙作のソルバーも公開しておきます。

```py:solver.py
import socket

SERVER = 'zerois-o-reiwa.seccon.jp', 23615


def receiver(s):
    while True:
        data = s.recv(1024)
        if not data:
            exit(0)
        print data.rstrip()
        for line in data.splitlines():
            yield line.strip()


def send(s, msg):
    print msg.rstrip()
    s.send(msg)


if __name__ == '__main__':
    s = socket.socket()
    s.connect(SERVER)
    for line in receiver(s):
        if line.startswith('0='):
            answer = eval(line[2:].replace('?', '0'))
            q = line.index('?')
            if line[q-1] == '*' or q+1 < len(line) and line[q+1] == '*':
                m = q - 1
                while line[m] == '*':
                    i = m - 1
                    while line[i].isdigit():
                        i -= 1
                    value = int(line[i+1:m])
                    if value != 0:
                        answer /= value
                    m = i
                m = q + 1
                while m < len(line) and line[m] == '*':
                    i = m + 1
                    while i < len(line) and line[i].isdigit():
                        i += 1
                    value = int(line[m+1:i])
                    if value != 0:
                        answer /= value
                    m = i
        elif line.startswith('?='):
            send(s, '%s\n' % abs(answer))
```