happy hacks

pentathon2025 quagmire writeup

We are given an ELF

$ file glade
glade: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=54b9aaa22e18333549ed3c33a7a0562d2022cee6, for GNU/Linux 3.2.0, stripped

It runs like this

$ ./glade
_______________________________
|_U_    |    ___|___    |    ___|
|    ___    |       |   ____    |
|___|   |_______|___________|   |
|           |       |   ________|
#.####..##.#.##.....#.##...####..
..###.#.####.#..#.#.#..#.. .###.#
.#.###.#.#.#..#.#.....##.#.....#.
...##...###.#..##..###.#.#######

Enter the moves [a/w/s/d]:

By the looks of the initial output and the a/w/s/d moves - this is likely a maze that needs to be figured out.

With radare2 the maze is likely here

│           0x0000152e      488d15ab0c..   lea rdx, [0x000021e0]
│           0x00001535      b980000000     mov ecx, 0x80
│           0x0000153a      4889c7         mov rdi, rax
│           0x0000153d      4889d6         mov rsi, rdx
│           0x00001540      f348a5         rep movsq qword [rdi], qword [rsi]
....
s 0x000021e0
[0x000021e0]> pxd4
- offset -   E0 E1  E2 E3  E4 E5  E6 E7  E8 E9  EA EB  EC ED  EE EF  0123456789ABCDEF
0x000021e0              0             0             0             1  ................
0x000021f0              1             0             1             0  ................
0x00002200              0             0             1             1  ................
0x00002210              1             0             0             0  ................
0x00002220              0             0             0             1  ................
0x00002230              1             0             1             0  ................
0x00002240              0             0             1             1  ................
0x00002250              1             0             0             0  ................
0x00002260              1             0             1             1  ................
0x00002270              1             1             0             1  ................
0x00002280              1             1             1             0  ................
0x00002290              0             0             1             1  ................
0x000022a0              1             0             1             0  ................
0x000022b0              0             1             1             1  ................
0x000022c0              1             1             0             1  ................
0x000022d0              1             0             1             0  ................

Lets decompile with IDA and see what this 0x000021e0 should be. With some analysis we get to this decompilation

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  size_t len_input; // rbx
  size_t len_enc_flag; // rsi
  char curr_move; // [rsp+Fh] [rbp-501h]
  int reached; // [rsp+10h] [rbp-500h]
  int i; // [rsp+14h] [rbp-4FCh]
  int j; // [rsp+18h] [rbp-4F8h]
  int dead_end; // [rsp+1Ch] [rbp-4F4h]
  int k; // [rsp+20h] [rbp-4F0h]
  int curr_maze_idx; // [rsp+24h] [rbp-4ECh]
  cell maze[64]; // [rsp+30h] [rbp-4E0h] BYREF
  char enc_flag[40]; // [rsp+430h] [rbp-E0h] BYREF
  char flag_dec[48]; // [rsp+460h] [rbp-B0h] BYREF
  char input[104]; // [rsp+490h] [rbp-80h] BYREF
  unsigned __int64 canary; // [rsp+4F8h] [rbp-18h]

  canary = __readfsqword(0x28u);
  *(_QWORD *)enc_flag = 0xE8482736A5706406LL;
  *(_QWORD *)&enc_flag[8] = 0x6A8A56B322AE3F12LL;
  *(_QWORD *)&enc_flag[16] = 0xC348522ABB1A2CB4LL;
  *(_QWORD *)&enc_flag[24] = 0x2F3FEB54C9C7994LL;
  *(_QWORD *)&enc_flag[32] = 0xAFF6C5;
  qmemcpy(maze, src_, sizeof(maze));
  reached = 0;
  for ( i = 0; aU[i]; ++i )
    putchar(aU[i]);
  printf("\nEnter the moves [a/w/s/d]: ");
  __isoc99_scanf("%s", input);
  if ( strlen(input) == 39 )
  {
    for ( j = 0; j < strlen(input); ++j )
    {
      curr_move = input[j];
      curr_maze_idx = reached;
      dead_end = 0;
      if ( curr_move == 'd' && maze[curr_maze_idx].d == 1 && reached % 8 != 7 )
      {
        ++reached;
        dead_end = 1;
      }
      else if ( curr_move == 'a' && maze[curr_maze_idx].a == 1 && (reached & 7) != 0 )
      {
        --reached;
        dead_end = 1;
      }
      else if ( curr_move == 's' && maze[curr_maze_idx].s == 1 && reached <= 55 )
      {
        reached += 8;
        dead_end = 1;
      }
      else if ( curr_move == 'w' && maze[curr_maze_idx].w == 1 && reached > 7 )
      {
        reached -= 8;
        dead_end = 1;
      }
      if ( !dead_end )
      {
        puts("you've reached a dead end");
        return 1;
      }
    }
    if ( reached == 40 )
    {
      len_input = strlen(input);
      len_enc_flag = strlen(enc_flag);
      rc4_decrypt(enc_flag, len_enc_flag, input, len_input, flag_dec);
      printf("You're out, here's your flag: ");
      for ( k = 0; k < strlen(enc_flag); ++k )
        putchar((unsigned __int8)flag_dec[k]);
      putchar(10);
    }
    else
    {
      puts("you've come the wrong way...");
    }
    return 0;
  }
  else
  {
    puts("you're not there yet");
    return 1;
  }
}

maze is 0x000021e0 - an array of 256 DWORD. This is actually an array of

struct cell
{
    _DWORD a;
    _DWORD w;
    _DWORD s;
    _DWORD d;
};

Where the fields - a, w, s, d are booleans that determine - if the current cell permits move in that direction.

With this information we can extract and build the maze as an image

[0x000021e0]> pcw 1024

Which is then used to build the image

maze = [_maze[i : i + 4] for i in range(0, 256, 4)]

score = {"d": 1, "a": -1, "s": 8, "w": -8}
idxs = ["a", "w", "s", "d"]

class Cell:
    def __init__(self, a, w, s, d):
        self.a = a
        self.w = w
        self.s = s
        self.d = d

entries = [Cell(maze[i][0], maze[i][1], maze[i][2], maze[i][3]) for i in range(64)]

maze = [entries[i : i + 8] for i in range(0, 64, 8)]

from PIL import Image, ImageDraw, ImageFont

maze_size = 8
cell_size = 50

image_size = maze_size * cell_size
img = Image.new("RGB", (image_size, image_size), color="white")
draw = ImageDraw.Draw(img)

font = ImageFont.load_default()

def draw_maze(maze, draw, cell_size):
    for i in range(maze_size):
        for j in range(maze_size):

            x1 = j * cell_size
            y1 = i * cell_size
            x2 = (j + 1) * cell_size
            y2 = (i + 1) * cell_size

            index = i * maze_size + j
            draw.text(
                (x1 + cell_size // 4, y1 + cell_size // 4),
                str(index),
                fill="black",
                font=font,
            )

            entry = maze[i][j]

            if not entry.a:
                draw.line([(x1, y1), (x1, y2)], fill="black", width=3)
            if not entry.w:
                draw.line([(x1, y1), (x2, y1)], fill="black", width=3)
            if not entry.s:
                draw.line([(x1, y2), (x2, y2)], fill="black", width=3)
            if not entry.d:
                draw.line([(x2, y1), (x2, y2)], fill="black", width=3)

Which results to

We also know that

    if ( reached == 40 )
    {
      len_input = strlen(input);
      len_enc_flag = strlen(enc_flag);
      rc4_decrypt(enc_flag, len_enc_flag, input, len_input, flag_dec);
      printf("You're out, here's your flag: ");
      for ( k = 0; k < strlen(enc_flag); ++k )
        putchar((unsigned __int8)flag_dec[k]);
      putchar(10);
    }

We need to reach the index 40 - this can be achieved visually and verified with

def plot_moves(maze, moves, draw, cell_size):
    x, y = 0, 0
    draw.rectangle(
        [x * cell_size, y * cell_size, (x + 1) * cell_size, (y + 1) * cell_size],
        outline="red",
        width=3,
    )

    for move in moves:
        if move == "w" and maze[y][x].w:
            y -= 1
        elif move == "s" and maze[y][x].s:
            y += 1
        elif move == "a" and maze[y][x].a:
            x -= 1
        elif move == "d" and maze[y][x].d:
            x += 1

        draw.rectangle(
            [x * cell_size, y * cell_size, (x + 1) * cell_size, (y + 1) * cell_size],
            outline="red",
            width=3,
        )
        move_text = move.upper()
        text_position = (
            x * cell_size + cell_size // 4 + 10,
            y * cell_size + cell_size // 4 + 10,
        )
        draw.text(text_position, move_text, fill="red", font=font)

draw_maze(maze, draw, cell_size)

img.save("maze.png")

moves = "dsdsdwdsdwddssaasddssasawwassawwawwaass"
plot_moves(maze, moves, draw, cell_size)

img.save("maze_with_moves.png")

img.show()

Which gives out

Which also gives the flag

$ ./glade
_______________________________
|_U_    |    ___|___    |    ___|
|    ___    |       |   ____    |
|___|   |_______|___________|   |
|           |       |   ________|
#.####..##.#.##.....#.##...####..
..###.#.####.#..#.#.#..#.. .###.#
.#.###.#.#.#..#.#.....##.#.....#.
...##...###.#..##..###.#.#######

Enter the moves [a/w/s/d]: dsdsdwdsdwddssaasddssasawwassawwawwaass
You're out, here's your flag: flag{hello_mortal_of_a_hollow_vase}

** flag{hello_mortal_of_a_hollow_vase} **

#CTF #reversing #writeup