← Back to Writeups
CUSTOMEasyMisc

Knitted Flag

XESXOR9/10/20265 min read
#misc#custom#easy#knitout2_parsing#loop_identity_tracking#tube_unspiraling#front_back_bed_rendering#pixel_font_ocr

Knitted Flag

Platform: CUSTOM | Category: Misc | Type: Challenge | Difficulty: Easy | OS: N/A | **Author: XESXOR Techniques: knitout2_parsing, loop_identity_tracking, tube_unspiraling, front_back_bed_rendering, pixel_font_ocr

Summary

Walkthrough of the CUSTOM Misc Knitted Flag. 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

Description

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.

Analysis

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 setup
  • knit ×19554 — the body
  • xfer ×2668 — exactly 1334 f->b and 1334 b->f
  • drop ×40 — cast-off

The 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.

Solution

Un-spiral the tube. To undo the spiral rotation, track each loop's identity through every xfer:

  1. Maintain a dict (bed,needle) -> stable wale id.
  2. Cast-on tuck creates a new wale id.
  3. xfer moves the loop's id to the opposing bed at the same needle (loops[dest] = loops.pop(src)).
  4. 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).

python
#!/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.

Critical reading gotchas (caused many wrong submissions)

  • Carrier-color rendering is a deliberate decoy — do not waste time on it.
  • Descenders. Truncating glyph cells at 7 rows hides descenders, making 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).
  • The confirmed flag uses: REDACTED (lowercase l then 1), kN1t0uT (1 and zero), 7ab1ECl0thS (1, l, zero), unraV3L3d (capital V and L).
  • The metadata listed handoutType "Dynamic", but the flag is a fixed congratulatory sentence — the "dynamic" label is misleading.

Key Takeaways / Lessons

  • Mastering knitout2_parsing, loop_identity_tracking, tube_unspiraling, front_back_bed_rendering, pixel_font_ocr was key to this challenge.
  • Always check for common misconfigurations and verify assumptions.

Crafted by XESXOR Research • 2024-05-31 • CUSTOM • Knitted Flag • Difficulty: Easy