BaltCTF 2013 – Positive

BaltCTF 2013 - Positive - task description

Die “professional programming and coding” – Challenge “Positive” gibt zunächst keine Beschreibung des eigentlichen Ziels. Daher verbinden wir als erstes direkt zum angegebenen System.

MANUAL: Be positive, change all minuses to pluses!
Rules: clicking on a cell changed it and it's neighbours.
Format: 
a) "\d\d" - number of cell
b) "(?:\d\d)+" numbers of cells

++---+---+
----------
-+--------
--++------
-+---+-+--
---+------
----------
----++---+
-+---+----
-----+-+--

Mit den ersten Zeilen der Ausgabe sind wir etwas schlauer. Auf dem Spielfeld müssen durch Angabe der Spalte und Zeile alle Minus in Plus getauscht werden. Der Haken dabei ist, dass die benachbarten Felder ebenfalls getauscht werden. Die Eingabe von “11” als Lösung tausch damit neben 11 auch die Positionen 10, 12, 01, 02.

( Bei diesem Spiel handelt es sich um das so genannte “Lights Out” Spiel, das über bestimmte Algorithmen einfach gelöst werden kann. Diesen Hinweis hatte ich beim Lösen der Challenge leider nicht, was sich auch in meinem Lösungsweg widerspiegeln wird ;-). )

Zum Lösen der Challenge habe ich ein Python-Skript geschrieben, das ich im Folgenden schrittweise erläutern möchte:

#!/usr/bin/env python
import socket
import time
import random

s=socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("194.106.195.60",9502))

# fetch manual
data = s.recv(1024)
print "MANUAL: " + data

down = 1
solved = 0

while 1:
    # fetch board
    board = s.recv(1024).split("\n")
    print "BOARD: " + str(board)

Der TCP-Socket wird zunächst deklariert und die Verbindung zum Server hergestellt (Zeilen 6-7). Anschließend wird die Anleitung eingelesen, ausgegeben (Zeilen 10-11) und die Endloschleife zum Lösen des Boards begonnen (Zeile 16). Dann wird das Board eingelesen und am Zeilenumbruch getrennt (Zeile 18). Als Datentyp handelt es sich beim Board damit um ein Array, dass die Zeilen als Elemente enthält.

    if "Solved" in board[0]:
        print board
        solved += 1
        print "\nSolved: " + str(solved)
        board = s.recv(1024).split("\n")
        print "FLAG: " + str(board)

Als Nächstes wird gesprüft um die “solved” Meldung in der ersten Zeile auftritt. Wenn das der Fall ist, wurde das Board mit der letzten Operation erfolgtreich gelöst und die Flagge sollte ausgegeben werden.

Zum Lösen des Boards habe ich mir nun Folgendes überlegt. Die gesamte erste Zeile kann in Plus umgewandelt werden, indem in der zweiten Zeile unterhalb der Minus getauscht wird. Im Beispiel:

++---+---+
  *** ***

Diese Zeile wird durch die Lösung “213141617181” daher zu:

++++++++++

Wenn mann dieses Verfahren durchgehend anwendet, kann man die ersten 9 Zeilen in Plus umwandeln, nur um die letzte Zeile muss man sich separat kümmern. Der Quellcode zum tauschen der ersten 9 Zeilen lautet:

    if down == 1:
        solution = ""
        found = 0
        for y in range(0,9):
            line = board[y]
            for x in range(10):
                if line[x] == "-":
                    solution = solution + str(y+1) + str(x)
                    found = 1
            if found == 1:
                break

Die ersten neun Zeilen werden enumeriert (Zeile 4) und für jedes Minus (Zeile 7) wird die darunter liegendene Zeile an der entsprechenden Zeile getauscht (Zeile 8). Die Variable found wird dabei nach einem Fund auf Eins gesetzt (Zeile 9). Da sich die Zeilen nach dem Tausch ändern, wird nach dem Fund in einer Zeile die Schleife beendet um die Lösung abzusenden.

        if found == 0:
            line = board[9]
            pos = line.find("---")
            if pos != -1:
                solution = "9" + str(pos+1)
            else:
                pos = line.find("--")
                if pos != -1:
                    solution = "9" + str(pos+1)
            if solution == "":
                solution = "9" + str(random.randint(0,9))

            print "GOING UP..."
            down = 0

Sollte in keiner der ersten neun Zeilen ein Tausch vorgenommen worden sein (Zeile 1), dann sind bis auf die letzte Zeile alle anderen Plus und wir kümmern uns jetzt um die letzte Zeile. Dazu wird zunächst auf drei Minus in Folge “—” geprüft und in der Mitte getauscht (Zeilen 3-5). Anschließend wird auf zwei Minus “–” geprüft und entsprechend getauscht (Zeilen 6-9).

Sollte es trotz aller Prüfungen immernoch keinen Tausch geben (Zeile 10), soll per Zufall eine Position getauscht werden (Zeile 11). Zuletzt soll die Richtung gewechselt werden und nun entsprechend von unten nach oben sortiert werden (Zeilen 14ff).

    print "SOLUTION: " + solution
    s.send(solution)

s.close()

Nach Abarbeitung aller Operationen und Erhalt mindestens eines Tausches, wird dieser ausgegeben und an den Server gesendet (Zeilen 1-2).

Auch wenn dieser Algorithmus teilweise chaotisch ist und nicht sofort zum Erfolg führt, ist das Spielfeld nach relativ kurzer Zeit (wenigen Sekunden – einige Minuten) gelöst. Die erste Ausführung des Skriptes liefert folgendes Ergebnis:

rup0rt@lambda:~/BaltCTF2013$ ./positiv.py
MANUAL: Be positive, change all minuses to pluses!
Rules: clicking on a cell changed it and it's neighbours.
Format: 
a) "\d\d" - number of cell
b) "(?:\d\d)+" numbers of cells

BOARD: ['+--+-++-++', '----+----+', '----+--+-+', '+++-+---+-', '-+---+----', '+++++++++-', '++++--+---', '+-+--+++++', '--+-+---+-', '+-+----+++', '']
SOLUTION: 11121417
BOARD: ['++++++++++', '+----+++++', '-++------+', '+++-+---+-', '-+---+----', '+++++++++-', '++++--+---', '+-+--+++++', '--+-+---+-', '+-+----+++', '']
SOLUTION: 21222324
BOARD: ['++++++++++', '++++++++++', '++-+-+---+', '+--+----+-', '-+---+----', '+++++++++-', '++++--+---', '+-+--+++++', '--+-+---+-', '+-+----+++', '']
SOLUTION: 3234363738
[...]
BOARD: ['++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '-+++-+++++', '+++--+-+--', '++++++++++', '++++++++++', '++++++++++', '']
SOLUTION: 6064
BOARD: ['++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '--+++--+--', '-+++-+++++', '++++++++++', '++++++++++', '']
SOLUTION: 707175767879
BOARD: ['++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '-+-+++++++', '--+++--+--', '++++++++++', '']
SOLUTION: 8082
BOARD: ['++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '+---+--+--', '-+-+++++++', '']
SOLUTION: 91929395969899
BOARD: ['Solved!!! Have a fun and finally you will get a flag', '']
['Solved!!! Have a fun and finally you will get a flag', '']

Solved: 1
FLAG: ['+---+-+---', '-------+--', '---+-++--+', '+++-++---+', '++-+--++-+', '---++-+---', '+++++---+-', '--+----+++', '+---+-+-+-', '-+--------', '']
SOLUTION: 11121315171819
BOARD: ['++++++++++', '+-+--+-++-', '-++---+++-', '+++-++---+', '++-+--++-+', '---++-+---', '+++++---+-', '--+----+++', '+---+-+-+-', '-+--------', '']
SOLUTION: 2123242629
BOARD: ['++++++++++', '++++++++++', '+-+------+', '+-++-++---', '++-+--++-+', '---++-+---', '+++++---+-', '--+----+++', '+---+-+-+-', '-+--------', '']
SOLUTION: 31333435363738
[...]

Das Board wurde tatsächlich gelöst! Jedoch scheint das nicht zu genügen. Der Aussage “have fun and finally you will get a flag” lässt vermuten, dass wir das Spielfeld mehrmals lösen müssen. Also lassen wir das Skript weiter laufen… 10, 20, 50, 75 … 100 Mal müssen wir das Board lösen, bevor wir diese Ausgabe erhalten:

[...]
BOARD: ['++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '-+-+-+-+--', '-+++++-+++', '++++++++++', '++++++++++', '']
SOLUTION: 707274767879
BOARD: ['++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++-+-+++++', '-+-+-+-+--', '++++++++++', '']
SOLUTION: 8284
BOARD: ['++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '++++++++++', '--+++--+--', '++-+-+++++', '']
SOLUTION: 909195969899
BOARD: ['the flag is: 2e70bd4bbe1ed7c69a088c24c5a6fc95', '']
['the flag is: 2e70bd4bbe1ed7c69a088c24c5a6fc95', '']

Solved: 100

Die Lösung lautet somit “2e70bd4bbe1ed7c69a088c24c5a6fc95“.

UPDATE (17.05.2013): Nach dem Wettkampf wurde der Quellcode veröffentlicht. Wer Interesse hat, kann ihn sich deshalb zusätzlich ansehen:

#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import random, socket, signal, sys, re, os, time

FLAG="2e70bd4bbe1ed7c69a088c24c5a6fc95"
PORT=2
Timeout=20

slow="""__________________________________________________________________
__________________________________________________________________
______________________________________†888888‡†___________________
_______________________________8†_†††††††††††††††††8†_____________
___________________________†‡_____††††††††††††††††††††8___________
________________________‡†_______†‡‡‡‡‡‡‡8††††††††††††_†‡†________
____________________‡†____†‡‡†_______________‡††††††††††††8_______
_______________________________________________†‡†††††††††‡‡______
_________________________________________________8†††††††††‡‡†____
___________________________________________________††††††††‡‡†8___
___________________________________________________8†††††††‡‡‡‡___
___________________________________________________8†††††††‡‡‡†___
___________________________________________________8††††††‡‡‡††___
___________________________†‡‡†††††††††††††‡8†_____††††††‡‡‡‡‡†___
_______________________†8††††††††††††††††††††††8†_8‡††††‡‡‡‡‡‡‡___
____________________‡††††††††††††††††††††††††††††††‡†††‡‡‡‡‡‡†8___
________‡‡††‡_†8‡††††††††††88†‡8†††‡8†††††††††††††††‡‡‡‡†‡‡‡‡‡____
______††††††8††††††††††††††††8†††888††8††††††††††††‡‡†8††††††‡____
______8†‡†8††††††††††††††††††‡†8††††††8††††††††††††‡‡‡‡¶††††8_____
_______‡‡__8†††††††††8____‡‡††††‡†††††‡†††††††††††‡‡‡‡††8†††______
________‡¶_8††††††_†8_______†††††‡8888††††††††††‡‡‡‡‡‡††‡8†_______
_______‡†‡†________‡8________‡††††††‡8†††††††‡‡‡‡‡‡‡‡‡‡‡‡‡________
_____8†________________†8‡__‡†††††‡‡‡‡†††‡‡‡‡‡‡‡‡‡‡†‡†‡‡†8________
___‡________________________‡8†††‡‡‡‡††‡‡†‡‡‡‡‡‡‡‡‡‡‡†‡†‡‡†_______
__†____‡_____‡_______________†8†‡‡‡††††‡‡‡‡‡‡‡‡‡†‡‡8††††‡††_______
___†_________________________†8††††††††††‡‡‡‡‡‡‡‡‡††††††‡‡†_______
_____8_‡__‡888‡††____††‡¶‡____‡‡†††††††††††‡‡†‡‡†¶†††††‡†‡________
_______†_¶8¶†††††††‡_†††8___†8‡‡††††††††††‡†8‡‡8_¶‡‡†††††‡________
________‡††¶¶††††††‡†8‡__†‡‡‡‡††8†††††††††‡†8______8‡‡__‡_________
________8‡‡____††††_____88††††††††‡‡‡†††††‡†8_____________________
______†††‡‡__‡‡†___‡‡8______________8‡‡‡888‡†_____________________
______________________________________†¶___‡______________________"""

class Field:
    def __init__(self):
        self.w = 10
        self.h = 10
        self.game = [0]*100
        random.seed()
        t=random.randint(15,85)
        for i in range(t):
            random.seed()
            self.game[random.randint(0,99)]=1

    def check(self):
        if 0 in self.game:
            return False
        return True

    def change(self, i):
        self.game[i]=(self.game[i]+1)%2

    def step(self, i):
        if i>=self.w:
            self.change(i-self.w)
        if i<self.w*(self.h-1):
            self.change(i+self.w)        
        if i%self.w!=0:
            self.change(i-1)
        if i%self.w!=self.w-1:
            self.change(i+1)
        self.change(i)

    def write(self, stream):
        screen = ""

        for i in range(self.h):
            for j in range(self.w):
                if self.game[i*self.h+j]:
                    screen+='+'
                else:
                    screen+='-'
            screen+='\n'

        stream.send(screen)

sock = socket.socket()
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock.bind(('', PORT))
sock.listen(10)

signal.signal(signal.SIGCHLD, signal.SIG_IGN)

while 1:
    client, addr = sock.accept()
    if os.fork() == 0:
        break
    client.close()
sock.close()

f = Field()
client.send(
"""Be positive, change all minuses to pluses!
Rules: clicking on a cell changed it and it's neighbours.
Format: 
a) "\d\d" - number of cell
b) "(?:\d\d)+" numbers of cells
""")

def handle(line):
    if len(line) < 1:
        return (True, None)
    if len(line) == 1 and line[0] in "qxQX":
        return (False, "Bye")
    global f

    m = re.match("^[0-9]{2,}$",line)
    if m is None or len(line)%2==1:
        return (True, "Error input")

    m=re.findall("[0-9]{2}",line)
    m=map(int,m)
    for i in m:
        if i>=f.w*f.h:
            return (True, "Error number")
    for i in m:
        f.step(i)

    return (True, None)

data = ""

client.settimeout(Timeout)
games=0

try:
    while 1:
        if f.check():
            games+=1
            f = Field()
            if games==100:
                client.send("the flag is: "+FLAG+"\n")
                sys.exit(0)
            client.send("Solved!!! Have a fun and finally you will get a flag\n")
        f.write(client)

        data=client.recv(4096)
        if len(data)==0:
            sys.exit(0)

        cont, msg = handle(data.replace('\n',''))
        if msg!=None:
            client.send(msg + "\n")
        if cont==False:
            client.close()
            sys.exit(0)
except socket.timeout:
    client.send("So sloooow\n"+slow+"\n")
    sys.exit(0)

sys.exit(0)

Leave a Reply

Your email address will not be published. Required fields are marked *