> cat ctf/sec-t-ctf-2026/numbers-station-sec-t-ctf-2026-writeup.md

Numbers Station - SEC-T CTF 2026 Writeup

Published: 2026-09-12 - Estimated reading time: 4 minutes

Description

We pulled this off an unattended transmitter. It has been running on a loop for weeks, and as far as anyone can tell it is only playing music.

It is not only playing music.

Paste it into strudel.cc and press play. Everything you can hear is a decoy.

Solution

The challenge provides some strudel.cc code.

// SEC-T CTF 2026 — intercepted broadcast, channel 7
//
// Paste into https://strudel.cc and press play.
//
// Recovered from a single unattended transmitter. The music is the music.
// The carrier is not.

setcpm(136 / 4)

// --- ground ---------------------------------------------------------------

let kick = s("bd:5")
  .struct("x ~ x ~ x ~ x ~")
  .bank("RolandTR909")
  .shape(.35)
  .gain(1.1)

let sub = note("<c1 c1 as0 gs0>")
  .s("sine")
  .struct("x ~ ~ ~ x ~ ~ ~")
  .clip(.9)
  .attack(.005).release(.18)
  .gain(.9)

// --- pulse ----------------------------------------------------------------

let hats = s("hh")
  .bank("RolandTR909")
  .struct("x*16")
  .gain(sine.range(.25, .55).fast(2))
  .pan(sine.range(.35, .65).slow(3))
  .hpf(7000)

let ohat = s("oh")
  .bank("RolandTR909")
  .struct("~ x ~ x")
  .gain(.4)
  .room(.3)

let clap = s("cp")
  .bank("RolandTR909")
  .struct("~ ~ x ~")
  .room(.45).size(.7)
  .gain(.7)

// --- the signal -----------------------------------------------------------

let arp = n("0 3 5 7 5 3 7 10".fast(2))
  .scale("c3:phrygian")
  .s("sawtooth")
  .cutoff(perlin.range(400, 2400).slow(8))
  .resonance(14)
  .attack(.01).decay(.12).sustain(.1).release(.2)
  .delay(.45).delaytime(.1875).delayfeedback(.55)
  .room(.5)
  .gain(.5)

let air = stack(
  s("pink").gain(.16).lpf(perlin.range(600, 3000).slow(16)),
  s("crackle").gain(.9).density(.6)
).room(.8)

// --- carrier --------------------------------------------------------------

// 169 steps in 4 cycles. Muted at the output stage: the
// transmitter keys it, the speaker never sees it. Whatever it is modulating,
// it is not the air.

let carrier = note("[10,13,14,15] [9,13,16] [9,13,16] [9,13,16] [9,12,16] [10,11,12,15] ~ [9,10,11,12,13,14,15,16] [9,13,16] [9,13,16] [9,13,16] [9,13,16] [9,13,16] [11,12,13,14] [10,15] [9,16] [9,16] [9,16] [10,15] [16] [16] [16] [9,10,11,12,13,14,15,16] [16] [16] [16] ~ [13] [13] [8,9,10,11,12,14,15,16,17] [8,17] [8,17] [17] [17] [10,11,12,13,14,15,16,17] [9] [9] ~ ~ ~ [10,11,12,13] [9,14] [9,14] [9,14] [10,11,12,13] ~ [10,11,12,13] [9,14] [9,14] [9,14] [10,11,12,13] ~ ~ [9,10,11,12,13,14,15,16,17] [12] [11,13] [10,14] [9] ~ [6] [6] [6] [6] [6] [6] [6] [10,11,14] [9,12,14] [9,12,14] [9,12,14] [9,10,11,12,13] ~ [14] [14] [9,10,11,12,13,14,15,16] [9,14] [9,14] ~ ~ [6] [6] [6] [6] [6] [6] [6,14] [14] [9,10,11,12,13,14,15,16] [9,14] [9,14] ~ ~ ~ [9,10,11,12,13,14,15,16,17] [13] [14] [14] [9,10,11,12,13] ~ [10,11,12,13] [9,12,14] [9,12,14] [9,12,14] [9,12,13] ~ [6] [6] [6] [6] [6] [6] [6] ~ [9,10,11,12,13,14] [14] [9,10,11,12,13,14] [14] [9,10,11,12,13,14] ~ [10,11,12,13,14] [9] [9] [9] [9,10,11,12,13,14] ~ ~ [9,12,13] [9,12,14] [9,12,14] [9,11,14] [10,11,14] ~ ~ [9,14] [9,14] [9,10,11,12,13,14,17] [9] [9] ~ [10,11,12,13] [9,10,13,14] [9,14] [9,14] [9,14] ~ ~ [8,17] [8,17] [8,9,10,11,12,14,15,16,17] [13] [13] ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~")
  .slow(4)
  .s("square")
  .postgain(0)

// --- arrangement ----------------------------------------------------------

// The carrier runs through every section. It costs nothing to leave it in,
// since it makes no sound, and it means the transmission is on screen from the
// first bar rather than fourteen seconds in.
arrange(
  [4, stack(air, sub, carrier)],
  [4, stack(air, sub, kick, carrier)],
  [8, stack(air, sub, kick, hats, arp.gain(.32), carrier)],
  [8, stack(air, sub, kick, hats, ohat, clap, arp, carrier)],
  [8, stack(air, sub, kick, hats, ohat, clap, arp, carrier)],
  [4, stack(air, sub, kick, hats, carrier)],
  [2, stack(air, sub, carrier)]
)

From the comments, we can see that the carrier is muted and looks very suspicious. Since strudel.cc has multiple ways to visualize the sound, we can use a function like pianoroll() to visualize the carrier and see if it contains any hidden information. To get the visualization directly in the strudel REPL, we use _pianoroll().

let carrier = note("[10,13,14,15] [9,13,16] [9,13,16] [9,13,16] [9,12,16] [10,11,12,15] ~ [9,10,11,12,13,14,15,16] [9,13,16] [9,13,16] [9,13,16] [9,13,16] [9,13,16] [11,12,13,14] [10,15] [9,16] [9,16] [9,16] [10,15] [16] [16] [16] [9,10,11,12,13,14,15,16] [16] [16] [16] ~ [13] [13] [8,9,10,11,12,14,15,16,17] [8,17] [8,17] [17] [17] [10,11,12,13,14,15,16,17] [9] [9] ~ ~ ~ [10,11,12,13] [9,14] [9,14] [9,14] [10,11,12,13] ~ [10,11,12,13] [9,14] [9,14] [9,14] [10,11,12,13] ~ ~ [9,10,11,12,13,14,15,16,17] [12] [11,13] [10,14] [9] ~ [6] [6] [6] [6] [6] [6] [6] [10,11,14] [9,12,14] [9,12,14] [9,12,14] [9,10,11,12,13] ~ [14] [14] [9,10,11,12,13,14,15,16] [9,14] [9,14] ~ ~ [6] [6] [6] [6] [6] [6] [6,14] [14] [9,10,11,12,13,14,15,16] [9,14] [9,14] ~ ~ ~ [9,10,11,12,13,14,15,16,17] [13] [14] [14] [9,10,11,12,13] ~ [10,11,12,13] [9,12,14] [9,12,14] [9,12,14] [9,12,13] ~ [6] [6] [6] [6] [6] [6] [6] ~ [9,10,11,12,13,14] [14] [9,10,11,12,13,14] [14] [9,10,11,12,13,14] ~ [10,11,12,13,14] [9] [9] [9] [9,10,11,12,13,14] ~ ~ [9,12,13] [9,12,14] [9,12,14] [9,11,14] [10,11,14] ~ ~ [9,14] [9,14] [9,10,11,12,13,14,17] [9] [9] ~ [10,11,12,13] [9,10,13,14] [9,14] [9,14] [9,14] ~ ~ [8,17] [8,17] [8,9,10,11,12,14,15,16,17] [13] [13] ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~")
  .slow(4)
  .s("square")
  ._pianoroll()
  .postgain(0)

Playing this updated version, we get the flag as output from the visualization.

[ LOADING IMAGE... ]
Carrier Visualization

Flag: SECT{look_at_the_music}