Description
While going through FBI servers you find an interesting WAV file. Can you find the flag?
Setup
Download the WAV audio file.
wget <url>/wave.wavSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Inspect the sample values
ObservationThe description frames the WAV as something recovered rather than recorded, with a flag hidden inside. So the samples themselves may encode data rather than sound, and printing the raw values will show the pattern.Open wave.wav in Audacity or load it in Python. Notice that all sample values are positive integers in the thousands - for example 2008, 2506, 1508 - and that the last two digits look like random noise. This is not normal audio; the samples encode data.pythonpython3 << 'EOF' from scipy.io import wavfile _, data = wavfile.read("wave.wav") print("First 6 samples:", data[:6].tolist()) # Truncate each sample to its first two digits to strip the noise rounded-lg = [int(str(s)[:2]) for s in data] unique = sorted(set(rounded-lg)) print("Unique rounded-lg values:", unique) print("Count:", len(unique)) EOFExpected output
picoCTF{mU21C_1s_1337_...}What didn't work first
Tried: Open wave.wav in a hex editor and search for the ASCII string 'picoCTF' directly in the file bytes.
The flag is nowhere in the file as raw ASCII. It lives as quantized amplitude levels spread across thousands of samples, so a hex editor finds nothing: each character is carried by several samples as a level rank, never written literally.
Tried: Use steghide or zsteg to extract hidden data from the WAV file.
steghide and zsteg look for least-significant-bit steganography in image or audio samples. Nothing here hides in the LSBs; the data sits in the most significant part of each sample, as one of 16 discrete amplitude levels. Neither tool knows to look for a rank-based scheme, so both report nothing.
Learn more
The raw samples range from roughly 1000 to 8509. The last two digits of each sample are random noise added by the encoder. Chopping them off with
int(str(sample)[:2])leaves exactly 16 distinct two-digit values: 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85. Because there are exactly 16 levels, each one maps to one hexadecimal digit (0 through f). The mapping is by sorted rank: the smallest rounded-lg value (10) represents hex 0, the next (15) represents hex 1, and so on up to 85 representing hex f.Step 2Decode the hex string and convert to ASCII
ObservationThere are exactly 16 unique sample levels once rounded, matching the 16 hexadecimal digits. Each level is one hex nibble, so the full sample sequence spells the flag as hex-encoded ASCII.Truncate each raw sample to its first two digits to strip the noise, then map each rounded-lg value to a hex digit by its position in the sorted list of the 16 unique values. Concatenate all hex digits into one string and decode as ASCII to reveal the flag.pythonpython3 << 'EOF' from scipy.io import wavfile _, data = wavfile.read("wave.wav") # Strip noise: keep only the first two significant digits rounded-lg = [int(str(s)[:2]) for s in data] # Build sorted list of the 16 unique levels unique = sorted(set(rounded-lg)) # [10, 15, 20, ..., 85] # Map each rounded-lg value to its hex digit by rank (0-15) hex_str = "".join(hex(unique.index(v))[2:] for v in rounded-lg) # Decode hex string to ASCII flag = bytearray.fromhex(hex_str).decode() print(flag) EOFWhat didn't work first
Tried: Decode the raw sample values directly as ASCII without stripping the noise digits first.
Raw samples run from about 1000 to 8509, well outside the printable ASCII range. Feed them to chr() or bytearray() and you get garbage or an error. Stripping the noise, by keeping only the first two digits, collapses the 16 noisy levels back to their clean quantized values before the rank lookup.
Tried: Assume the 16 unique values map directly to ASCII characters instead of hex nibbles, and build the string by looking up chr(unique.index(v)) for each sample.
With only 16 distinct levels, mapping them to 0 through 15 lands entirely in the non-printable control range, while flag characters like p, i, and c sit far above 15. Treat the levels as hex digits instead, concatenate them into a hex string, and decode that to ASCII: two samples per output character, not one.
Learn more
The WAV file encodes data not as audio but as a sequence of quantized amplitude levels. Each sample represents one hex nibble. The encoder wrote the flag as a hex string, turned each hex character into one of 16 amplitude levels (10, 15, ..., 85), and appended two digits of random noise to disguise the pattern. Reversing the process - strip the noise by taking the first two digits, look up the rank in the sorted unique list, convert the rank to a hex character - reconstructs the original hex string. Decoding that hex string as ASCII gives the flag.
This is a covert channel that hides information in the sample values themselves rather than in the audio waveform. The audio sounds bizarre but the discrete staircase pattern in Audacity's waveform view is the telltale sign.
Interactive tools
- StegallDrop any file and Stegall runs every applicable steg technique in parallel: LSB sweeps, bit planes, spectrograms, polyglot carving, metadata, whitespace decode, and a 6-layer base/ROT/XOR/zlib cascade. Recursively unpacks results and surfaces flag matches.
- Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
Flag
Reveal flag
picoCTF{mU21C_1s_1337_...}
Raw WAV samples (~1000-8509) encode hex digits as 16 discrete amplitude levels with noise in the last two digits. Strip the noise by taking the first two digits, map each rounded-lg value to its rank in the sorted unique list (0-15), concatenate into a hex string, and decode as ASCII.