GRCon 2023 CTF Challenge (NRSC5)

This year Clayton “Argilo” Smith offered me an opportunity to provide one of the challenges for the GRCon capture the flag event (CTF). I was quite eager to do this since we had recently finished implementing a multitude of changes to the gr-nrsc5 transmitter and nrsc5 receiver. And by We, I really mean mostly Clayton, but I technically did contribute some code that he rewrote later ;).

In case you’re not familiar, NRSC5 is the spec that defines the U.S. terrestrial VHF digital broadcast radio standard. I.e. HD Radio. I personally find this commercialized name to be a bit deceptive since it would imply a higher level of audio quality which is generally just not the case. The maximum throughput of the hybrid standards is around 125kbps which could actually make for fairly decent audio. Unfortunately, this is never seen OTA. This is because this rate is for the MP3 mode which is actually composed of two logical channels split into about 100kbps and 25kbps. See the table below for the defined modes and associated data rates.

NRSC5 Data rates

So the maximum possible audio program quality is 100kbps. In addition to this, there’s a considerable amount of overhead that limits the practical codec rate to around 96kbps. Some of this overhead comes from the almost hilarious amount of meta-data being sent by the transmitter. Things like FCC ID, exciter version, and even things like time with leap-second corrections. Fortunately, some of this overhead space can be used to send a multitude of content such as ID3 tags, station logos, album art, and even weather maps. Though this last portion is not yet explicitly supported by gr-nrsc5. So all-in-all 96kbps audio wouldn’t sound too bad, but the standard actually supports up to 8 audio programs! Most stations thus opt to have at least two channels, so practically speaking, bitrates of 40-70kbps are pretty common. I find that at this bitrate the analog signal sounds noticeably better at high SNR. Although at lower SN ratios one might find the lack of any hiss a more pleasant tradeoff.
Earlier I alluded to something interesting, I mentioned hybrid modes. HD Radio is actually a hybrid digital/analog waveform consisting of a ~230kHz wide analog FM carrier sandwiched in between two ~73kHz QPSK modulated OFDM carriers (MP3). See the graphic below.

The standard actually does define fully digital OFDM modes, but these have yet to be used by any station on the air, as far as we know. The page below shows what a typical FM station with an HD transmitter might look like on your waterfall.

Okay okay, enough geeking out over NRSC5, let’s talk about the actual challenge! Earlier I mentioned the dubious nature of the “HD Radio” moniker, thus partially inspired by last year’s NTSC challenge by Clayton, I opted to call my track Not Really Superior Clamor V i.e. NRSC5. This signal is a fully standard-compliant (more or less) stereo WFM signal with RDS, SCA, and the MP3 HD mode. The HD signal carriers 8 programs with 8 associated station logos. This required a minor modification to the L2 PSD size in gr-nrsc5 to make this possible. I was very much pushing the limits of the waveform.

Driving in Tempe at the crack of dawn,
for it was the first day of GRCon.
I turned the dial in my car
to hear the voices near and far.
The sound of pop and news fills my cruise,
but this is not the muse I choose
For there it was on ninety point seven,
the place with the bass that would take me to heaven.

The challenge text

What’s more is the fact that the signal audio was pulled live from my personal radio station VladFM. This was actually done via a rather elaborate bash script I call “” which calls an ffmpeg instance to pipe audio from the internet, python scripts to update the HD1 metadata, HD8 logo, and run the flowgraph itself.

Now Live on VladFM:    The night – Night Owl Collective – COPYCATT – Sweet Soul

This proved amusingly confusing to people who expected some sort of message at a regular interval with a flag, like a station name at the top of the hour. Or some people even recorded the signal for 10s of minutes waiting for a loop only to find out that it would never loop in over 4000 hours! See below the flowgraph used to generate this monstrosity.

FM+NRSC5 transmitter:

My track had a whopping 17 flags to find, though most of these are actually relatively easily found via an appropriate application of: $ grep . Probably the most difficult flags to find were: 8, 9, 11, 13, and 15 since they required a fair bit of digging/work to get.
Here’s the total list of flags found inside the signal. Most of these are relatively self explanatory and can be found by running the nrsc5 receiver and scraping the logs, or even found visually with nrsc5-dui. A number of these flags do however require manipulation of the AAS files sent by the transmitter, i.e. the station logos or in some cases the audio.

  1. RDS text (25)
  2. HD 8 audio (25)
  3. HD Station Message (25)
  4. PSD Artist on HD programs 2-5 (35)
  5. XOR HEX (Key in Title, Message in Artist) (50)
    4A63C7DEA2667B3222CDD6A1746702 – Message
    7f4DA1B2C30100 – Key
  6. HD6 audio ROT 69 (N=69 indicated by Title and Artist) (60)
    audio is: Flag 6 is UfEfkXfXvekcvZekfKyrkXffuEzxyk
  7. HD 7 title (25)
  8. Reversed audio across HD 2-5 in sequence made by FCC ID (4213) numbers are indicated by Title (125)
  9. SCA on FM 67kHz SSTV Robot36 (100)
  10. Logo Tiles across HD 2-5 making QR code (75)
  11. text steganography with flag on HD1 logo (steghide text file) (60)
  12. black image on HD6 logo contrast enhanced to reveal flag (50)
  13. HD6 logo text steganography (openstego) with flag (65)
  14. HD7 logo (25)
  15. HD8 logo changed from rickroll QR to QR with AES128 encrypted text: CspPWTYA5xD15CcCO0+FPwvM/D58eDzREtrnXxrKB0I= (75)
    key is in the station slogan (kjhASty34e987o76w6ta&B6g)
    switch interval is 5 minutes
  16. HD7 audio spectrum paint (50)
  17. black pixels representing ascii on bottom of HD8 logo (50)

Let’s start with a demo of the first flag I’d expect most people to get, the RDS text, see the bottom right of the video. This is one of the only two flags in the analog portion of the signal.

Finding RDS Flag 1.

Next, lets take a look at getting flag 9. the second flag found in the analog signal. This is actually hidden as an SCA sub-carrier on 67kHz offset of the FM baseband. This is a service rarely used in the broadcast FM industry for additional (usually) subscription services such as talk or book reading, specialized programming for specific markets, or even Muzak. If you demodulate the FM signal without filtering at 15khz, you will find the added sub-carriers present in the baseband like in the image below.

FM Baseband spectrum allocation

The signal modulated up at 67kHz is actually an USB signal sending Robot36 SSTV containing an image with the text: “9.flag{BANNER}”. The astute amongst you would have noticed by now that I actually violated the typical mode of operation for SCA since I failed to modulate this SSTV signal with FM. which is the normal way that SCA audio is transported according to the few sources I can find. See the below video for a demonstration on extracting the flag with QSSTV.

Flag 9. SSTV

Now let’s move on to the flags contained in the HD signal. The following video will show using nrsc5-dui to receive the signal as it was sent on 90.7 with an RTL-SDR at the venue (you can also modify the python script to invoke nrsc5 with a baseband recording). This will allow you to get flags 2-7 which are found in the various sent meta-data like station slogan, titles, and artist info.
Flag 4. is simply split across channels 2-5 requiring you to paste the text together in the correct order.
Flag 5. consists of two hexadecimal values which when XOR’d together will produce values representing ASCII text: “flag{moldbug}”
Flag 6. is spoken audio on HD6: “Flag 6 is UfEfkXfXvekcvZekfKyrkXffuEzxyk” this is text encoded with ye olde ROT13 cipher common on forums back in the day. Albeit with one small modification. The rotation amount is actually indicated in the title and artist info as N=69 which is also equivalent to ROT17. de-rotated this is, “flag{DoNotGoGentleIntoThatGoodNight}”
For both of these flags, the site is invaluable.

Flags 2-7. via nrsc5-dui

Next we’ll get flag 16. which is actually a VERY slow spectrum paint done on HD7 using gr-paint. This was rather interesting since the audio codec used by NRSC5 is some proprietary version of HE-AAC. When driven at the very low bit-rates needed to cram in this many channels, it begins to do some very strange things. namely, at 10kbps, the truly faithfully replicated pass-band is just 5khz. Above this frequency, the codec does something called spectral band replication which essentially duplicates the fundamental frequency to the higher harmonics to coarsely emulate the harmonics of the human voice without actually encoding the information with extra bits. While very clever, this is massively destructive to spectrum painting, so I had to limit the bandwidth to 5kHz. This in combination with a rather large FFT accidentally resulted in a very slow transmission of the paint image.

Flag 16. gr-paint

For flag 8. there is a snippet of looped audio sent on programs 2-5. On first listen, each snippet may sound identical, but they actually contain a section of reversed audio. When these are reversed and put back together in the correct order indicated by the transmitted FCC ID (4213) you will obtain the flag: “compliance by muse”. I was a little too aggressive with mixing this audio and unintentionally made it harder to understand than it needed to be. See the audio snippet below to hear what a correctly reconstructed signal sounds like.

For the following flags, you can continue to use nrsc5-dui, but you must now examine the files which it has downloaded from the signal. Only one of these is easily visually decoded (flag 14.). The rest will require more effort. See the video below showing the downloaded data.

Let’s start with flag 10. This one is a tiled QR code split across channels 2-5. When re-assembled properly and scanned it reveals a plaintext flag.

Flag 10.

Flag 12. is a contrast hidden text. i.e. the color value of the text is just 1 index above the base, black value of 0. Essentially, cranking the brightness or contrast in a tool like Photoshop should bring this one out. See the video below:

Flag 12. extraction with photoshop

Next we start to get really into the weeds. Many of you probably saw the big red QR code sent on HD8 and scanned this. Unfortunately for you, you will have found that this is not the flag. The below image does however contain some useful information.

Flag. 17

If you look very closely, you’ll see a little garbage at the bottom. Many people thought this was a barcode which isn’t a bad guess. In reality, this is actually binary encoding of ASCII text. Black representing a 1, and red presenting a 0. This is easiest to distinguish in a tool like PS which shows pixel boundaries.

flag 17. binary closeup

Again, comes in clutch and allows easy decoding of the string: 00110001001101110010111001100110011011000110000101100111011110110100001101010011010011000110010101110111011010010111001101111101

Flag 17. plaintext

What almost nobody caught was the fact that every 5 minutes, for exactly one minute, the transmitter would send a new image with the exact same filename and dimensions. This image contained a new QR code with AES ciphertext. The key for this flag is actually in the station slogan (kjhASty34e987o76w6ta&B6g) which drove many people nuts looking for something to decrypt with it. Many people mistakenly tried to decode flag 5. this way.

Flag 15. AES ciphertext

The ciphertext “CspPWTYA5xD15CcCO0+FPwvM/D58eDzREtrnXxrKB0I=” can be decoded with to obtain the actual flag “15.flag{OnceMoreUntoTheBreach}”
I will admit that this is cruel and unusual punishment, though the following flags may make you hate me even more.

Flags 11. and 13. are quite evil requiring the use of stenography tools to find secret embedded files. There are many, many ways to do this with image files, and just as many tools to apply said techniques. I was entertained by the idea of using one of the more “classic” methods of modifying the LSB of each pixel to encode information, but I didn’t feel like manually implementing this so I opted to use OpenStego and Steghide instead. These were just the first tools I found via google search. Flag 11. is hidden on the HD1 logo with Steghide.

Flag. 11 extraction with steghide
Flag 11. carrier file

Flag 13. is actually doubly villainous since it was applied to a file that already had another flag, number 12. I figured this would really throw people for a loop, and indeed it did, since nobody got this one. And this was the ONLY unsolved riddle of the whole challenge. Since this was a PNG file, I had to use the OpenStego tool.

Flag. 13 extraction with OpenStego
Flag. 13 carrier file

There are some flags/files not explicitly gone over in this post, so please do see my GitHub repository if you haven’t already. There is also a flowgraph there called “raw_to_u8.grc” that can help you convert the raw baseband file to uint8 which can be read by the software nrsc5 receiver.

Hey you, you’re finally awake! Congrats, you made it to the end of the post! Just one more thing…
I want to sincerely thank everyone who made this challenge possible, like Clayton and the organizers of GRCon. I also want to thank all the players who suffered through my gauntlet, I hope you had as much fun playing with my signals as I did making them!





Leave a Reply

Your email address will not be published. Required fields are marked *