11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-05-01

SECCON 令和CTF」に出題した問題「零は?」のサーバプログラムです。
サーバが停止するので、代わりに自分の環境で起動して、自作ソルバーの動作確認にご利用ください。

令和⇒れいわ⇒零は⇒0= という おっさん的発想で問題ネタができました。
ポート番号は「ZEROIS」の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処理が必要になります。

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()

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

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))
11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?