HackThisSite - Steganography Mission 2

Challenge

did I hear that correctly?

我这个听对了吗?

题目提供一个 WAV 音频文件,要求找出隐藏的密码。

Solution

先确认 WAV 的基本参数:

1
2
3
4
5
6
7
8
$ soxi 2.wav
Input File : '2.wav'
Channels : 2
Sample Rate : 22050
Precision : 16-bit
Duration : 00:00:10.00 = 220500 samples ~ 750 CDDA sectors
Bit Rate : 706k
Sample Encoding: 16-bit Signed Integer PCM

这是 22050 Hz、16-bit、双声道、10 秒的 PCM 音频。直接播放只能听到噪声;反向播放、交换声道也没有可懂内容。频谱分析显示,异常的时间变化集中在约 1.2–1.6 kHz,说明载荷不在普通听感或 WAV 元数据里,而是画在时频平面上。

命令行直接渲染频谱:

1
$ sox 2.wav -n spectrogram -o spec.png -z 90

为了让字符更清楚,限制频率范围并提高时频分辨率。下面的脚本使用 NFFT=1024HOP=64,将 0–2000 Hz 的频谱保存为灰度图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#!/usr/bin/env python3
"""Render the spectrogram band that carries the password.

Usage:
uv run --with numpy --with pillow python render_spec.py 2.wav spec.png
"""
import sys
import wave

import numpy as np
from PIL import Image

NFFT, HOP = 1024, 64
FMAX = 2000
DYNAMIC_RANGE = 70.0


def load_mono(path):
with wave.open(path, "rb") as w:
sample_rate = w.getframerate()
channels = w.getnchannels()
frames = w.readframes(w.getnframes())
pcm = np.frombuffer(frames, dtype=np.int16).astype(np.float64)
return sample_rate, pcm.reshape(-1, channels).mean(axis=1)


def render(path):
sample_rate, samples = load_mono(path)
window = np.hanning(NFFT)
freqs = np.fft.rfftfreq(NFFT, 1.0 / sample_rate)
keep = int(np.searchsorted(freqs, FMAX))
frame_count = 1 + (len(samples) - NFFT) // HOP
magnitude = np.empty((keep, frame_count))

for index in range(frame_count):
start = index * HOP
frame = samples[start:start + NFFT] * window
magnitude[:, index] = np.abs(np.fft.rfft(frame))[:keep]

db = 20.0 * np.log10(magnitude + 1e-9)
db -= db.max()
image = np.clip((db + DYNAMIC_RANGE) / DYNAMIC_RANGE, 0.0, 1.0)
image = (image[::-1] * 255).astype(np.uint8)
return image


def main():
source = sys.argv[1] if len(sys.argv) > 1 else "2.wav"
target = sys.argv[2] if len(sys.argv) > 2 else "spec.png"
image = Image.fromarray(render(source), "L")
image = image.resize((image.width, image.height * 4), Image.NEAREST)
image.save(target)
print("wrote %s (%d x %d, 0-%d Hz)" %
(target, image.width, image.height, FMAX))


if __name__ == "__main__":
main()
1
2
$ uv run --with numpy --with pillow python render_spec.py 2.wav spec.png
wrote spec.png (3430 x 372, 0-2000 Hz)
jb298abc9qb2