聊到CTF,很多人第一反应是Web、逆向、Pwn这些听着就硬核的方向。但真正打比赛久了你会发现,新手队和中等队之间的分水岭,往往出现在Misc,也就是杂项。而杂项里出镜率最高、最值得花时间研究的一块,就是隐写。
隐写(Steganography)和信息隐藏听起来很像,但角度完全不同:信息隐藏关注的是“怎么藏”,隐写追求的是“藏了之后谁也看不出来”。在CTF里,出题人把flag藏进图片、音频、Word文档甚至压缩包里,选手要做的就是通过工具和思路把它找出来、恢复出来。这个过程会涉及文件格式解析、编码识别、信号处理、密码破解等一堆看似乱七八糟的知识,但实际上是有章可循的。
这篇文章不是从零开始堆概念的教程,而是想把我做过、复盘过的一系列隐写题整理成一条可复用的思路线:拿到一个文件之后先看什么、用什么工具、在什么场景下切换到下一个方案。如果你刚接触CTF的Misc方向,或者打了一阵子比赛但遇到隐写题还是只会挨个试工具,这篇应该能帮你把思路理清楚。
1. 先搞清楚杂项在考什么:隐写题的出题逻辑与审题习惯
1.1 杂项不是“乱炖”,而是信息处理能力的试炼场
CTF里的Misc题型相当杂:编码转换(Base家族、凯撒、栅栏等)、流量包分析(pcap里的HTTP/DNS隧道)、内存取证、磁盘取证,以及今天要重点展开的隐写。有些题目会叠加好几层元素,比如一段音频先解密出一个密码,用密码打开藏在图片尾部的压缩包,压缩包里又是一张带LSB隐写的图,最后才找到flag。这种“套娃”设计经常被新人吐槽,但出题人真正的意图其实是考察一件事:在信息不完整、格式不明确的情况下,你能不能从一个原始文件里提取出有意义的数据。
这和真实安全工作中的取证分析非常接近。应急响应时拿到一个被上传到服务器上的恶意文件,不也得先从文件类型、字符串、结构异常开始分析吗?隐写题锻炼的正是这种底层能力。近两年比赛还经常在杂项里加入AI安全相关的内容,隐写的载体从图片音频扩展到了模型文件,万变不离其宗:先识别文件,再找隐藏通道,最后恢复数据。
1.2 拿到隐写题,先做这三步再动手
我有一个习惯:拿到题目文件,第一件事不是打开图片看长什么样,而是先做三个最基础的操作,把低级坑先填掉。
第一步,用file看真实类型。很多题目会把文件后缀改掉,把zip改成png,或者给txt塞进一个图片数据。file命令通过文件头判断,一次就能看出真实格式。第二步,用strings加正则扫一遍字符串。直接执行strings file | grep -iE 'flag|ctf|key|pass|secret',经常能直接命中明文的flag。虽然大部分题没这么简单,但这是成本最低的检查。第三步,用binwalk -e或foremost看有没有附加文件。图片尾部藏压缩包、藏另一个图片,这类题用binwalk基本一次就能分离出来。
这套“三板斧”不是最优解,但能把七八成的基础题目过滤掉,避免后面的分析方向跑偏。
1.3 提示信息一律当“有效信息”处理:猫图题目的启发
之前看到一条热搜词“该程序员喜欢猫,连采用隐写算法都和猫相关”,看完我笑了,确实是出题人会干的事。解题时,题目描述或者文件名里隐藏的提示,往往比工具本身更关键。
有一次练习赛遇到过类似的题目:一张普通的猫图,文件名cat_lover.png,题目描述只有一句话“该程序员喜欢猫”。当时队友把binwalk、zsteg全跑了一遍,没有发现异常,最后我顺手看了一眼EXIF信息,发现图片的Comment字段里写着一串Base64,解码后得到一个zip密码,再结合binwalk分离出的压缩包才拿到flag。
关键点在于,出题人不会无缘无故在描述里写“程序员喜欢猫”。猫对应的是cat命令,也可以理解为图片内容暗示你去关注EXIF、关注文件名传递的信息。凡是题目里出现的提示词,都要当成解题线索处理:文件名、题目描述、图片尺寸、修改时间,这些不起眼的元数据里可能就藏着密码或密钥。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 图片隐写:从文件结构到像素位面的三个层次
2.1 第一层:藏在文件结构里的附加数据
图片隐写是Misc里最常见的问题。我习惯把图片隐写分成三个层次来看:文件结构层、空间域像素层、频域变换层。不同层次隐藏方式不同,对应工具也不同。
先说文件结构层。以PNG为例,一个完整PNG有固定的块结构:文件头89 50 4E 47 0D 0A 1A 0A,然后是IHDR(宽、高、位深、颜色类型),中间是多块IDAT(存储压缩后的像素数据),最后是IEND。正常图片在IEND之后就结束了,但出题人可以直接在IEND之后追加一个压缩包、一段文本甚至一张小图片。很多看图工具会自动忽略尾部多余数据,所以肉眼根本看不出异常。
这时候binwalk -e是最趁手的工具,它通过特征签名识别文件内嵌的各种格式。foremost则按文件头去“雕刻”并重新组织出文件,适合文件数据被切碎或者没有完整结构的情况。给个直观的例子,命令行输出大概长这样:
bash复制$ binwalk cat.png
DECIMAL HEXADECIMAL DESCRIPTION
0 0x0 PNG image, 800 x 600, 8-bit/color RGBA
524300 0x8000C Zip archive data
看到Zip archive data这一行,基本就能确定图片尾部埋了一个压缩包。用dd或直接用binwalk的-e参数把文件提取出来,后续再按照压缩包的路子走。
提示:文件结构层的问题,不要一上来就打开图片看。先在十六进制里确认文件边界,再用分离工具,思路会清晰很多。
2.2 第二层:LSB最低有效位与视觉冗余
文件结构层搜完,如果什么都没有,就要考虑真正的像素级隐写了,最经典的就是LSB隐写。
PNG的每个像素由R、G、B三个颜色分量组成,每个分量用8位二进制表示,范围0到255。8位里最高位对颜色影响最大,最低位(Least Significant Bit)只影响数值上的1,反映到屏幕上人眼几乎分辨不出来。颜色从0变到1,从255变到254,看起来完全一样。LSB隐写就是把flag的二进制位拆开,塞进颜色分量的最低位,或者直接替换成密文。
提取时思路反过来:把每个像素的RGB最低位取出来,按顺序拼接成二进制,再转成ASCII。命令行里zsteg可以直接检测并提取PNG/BMP的LSB信息,stegsolve则能逐位平面查看R、G、B各通道的第0位到第7位,肉眼直接看出藏了什么图案。我平时也会自己写Python提取,因为比赛题目可能在LSB基础上做二次变换,比如只有奇数行的低位才参与隐藏,或者RGB三个通道分别存不同信息:
python复制from PIL import Image
img = Image.open('cat.png')
pixels = img.load()
bits = ''
for y in range(img.height):
for x in range(img.width):
r, g, b = pixels[x, y][:3]
bits += str(r & 1)
bits += str(g & 1)
bits += str(b & 1)
# 按8位一组切分,再转成ASCII
for i in range(0, len(bits), 8):
byte = bits[i:i+8]
if len(byte) == 8:
ch = chr(int(byte, 2))
if ch.isprintable() or ch == '\n':
print(ch, end='')
脚本很简单,关键是理解“最低位”这个概念。出题人再怎么魔改,万变不离其宗。
2.3 第三层:DCT频域与JPEG隐写
LSB隐写一般用在PNG、BMP这种无损格式上。遇到JPEG就不一样了。JPEG压缩时会把图像从空间域通过离散余弦变换(DCT)转到频域,再量化和编码。隐写算法可以在DCT系数上做修改,把要隐藏的信息嵌入到中高频系数中。人眼对高频细节不敏感,所以这种修改肉眼完全看不出来。
遇到JPEG优先试steghide:
bash复制$ steghide extract -sf image.jpg
Enter passphrase:
如果工具提示要求输入密码,说明图片确实有隐藏信息。密码可能是空密码,也可能在EXIF里有提示。想判断是否存在DCT隐写,可以借助stegdetect这类工具,虽然检测率不算完美,但能作为辅助判断。
这类题有一个容易卡住的地方:有些出题人会把隐藏内容做二次加密或者编码,你就算提取出来看到的还是一串看不出规律的东西,需要再做一次Base64解码、十六进制转ASCII或者XOR解密。所以提取隐写信息只是过程,拿到之后还需要走一遍“字符串识别 + 编码识别”的思路。
2.4 别忽略宽高、通道和元数据
图片隐写还有几个容易被忽略的考点,值得单独提一下。
第一是宽高被修改。有的题会改PNG的IHDR数据块中的宽高字段,让图片只显示一部分,真正的flag区域被“裁切”在画面外。用pngcheck检查CRC,如果报错,大概率是IHDR被改过。把宽度或者高度改回正确值,图片恢复正常就能看到隐藏内容。具体做法是用十六进制编辑器定位IHDR,按CRC反推宽高,或者直接一个个试常见尺寸。
第二是alpha通道。RGBA图片有4个通道,A是透明度。有些题会把信息藏在alpha通道的低位,zsteg默认就能检测到,但如果你只用常规取色工具看RGB,会完全错过。
第三是元数据。前面猫图例子提到的EXIF信息经常藏flag。exiftool可以查看完整元数据,建议无论什么题都顺手跑一遍,成本极低,但经常会救命。
3. 音频隐写:频谱、波形与藏在声音里的flag
3.1 频谱图:把flag“画”在声音上
音频隐写是这两年Misc的热门方向,其中最有代表性的就是频谱图隐写。
原理其实不复杂:一段普通音频,在时域上看是振幅随时间变化的波形。但如果对整段音频做傅里叶变换,用Audacity可视化,能看到它在不同频率上的能量分布。出题人可以把flag的字符画成特定频率区域的图案,叠加到音频频谱上。播放时这段声音听起来只是刺耳的高频噪声,人耳几乎察觉不到,但只要打开频谱图,就能看到清晰的文字。
实操时用Audacity打开音频,点轨道左上角的下拉菜单选择“频谱图”。如果图案不清晰,调整“窗口大小”参数,数值越大频率分辨率越高,图形越锐利。隐藏信息如果画在高频段,把频谱显示的频率上限调高,或者把增益拉大,文字会更容易显现。
我印象很深的一道题,把flag转成一张极简像素画,再用程序生成对应频率分量的音频,整个文件只有几秒钟,听起来就像一段刺耳的“啾啾”声。很多玩家一开始都在找摩斯码、找密码,压根没想到开频谱图,说明这题考的就是工具熟练度。
提示:拿到音频先别急着听内容,先看频谱图。这个操作成本很低,但经常能直接看到清晰的大字。
3.2 波形里的摩斯码和DTMF拨号音
频谱图之外,还有一个最简单但也容易被忽略的:直接看波形。
隐写题经常把信息编码成摩斯码。摩斯码用点和划两种长度的信号表示字母和数字,在波形上表现为短音和长音。用Audacity放大波形,看到一长串宽窄不同的脉冲,窄的记为点,宽的记为划,间隔短的算字母内区分,间隔长的算字母间隔,整理出来用在线解码器就能得到字符串。这类题虽然古老,但依然会出现在比赛里。
另一类是DTMF双音多频,就是老式电话按键发出的“嘟——嘟”声音,每个按键对应两个频率的组合。把音频放进Audacity做频率分析,读出每个音段的两个频率,查表对应成数字,再转ASCII或者当密码用。这种题考的是对常见编码方式的敏感度,思路是先识别调制方式,再做解码。
3.3 音频尾部、隐藏轨道与SSTV
音频文件本身也是一种文件,所以前面图片文件结构层的排查手段在音频上同样适用。把mp3/wav文件丢给binwalk,检查尾部是否有附加文件;同时用strings扫一遍字符串。wav文件的数据块之后如果多出一段非音频数据,经常就是flag本体。
还有一种进阶玩法是SSTV(慢扫描电视)。它把一张图片调制到音频信号里,接收端用SSTV解码器把图像恢复出来。这类题目需要用到RX-SSTV这样的专用解码工具,最好在Audacity里单独播放音频,或者用虚拟声卡把音频“播放”给解码器。SSTV在CTF里不算高频,但一旦遇到,不了解的话会很懵。这类题的提示通常很明显:音频里有规律的“滴——滴——”间隔声,且每个音段持续时间较长,这根本不是语音或音乐,大概率是数字调制信号。
4. 文档和压缩包里的信息隐藏:Word、Zip与套娃密码
4.1 藏在Word里的不可见字符和隐藏文字
很多人一听到隐写就只想到图片和音频,其实Word文档也是很好的隐写载体,出题率不低。
最简单的一招是把文字颜色改成白色,和白色背景融为一体。肉眼很难发现,但全选文档后看文字颜色,或者直接复制内容到记事本,白色文字就会现出原形。Word还提供了“隐藏文字”的字体效果,勾选后文字带虚线下划线,普通视图和打印都不显示。可以在“文件 → 选项 → 显示 → 始终在屏幕上显示隐藏文字”里打开查看。
还有一种更隐蔽的方式是零宽字符。零宽空格(U+200B)、零宽连接符(U+200D)这类Unicode字符在视觉上是不可见的,但确实存在于文本中。把一段二进制信息编码成零宽字符插入普通句子中,人眼看起来毫无异常。用Python或者专门的在线工具把不可见字符转成二进制,再解码就能拿到信息。
另外要记住:现代Word文档本质上是一个zip压缩包,把docx后缀改成zip,解压后可以看到document.xml等一堆XML文件。有时候flag就明文写在XML里,有时候是藏在某个图片资源中,这类题直接按文件分析流程处理即可。
4.2 Zip伪加密:其实根本没加密
压缩包在隐写题里出现得太频繁了,必须单独说说。最典型的就是伪加密。
ZIP格式在文件头里有一个“通用位标记”字段,其中最低位表示是否加密。某些出题人故意把中央目录里的这个标记位改成1,但实际上并没有对数据做任何加密。于是你双击压缩包提示需要密码,暴力破解也永远解不出来,因为根本没有真实密码。处理办法是用十六进制工具打开zip文件,找到加密位改回0,保存后重新解压,文件直接出来了。
判断是不是伪加密有个简单规律:压缩包里全是文本或者小文件,压缩方式还是“仅存储”,却提示要密码,就可以先怀疑伪加密。更稳妥的办法是用十六进制编辑器找到ZIP的中央目录文件头(PK\x01\x02开头),看偏移8处的通用位标记。如果本地文件头没有加密标记而中央目录却有,八成就是伪加密,改回来就好。
4.3 多密码嵌套与自动解压脚本
再进阶一点的是套娃压缩包:一个zip里面套着另一个zip,每个压缩包都有各自的密码。密码可能藏在前一个文件内的某个文本里,也可能需要爆破。
破解zip密码的工具不少,fcrackzip适合简单字典攻击,John the Ripper配合zip2john可以处理更复杂的hash破解。如果密码是纯数字短密码,hashcat配合掩码法几分钟就能出来。
但嵌套多了之后手动解压非常折磨,我一般会写一个Python脚本,循环执行“解压 → 读取密码文件 → 继续解压下一层”,配合字典自动尝试。这类题目名字常直接叫“套娃”或“nested”,看到标题基本能猜到套路。网上有很多现成的多密码嵌套自动解密工具,但比赛时自己写一个更保险,因为出题人经常会在某个环节加个变形:密码后面多了一个换行符、某个压缩包根本不需要密码、某层的文件名是乱序的。这些细节只有自己可控的脚本才处理得顺手。
5. 从一道隐写题到拿flag:我的完整排查链路
5.1 一套可以直接抄的排查流程
整理了我打完几十道隐写题后沉淀下来的排查流程,可以直接照做:
- 用
file看真实文件类型,不要相信后缀。后缀与真实类型不符时,先弄清它到底是什么。 - 用
strings扫字符串并grep关键字,扫完ASCII再扫一下UTF-16编码的字符串。 - 用
binwalk -e或foremost分离内嵌文件,检查是否有多段数据。 - 如果是图片,用
exiftool看元数据,再用pngcheck验证PNG结构是否异常。 - 图片无异常,上
zsteg扫LSB,用stegsolve逐位平面看一遍。 - 如果是JPEG,试
steghide;没有密码提示再用stegdetect判断。 - 如果是音频,Audacity开频谱图,同时看波形里是否有摩斯码或DTMF。
- 如果是文档,先改后缀解包,搜索可见字符串,处理隐藏文字和零宽字符。
- 解出的文字如果不像flag,按编码识别走一遍:Base64、URL编码、十六进制、XOR等。
- 如果压缩包有密码,先看提示和文件内容,再考虑字典爆破或掩码爆破。
这套流程不是绝对的,有的题故意把flag拆成两半,一半在图片里,一半在音频里,需要并行处理。但按照这个顺序排查,能把大部分常见隐写题的坑先填上。
5.2 隐写题排查速查表
为了方便复盘,我整理了一张速查表,比赛卡壳时经常拿出来对照:
| 现象 | 可能的隐藏方式 | 工具/方法 |
|---|---|---|
| 后缀和真实类型不符 | 文件头被改 | file、十六进制编辑器 |
| 图片尾部有多余数据 | 附加压缩包/文本 | binwalk -e、foremost、dd |
| PNG打开提示CRC错误 | 宽高被改 | pngcheck、十六进制修正IHDR |
| 颜色有细微噪点 | LSB隐写 | zsteg、stegsolve、自写脚本 |
| JPEG无异常但有密码提示 | DCT频域隐写 | steghide、stegdetect |
| 音频听起来像噪声 | 频谱图画字 | Audacity频谱图、提高窗口大小 |
| 波形有长短脉冲 | 摩斯码 | 波形放大、在线解码 |
| Word里感觉有信息但看不到 | 白色字体/隐藏文字 | 全选改颜色、显示隐藏文字 |
| 压缩包要密码但怀疑无加密 | 伪加密 | 十六进制修改通用位标记 |
| 压缩包套娃 | 多层密码 | Python循环解压、字典爆破 |
这张表不能覆盖所有题目,但能覆盖大部分基础题和中等题。遇到偏题怪题时,最重要的是回到“文件结构 + 特征识别”的思路去推,而不是继续盲目套工具。
5.3 我踩过的几个坑
说几个我实际踩过的坑,都是血泪教训。
第一次用zsteg扫一个PNG,输出一堆“b8,bgr,lsb,xy”之类的提示,颜色全是绿的,但提取出来全是乱码。后来才发现隐藏信息不是从第0位开始藏的,而是藏在了第2位,用默认参数提取当然不对。解决办法是手动指定通道和位面,比如zsteg -b 2,或者逐个通道去尝试。
还有一次,binwalk成功分离出一个zip,我满心欢喜去解压,结果提示密码错误。折腾了半小时,最后发现密码就写在我最早strings输出的一大段乱码里,只不过不是ASCII,而是Base64编码后再倒序的字符串。从那以后我养成一个习惯:strings的输出不要急着丢,所有看起来有规律的长字符串都可能经过编码变换。
还有一个容易
