Back to writeups
FlagYardpwnHardsolved

fs

12 Jul 2026 · by 0xIDA

Solvers

fs

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

Solvers

Summary

Hard heap note challenge on glibc 2.31 built without tcache. UAF (free without nulling content) plus edit/view length derived from the chunk size field enable an off-by-one size corruption, unsorted-bin libc leak, fastbin poison into __malloc_hook-0x33, and a one_gadget shell.

Challenge Description

there is always a way

  • Protocol: TCP
  • Files: fs, libc.so.6 (2.31 no-tcache), ld.so.2
  • Protections: Full RELRO, no canary, NX, PIE
  • Menu: create / edit / view / delete (indices 0–4)
  • Fixed malloc(0x66) → 0x70 chunks

Vulnerability Analysis

Struct layout (stack)

struct file { char name[8]; char *content; }; // 0x10 each, 5 slots

Bugs

  1. UAF: delete calls free(content) then memset(slot, 0, 8) — only clears the name, not the content pointer.
  2. Size-derived R/W: edit/view use len = *(uint64_t*)(content - 8) - 8 (raw size field, flags included) → for in-use 0x71, len = 0x69 into 0x60 usable user data → overflow into next chunk (prev_size + 1 byte of size). Classic off-by-one on next size LSB.
  3. create already read(0x66) into 0x60 usable (6-byte overflow into next prev_size).

Handout libc

Path strings show glibc_2.31_no-tcache — no tcache freelist; 0x70 frees go to fastbin.

Exploitation

Stage 1 — libc leak

  1. Create five 0x70 notes.
  2. Edit note 0 with 0x60 padding + fake prev_size + 0xe1 so note 1’s size becomes 0xe1 (span 0xe0 bytes → lands on note 3).
  3. Delete note 1 → free large chunk into unsorted bin (size 0xe0 > max fast 0xa0).
  4. View UAF note 1 → fd = main_arena+0x60 = libc + 0x3b5bc0.

Stage 2 — fastbin → malloc_hook

  1. Create once to drain/split unsorted.
  2. Free notes 3 and 4 into fastbin (4 → 3).
  3. UAF-edit note 4’s fd to chunk pointer __malloc_hook - 0x33 (not the user pointer).
    Nearby misaligned field: size 0x7f at malloc_hook-0x2bchunksize 0x78 maps to the same fastbin index as 0x70.
  4. Alloc once (pops 4), alloc again into fake user at malloc_hook-0x23.
  5. Write A*0x23 + one_gadget → set __malloc_hook.

one_gadget used: 0xc4eef (execve("/bin/sh", r13, r12) with NULL constraints — satisfied here).

Stage 3 — trigger

Create again: after reading the name, malloc(0x66) hits the hook → shell → cat flag.

# Core offsets (handout libc)
LIBC_UNSORTED = 0x3b5bc0
ONE_GADGET    = 0xc4eef
# fake_chunk = libc.sym['__malloc_hook'] - 0x33

Flag Retrieval

Live on tcp.flagyard.com:27644:

uid=1000 gid=1000 groups=1000
FlagY{3ddc0a11c5b726276bc1c3aa378093cc}

Submit: Success.

Key Takeaways

  1. Confirm tcache presence (this handout is explicitly no-tcache) — changes freelist attack surface.
  2. Fastbin fd is a chunk pointer; target malloc_hook-0x33, not -0x23.
  3. Size 0x7f / 0x78 shares fastbin index with 0x70 — classic misaligned hook write.
  4. Edit/view length from raw chunk size is a powerful OOB when combined with UAF.

Tools Used

  • pwntools, one_gadget, objdump, checksec
  • Flagyard MCP (spawn/submit)

Files

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