Write-Up от Алексея Ропана

Задача:

Universal dangerous positive: 194.226.244.125:1024. Отправь мне пароль: “3k8bbz032mrap75c8iz8tmi7f4ou00”. Формат флага — “RUCTF_.*”

Решение:

Точно не помню, но по-моему пароля сразу не было. Поэтому задача долго не сдавалась. В экспериментах удалось выяснить, что надо отправлять по протоколу UDP (потом уже обратили внимание на Universal Dangerous Positive). После третьего подхода, когда все же пароль был выложен, было получено сообщение:

Hello stranger!
You can go down(1280) with password: g5op3e6wxcy71m24dgcmhq714nd799
You can go right(1025) with password: 084399ptmy7u4bxwy003x2ko2oa06n

И тут задача показалось достаточно простой. Нужно просто ходить по лабиринту, отправляя для перехода нужный пароль. На python это было реализовано так:

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.sendto(msg, (host, port))
reply, addr = sock.recvfrom(size)

Но, отправляя следующий пароль, в ответ ничего не приходило. Казалось, что это снова недоработка (как и вначале, когда пароль не был выложен). Попробовал ещё различные методы отправки сообщений через udp, но в ответ все та же тишина. И тут я обратил внимание на числа, которые указаны после направления движения (1025 и 1280). 1025 было очень похоже на начальный порт, только отличался на единицу. Ах, значит нам дается не только следующий пароль, но порт. Отлично. Немного правок в коде и был сделан первый шаг и получено сообщение:

Next step
You can go left with password: 3k8bbz032mrap75c8iz8tmi7f4ou00
You can go rigth with password: irbzlsqs1j681g84u9jq13u54265ir

О, и вправду, пойдем налево и окажемся в начальной команте (с паролем 3k8bbz032mrap75c8iz8tmi7f4ou00). Ррррр. Только тут уже не было портов и было непонятно куда переходить. Ладно, порт можно перебирать. Это первое что пришло в голову, но сразу не было оценено, и быстро написано. Следующие комнаты открывались, но очень долго. Кстати, наш сокет нужно было немножко поправить, чтобы он не ждал долго ответа:

sock.settimeout(timelimit)

0.3 секунды хватало с запасом, вроде как. Итак, двери открывались (именно так можно назвать процесс перебора порта и переход по в соседнюю клетку), но очень долго. И снова проанализировав информацию (т.е. пораскинув головой, а не руками) было замечена зависимость порта от направления движения. Когда шли вправо порт увеличивался на один, а влево уменьшался на один. Соответственно вверх и вниз порт увеличивался и уменьшался на 256. То есть у нас получался лабиринт 256х256 без первых 4 строчек (так как порты до 1024 не использовались). В итоге получили быстро бегающего по лабиринту бота, который что-то искал. Но через некоторые двери так и не удавалось пройти, даже пытаясь несколько раз. Леша объяснил это тем, что пакету udp могут теряться. Да ещё были странности такие как то, что в большинство дверей он заходил с первого раза, а в некоторые так и не смог зайти после долгих попыток. Таких двери откладывалися на потом, а пока обследовалось то, что можно было обследовать. Ясное дело, что зная порт и пароль можно было продолжать движение с любой комнаты. Поэтому вся информация сохранялась. По скорости изучения лабиринта нужно было около трех часов на все. Но было чувство, что в коде был баг, так как он почему-то оказывался в первой двери несколько раз при одном запуске. Но было уже утро. Я запустил скрипт и пошел спать. Проспал около четырех часов. Первое, что я увидел на мониторе – это то, что было обследовано 51% лабиринта и стало как-то грустно. Но и тут же было сообщение о том, что найдено необычное сообщение:

Congratulations! Flag is RUCTF_77pd9u784g059t0z18hjtn5d

Блуждание по лабиринту закончилось успешно, хоть и не удалось побывать в каждом его закоулке.

Исходный код бота:

import socket
import sys
import random
import re
import json
import Queue
import datetime
 
timelimit = 0.3
 
class World(dict):
    def reopen(self):
        try:
            with open(self.filename, "r") as fo:
                data = json.load(fo)
        except:
            data = {}
        super(World, self).__init__(data)
 
    def save(self):
        try:
            dump = json.dumps(self, indent=4)
            with open(self.filename, "w") as fo:
                fo.write(dump)
        except Exception, error:
            print "ERROR SAVE", error
            assert False
            pass
 
    def __init__(self, filename):
        self.filename = filename
        self.reopen()
 
world = World("world.json")
 
queue = Queue.Queue()
 
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timelimit)
 
def get_port(port, move):
    if move == "down":
        return port + 256
    elif move == "up":
        return port - 256
    elif move == "left":
        return port - 1
    elif move == "right" or move == "rigth":
        return port + 1
    assert False, "Incorret move: " + move
 
if world:
    for w in world.values():
        msg, host, port = w['position']
        queue.put((msg, host, port))
else:
    host= '194.226.244.125'
    port = 1024
    msg = "3k8bbz032mrap75c8iz8tmi7f4ou00"
    queue.put((msg, host, port))
 
prev_time = None
while not queue.empty():
    curr_time = datetime.datetime.now()
 
    if prev_time == None or (curr_time - prev_time).seconds > 180:
        world.save()
        print len(world), "is", "%.3f%%" % (float(len(world)) / 2**16 * 100)
        prev_time = curr_time
 
 
    msg, host, port = queue.get()
    if msg in world:
        reply = world[msg]['reply']
    else:
        i = 0
        while i < 16:
            fail = False
            try:
                sock.sendto(msg, (host, port))
                reply, addr = sock.recvfrom(1024)
            except socket.timeout:
                if i == 0:
                    print "failed..."
                sock.close()
                sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
                sock.settimeout(timelimit)
                fail = True
            if not fail:
                break
            i += 1
        if fail:
            print "FAILED", msg, port
            continue
        if i != 0:
            print i, "ok"
 
    result = reply.split("\n")[0]
    if result != "Next step":
        print msg, port, result
 
    world[msg] = {'position': (msg, host, port), 'reply': reply}
 
    for i in reply.split("\n")[1:]:
        data = i.split()
        directory = data[3]
        m = data[-1]
        if m in world:
            continue
        move = re.sub("([a-z]*).*", "\\1", directory)
        p = re.sub("[a-z]*\(([0-9]*)\)", "\\1", directory)
        queue.put((m, host, int(p) if p.isdigit() else get_port(port, move)))
 
world.save()