Solution de ribt pour Ventriglisse

misc algo

29 novembre 2023

Quand on se connecte en utilisant netcat comme demandé, on reçoit les instructions suivantes :

Hey there!
Welcome to our new attraction: the famous Ventriglisse!
A square area is filed with soap and some water and is very slippery!
Here is an example (base64-encoded image):
------------------------ BEGIN MAZE ------------------------
iVBORw0KGgoAAAANSUhEUgAAAoAAAALACAIAAACM91QYAAAlKUlEQVR4nO3d
[...]
AgaABAoYABL8P4QUmTzsPkWVAAAAAElFTkSuQmCC
------------------------- END MAZE -------------------------
You are intially located outside the area at the bottom red mark.
At the beginning, you jump right in the area (head first!) and you slide up and stop at the first wall encountered.
Once you hit a wall, you can jump again in another direction to continue your amazing soapy journey.
Your goal is get out of the zone at the red mark at the top.
Send me how you would like to move within the maze as follows:
 - N to go North
 - S to go South
 - E to go East
 - O to go West
On the example above, a valid path would be NESENONOSENENON.
Note that a valid path would always start and end by 'N'.
In this example, paths starting by NO, NN or NEN would be invalid (as your head would hit a wall).
Press a key when you are ready...

Une image PNG est encodée en base64, voici l’exemple fourni :

labyrinthe

Créons déjà un parser pour transformer cette énorme matrice de pixels RGB en un tableau facile à traiter :

def parse(img):
    data = {}
    xDebut, yDebut = 64, 128 # c'est constant
    haut, larg = 0, 0
    while img[xDebut+larg*64, yDebut] == (0, 0, 255):
        larg += 1
    xFin = xDebut+larg*64 - 1
    while img[xFin, yDebut+haut*64] == (0, 0, 255):
        haut += 1
    yFin = yDebut+haut*64 - 1

    data["dimension"] = (larg, haut)

    for x in range(larg):
        if img[xDebut+32+x*64, yDebut-10][2] < 10:
            data["sortie"] = (x, 0)

    for x in range(larg):
        if img[xDebut+32+x*64, yFin+30][2] < 10:
            data["entrée"] = (x, haut-1)

    data["walls"] = []

    for y in range(haut):
        for x in range(larg):
            if x > 0 and img[xDebut+x*64, yDebut+32+y*64] == (0, 0, 255):
                data["walls"].append(((x-1, y), (x,y)))
            if y > 0 and img[xDebut+32+x*64, yDebut+y*64] == (0, 0, 255):
                data["walls"].append(((x, y-1), (x,y)))

    return data

Cette fonction trouve la dimension du labyrinthe (je n’avais pas vu que c’était fourni dans l’énoncé 😕), les coordonnées de l’entrée et de la sortie et la position de tous les murs. Voilà ce que ça donne pour l’image exemple :

{'dimension': (8, 8),
 'entrée': (2, 7),
 'sortie': (3, 0),
 'walls': [((6, 0), (7, 0)),
           ((2, 0), (2, 1)),
           ((2, 1), (3, 1)),
           ((7, 0), (7, 1)),
           ((1, 1), (1, 2)),
           ((1, 3), (2, 3)),
           ((2, 2), (2, 3)),
           ((3, 2), (3, 3)),
           ((5, 2), (5, 3)),
           ((5, 3), (6, 3)),
           ((7, 2), (7, 3)),
           ((1, 5), (2, 5)),
           ((0, 5), (0, 6)),
           ((0, 7), (1, 7)),
           ((4, 7), (5, 7))]}

Les murs sont définis par les deux cases qu’ils séparent.

Ne reste plus qu’à créer les fonctions usuelles pour calculer les déplacements :

def avancer(pos, dir):
    x, y = pos
    if dir == "N": y -= 1
    if dir == "S": y += 1
    if dir == "O": x -= 1
    if dir == "E": x += 1
    return (x,y)

def positionFinale(data, pos, direction):
    x, y = pos
    while mouvementPossible(data, (x, y), direction):
        x, y = avancer((x, y), direction)
    return (x,y)

def mouvementPossible(data, pos, direction):
    nextPos = avancer(pos, direction)
    if nextPos[0] < 0 or nextPos[1] < 0:
        return False
    if nextPos[0] >= data["dimension"][0] or nextPos[1] >= data["dimension"][1]:
        return False
    if (pos, nextPos) in data["walls"] or (nextPos, pos) in data["walls"]:
        return False
    return True

Et enfin trouver le plus court chemin (on essaye toutes les possibilités) :

def trouverChemin(data):
    pos = data["entrée"]
    nextPos = positionFinale(data, pos, "N")

    posEssayes = [pos]
    aEssayer = {'pos': [nextPos], 'path': ["N"]}

    while len(aEssayer) > 0:
        pos, path = aEssayer['pos'].pop(0), aEssayer['path'].pop(0)
        for direction in "NSEO":
            if mouvementPossible(data, pos, direction):
                nextPos = positionFinale(data, pos, direction)
                if nextPos == data["sortie"]:
                    return path+direction
                if nextPos not in posEssayes and nextPos not in aEssayer['pos']:
                    aEssayer['pos'].append(nextPos)
                    aEssayer['path'].append(path+direction)
            posEssayes.append(pos)

def solve(img):
    data = parse(img.load())
    chemin = trouverChemin(data)
    if chemin[-1] != "N":
        chemin += "N"
    return chemin

Plus qu’à faire le programme final qui discute avec le sreveur :

from pwn import *
from base64 import b64decode
from PIL import Image, ImageDraw
from io import BytesIO
from solve import solve

conn = remote('challenges1.france-cybersecurity-challenge.fr', 7002)
conn.recvuntil('Press a key when you are ready...')
conn.send('\n')

while True:
    try:
        conn.recvuntil('------------------------ BEGIN MAZE ------------------------')
        maze = b64decode(conn.recvuntil('------------------------- END MAZE -------------------------', drop=True))
        print(conn.recvuntil(':').decode())
        img = Image.open(BytesIO(maze))
        img.save("maze2.png")
        path = solve(img)
        print(path)
        conn.send(path+'\n')
        print(conn.recvuntil('...').decode())
    except EOFError:
        print(conn.recv().decode())
        break

Notre programme résout les labyrinthes bien plus vite que le serveur ne les génère.

Congratulations! Here is your flag: FCSC{c78b1d02700bbe83a8c4ec8cec7ce3109dfa1620189a460189a1e345447ae5f2}

Bonus

Voici à quoi ressemble le dernier labyrinthe :

gros labyrinthe

(La solution est NESONOSONONOSESONONENESENONESONONENON mais vous pouvez vérifier à la main 😉)