This is a challenge from the Hack The Box cyber apocalypse CTF (2025).
The goal of this challenge is to exploit a stack overflow. However, this binary has stack canaries protection so we need to leak the canary in order to avoid this :

Stack canaries
Stack canaries were implemented to prevent buffer overflow attacks. The way it works is a random value is placed on the stack and assigned to a variable. This value changes every time the program starts, so it will always be different. At the end of the function, that value is compared to the initial value, and if it’s the same, then the function will return.
int main(){
uint64_t canary = *(uint64_t*)((char*)fbase + 0x28)
[function does stuff]
if (canary == *(uint64_t*)((char*)fbase + 0x28))
return [something]
}
Another thing to know about stack canaries is that they are a 8 bytes and end in 00. Binaries on Linux are usually little-endian so we would start by reading the 00 and then the rest of the bytes.
Example:
Big endian 0xc5 30 e6 da 6f ff 6e 00
Little endian 0x00 6e ff 6f da e6 30 c5
We can say that the stack canaries are then 7 bytes long and null terminated. This makes them easier to spot in, for example, a pwndbg dump. However, this also means that if you try to read the canary with any sort of string function (printf, strcpy, strlen), it will stop at the first occurrence of a null-terminated string. Later on I will explain how we can circumvent this issue.
Leak the canary
First we need to leak the canary.
Analysing the code, we see that we are required to enter up to 0x66 (102) bytes into buf. Then, it’s performing a string search of the string “Quack Quack ” in all of our input. That means we can write AAAAQuack Quack AAA and we will satisfy the if condition.

This is where we have to start paying attention. As you notice on the above image, we have a printf which will print a string as of rax_1[0x20] until it meets a null byte. Reminder that strstr returns a pointer to the beginning of the substring, or NULL if the substring is not found.
This is where we will be able to leak the canary. Basically, we can ensure to have our strstr pointer point right next to the value of the canary (in memory). Once we have that set-up, we will start reading as of rax_1[0x20] (rax_1[32]).

In order to circumvent the string termination issue, we can make sure that our rax_1[0x20] starts reading one byte into the canary. To do that, we simply add one more byte to our buffer of A’s to perfectly place the rax_1 pointer and overwrite the null byte of the canary.

We inspect the rsi register because that is where our values are stored.
Cool, let’s make a script to retrieve this shall we?
with conn() as p:
buf = b'A' * (101 - len('Quack Quack '))
p.sendlineafter(b'> ', buf + b'Quack Quack ')
p.recvuntil(b'Quack Quack ')
canary = u64(p.recv(7).rjust(8, b'\0'))
print(hex(canary))
If you’ve noticed this line canary = u64(p.recv(7).rjust(8, b'\0')), this is basically because we are unpacking 7 bytes but u64 needs to be aligned so we need to unpack 8 bytes. This is fine, we can use rjust (adjust to the right), which will add ‘0’ byte until it reaches the size of 8 bytes. In this case, it will only add one 0 byte. If you’ve noticed, this is also the byte that we truncated when we calculated the size of buff.

For the second vulnerability, we have a classic stack buffer overflow where we can overwrite the return address to the duck_attack function, which will print the flag. This is called a ret2win.
On the second read, we can read 0x6a bytes (106 decimal).
If we calculate how far the canary is on the second read call, we count 0x58 bytes.
Soooo, we can see this in the stack view in Binja:

We can see here we have 0x58 bytes (88 decimal) before we need to put the stack canary back, and then we have to overwrite the rbp and then we can overwrite the return address. So that means we need 0x58 bytes of A, then canary, then 8 bytes for the rbp and then when can finally overwrite the return address, so then we add the duck_attack symbol (classic ret2win).
buff2 = b'A' * 0x58
overwrite_rbp = b'A' * 8
payload = flat([buff2, canary, overwrite_rbp, elf.symbols.duck_attack])

Running the script, we see we retrieve the flag (I created a test flag for this challenge as this did not come with one).


Leave a comment