Something's a little off about this stack cookie...
TL;DR
The "canary" turns out to be a XORed printf address. Exploit a null-byte leak with reads to leak the stack canary, followed by the utilization of One Gadget to defeat buffer size restriction.
Basic File Checks
The challenge name itself suggests the existence of a stack canary, and thus this might just be a classic canary leak + rewrite challenge.
┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ ./weird_cookie
Do you think you can overflow me?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Are you sure you overflowed it right? Try again.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nope. :(
However, checking the binary protection shows that a stack canary is in fact missing.
┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ checksec --file=weird_cookie
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO No canary found NX enabled PIE enabled No RPATH No RUNPATH 75 Symbols No 0 3 weird_cookie
Static Code Analysis
Based on the decompiled code, the hardcoded canary value is easily visible, meaning that we just have to rewrite the canary value, thereby saving us the effort and time to hunt for a read primitive that can be used to leak the stack canary.
Next, the program will store the user input inside the local_38 variable with read() function, reading up to a maximum of 64 bytes (0x40) bytes. This is an obvious buffer overflow because local_38 can only hold up to 40 bytes.
undefined8 main(void){char local_38 [40];long local_10;setup(); local_10 =0x123456789aac8ee9; saved_canary =0x123456789aac8ee9;memset(local_38,0,0x28);puts("Do you think you can overflow me?");read(0,local_38,0x40);puts(local_38);memset(local_38,0,0x28);puts("Are you sure you overflowed it right? Try again.");read(0,local_38,0x40);if (local_10 != saved_canary) {puts("Nope. :(");exit(0); }return0;}
Revisiting Exploit Plan
Our initial plan was to rewrite the canary value and exploit the buffer overflow to control the EIP and hopefully direct program execution to a win() function. However, a win() function simply did not exist in the challenge, so we had to improvise to a ret2libc attack instead.
Analyzing the decompiled code and assembly for a second time revealed that the hardcoded canary is in fact an encrypted version of printf address. The hardcoded value visible from the previous section was instead the XOR key.
Finding Read Primitive
Now, because PIE is enabled, we need to find a way, or a read primitive to leak the encrypted canary on runtime, and decrypt it with the hardcoded XOR key.
Conveniently, since the read() function does not perform any string termination, it will cause some data to be leaked when we overwrite the null byte. As a result, supplying exactly 40 characters as the user input will leak the XOR-ed canary.
┌──(kali💀JesusCries)-[~/Desktop/CTF/NahamCon CTF 2023/Weird Cookie]
└─$ ./weird_cookie
Do you think you can overflow me?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
*�M�)4
Are you sure you overflowed it right? Try again.
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Nope. :(
One Gadget
To sum up, the current plan is to leak the XOR-ed printf address, then perform a ret2libc attack. But since the buffer size is not large enough, we can only accommodate 24 bytes worth of addresses after the buffer overflow (64-40=24), meaning ret2libc attack will not be possible.
Deducting 8 bytes required for the canary rewrite, and another 8 bytes for stack alignment, means that we have to overwrite the saved EIP with only a single address.
Luckily, we can use one_gadget to help us spawn a shell, using just a single address.
Putting everything together, the stack canary (8 bytes) + ret gadget (8 bytes) + one gadget (8 bytes) = 24 bytes in total, which is just exactly enough to fit in the limited buffer space.