UIUCTF 2021 - Constructive Criticism

Misc – 408 pts (14 solves) – Chall author: Pranav Goel

Some lo-fi bops on Soundcloud seem to be hiding something. There are cuts in the song where the emphasis jumps from one stereo-channel to the other. Turns out the separate channels in the WAV files are not exactly the same. At some parts they align, and at others they are slightly different. Visualising this difference reveals a bit-like structure which we decode in order to obtain the flag! A unique challenge for sure!

Check out write-ups by my teammates on K3RN3L4RMY.com


Upon visiting this aspiring Soundcloud artist’s playlist, we find 20 songs all seemingly only playing on the right stereo-channel. Yuck! Luckily, once the songs are downloaded it seems they play properly after all. But wait, what’s that? It sounds as if there are some cuts in there where the emphasis suddenly shifts from the lift stereo-channel to the right and vice versa. Interesting… Let’s buy a one-way-ticket to Audacity.

Three channels? This seems a bit fishy, let’s take a closer look at the waveforms in Python. More specifically, I overlaid the first two channels to see if they would match up and it appears they are not quite the same.

from scipy.io import wavfile
import matplotlib.pyplot as plt

samplerate, data = wavfile.read('.Tracks/Ambulo X Kasper Lindmark – Pleasant_track_1.wav')

stream_1, stream_2, stream_3 = list(zip(*data))


“The waveforms Mason, what do they mean?!?”


Turns out there are certain parts of the song where these two streams do and do not align. Yes, and no. ‘0’, and ‘1’. BITS! I see bits! Let’s try to fish out the difference between the two streams by assigning a ‘1’ bit to every significant difference between the two channels. For visual aid, I have divided the plot into 24 sections to highlight the bit structure.

bits = [0 if abs(stream_1[i] - stream_2[i]) < 1 else 1 for i in range(0, len(data), 100)]

plt.scatter(range(len(bits)), bits, s=0.5)

for i in range(24):
    plt.axvline(len(stream_1)//24//100*(i+1), c='black', alpha=0.3)


If we translate these bits to bytes we read something familiar…



It is the start of the flag wrapper! However, let’s not do all of this by hand and just bash together a Python script to do it for us. Note that in the final file the bit spacing was a factor of two smaller.

track_list = """
Ambulo X Kasper Lindmark – Pleasant_track_1.wav
BVG X Møndberg – Insomnia_track_2.wav
Bcalm X Banks – Because_track_3.wav
Dontcry X Glimlip X Sleepermane - Jiro Dreams_track_4.wav
Flovry X Tender Spring - First Heartbreak_track_5.wav
Hoogway – Everything (You Are)_track_6.wav
Ky Akasha – Memory Within A Dream_track_7.wav
Loafy Building X Hoffy Beats – Sleepless Wonder_track_8.wav
Mila Coolness – Far Away_track_9.wav
Miramare X Clément Matrat – Foam_track_10.wav
S N U G – Dreams Of You_track_11.wav
Softy X Kaspa_track_12.wav
Tenno – Daydreaming_track_13.wav
WYS – Snowman_track_14.wav
Drkmnd - Satellite Nights_track_17.wav
No One's Perfect X Kanisan – Gentle Wind_track_19.wav
Tysu X Spencer Hunt – Rainy Day_track_20.wav

flag = b""
for track in track_list:
    print('Working on track', track_list.index(track), 'Flag progress:', flag, end='\r', flush=True)
    samplerate, data = wavfile.read('C:/Users/Nika/Downloads/Tracks/'+track)
    stream_1, stream_2, stream_3 = list(zip(*data))
    bits = [0 if abs(stream_1[i]-stream_2[i]) < 1 else 1 for i in range(len(data))]
    if track == "Tysu X Spencer Hunt – Rainy Day_track_20.wav":
        step = len(bits) // 24 // 2
        step = len(bits) // 24
    flag += long_to_bytes(int(''.join([str(int(sum(bits[i:i+step]) > step // 2)) for i in range(0,len(bits),step)]),2))

print('\n' + flag.encode())

After some running the code neatly returns our flag! ^w^

Too slow for your liking? A smaller sample size in creating the ‘bits’ list might speed things up, although a pre-sampling of the ‘data’ might prove more efficient as most of the time goes to the ‘zip(*data)’ part.