High SF, Dragged Down by Drift | No Name on the Door | GaianSpace CTF 2026
đ ä¸ćçďźçšéŤä¸ć
Sixth post in the series. In the previous post, Two Voices, the satellite was still using a standard SF9. Here the stranger has changed its downlink config into implicit header + a different SF per packet, and because high-SF symbols are long, Doppler/clock drift makes the decode start going wrong partway through. This is the post where the demodulator is finally forced to add drift correction.
Rest of the series: Channel HopăTrue Color Is a LieăIsodata SevenăSubsidy ClaimăHopscotch
What the challenge is about

01:40. The stranger is still talking to GAIAN-723, and the satellite responds every few minutes, but every response uses a voice the ground-station modem ârefuses to listen to.â GAIAN-723 has changed how it speaks: packets no longer mark their length, and every packet sounds slightly different from the last. The ground-station log has a line at 01:31: DL CFG CHANGED BY TC â NOT FROM MOC.
Goal: find the COâ reading GAIAN-723 reported when it acknowledged the strangerâs command at 01:37, and how long it says itâs been awake.
Flag format: GAIANSPACE{[CO2 in ppm]_[uptime in seconds]}.
Every packet has a different SF
The log shows the downlink config was changed by a telecommand at 01:31, after which âthe header is gone and the length is unknownâ, i.e. an implicit header.
This recording is a fresh segment after the 01:36 roll-over, starting at 01:36:45, so 01:37 is after the 15-second mark in the recording.
Measuring per-band energy, I find 8 satellite packets, then search the preamble peaks over SF 7â12, and it turns out every packet has a different SF (SF7, 8, 9, 10 in rotation), which should be what the challenge means by âeach one sounds slightly differentâ:
| Start (s) | Length | SF |
|---|---|---|
| 7.50 | 0.84 s | 10 |
| 13.07 | 0.57 s | 8 |
| 34.88 | 0.84 s | 10 |
| ⌠| ⌠| ⌠|
The strangerâs commands are short, strong pulses (SF7, BW250k). Recording start 01:36:45 + 34.65 s â 01:37:19, so the satellite packet closest after that command (34.88 s, SF10) is the ACK the challenge wants.
Drift correction for high SF
The hardest packet here is SF10. The bigger the SF (Spreading Factor), the longer each symbol. An SF10 symbol takes 4096 samples (SF7 is only 512), so the longer a symbol drags on, the more deadly the following problem becomes.
What is drift? The satellite flies overhead at several kilometres per second, so the frequencies on each end keep shifting due to the Doppler effect (like an ambulanceâs pitch changing as it passes). Add the clock error of each sideâs quartz oscillator, and the signalâs frequency isnât off by a fixed amount. It slides slowly the whole time. The longer a symbol, the more it slides within a single packet.
How does that hurt decoding? Recall the demodulation skeleton: after dechirping each symbol you get an FFT peak, and whichever integer bin the peak lands in is the symbolâs value.
But drift makes the peak not land exactly on an integer bin. It carries a fraction (e.g. bin 100.2, 100.35, 100.5âŚ), and that fractional part grows steadily over time.
I measured it drifting from â0.2 slowly to â0.5, then flipping to +0.5.
At first, off by 0.2, rounding still lands in the right bin; but as it approaches 0.5, rounding jumps to the wrong bin, so the first half of the packet decodes correctly and the second half turns to garbage right around CO2=. Thatâs why âa single fixed offset collapses partway through.â
So first confirm the problem is purely drift: this ACK packetâs spectrogram is a clean chirp, with no frequency hop or interference midway:

The fix is three steps:
- Extract each symbol peakâs fractional part and line them up over time into a curve;
- Because the fraction âwraps aroundâ when it crosses Âą0.5, use
np.unwrapto stitch those jumps into a continuous curve; - Fit it with a quadratic polynomial (drift is smooth, so quadratic is enough), then subtract that fitted curve from each symbolâs bin, and the peaks settle back onto integer bins.
The left panel shows the symbol peaksâ drift (blue dots) and the quadratic fit (red line); the right panel shows the timeline of packets and commands:

With the drift subtracted, the whole packet decodes cleanly. This packet also ends with a CRC-16 (*18FC), which I recomputed with CCITT parameters and matched exactly, a stamp of approval on âdecoded correctly.â
Result
1 | TM,ACK,1,GET HK,HK,V=4.51,I=0.1,T=21.82,P=996.24,CO2=432,UP=132*18FC |
The challenge wants COâ and UP (uptime in seconds): CO2=432, UP=132. Thereâs a trap here: the ACK at 01:36:52 is almost identical (CO2=424,UP=105), and the only way to tell them apart is by time, so you have to convert the recordingâs start into real time to land on the 01:37 one. The two UP values differ by 27 seconds, matching the gap between the two responses, which cross-checks nicely.
Flag
1 | GAIANSPACE{432_132} |
Takeaways
- âEvery packet is differentâ usually means SF or BW is hopping. Do an independent parameter search per packet; donât assume the previous packetâs parameters carry over.
- An implicit header has no length and no CR: infer the length from the symbol count on the air, and pick the CR by âwhich one decodes clean text or passes CRC.â First few bytes matching but the rest garbled is often a CR or drift issue, not a wrong overall direction.
- High-SF symbols are long and very sensitive to drift. Plot the trend of the symbol peaksâ fractional part and the drift is obvious; correct it with unwrap + polynomial fit.
- Two identical-looking ACKs can only be told apart by time. Convert the recordingâs start to real UTC first, then match the moment the challenge states, so you donât grab the wrong one. In the next post, Hopscotch, the satellite goes one step further: it hops across multiple frequencies (FHSS), and the demodulator has to reassemble the hop sequence before it can decode.

