简介一套面向CTF音频杂项方向的出题与解题辅助脚本适用于需要掌握WAV音频隐写分析、信号处理与Python脚本逆向的参赛选手。压缩包共3个文件约140KB包含两个WAV音频样本和一个Python脚本。音频素材可用于练习频谱分析、倒相位分析及时域分析脚本则展示基于音频数据生成或还原flag的典型逻辑。已有1931人浏览学习适合作为CTF Misc音频题型的入门实操参考。通过分析脚本导入的音频处理库、数据读取流程、傅立叶变换或滤波等信号转换函数以及加密判断与输出逻辑读者可学习如何将隐藏信息从音频中提取为最终flag并理解出题者构造题目的常见思路。实际练习时还可结合运行脚本观察输出与音频样本对比进一步理解隐写信号的生成与提取流程。资源体积小巧、结构清晰便于快速上手和对照练习。1. CTF音频杂项出题脚本一条命令把 flag 藏进 WAVCTF 音频杂项出题脚本这个工具刚拿到手的时候我第一反应是“这能省多少事”。过去做一道音频杂项题从拼素材、调时长、藏字符到验证解出少说两三个小时打底还经常因为手误导致题目根本解不出来。这套脚本把频谱文字、摩斯电码、文件追加三类最常见的音频隐写方式做成了命令行工具输入一个 flag 文本自动输出可直接分发的 WAV 文件与校验记录。适合要给训练赛出题的学长、带新人的社团负责人或者想在自己靶场里补几道杂项题的一线运维。关键是它把“出题”和“验题”两个环节绑定在一起不用再靠耳朵反复听、靠眼睛反复看声谱图。2. 三类主流藏法背后的算法逻辑频谱文字、摩斯电码与文件追加音频杂项题听起来玄学本质上是“把文本信号编码进音频载体”。网上很多人用 Audacity 手工操作一次两次可以数量一多就翻车。脚本能稳定出题是因为它把三类隐藏方式背后最基础的算法逻辑固化下来了下面逐个拆开讲。2.1 频谱文字与短时傅里叶变换把 flag 画进声谱图频谱文字的考点是让选手打开声谱图看到文字核心原理是短时傅里叶变换STFT的逆过程。音频在时间轴上被切成许多帧每一帧做傅里叶变换后得到频域能量分布。要让声谱图上出现一个“C”就要在对应的时间片内叠加一组特定频率的正弦波让这些频率点的能量明显高于周围。文字的行映射到频率轴文字的列映射到时间轴拼起来就形成视觉上的字形。常见做法是先用 PIL 把文本渲染成黑白位图再把位图像素矩阵翻译成频率开关信号。每个“亮”的像素对应一个频率点在一个时间切片内发声“暗”的像素对应静音。实际生成时不会逐像素循环那样速度太慢正确做法是先把像素矩阵转成 numpy 数组用矩阵运算批量构造波形片段然后按列拼接。采样率一般取 44100 Hz频率范围建议落在 500 Hz 到 4000 Hz 之间。太低会被环境低频噪声盖住太高在有损压缩时容易丢细节。频率间隔由位图高度和可用频宽共同决定。假如位图高度是 64 像素频宽取 3500 Hz则每个像素对应的频率步进大约是 54 Hz。这个步进值决定了文字在竖直方向的可读性步进太大相邻频率间隔明显、字迹发虚步进太小则每条谱线过于密集压缩后糊成一片。脚本里通常提供一个--freq-min和--freq-max参数默认值就是 500 和 4000做简单题目基本不用改。2.2 摩斯电码转音频从文本到脉冲波的编码链路摩斯电码题是杂项里最经典的老朋友考点是选手能听出或从波形图上识别出点划节奏。这里的核心是把文本先映射成摩斯符号串再把符号串映射成不同长度的音频脉冲。标准编码表把 A 到 Z、0 到 9 映射为点和划的组合例如 “CTF” 编码后是-.-. - ..-..。点用短音表示划用长音表示字符内部点划之间有一个单位静音字符与字符之间有三个单位静音单词之间是七个单位静音。出题脚本生成音频时点时长、划时长和静音间隔的比例是关键。按照国际标准点为一个时间单位划为三个时间单位字符内间隔一个单位字符间间隔三个单位词间隔七个单位。实际出题时点时长不建议低于 80 毫秒太短了人耳跟不上选手用解码器也容易误判。我一般会把点时长设为 100 毫秒划 300 毫秒载波频率设为 800 Hz 或 1000 Hz。生成时先构造一个静音段再按符号序列交替叠加正弦波和静音最后统一转成 16 位 PCM 写入 WAV。这里有个容易被忽略的细节摩斯音频的波形包络不能做成瞬间起振和瞬间截止否则会有爆音。脚本里会在每个脉冲前后加几毫秒的淡入淡出通常用余弦窗函数处理。有些选手喜欢直接把波形拖进 Audacity 看包络包络太硬会看到明显的矩形突变虽然不影响解码但听起来非常糟。出题体验也在评分考量范围里。2.3 文件追加与多载体组合不只有“听”这一种解法音频杂项不全是靠听的文件追加是另一条技术线。WAV 文件由 RIFF 头、fmt 子块和 data 子块组成绝大多数播放器读到 data 子块末尾就停止解析文件尾部多出来的字节不会影响播放。因此可以在 WAV 文件末尾直接附加一段二进制数据比如把一段文本或一个 ZIP 压缩包粘在后面。选手用strings扫描能直接看到明文 flag用binwalk扫描能识别出附加的 ZIP 文件头解压后得到另一阶段的线索。这种方式的算法实现不复杂关键是搞清楚 data 子块的真实结束位置。用wave标准库打开 WAV 文件后调用getnframes()和getframerate()可以算出音频数据长度结合文件头偏移得到追加的写入起点。如果追加的是 ZIP 文件需要保证 ZIP 的PK头从偶数地址开始因为 WAV 的 data 区域如果长度是奇数追加数据前要补一个空字节对齐否则某些解压工具会报错。多载体组合则是把上述方式混在一道题里例如主音频用频谱文字给出第一段提示尾部追加一个加密压缩包压缩包密码藏在摩斯音频里。脚本需要支持流水线输出每个环节生成独立文件后由调度逻辑把各文件串成最终题目。这类组合题更适合竞赛正式赛因为它的解题路径不是线性的选手需要同时掌握音频分析和文件分析两种能力。3. 实操造题脚本目录、命令行调用与参数调优原理清楚了下面进入动手环节。这份脚本按“核心库 命令行入口”的结构组织拿到手里先看目录和依赖再跑 demo最后再动参数。3.1 脚本目录结构与依赖环境源码包解开后是一个标准的 Python 项目目录核心模块和入口文件分离。依赖只有两个numpy 负责波形生成与矩阵运算Pillow 负责把文字渲染成位图。wave、struct、hashlib 都是标准库不需要额外安装。建议在 Python 3.8 以上环境运行Windows 和 Linux 都验证过。audio_misc_builder/ ├── builder.py # 核心实现频谱文字、摩斯音频、文件追加 ├── cli.py # 命令行入口参数解析 ├── verify.py # 自检脚本回读生成结果并输出验证信息 ├── requirements.txt # 依赖清单numpy、Pillow ├── examples/ # 预生成的演示音频与原始 flag 记录 └── output/ # 出题输出目录自动创建先装依赖再跑通 demo确保环境没有问题pip install -r requirements.txt python cli.py --mode spectext --text CTF{demo_spectext} --output output/demo.wav命令行参数解析集中在cli.py--mode指定隐藏方式--text传入 flag 文本--output指定输出文件路径。跑完这条命令后output/demo.wav就是一道频谱文字题。可以先用 Audacity 打开看声谱图确认文字清晰后再继续调节参数。3.2 生成频谱文字题像素矩阵到波形拼接频谱文字核心函数接收文本、输出路径和一个参数对象内部拆成四个步骤渲染位图、提取像素矩阵、构造频率时间网格、逐列合成波形。下面是简化后的核心逻辑import numpy as np from PIL import Image, ImageDraw, ImageFont def text_to_mask(text, height64, width256): img Image.new(L, (width, height), 0) draw ImageDraw.Draw(img) draw.text((8, 8), text, fill255, fontImageFont.load_default()) mask np.array(img) 128 return mask def mask_to_audio(mask, sample_rate44100, slice_ms50, freq_min500, freq_max4000): n_slices, n_freqs mask.shape slice_len int(sample_rate * slice_ms / 1000) freqs np.linspace(freq_min, freq_max, n_freqs) audio np.zeros(slice_len * n_slices, dtypenp.float64) for i in range(n_slices): base i * slice_len t np.arange(slice_len) / sample_rate for j in range(n_freqs): if mask[i, j]: tone np.sin(2 * np.pi * freqs[j] * t) audio[base:base slice_len] tone * 0.15 return audiotext_to_mask把文本渲染成黑白位图并转成布尔矩阵矩阵行数对应频率网格数列数对应时间切片数。mask_to_audio遍历每个时间切片把所有“亮”的频率点以正弦波叠加方式写入波形。放大系数 0.15 是为了防止多频率叠加时超出 int16 范围如果文字笔画较粗导致同时发声的频率点很多这个系数需要往低调到 0.1。频率范围越宽单字垂直方向占的谱线越多文字越清晰但背景噪声也越明显。出题前用verify.py看一眼声谱图比直接给人试听更靠谱。3.3 生成摩斯电码题编码表、时间比与音色设置摩斯题的核心是严格控制点划时长比。脚本内置标准摩斯表和字符间隔映射生成时逐字符编码、逐符号合成。核心函数如下def text_to_morse(text): table {A: .-, B: -..., C: -.-., D: -.., E: ., F: ..-., G: --., H: ...., I: .., J: .---} words text.upper().split() encoded_words [] for word in words: encoded /.join(table.get(ch, ) for ch in word) encoded_words.append(encoded) return / .join(encoded_words) def morse_to_audio(morse, sample_rate44100, freq800, dit_ms100, amp0.8): dit int(sample_rate * dit_ms / 1000) dah dit * 3 gap dit char_gap dit * 3 word_gap dit * 7 t_dit np.arange(dit) / sample_rate t_dah np.arange(dah) / sample_rate tone_dit amp * np.sin(2 * np.pi * freq * t_dit) tone_dah amp * np.sin(2 * np.pi * freq * t_dah) silence np.zeros(dit, dtypenp.float64) audio np.zeros(dit, dtypenp.float64) for word in morse.split( / ): for i, symbol in enumerate(word): if symbol .: audio np.concatenate([audio, tone_dit]) elif symbol -: audio np.concatenate([audio, tone_dah]) if i ! len(word) - 1: audio np.concatenate([audio, silence]) audio np.concatenate([audio, np.zeros(char_gap)]) return audiotext_to_morse先在字符层面编码并由/分隔单词morse_to_audio遍历符号序列拼接出完整波形。这里的参数需要注意几个dit_ms控制点时长划长自动按三倍计算freq是载波频率800 Hz 是折中方案太低听起来发闷太高在部分耳机上反而刺耳采样率决定了时长精度44100 Hz 下 100 毫秒时长对应 4410 个采样点误差可以忽略。实际出题时建议把载波频率调成 1000 Hz因为很多频谱分析工具的默认显示范围在 250 Hz 到 2 kHz1000 Hz 正好居中。如果选手群体常用手机外放听题频率太高的音在劣质扬声器上会明显衰减所以不推荐超过 1500 Hz。点时长低于 80 毫秒时解码器容易把连续的点误判成划我一般锁死在 100 毫秒不让人改。3.4 追加隐藏数据与自校验输出追加模式的处理逻辑更偏向文件操作核心是找到 WAV 的 data 区尾部然后把 payload 字节写到文件末尾。自校验逻辑同时生成一份 JSON 记录内含 flag 原文、隐藏方式、参数键值对和文件哈希防止出题人自己忘记答案。import wave, struct, json, hashlib def append_payload(src_wav, payload_path, output_wav): with wave.open(src_wav, rb) as wf: frames wf.getnframes() rate wf.getframerate() sampwidth wf.getsampwidth() channels wf.getnchannels() data_len frames * sampwidth * channels header_len 44 payload open(payload_path, rb).read() if data_len % 2 1: payload b\x00 payload with open(src_wav, rb) as fin: wav_data fin.read() with open(output_wav, wb) as fout: fout.write(wav_data) fout.write(payload) def generate_record(flag, mode, params, output_wav): digest hashlib.sha256(open(output_wav, rb).read()).hexdigest() record {flag: flag, mode: mode, params: params, sha256: digest} with open(output_wav .json, w) as f: json.dump(record, f, ensure_asciiFalse, indent2)append_payload先通过wave模块算出音频数据的真实长度这里的关键是对奇数长度的 data 区做填充字节处理避免与后续 ZIP 头错位。如果原始 WAV 的 data 区长度是偶数则无需填充。generate_record输出一份同名 JSON 文件记录所有出题参数和原 flag。这份 JSON 在赛前验证和复盘时非常有用题目被解出后可以直接对比选手提交的 flag 是否匹配不用再重新听音频猜答案。4. 出题翻车与排查五个真实的音频题踩坑记录脚本生成题目虽然省去手工操作但音频通道的坑一个都躲不掉。以下五条是实际使用中反复出现过的问题按现象到原因再到解决方式逐条说明。4.1 频谱文字在 Audacity 里清晰转成 MP3 后糊成一团现象脚本生成的 WAV 文件用频谱图看文字分明打包发题时为了省空间压成 MP3选手反馈文字边缘发虚部分字母根本认不出来。原因有损压缩会丢弃高频分量和弱能量信号。频谱文字的每个频率点能量本身不高叠加后虽然视觉突出但 MP3 编码器对 16 kHz 以上和低能量频段有较强的衰减导致笔画断裂。解决出题文件强制保留 WAV 源文件不做任何有损转换。如果平台只接受 MP3把频率范围限制在 1000 Hz 到 3000 Hz 之间并调高叠加幅度到 0.25让谱线能量压过压缩量化噪声。我在脚本里默认输出 WAV就是不想在这个环节给自己埋雷。4.2 摩斯码解出来全是乱码点划比例完全对不上现象用在线摩斯解码器识别生成的音频输出字符和原始 flag 牛头不对马嘴点的数量明显偏多。原因点划静音间隔比例设置错了最常见的是把字符内间隔和字符间间隔都设成相同长度解码器无法区分“同一个字符内的点划分隔”和“两个字符之间的分隔”。解决严格按点 1、划 3、字符内间隔 1、字符间间隔 3、词间隔 7 的比例生成。脚本里默认值就是这样但如果你手动改过dit_ms要检查间隔是否跟随变化。另一个排查点确认文本里的空格没有被编码成空字符串否则两个单词会直接连在一起。4.3 flag 文本过长频谱图文字挤成一条黑带现象把一段 40 多字符的完整描述写进频谱题结果声谱图上文字全部挤在中间看不清任何字形整段像一条实心黑带。原因位图宽度固定为 256 像素文字过长时每个字符可用的横向像素不足字形被压缩成竖条。时间方向的分辨率也被同步拉高单位切片内亮起的频点变多视觉上自然糊成一团。解决脚本在spec_text()入口加长度检查超过 30 个字符时直接抛出警告并建议拆分。如果要出长文本横向拉到位图宽度 512 或 1024同时把slice_ms调小到 30 毫秒保持整体音频时长不变。4.4 追加 ZIP 后 WAV 播放到最后出现爆音现象用追加模式生成的文件播放到末尾有明显的“啪”一声虽然不影响前面内容但选手会怀疑文件损坏。原因WAV 的 data 区字节数是奇数时追加的 ZIP 头没有落在偶数地址边界。播放器在读取最后一个采样帧时解析到错误的字节宽度产生异常脉冲。解决追加 payload 前判断data_len是否为奇数如果是则先补一个\x00字节。使用ffprobe -f lavfi -i output.wav做一个快速检查如果时长和原文件一致说明 data 区没有被破坏。4.5 出题人自己忘了原始 flag复盘时对不上答案现象文件生成后没有保存出题记录第二天准备验证时已经记不清当时埋的是什么 flag只能重新跑频谱图去“读”文字。原因出题流程中缺少记录环节。音频隐写题不像 Web 题那样有明确的配置仓库修改参数后容易遗忘。解决每次生成强制输出 JSON 记录文件包括 flag 原文、隐藏模式、所有时间参数和 WAV 的 SHA256。我把这个逻辑固化到脚本主流程中不传--record-dir就抛异常从机制上杜绝“出完题忘了答案”的尴尬。5. 进阶玩法批量出题加半自动自检的闭环流程出题次数多了以后我发现单题生成只解决了一半问题真正花时间的是批量造题和逐题检查。一个 20 题的训练赛手动跑 20 次命令、再用 Audacity 逐个看声谱图至少浪费一晚上。这段最后分享一个批量生成的思路以及配套的半自动自检方法。批量出题我通常维护一个题目配置列表每个条目指定模式、flag、参数覆盖项和输出路径然后用循环调用cli.py。配置列表可以手工维护也可以用脚本按特定规则生成比如前缀加编号。生成结束后统一执行verify.py它会按模式自动做基本检查频谱文字模式输出 PNG 声谱图缩略图摩斯模式回放并用解码库重新解码对比追加模式用strings扫描确认 payload 是否完整落盘。import subprocess, json, sys cases [ {mode: spectext, flag: CTF{spectrum_ch1}, output: output/spec_01.wav}, {mode: morse, flag: CTF{morse_ch2}, output: output/morse_02.wav}, {mode: append, src: output/spec_01.wav, payload: payload.zip, output: output/final_03.wav}, ] for item in cases: args [python, cli.py] for key, value in item.items(): args.extend([f--{key}, str(value)]) result subprocess.run(args, capture_outputTrue, textTrue) if result.returncode ! 0: print(fcase failed: {item[output]}, {result.stderr}) sys.exit(1) print(fgenerated: {item[output]}) subprocess.run([python, verify.py, --input-dir, output])这个循环脚本把三道不同类型的题串成一次批处理生成失败时立即退出并打印错误全部成功后再统一跑自检。verify.py会逐题目读出 JSON 记录拿记录里的 flag 与从音频文件提取到的内容做比对匹配则标记通过。整个过程不需要人工听任何一段音频节省下来的时间足够把题目难度和干扰参数再调一轮。我自己的习惯是任何音频题在发布前必须跑一遍完整流程生成、自检、手动抽查一个点。之前有一次因为忘记加淡入淡出整张频谱图上多了一道横纹虽然不影响解题但被选手截图发出来讨论多少有点丢人。从那以后每次出题都强制走一遍批量自检和记录核对不再以“应该没问题”收尾。如果你也在备赛期或者日常训练中需要给杂项题库补量这套脚本已经把生成、记录、自检三个环节打包好了拿到手里就能直接用起来。希望帮到你。本文还有配套的精品资源点击获取