crypto

stream-cipher-fans-when

tldr;

  • cipher.py — the vulnerable encryption scheme

  • AIW-truncated.txt — the public-knowledge plaintext source (Alice in Wonderland)

  • encrypted.bin — ciphertext we must decrypt

details — what to do

  • The generating code that places the secret into the ciphertext path is in cipher.py:

    • This write the encrypted data to encrypted.bin:

with open('AIW.txt', 'rb') as f:
     aiw = f.read()[random.randint(0, 1000):]
with open('encrypted.bin', 'wb') as f:
     for block in encrypt(aiw):
         f.write(block)

an meth example

meth

solve

import re
from pathlib import Path
import numpy as np

CHUNK = 256
enc = np.frombuffer(Path("encrypted.bin").read_bytes(), dtype=np.uint8)
C = enc.reshape((-1, CHUNK))  # (B,256)
B = C.shape[0]


def raw_block(counter: int) -> np.ndarray:
    data = (str(counter) * 1337).encode()
    state = np.zeros(CHUNK, dtype=np.uint8)
    for i in range(0, len(data), CHUNK):
        chunk = data[i : i + CHUNK]
        chunk = chunk + b"\0" * (CHUNK - len(chunk))
        state ^= np.frombuffer(chunk, dtype=np.uint8)
    return state


S = np.stack([raw_block(i) for i in range(B)], axis=0)  # (B,256)


def byte_w(b: int) -> float:
    if not (b in {9, 10, 13} or 32 <= b <= 126):
        return -10.0
    w = 1.0
    if b == 32:
        w += 0.7
    if b in b"etaoinshrdlucmfwypvbgkqjxz":
        w += 0.4
    if b in b"ETAOINSHRDLUCMFWYPVBGKQJXZ":
        w += 0.2
    if b in b",.;:'\"!?-()[]{}_*/\\":
        w += 0.15
    if b in b"0123456789":
        w += 0.05
    return w


W = np.zeros((CHUNK, CHUNK), dtype=float)
for j in range(CHUNK):
    colC = C[:, j]
    for k in range(CHUNK):
        W[j, k] = float(sum(byte_w(int(b)) for b in (colC ^ S[:, k])))


def hungarian(cost):
    cost = np.array(cost, dtype=float)
    n = cost.shape[0]
    u = np.zeros(n + 1)
    v = np.zeros(n + 1)
    p = np.zeros(n + 1, dtype=int)
    way = np.zeros(n + 1, dtype=int)
    for i in range(1, n + 1):
        p[0] = i
        j0 = 0
        minv = np.full(n + 1, float("inf"))
        used = np.zeros(n + 1, dtype=bool)
        while True:
            used[j0] = True
            i0 = p[j0]
            delta = float("inf")
            j1 = 0
            for j in range(1, n + 1):
                if not used[j]:
                    cur = cost[i0 - 1, j - 1] - u[i0] - v[j]
                    if cur < minv[j]:
                        minv[j] = cur
                        way[j] = j0
                    if minv[j] < delta:
                        delta = minv[j]
                        j1 = j
            for j in range(0, n + 1):
                if used[j]:
                    u[p[j]] += delta
                    v[j] -= delta
                else:
                    minv[j] -= delta
            j0 = j1
            if p[j0] == 0:
                break
        while True:
            j1 = way[j0]
            p[j0] = p[j1]
            j0 = j1
            if j0 == 0:
                break
    a = np.zeros(n, dtype=int)
    for j in range(1, n + 1):
        a[p[j] - 1] = j - 1
    return a


cost = W.max() - W
perm = hungarian(cost)  # perm[j] = k
P = C ^ S[:, perm]
pt = bytes(P.reshape(-1).tolist())
Path("decrypted.txt").write_bytes(pt)
m = re.search(rb"[a-zA-Z]*\{[^}]+\}", pt)
print("Flag:", m.group(0).decode() if m else "<not found>")

> python solve.py
Flag: watctf{https://graydon2.dreamwidth.org/319755.html}
  • watctf{https://graydon2.dreamwidth.org/319755.html}

java-oracle

Consult the Java Oracle Secure Vault — but beware, its predictions leak more than your morning espresso. nc challs.watctf.org 2013

tldr;

import binascii

import json
import socket
import sys

BS = 16


def recv_line(f):
    line = f.readline()
    if not line:
        raise RuntimeError("disconnected")
    return line.decode(errors="ignore").rstrip("\r\n")


def split_blocks(b, n=16):
    return [b[i : i + n] for i in range(0, len(b), n)]


def unpad(b: bytes) -> bytes:
    if len(b) < BS or len(b) % BS != 0:
        raise ValueError
    p = b[-1]
    if p < 1 or p > BS or b[-p:] != bytes([p]) * p:
        raise ValueError
    return b[:-p]


def build_batch(Ci: bytes, I: bytearray, pos: int):
    pad = BS - pos
    tail = bytearray(BS)
    for j in range(pos + 1, BS):
        tail[j] = I[j] ^ pad
    lines = []
    for g in range(256):
        J = bytearray(BS)
        J[pos] = g
        for j in range(pos + 1, BS):
            J[j] = tail[j]
        lines.append(binascii.hexlify(bytes(J) + Ci).decode())
    return lines


def recover_block(f, Ci: bytes, Prev: bytes) -> bytes:
    I = bytearray(BS)
    for pos in range(BS - 1, -1, -1):
        lines = build_batch(Ci, I, pos)
        f.write(("\n".join(lines) + "\n").encode())
        f.flush()
        hit = None
        for idx in range(256):
            s = recv_line(f)
            if ("Valid padding" in s) or ("Access granted!" in s):
                if hit is None:
                    hit = idx
        if hit is None:
            raise RuntimeError("no hit")
        I[pos] = hit ^ (BS - pos)
    return bytes(a ^ b for a, b in zip(I, Prev))


def main():
    host, port = sys.argv[1], int(sys.argv[2])
    s = socket.create_connection((host, port), timeout=900)
    s.settimeout(900)
    f = s.makefile("rwb", buffering=0)

    banner = recv_line(f)
    recv_line(f)

    raw = bytes.fromhex(banner)
    iv0, ct = raw[:BS], raw[BS:]
    C = split_blocks(ct, BS)

    rec = []
    for i in range(len(C) - 1, -1, -1):
        Ci = C[i]
        Prev = iv0 if i == 0 else C[i - 1]
        Pi = recover_block(f, Ci, Prev)
        rec.insert(0, Pi)

    pt = unpad(b"".join(rec))
    print("[+] Plaintext:", pt)
    j = json.loads(pt.decode())
    print("[+] Flag:", j["access_code"])

main()

> python solve.py
[+] Plaintext: b'{"access_code": "watctf{quantum_helix_padding_oracle}", "facility": "quantum_reactor_z9", "clearance": "alpha"}'
[+] Flag: watctf{quantum_helix_padding_oracle}
  • Flag: watctf{quantum_helix_padding_oracle}

permpress

import re
import socket
import sys

HOST = "challs.watctf.org"
PORT = 2333
TIMEOUT = 30.0

# --- FNV-1a 64-bit (matches simplehash::fnv1a_64 on i32::to_le_bytes) ---
FNV_OFFSET_64 = 0xCBF29CE484222325
FNV_PRIME_64 = 0x100000001B3


def fnv1a_64(data: bytes) -> int:
    h = FNV_OFFSET_64
    for b in data:
        h ^= b
        h = (h * FNV_PRIME_64) & 0xFFFFFFFFFFFFFFFF
    return h


def hash_bucket(x: int) -> int:
    b = x.to_bytes(4, "little", signed=True)  # i32::to_le_bytes
    return fnv1a_64(b) % 32


# Precompute 32 buckets (each is size 8 with FNV-1a here)
BUCKETS = [[] for _ in range(32)]
for v in range(256):
    BUCKETS[hash_bucket(v)].append(v)


def makefile(sock: socket.socket):
    return sock.makefile("rwb", buffering=0)


# -------- robust recv helpers (do NOT require newline) --------
def recv_until(fp, token: bytes) -> bytes:
    buf = b""
    while token not in buf:
        b = fp.read(1)
        if not b:
            raise RuntimeError("connection closed while waiting for prompt")
        buf += b
    return buf


def recv_line_containing(fp, needle: bytes) -> bytes:
    line = b""
    while True:
        ch = fp.read(1)
        if not ch:
            raise RuntimeError("connection closed while reading line")
        line += ch
        if ch == b"\n":
            if needle in line:
                return line
            line = b""


def wait_for_choice_prompt(fp):
    recv_until(fp, b"Enter your choice:")


def sendline(fp, s: str):
    fp.write(s.encode() + b"\n")
    fp.flush()


# -------- oracle interaction --------
def alt_pair_perm(a: int, b: int) -> list[int]:
    out = [0] * 256
    for i in range(128):
        out[2 * i] = a
        out[2 * i + 1] = b
    return out


def submit_perm_get_value(fp, perm_list):
    # IMPORTANT: assume we're ALREADY at the "Enter your choice:" prompt.
    sendline(fp, "1")
    recv_until(fp, b"Enter the permutation")
    sendline(fp, " ".join(map(str, perm_list)))

    # The oracle prints a full line with println!
    line = recv_line_containing(fp, b"The oracle has divined")
    m = re.search(rb"divined\.\.\.\s*(-?\d+)", line)
    if not m:
        raise RuntimeError(f"unexpected oracle line: {line!r}")
    val = int(m.group(1))

    # After printing the result, the server reprints the menu + prompt.
    wait_for_choice_prompt(fp)
    return val


def sample_pair(fp, a: int, b: int, max_tries: int = 16) -> set[int]:
    vals = set()
    perm = alt_pair_perm(a, b)
    for _ in range(max_tries):
        vals.add(submit_perm_get_value(fp, perm))
        if len(vals) >= 2:
            break
    return vals


def recover_bucket(fp, items: list[int]) -> dict[int, int]:
    assert len(items) >= 2
    a = items[0]
    pair_sets = {}
    for s in items[1:]:
        vs = sample_pair(fp, a, s)
        tries = 0
        while len(vs) < 2 and tries < 24:
            vs |= sample_pair(fp, a, s, max_tries=4)
            tries += 1
        if len(vs) < 2:
            raise RuntimeError(f"couldn't separate values for pair ({a},{s}), got {vs}")
        pair_sets[s] = vs

    inter = None
    for vs in pair_sets.values():
        inter = vs if inter is None else (inter & vs)
    if not inter or len(inter) != 1:
        raise RuntimeError(f"anchor intersection ambiguous: {inter}")
    val_a = next(iter(inter))

    mapping = {a: val_a}
    for s, vs in pair_sets.items():
        rest = list(vs - {val_a})
        if len(rest) != 1:
            raise RuntimeError(f"pair ambiguity for {s}: set={vs} anchor={val_a}")
        mapping[s] = rest[0]
    return mapping


def guess_and_finish(fp, perm_map: dict[int, int]):
    full = [perm_map[i] for i in range(256)]
    # We should currently be at the prompt thanks to the last submit_perm_get_value
    sendline(fp, "2")
    recv_until(fp, b"Enter the permutation")
    sendline(fp, " ".join(map(str, full)))
    # Print whatever comes (flag or failure)
    while True:
        ch = fp.read(1)
        if not ch:
            break
        sys.stdout.write(ch.decode(errors="ignore"))
        sys.stdout.flush()


def main():
    print("[*] Buckets (sizes):", [len(b) for b in BUCKETS])
    with socket.create_connection((HOST, PORT), timeout=TIMEOUT) as s:
        s.settimeout(TIMEOUT)
        fp = makefile(s)

        # Eat initial banner/menu until the FIRST prompt
        wait_for_choice_prompt(fp)

        perm_map = {}
        for bi, items in enumerate(BUCKETS):
            print(f"[*] Recovering bucket {bi} with {len(items)} items...")
            mapping = recover_bucket(fp, items)
            perm_map.update(mapping)
            print(f"    recovered {len(mapping)} indices (total {len(perm_map)}/256)")

        print("[*] All indices recovered; submitting guess...")
        guess_and_finish(fp, perm_map)


main()

Last updated