Skip to content

Instantly share code, notes, and snippets.

@ky28059
Last active February 26, 2024 03:30

bi0sCTF 2024 — A Block and a Hard Place

Are you the Far Lands because you're a Maze? Or are you a Maze because you're the Far Lands?

We're given a terminal prompt that looks like this:

image

It looks like we're blindly navigating some sort of "maze", with walls blocking regular movement between certain cells.

As a first order of business, we should probably map out what this maze looks like. We can represent each maze position as a 2x2 grid, where the right and bottom two cells represent whether there is a wall to the right or bottom of the cell, respectively:

..    .X    ..    .X
..    .X    XX    XX

(we don't need to check for left or top walls as we'll have already found them when iterating over other positions.)

Then, writing a python script to map out the maze's walls, printing one row of the maze at a time,1

import pwn
import numpy as np

conn = pwn.remote('13.201.224.182', 32545)
SIZE = 36 * 2


def print_buf(grid: np.ndarray):
    for row in grid:
        print(''.join('█' if e else ' ' for e in row))


def goto_top():
    while True:
        conn.sendlineafter(b'> ', b'w')
        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'W')
            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                break


def goto_left():
    while True:
        conn.sendlineafter(b'> ', b'a')
        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'A')
            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                return


def check_floor(x: int, grid: np.ndarray):
    conn.sendlineafter(b'> ', b's')
    if conn.recvline().decode().strip() != 'Moved!':
        grid[1, x] = 1
        grid[1, x + 1] = 1
    else:
        conn.sendlineafter(b'> ', b'w')


def map_line_right():
    grid = np.zeros((2, SIZE), dtype=np.uint8)
    x = 0

    while True:
        check_floor(x, grid)

        conn.sendlineafter(b'> ', b'd')
        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'D')

            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                conn.sendlineafter(b'> ', b's')
                print_buf(grid)
                return
            else:
                grid[0, x + 1] = 1
                grid[1, x + 1] = 1

        x += 2


def map_line_left():
    grid = np.zeros((2, SIZE), dtype=np.uint8)
    x = SIZE

    while True:
        check_floor(x, grid)

        conn.sendlineafter(b'> ', b'a')
        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'A')

            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                conn.sendlineafter(b'> ', b's')
                print_buf(grid)
                return
            else:
                grid[0, x - 1] = 1
                grid[1, x - 1] = 1

        x -= 2


goto_top()
goto_left()

# Skip top padding
for i in range(3):
    conn.sendlineafter(b'> ', b's')

for i in range(0, SIZE, 2):
    map_line_right()
    map_line_left()

we get


        ██████████████  ████  ██    ████  ████████  ██████████████
       █             █ █   █ █ █   █   █ █       █ █             █
       █  ██████████ █ █  ██ █ █████   █ █  ██████ █  ██████████ █
       █ █         █ █ █ █   █         █ █ █       █ █         █ █
       █ █  ██████ █ █ █ █  ██  ██████████ █       █ █  ██████ █ █
       █ █ █     █ █ █ █ █ █   █       █   █       █ █ █     █ █ █
       █ █ █     █ █ █ █ ███████      ██   ███  ██ █ █ █     █ █ █
       █ █ █     █ █ █ █   █         █       █ █ █ █ █ █     █ █ █
       █ █ █     █ █ █ █   █████  ██ ███  ████ ███ █ █ █     █ █ █
       █ █ █     █ █ █ █       █ █ █   █ █         █ █ █     █ █ █
       █ █ ███████ █ █ █████   ███ █   ███         █ █ ███████ █ █
       █ █         █ █     █       █               █ █         █ █
       █ ███████████ █  ██ █  ██  ████  ██  ██  ██ █ ███████████ █
       █             █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █             █
       ███████████████ █ ███ █████████ █ █ █ █████ ███████████████
                       █       █ █ █   █ █ █   █
          ██  ██  ████ █      ██ █ █  ██ █ █  ████████  ██████████
         █ █ █ █ █   █ █     █   █ █ █   █ █ █ █     █ █         █
        ██ ███ █ █████ █     █████ █ █  ██ █ ███     ███  ██    ██
       █       █       █           █ █ █   █             █ █   █
       █████  ████████ █  ████  ██ █ █ ███ ███████  ██  ██████ █
           █ █ █     █ █ █   █ █ █ █ █   █       █ █ █ █ █ █ █ █
          ██ █████████ ███  ██ █████ █████  ████████ ███ █████ ███
         █     █           █     █         █     █         █     █
         █  ██████  ██████ █  ██████  ████ █  ██████  ████ █  ████
         █ █   █ █ █     █ █ █   █ █ █   █ █ █   █ █ █   █ █ █
         ███   █ █████  ██ ███   █ ███  ██ ███   █ ███  ██ ███
               █   █ █ █         █     █         █     █
            ██ █   ███████    ██ █    ████    ██ █    ████    ██
           █ █ █     █ █ █   █ █ █   █ █ █   █ █ █   █ █ █   █ █
        ██ ███ █    ████ ███ ███ █   █████  ██ █ ███ █████  ██████
       █ █     █   █ █     █     █     █   █   █   █   █   █ █ █ █
       █ █  ██████ ███████ █  ██ ███   █████   ███ █   ███ █ █████
       █ █ █   █ █   █   █ █ █ █   █             █ █     █ █   █
       ███████████████  ██ █████████  ██████  ██ █ ███████ █  ████
         █ █   █ █     █     █ █     █     █ █ █ █         █ █ █ █
         ███   █ █  ██ ███  ██████  ████   █ ███ █         █ █ ███
               █ █ █ █   █ █ █ █ █ █ █ █   █     █         █ █
            ██████ ███  ██ █ █ █ ███ ███  ████  ██    ██   ███  ██
           █   █       █   █ █ █         █ █ █ █     █ █       █ █
        ██████████████ ███ ███ █  ██  ████ ███ ███████ █████  ████
       █   █   █     █   █     █ █ █ █                     █ █ █
       █████   ███████  ██  ████ █ █ █  ██  ██    ██████   ███ █
                       █   █     █ █ █ █ █ █ █   █     █       █
        ██████████████ █████████ █████ █ █ █████ █  ██ █  ██   ███
       █             █     █   █   █   █ █   █ █ █ █ █ █ █ █     █
       █  ██████████ █  ██████ █  ████████  ██ █ █ ███ █ ███    ██
       █ █         █ █ █   █ █ █ █ █   █   █   █ █     █       █
       █ █  ██████ █ █ ███ █ █ █ █ █  ████ █   █ ███████      ████
       █ █ █     █ █ █   █ █ █ █ █ █ █ █ █ █   █             █ █ █
       █ █ █     █ █ █  ██████ █ █████ ███ █  ████  ██    ██ █████
       █ █ █     █ █ █ █ █ █   █   █       █ █ █ █ █ █   █ █   █
       █ █ █     █ █ █ ███ █  ████ █  ██  ██ ███████████ ███  ████
       █ █ █     █ █ █     █ █ █ █ █ █ █ █     █ █ █ █ █     █ █ █
       █ █ ███████ █ █  ██ █████ █ █ █ █ ███  ██ █████ █     █████
       █ █         █ █ █ █   █   █ █ █ █   █ █     █   █       █
       █ ███████████ █ █████ █████████ █   █ █  ██ █   ███  ████
       █             █   █ █     █ █   █   █ █ █ █ █     █ █
       ███████████████   ███     ███   █████ ███ ███     ███

Looks a bit like a QR code, doesn't it? 🤔

First, though, we need to fix up all of the top-left corners which got messed up by the traversal algorithm. Doing so yields


       ███████████████ █████ ███   █████ █████████ ███████████████
       █             █ █   █ █ █   █   █ █       █ █             █
       █ ███████████ █ █ ███ █ █████   █ █ ███████ █ ███████████ █
       █ █         █ █ █ █   █         █ █ █       █ █         █ █
       █ █ ███████ █ █ █ █ ███ ███████████ █       █ █ ███████ █ █
       █ █ █     █ █ █ █ █ █   █       █   █       █ █ █     █ █ █
       █ █ █     █ █ █ █ ███████     ███   ███ ███ █ █ █     █ █ █
       █ █ █     █ █ █ █   █         █       █ █ █ █ █ █     █ █ █
       █ █ █     █ █ █ █   █████ ███ ███ █████ ███ █ █ █     █ █ █
       █ █ █     █ █ █ █       █ █ █   █ █         █ █ █     █ █ █
       █ █ ███████ █ █ █████   ███ █   ███         █ █ ███████ █ █
       █ █         █ █     █       █               █ █         █ █
       █ ███████████ █ ███ █ ███ █████ ███ ███ ███ █ ███████████ █
       █             █ █ █ █ █ █ █ █ █ █ █ █ █ █ █ █             █
       ███████████████ █ ███ █████████ █ █ █ █████ ███████████████
                       █       █ █ █   █ █ █   █
         ███ ███ █████ █     ███ █ █ ███ █ █ █████████ ███████████
         █ █ █ █ █   █ █     █   █ █ █   █ █ █ █     █ █         █
       ███ ███ █ █████ █     █████ █ █ ███ █ ███     ███ ███   ███
       █       █       █           █ █ █   █             █ █   █
       █████ █████████ █ █████ ███ █ █ ███ ███████ ███ ███████ █
           █ █ █     █ █ █   █ █ █ █ █   █       █ █ █ █ █ █ █ █
         ███ █████████ ███ ███ █████ █████ █████████ ███ █████ ███
         █     █           █     █         █     █         █     █
         █ ███████ ███████ █ ███████ █████ █ ███████ █████ █ █████
         █ █   █ █ █     █ █ █   █ █ █   █ █ █   █ █ █   █ █ █
         ███   █ █████ ███ ███   █ ███ ███ ███   █ ███ ███ ███
               █   █ █ █         █     █         █     █
           ███ █   ███████   ███ █   █████   ███ █   █████   ███
           █ █ █     █ █ █   █ █ █   █ █ █   █ █ █   █ █ █   █ █
       ███ ███ █   █████ ███ ███ █   █████ ███ █ ███ █████ ███████
       █ █     █   █ █     █     █     █   █   █   █   █   █ █ █ █
       █ █ ███████ ███████ █ ███ ███   █████   ███ █   ███ █ █████
       █ █ █   █ █   █   █ █ █ █   █             █ █     █ █   █
       ███████████████ ███ █████████ ███████ ███ █ ███████ █ █████
         █ █   █ █     █     █ █     █     █ █ █ █         █ █ █ █
         ███   █ █ ███ ███ ███████ █████   █ ███ █         █ █ ███
               █ █ █ █   █ █ █ █ █ █ █ █   █     █         █ █
           ███████ ███ ███ █ █ █ ███ ███ █████ ███   ███   ███ ███
           █   █       █   █ █ █         █ █ █ █     █ █       █ █
       ███████████████ ███ ███ █ ███ █████ ███ ███████ █████ █████
       █   █   █     █   █     █ █ █ █                     █ █ █
       █████   ███████ ███ █████ █ █ █ ███ ███   ███████   ███ █
                       █   █     █ █ █ █ █ █ █   █     █       █
       ███████████████ █████████ █████ █ █ █████ █ ███ █ ███   ███
       █             █     █   █   █   █ █   █ █ █ █ █ █ █ █     █
       █ ███████████ █ ███████ █ █████████ ███ █ █ ███ █ ███   ███
       █ █         █ █ █   █ █ █ █ █   █   █   █ █     █       █
       █ █ ███████ █ █ ███ █ █ █ █ █ █████ █   █ ███████     █████
       █ █ █     █ █ █   █ █ █ █ █ █ █ █ █ █   █             █ █ █
       █ █ █     █ █ █ ███████ █ █████ ███ █ █████ ███   ███ █████
       █ █ █     █ █ █ █ █ █   █   █       █ █ █ █ █ █   █ █   █
       █ █ █     █ █ █ ███ █ █████ █ ███ ███ ███████████ ███ █████
       █ █ █     █ █ █     █ █ █ █ █ █ █ █     █ █ █ █ █     █ █ █
       █ █ ███████ █ █ ███ █████ █ █ █ █ ███ ███ █████ █     █████
       █ █         █ █ █ █   █   █ █ █ █   █ █     █   █       █
       █ ███████████ █ █████ █████████ █   █ █ ███ █   ███ █████
       █             █   █ █     █ █   █   █ █ █ █ █     █ █
       ███████████████   ███     ███   █████ ███ ███     ███

Resizing the image in Photoshop to make the corner boxes more square-like, we get an interesting pattern that looks like this:

The key realization here is that the corners of a valid QR code should consist of a single ring around solid block of black:

Because the walls lie between cells, it's clear now that the walls of the maze separate regions of different color in the QR code. Then, this challenge turns into a sort of "A/B coloring" problem solvable with the following routine:

  1. Keep a state tracking the current "color": white/black (or filled/unfilled)
  2. Iterate over all cells, painting each cell the current color.
  3. When the program hits a wall in the maze while iterating, swap the current color.

Modifying our traversal script with the given algorithm,

import pwn

conn = pwn.remote('13.201.224.182', 32323)
fill = False


def goto_top():
    while True:
        conn.sendlineafter(b'> ', b'w')
        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'W')
            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                break


def goto_left():
    while True:
        conn.sendlineafter(b'> ', b'a')
        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'A')
            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                return


def map_line_right() -> str:
    global fill
    ret = ''

    while True:
        conn.sendlineafter(b'> ', b'd')
        ret += '█' if fill else ' '

        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'D')

            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                conn.sendlineafter(b'> ', b's')
                return ret
            else:
                fill = not fill


def map_line_left() -> str:
    global fill
    ret = ''

    while True:
        conn.sendlineafter(b'> ', b'a')
        ret += '█' if fill else ' '

        if conn.recvline().decode().strip() != 'Moved!':
            conn.sendlineafter(b'> ', b'A')

            if conn.recvline().decode().strip() != 'Jumped over a wall!':
                conn.sendlineafter(b'> ', b's')
                return ret[::-1]
            else:
                fill = not fill


goto_top()
goto_left()

# Skip top padding
for i in range(4):
    conn.sendlineafter(b'> ', b's')

while True:
    print(map_line_right())
    print(map_line_left())

we get

    ███████ ██ █  ██ ████ ███████    
    █     █ █  █████ █    █     █    
    █ ███ █ █ ██    ██    █ ███ █    
    █ ███ █ ██     ████ █ █ ███ █    
    █ ███ █ ████ █  █     █ ███ █    
    █     █   ████        █     █    
    ███████ █ █ █ █ █ █ █ ███████    
            ████ █  █ ██             
     █ █ ██ ███  █ ██ █ ███ █████    
    ████    ██████ █  ███████ ██     
      █ ███ █  █ █ ██    █ █ █ █     
     ███      ███     ███     ███    
     █  █ ███ █  █ ██ █  █ ██ █      
        ██ █     ███     ███         
      █ ███ █  █ ██ █  █ ██ █  █     
    █   ██ ███   ███  ██  ██  █ █    
    █ ██ ██  █ █  ███████ ███ ██     
     █  █   ███ ███   █ █     █ █    
        █ █  █ █ █ █  ███     █      
      ██    ██ █     █ █   █    █    
    ██  ███  ███ █ ███████████ █     
            ██   █ █ █ ██   ████     
    ███████   ██  ██ ██ █ █ █ ███    
    █     █ ██ █ █  ██  █   ████     
    █ ███ █  █ █ █ █ █  ███████ █    
    █ ███ █ █ ██  ████ █ █ ██ ██     
    █ ███ █   █ █ █ █   █ █ ███ █    
    █     █ █  ██ █ ██ ███  ████     
    ███████  █   █  ██ █ █   █       

which we can scan to get the flag.

bi0sctf{cZrfONl+bGtGiKWMnnR5Sg==}

Footnotes

  1. A bit sloppy, but we're somewhat forced to print the buffer before reading the next line instead of printing it all at the end because traversing the maze takes so long the connection times out (crashing the program) before the entire process is done.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment