Edited at

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

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