Новогодний смех (New Year Laughter)
Новогодний смех (New Year Laughter)
Platform: bug-makers | Category: Steganography | Type: Challenge | Difficulty: Easy | OS: Linux | Author: XESXOR | Date: 2026-05-29 | Status: Solved Techniques: amplitude_envelope_extraction, binary_pulse_decoding, on_off_keying_demodulation, ultrasonic_carrier_stego
Summary
Task: 73-second stereo WAV of New Year bells and laughter hiding a flag in the ultrasonic band. Solution: a faint ~20 kHz carrier near Nyquist is on/off-keyed; isolate it with a bandpass filter, extract the amplitude envelope, threshold to ON/OFF, and decode short/long pulses as binary OOK bits into ASCII.
Новогодний смех (New Year Laughter) — bug-makers
Description
Одна минута новогодних колокольчиков и смеха Пикси – это и так уже ультра-хорошо, но найти флаг еще лучше.
English summary: A ~73-second stereo WAV (new-year-h-ha-ha.wav) of New Year bells and "Pixie's laughter". The audible content is a decoy. The flag is hidden in an ultrasonic carrier near the Nyquist frequency. The description's wordplay ("ultra-good", "Pixie") and the flag spelling (u17ras0und) all point to ultrasound.
Analysis
The artifact is a clean RIFF WAVE file: 16-bit PCM, stereo, 44100 Hz, ~73 seconds, ~13 MB.
Initial recon ruled out the obvious carriers:
- No appended/embedded data:
binwalkwas clean; no extra RIFF chunks beyondfmt/data; no flag strings; metadata (exiftool) was normal. - No visible spectrogram text: Plain audible-range spectrograms (color and monochrome, segmented into 11 s chunks) showed only genuine laughter and bell bursts. (A sibling challenge by the same author hid visible text in the spectrogram, which made this the natural first guess — but it failed here.)
- No sample-level LSB: LSB extraction on the left channel, the right channel, and on raw bytes all produced random noise.
- No stereo-difference trick: An L−R difference spectrogram showed only periodic decorrelation of the laughter, no text (autocorrelation ~0.72 throughout — highly repetitive but not a clean loop).
Semantic clues drove the solution. The phrases "ультра-хорошо" (ultra-good) and "Пикси" (Pixie), combined with the flag's u17ras0und, strongly suggested an ultrasonic/ultrasound channel. With a 44100 Hz sample rate the Nyquist limit is 22050 Hz, leaving room for a carrier just below it that the loud laughter would mask.
Key insight: Rendering a single-channel spectrogram with a raised noise floor revealed a faint dashed carrier line at ~20 kHz running the full 73 seconds — an ultrasonic data channel hidden under the audible content.
bash read-only • copy allowedsox new-year-h-ha-ha.wav -n remix 1 spectrogram -m -z 60 -o full_z60.png
The dashes (tone turning on and off) indicated on/off keying (OOK), not a continuous tone.
Solution
1. Isolate the ultrasonic band
Bandpass-filter around 20 kHz and downmix to mono:
bash read-only • copy allowedsox new-year-h-ha-ha.wav band20.wav remix 1 bandpass 20500 1500
The isolated spectrogram clearly showed a 20 kHz tone keyed ON and OFF in a pattern of short and long bursts — classic binary OOK.
2. Demodulate the envelope and decode
- Compute the amplitude envelope
abs(samples)and smooth it with a 5 ms moving-average kernel. - Threshold at 15% of the maximum envelope to get a boolean ON/OFF signal.
- Run-length encode the transitions.
Observed structure:
- 240 ON segments.
- ON durations cluster into two groups: ~0.105 s (short) and ~0.307 s (long).
- OFF gaps are uniformly ~0.093 s (constant) — so the gaps carry no information. This rules out classic Morse letter/word spacing.
Therefore the scheme is binary OOK: short pulse = bit 0, long pulse = bit 1. 240 bits → 30 bytes of ASCII.
python read-only • copy allowed#!/usr/bin/env python3 import wave, numpy as np w = wave.open('band20.wav', 'rb') x = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float64) sr = 44100 # amplitude envelope, smoothed over 5 ms env = np.abs(x) win = int(sr * 0.005) env = np.convolve(env, np.ones(win) / win, 'same') # threshold to ON/OFF on = env > env.max() * 0.15 # run-length encode runs = [] state, start = on[0], 0 for k in range(1, len(on)): if on[k] != state: runs.append((state, (k - start) / sr)) state, start = on[k], k runs.append((state, (len(on) - start) / sr)) # long ON pulse = 1, short ON pulse = 0 (gaps ignored) bits = ''.join('1' if d > 0.2 else '0' for st, d in runs if st) flag = bytes(int(bits[i:i+8], 2) for i in range(0, len(bits) - 7, 8)) print(flag.decode()) # BugCTF{REDACTED}