做安全研究这几年,我最大的感受是:很多人把“加密”和“隐写”混为一谈。加密是把一段话说成天书,让别人看不懂;隐写是让敌人压根不知道你在说话。图片隐写这件事,核心不是“藏得深”,而是“看起来正常”。一张普通的风景照、一块纯色背景、甚至一个表情包,都可能只是外壳,真正的内容藏在你肉眼看不见的像素缝隙里。
这篇东西我会从隐写术的本质开始讲,然后手把手带你走完 LSB(最低有效位)隐写的完整流程:从位平面原理到 Python 代码实现,再延伸到 JPEG 场景下的频域隐写,最后聊一聊怎么反制、怎么检测——也就是站在取证者的角度,把那些“看不见的痕迹”重新揪出来。内容面向对信息安全、CTF 竞赛、数字取证感兴趣的人,也适合那些刚接触隐写、想搞清楚原理再动手的初学者。如果你只想跑个工具,那网上大把现成脚本,但如果你想知道为什么这个位能藏、那个位不能藏,为什么 PNG 能做而 JPEG 不能直接做,这篇文章应该能给你答案。
1. 隐写和加密不是一回事:先搞清楚你到底在做什么
这是个看起来特别基础、但很多人其实没认真想过的点。加密(Encryption)解决的是“内容保密”的问题:我有一份明文,通过算法变成密文,没有钥匙的人即使拿到了也读不懂。但密文本身是显眼的——它就在那儿,是一串毫无规律、熵值极高的数据,明眼人一看就知道“这玩意儿不一般”。
隐写(Steganography)解决的是另一个问题:不让人发现通信行为本身。古代的例子很出名:希罗多德记载过一个故事,有人把消息写在木板上,再涂一层蜡,看上去就是一块普通蜡板;还有人把奴隶头发剃光,在头皮上刺字,等头发长出来再送出去。这些做法的核心不是“内容不能被懂”,而是“不能被发现有消息存在”。内容被发现了,顶多算失败一半;连信息通道本身都暴露了,整场通信才叫彻底玩完。
现代语境下,数字隐写的思路没有变,只是载体从蜡板和头皮换成了图片、音频、视频、文本。图片在隐写里的地位尤为特殊,原因有三个:
第一,图片的数据量足够大。一张千万像素的手机照片,原始 RGB 数据动辄几十 MB,里面能用的“冗余空间”非常可观。
第二,图片天生有噪声。真实世界的照片,像素值本身就有大量随机波动,改动几个低位的数值,肉眼完全看不出来,更难以从统计上直接断言“这里被改过”。
第三,图片流通太自然了。社交媒体、聊天软件、邮件附件,每天都产生海量图片,没有哪个监控系统能逐张检查每一张 PNG 的每一个像素位。
所以,图片隐写的本质可以概括成一句话:在不破坏视觉合理性的前提下,把秘密数据写入载体图片中“人眼感知不到、但客观存在”的冗余空间里。这个冗余空间具体在哪,取决于图片本身的格式和压缩机制,后面几节会展开。
顺便说一句,很多人一提到隐写就联想到违法或者灰色产业,这个观点并不全面。隐写技术本身是中性的,正向应用非常多:数字水印是隐写,版权保护是隐写,票据防伪、恶意软件取证分析、医疗影像隐私标记,通通都算隐写的范畴。CTF 竞赛里隐写题更是独立的常规方向。关键问题是使用场景是否合法合规。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先看懂图片藏着秘密的“缝隙”:像素位平面与人眼感知冗余
想实现图片隐写,第一步不是写代码,而是搞清楚“缝隙”到底在哪。这里需要引入一个基础但重要的概念——位平面(Bit Plane)。
2.1 灰度图像的位平面是怎么拆出来的
假如有一张 8 位灰度图,每个像素的亮度用一个 0 到 255 的整数表示。把 255 换成二进制,就是 8 个 bit,从最高位(MSB,bit 7)到最低位(LSB,bit 0)。比如某个像素值是 200,二进制是 11001000,它的 bit 7 是 1,bit 0 是 0。
所谓位平面,就是把整张图里所有像素的同一个 bit 抽出来拼成一张二进制图。bit 7 位平面代表的是每个像素最高位的分布,bit 0 位平面是最低位的分布。你可以把灰度图看成 8 层“纸片”叠在一起,每一层只记录所有像素的第 n 位是 0 还是 1。
那关键问题来了:改哪一位对视觉影响最小?
很简单,改动高位的代价是致命的。把一个像素的 bit 7 翻转,相当于亮度值直接变化 128,在灰度范围只有 256 级的图像里属于“灾难级”的视觉破坏。而翻转 bit 0,亮度值只变化 1,人眼基本区分不了相邻灰度级。这也是“最低有效位(Least Significant Bit,LSB)”成为经典隐写位置的根本原因——LSB 承载的图像语义信息最少,改动它造成的感知失真最小。
2.2 三通道图片的隐藏空间,比你想的大
彩色图通常由 RGB 三个通道组成,每个通道的每个像素同样是 8 位。也就是说,一个像素点其实有 3 个 LSB 可以操作。如果三个通道全部用来藏数据,那么容量是:
隐藏容量(字节) = 图片宽度 × 图片高度 × 3 / 8
一张 800×600 的图片,理论上能存大约 800 × 600 × 3 ÷ 8 = 180000 字节的数据,大约 175 KB。这个容量放在纯文本场景下非常惊人——一本几十万字的纯文本小说,GBK 编码大约也就几百 KB,一张普通网络图片就能整个塞进去。
不过要注意,这只是理论上限。实际嵌入时通常只改一两个位平面,因为改动幅度越大,视觉和统计层面的破绽越多。追求高容量和追求隐蔽性是天然矛盾的,具体怎么取舍,取决于你的场景。
2.3 为什么 JPEG 不适合做简单的 LSB 隐写
这是新手最容易踩的坑:网上找一张 JPEG 图片,跑 LSB 嵌入脚本,输出也保存成 JPEG,结果提取出来的内容全是乱码。原因其实不复杂——JPEG 是有损压缩格式。
JPEG 在压缩时,会先把图像从 RGB 色彩空间转换到 YCbCr,然后分块做离散余弦变换(DCT),再用量化表把高频系数压掉,这个过程本身就丢失了大量信息。像素域的 LSB 在 DCT 和量化之后早就面目全非了。换句话说,LSB 隐写只适用于无损格式,PNG 和 BMP 是首选载体,TGA、无损 TIFF 也行。只要你保存时用了 JPEG,嵌入的数据基本等于直接被格式“洗掉”了。
如果你非要拿 JPEG 当载体,就得换个藏法——不进像素域,进频域。在 DCT 系数上做文章,才能扛住 JPEG 的压缩流程。这部分我会在第 4 节单独展开。
提示:在实际项目里,我拿到一张待分析的图片,第一件事永远是
file命令看真实格式。扩展名是.png但文件头是JFIF的情况非常常见,很多人改了后缀但没改内容,格式判断错,后面的隐写分析方向全跑偏。
3. 动手实现 LSB 隐写:不依赖现成工具的像素级操作
讲完原理,该上手了。我建议初学者先不要用 steghide、zsteg 这类现成工具,而是用 Python 手写一个完整的 LSB 嵌入和提取流程。原因很直接:只有自己操作过像素,你才真正理解隐写到底改了什么,后面做隐写分析时才不会一头
