← Back to Writeups
CUSTOMEasySteganography

Beeps and Boops

XESXOR9/2/20264 min read
#steganography#custom#easy#clipped_low_note_correction#equal_slot_segmentation#note_to_character_substitution#pitch_tracking

Beeps and Boops

Platform: metactf | Category: Steganography | Type: Challenge | Difficulty: Easy | OS: NA | Author: XESXOR | Date: 2026-04-10 | Status: Solved Techniques: clipped_low_note_correction, equal_slot_segmentation, note_to_character_substitution, pitch_tracking

Summary

Task: a WAV file and an old note chart hide text as note-to-character substitutions in a short melody. Solution: split the active audio into equal note slots, decode the pitches with the table, then correct repeated clipped low notes caused by a retro music tool's low-frequency floor.

Beeps and Boops — metactf

Description

Original organizer description was not preserved locally.

Known clue from the prompt: the description referenced Nintendo cartridges and a famously bricky console.

Official hint: I wonder if there's a tool to make this kind of music?

English summary: the challenge provides a WAV file, RandomSong.wav, and a note chart, Old_Notes.txt. The text file maps note names to characters, so the main task is to recover the intended pitch in each note slot and translate the melody back into the flag. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

Analysis

Old_Notes.txt is the key artifact. It is a direct substitution table from notes to characters: low octaves map to lowercase letters, mid octaves map to digits and uppercase letters, and higher octaves map to punctuation and braces. That immediately suggests the audio is not meant to be "heard" musically so much as read symbol-by-symbol.

The WAV is about 43 seconds long, but only the first ~22 seconds contain the real signal. The rest is mostly silence. Focusing on the active region and dividing it into 33 equal slots matches the expected flag length and gives one dominant note per slot. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

A first pass with pitch tracking got very close, but not all the way. The direct decode looked like this:

text
DjwjCTF{N1nT3nD0MjD3ACo0LMjH1n3}

That output is obviously flag-shaped, but several characters are wrong. The broken positions all cluster around low notes that repeatedly decoded as the same ~55 Hz tone. That matters because the substitution table places many lowercase letters in very low octaves:

  • C0 -> a
  • D0 -> c
  • F#0 -> g
  • A0 -> j

If the music tool cannot play notes below about 55 Hz, multiple intended low notes will clip to essentially the same audible pitch in the recording. In other words, several different letters become indistinguishable if we trust the raw detected pitch alone. This explains why repeated 55 Hz slots all decoded as the same note even though the surrounding text clearly required different characters. ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

The Nintendo-cartridge / bricked-console flavor text and the official hint both support that interpretation: this sounds like music exported from a retro or chiptune-oriented tool with hardware-like note limits, not from a full-range synthesizer.

Once those clipped low-note positions were corrected manually, the message became the full flag.

Solution

  1. Read Old_Notes.txt and treat it as a note-to-character substitution table.
  2. Ignore the silent tail of RandomSong.wav and keep the active ~22-second region.
  3. Split that region into 33 equal note slots.
  4. Extract the dominant pitch for each slot.
  5. Decode the note names with the substitution table.
  6. Notice that the result is almost a valid flag, but several low-frequency slots all collapse to the same ~55 Hz note.
  7. Use the hint and context to infer a retro music tool with a lower note limit.
  8. Reassign the clipped slots to the intended low notes (C0, D0, F#0, etc.) based on flag structure and the near-complete plaintext.
  9. Recover the final message: ‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍
text
DawgCTF{REDACTED}
python
#!/usr/bin/env python3

from pathlib import Path


def load_note_map(path: Path) -> dict[str, str]:
    mapping = {}
    for line in path.read_text().splitlines():
        if " - " not in line:
            continue
        left, right = line.split(" - ", 1)
        mapping[left.strip()] = right.strip()
    return mapping


def decode(notes: list[str], mapping: dict[str, str]) -> str:
    return "".join(mapping[n] for n in notes)


ROOT = Path("resources/dawgctf-sp26/Beeps and Boops")
note_map = load_note_map(ROOT / "Old_Notes.txt")
‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍

# First-pass pitch extraction was close but unreliable on clipped low notes.
# The corrected 33-slot reconstruction is enough to reproduce the final flag.
corrected_slots = [
    "D#3", "C0", "A#1", "F#0", "D3", "G4", "F3", "C6", "C#4", "D#2", "C#1",
    "G4", "F2", "C#1", "D#3", "D2", "C4", "C0", "D#3", "F2", "C3", "D3",
    "D1", "D2", "B3", "C4", "C0", "D0", "G3", "D#2", "C#1", "F2", "C#6",
]

flag = decode(corrected_slots, note_map)
print(flag)
assert flag == "DawgCTF{REDACTED}"

‍​‌‌​​​​‌​‌‌​​‌‌​​‌‌​​‌‌​​‌‌​​‌​​​​‌‌​​​​​‌‌​​‌‌​​‌‌​​‌​‌​‌‌​​‌​‌‍