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} **