Since 2016, the GNU Radio Conference has held a Capture the Flag (CTF) competition in parallel with its technical track. Secret messages (“flags”) are hidden in radio signals, which players have to find and then submit to a scoring system to earn points. The CTF is my favourite part of GRCon, so when the conference organizers asked whether I’d be willing to organize this year’s competition, I jumped at the opportunity. Luckily, I didn’t have to do it alone: Muad’Dib, Aerospace Corporation, Daniel Estévez, and Yamakaja all stepped up to contribute challenges of their own.

The event was a phenomenal success. There were 52 challenges, and 71 teams submitted 686 valid flags. Even though the competition period has ended, most of the challenges are still up and playable at https://ctf-2022.gnuradio.org/.

In this post, I’ll describe how I created my challenges, and how I anticipated they might be solved. If you don’t want any spoilers, stop reading here!

Signal identification

Screenshot of Gqrx receiving signals from the "Signal identification" challenge

This challenge track consisted of 13 flags embedded in 11 signals, all contained in a single SigMF recording. I expected that players might use the Signal Identification Wiki to help identify the signals and find decoding software.

From left to right, the signals were:

  1. FM-modulated Slow-Scan Television (SSTV). The flag appeared in a QR code within the image.
  2. Narrow-band FM voice.
  3. Automatic Packet Reporting System (APRS).
  4. M17 digital voice.
  5. Wide-band (broadcast) FM. Flags were in the mono audio (L+R) channel, stereo audio (L-R) channel, and Radio Data System (RDS) subcarrier.
  6. POCSAG 1200 bps.
  7. PSK 31.
  8. Upper sideband voice.
  9. AM voice.
  10. Morse Code (CW).
  11. Lower sideband voice.

This challenge has its origins in a tutorial session I created for the Ottawa Amateur Radio Club in 2014. I later published the source code, and in 2019 I adapted it for use as a CTF challenge at BSides Ottawa. For GRCon22, I added M17, a promising open-source alterative to proprietary digital voice protocols.

Most of the signals can be received directly within Gqrx, but a few require additional software. To test the challenges, I used the following:

  • SSTV → qsstv
  • M17 → m17-cxx-demod
  • POCSAG → multimon-ng
  • PSK 31 → fldigi

If you solved the SSTV challenge, you can also receive the SSTV images that the International Space Station occasionally broadcasts on 145.800 MHz!

Fox hunting

This challenge was inspired by amateur radio direction finding (also known as radio fox hunting) where competitors run through the woods searching for hidden radio transmitters.

I built two hidden transmitters (“foxes”) which transmitted Morse code. One ran at approximately 433 MHz, and was hidden at a fixed location in the conference area. The other transmitted at approximately 904 MHz, and was carried around by various conference organizers in their backpacks, making it a moving target. Players had to determine the exact frequency of each transmitter, copy the flag which was included in the Morse code message, and then physically locate the transmitter to read another flag which was printed on it. Here’s a peek inside the hidden transmitters:

Two hidden transmitters sitting on a table with covers removed

I built them with Adafruit Feather M0 RFM69HCW Packet Radios, which are available in 433 MHz and 915 MHz versions. These boards are normally used to transmit and receive FSK signals, but they also have an on-off keying mode where the transmitter can be switched on and off using a GPIO pin. That makes it easy to tap out a message in Morse code. Each transmitter is powered by a 2000 mAh lithium cell, which is enough to provide four days of power.

Gqrx does a fine job of receiving the Morse code, and it’s even possible to read it by eye from the waterfall:

Gqrx receiving morse code from the 433 MHz hidden transmitter

Although directional antennas can be helpful to find hidden transmitters, most players got by with omnidirectional antennas. I recommend putting on a pair of headphones and disabling both hardware and software AGC, thus making the audio amplitude directly proportional to the incoming RF signal power. Walking closer to the transmitter will then make the signal louder, and walking away will make it quieter. Once you’re close enough to the signal that the receiver is saturated (and audio begins clipping), reduce the RF input gain and keep searching for a stronger signal. When you get very close and your RF gain is already at a minimum, it may be necessary to switch to a worse antenna or remove the antenna completely. Beware that some things other than distance from the transmitter also affect received signal power. For instance, obstacles may absorb or reflect radio signals.

Signal to noise

The start of the flag for "Signal to noise", as viewed in inspectrum

In this challenge, the start of a spectrum-painted flag is apparent when the signal is viewed in inspectrum, but as time progresses the signal narrows in frequency, widens in time, and is engulfed by an ever-increasing amount of noise. By playing with the “FFT size”, “Power max”, and “Power min” settings it is possible to read off the first half of the flag: flag{a4146a8247. But the remainder is impossibly difficult to read.

Fortunately, Gqrx allows larger FFT sizes and finer control over the FFT rate, which is just barely enough to read off the rest of the letters:

The middle of the flag for "Signal to noise", as viewed in Gqrx The end of the flag for "Signal to noise", as viewed in Gqrx

By stitching these parts together, we get the complete flag: flag{a4146a8247fa439d6879}.

To make this challenge, I used gr-paint to generate a rectangular spectrum-painted image, then passed this through a Polyphase Arbitrary Resampler block with progressively increasing resampling rate, and added noise from a Gaussian noise source with progressively increasing amplitude. A Python script dynamically adjusts the parameters of the resampler and noise source after each line from the rectangular image is processed.

Never the same color

The name of this challenge hints at NTSC, an analog television standard. Engineers jokingly referred to it as “Never The Same Color” because its colour accuracy was sometimes poor.

The challenge consisted of an NTSC signal containing seven flags. One was in the video, four were in the audio (mono, stereo, second audio program, and PRO subcarrier), and two were in EIA-608 closed captions (channels CC1 and CC3). One twist was that the video flag only appeared once every 176 frames. The other frames contained a decoy. (If you’re curious, scan the QR code in the image below to see what it was!)

To produce the signal, I made some improvements to the NTSC signal generator from my SDR examples repository, and tested with a television set. In fact, every flag except for the PRO (professional) subcarrier can be received by an ordinary television set:

A television set receiving an NTSC signal and displaying a flag

Many players found the flags this way, piping the signal into a nearby television. (Even the television sets in guest rooms at the conference hotel could be coaxed into receiving NTSC.) But I was very impressed by Daniel Estévez, who built his own NTSC demodulator in a Jupyter notebook!

Shall we play a game?

This was my favourite challenge, and I had a lot of fun building it.

The challenge description asked the player to transmit an APRS packet at 903.5 MHz, with their team name as the source address and “new” in the comment field. A response would then arrive somewhere in the 900 MHz ISM band. In fact, it arrived at 926 MHz in the form of a spectrum-painted rules page for a variation of Wordle called “SDRdle”:

A Gqrx waterfall showing spectrum-painted text: "Guess the SDRdle in 6 tries. Each guess must be a valid 5-letter word. Transmit an APPRS packet to submit. After each guess, the color of the tiles will change to show how close your guess was to the word."

All the player had to do at this point was follow the instructions and transmit further APRS packets containing their guesses:

A Gqrx waterfall showing a spectrum-painted Wordle board, with "ATONE" as the guessed word A Gqrx waterfall showing a spectrum-painted Wordle board, with "ATONE" and "FLAIR" as the guessed words A Gqrx waterfall showing a spectrum-painted Wordle board, with "ATONE", "FLAIR", and "CAULK" as the guessed words. A message instructs the player to contact @argilo to get their flag.

To test the challenge, I used Dire Wolf to generate APRS packets and place them in WAV files:

echo "ARGILO>WORLD:>new" | gen_packets -r 48000 -o aprs.wav -

I then made a simple flow graph to FM-modulate them and transmit them on a HackRF.

The challenge server received APRS packets using rtl_fm piped into Dire Wolf, generated game board images using the Python Imaging Library, and transmitted them using gr-paint.

Conclusion

Source code for all of the above challenges can be found in my grcon22 GitHub repository. I’ve released the code under the GPL so that they can be adapted and used for other purposes. If you use them for an event of your own, I’d love to hear about it!

I hope players had as much fun solving the challenges as I did creating them.