$ cat writeup.md…
$ cat writeup.md…
GPN CTF 2024
Task: a knitout-2 industrial-knitting program (pattern.k) hides a flag in a double-knit tube. Solution: ignore the random yarn-carrier colors (decoy), track each loop's identity through every xfer to un-spiral the tube into 20 stable wales, render front-bed vs back-bed stitches as black/white, rotate 90 degrees and OCR the resulting pixel-font flag text.
I got a new knitting machine to help me with the tablecloths for the restaurant but I accidentally dropped my flag into it. Can you help me unravel it?
We are given a single file pattern.k (~22290 lines) in the knitout-2 format — a textual instruction language for industrial knitting machines. The goal is to recover the flag encoded in the knitted fabric.
Format recognition. The header lines ;!knitout-2, ;;Carriers: 1 2 3 4 5 6, followed by inhook 1..6 identify the knitout-2 instruction language. Operation counts:
tuck ×20 — cast-on setupknit ×19554 — the bodyxfer ×2668 — exactly 1334 f->b and 1334 b->fdrop ×40 — cast-offThe knit op format is knit <dir> <bed><needle> <carrier>, e.g. knit - f20 1. dir is +/-, bed is f(front) or b(back), needle is 1..20, and carrier 1..5 is the yarn color.
Geometry. This is a tube / double-knit program: a front bed (20 needles) and a back bed (20 needles), circumference 40. Each round is a - pass over needles 20→1, some xfers, a + pass over needles 1→20, more xfers. The balanced xfer operations rotate stitches around the tube (spiral knitting), so a naive "20-stitch course" grid comes out scrambled.
Red herring — the carrier colors. The yarn carriers 1..5 are uniformly random (counts ≈ 4371 / 3452 / 3863 / 3477 / 4391). Rendering pixels colored by carrier produces pure noise in every orientation. Color is NOT the data.
The real signal — fabric STRUCTURE. This is classic double-knit jacquard: for each stitch, whether the loop sits on the front bed or the back bed encodes the image (front = "ink" pixel, back = background; the back face shows the inverse design). The semantic hint in the description — "unravel" the "tablecloths" — points at the knit structure, not the yarn color.
Un-spiral the tube. To undo the spiral rotation, track each loop's identity through every xfer:
(bed,needle) -> stable wale id.tuck creates a new wale id.xfer moves the loop's id to the opposing bed at the same needle (loops[dest] = loops.pop(src)).drop removes it.This collapses the rotating tube into exactly 20 stable wales (columns) × 975 passes (rows).
Render. For each knit, plot front-bed stitch = black, back-bed stitch = white, indexed by the stable wale id, then ROTATE_90. The image is ~20 rows tall: the top band (rows 0–8, including descenders) is the front face = readable flag text; the bottom band is its inverse (the back face).
#!/usr/bin/env python3 # Un-spiral the knitout tube and render front/back-bed structure as a flag image. from PIL import Image lines = open("pattern.k").read().splitlines() ops = [s.split() for ln in lines if (s := ln.strip()) and not s.startswith(';')] loops = {} # (bed,needle) -> stable wale id nw = [0] # next wale id def gw(b, i): k = (b, i) if k not in loops: loops[k] = nw[0]; nw[0] += 1 return loops[k] passes = []; cur = []; last = None for p in ops: if p[0] == 'tuck': # cast-on -> new wale gw(p[2][0], int(p[2][1:])) elif p[0] == 'knit': d = p[1]; bed = p[2][0]; idx = int(p[2][1:]) w = gw(bed, idx) if last is not None and d != last and cur: # new pass on direction flip passes.append(cur); cur = [] last = d cur.append((w, bed)) elif p[0] == 'xfer': # move loop id to opposing bed sk = (p[1][0], int(p[1][1:])); dk = (p[2][0], int(p[2][1:])) if sk in loops: loops[dk] = loops.pop(sk) elif p[0] == 'drop': # cast-off loops.pop((p[1][0], int(p[1][1:])), None) if cur: passes.append(cur) H = len(passes); W = nw[0] # 975 passes x 20 wales img = Image.new('L', (W, H), 0); px = img.load() for y, c in enumerate(passes): for w, bed in c: px[w, y] = 255 if bed == 'b' else 0 # front bed = black ink, back = white rot = img.transpose(Image.ROTATE_90) # flag text now horizontal rot.resize((rot.width * 2, rot.height * 20), Image.NEAREST).save("walebed_rot90.png") # Dump top band as ASCII glyphs for careful manual OCR (include descenders). rw, rh = rot.size; rp = rot.load(); NB = 10 band = [[1 if rp[x, y] else 0 for x in range(rw)] for y in range(NB)] for s in range(3, rw, 12): # ~12px-wide fixed cells for y in range(NB): print("".join('#' if band[y][x] else '.' for x in range(s, min(s + 10, rw)))) print(f"--- cell @ {s} ---")
Read the pixel font carefully. The font is a fixed-width ~10px-wide, 7px-tall pixel font with ascenders and descenders. The flag uses heavy leetspeak with random-looking mixed case and substitutions, so every character must be read exactly. The plaintext message is:
congratulations you have understood knitout and unraveled the tablecloths
Note that "tablecloths" and "unravel" appear verbatim from the challenge description — they were the semantic hint to look at the knit structure, not the yarn color.
y (in "yOU") look like u. Extract 10 rows tall to reveal the descender and fix y.l vs 1 vs i. The font has distinct glyphs: 1 = top-left flag + wide base serif; i = dot-then-bar; l/L = plain bar (capital L has a bottom bar). The flag mixes all of these.o vs O vs 0 and case are distinguished by glyph height: top row 0 = tall (uppercase/ascender); top row 2 = short (lowercase).REDACTED (lowercase l then 1), kN1t0uT (1 and zero), 7ab1ECl0thS (1, l, zero), unraV3L3d (capital V and L).$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar