隐写术(steganography)是把一条信息藏起来,让没有人怀疑有信息存在的做法。加密是把信息打乱,让它无法被读懂;隐写术则是把它藏进某个普通的东西里,比如一张度假照片或一段语音备忘,让没人想到去查看。这个词来自希腊语,意思是“被遮盖的书写”。

数字隐写术主要围绕图片,因为一张照片里有几百万个数字,可以在没人看得出差别的情况下被轻微改动。本指南介绍实际存在的方法,从修改像素的最后一位,到在 JPEG 压缩内部做手脚;说明为什么大多数“图片里藏文件”(常说的“图种”)的做法其实比真正的隐写术简单;为什么通过聊天 App 发送照片常常会抹掉隐藏的部分;以及寻找隐藏数据的人,也就是隐写分析者,是怎么做的。

什么是隐写术

每个隐写系统都有同样的三个部分。载荷(payload)是秘密本身:一条信息、一个文件、一个文件夹。载体(carrier),也叫封面,是装着它的无害文件:一个 PNG、一个 JPEG、一段 WAV 录音。结果,也就是里面带着载荷的载体,叫作隐写文件或隐写包。Wikipedia 上的相关文章还补充了一个有用的第四个术语:嵌入密度(encoding density),即载体中被改动的比例。密度越低,改动越难被察觉,能放进去的数据也越少。

按 Wikipedia 的说法,相对于加密的优势在于,秘密“does not attract attention to itself as an object of scrutiny”(不会让自己成为被审视的对象而引起注意)。一个加密文件会宣告有东西在被保护。一张小狗的照片什么也不会宣告。这也是它的弱点:一旦有人怀疑这张照片,隐写术本身就什么也保护不了,所以严肃的工具会先加密载荷、再隐藏它。隐藏换来的是“不显眼”。当“不显眼”失效时,保护内容安全的是加密。

这个想法比计算机早了 2500 年

最古老的故事是实物的。希罗多德描述过一条信息被纹在信使剃光的头上,等头发长回来之后再派他出发。后来的几个世纪里,人们用过遇热显现的隐形墨水,到了二十世纪,微缩点把一页文字缩到一个印刷句号那么大。这个词本身最早出现在 1499 年,在约翰内斯·特里特米乌斯(Johannes Trithemius)的《Steganographia》中,那是一本伪装成魔法书的密码学和隐写术论著。

数字版本沿用同样的逻辑,只是把头皮换成了文件格式。载体只需要两个属性:它的某一部分可以在人察觉不到的情况下改变,而且知道去哪里找的程序能把这个改变读回来。图像在这两点上都极为充足。

LSB 隐写:改动每种颜色的最后一位

教科书上的方法是最低有效位(LSB)嵌入。典型图像中的一个像素,把红、绿、蓝存成 0 到 255 之间的数字,每个都写成八位二进制。最后一位恰好代表一个单位的变化。把它翻转,红色值 200 就变成 201。没有任何屏幕、任何眼睛能显示出这个差别。

一个红 200、绿 117、蓝 54 的像素,以二进制表示。把每个值的最后一位换成信息位 1、0、1,得到 201、116 和 55。两个色块看起来一模一样。
三个信息位存进一个像素。每个值最多只变动 255 级中的一级。

对图片里的每个值都这样做,容量就累积起来了。一张 1200 万像素的照片有 3600 万个颜色值,每个存一位就是 3600 万位,理论上约 4.5 MB。真实的工具用得少得多,一方面是为了保持不显眼,另一方面是因为它们会按由密码得出的伪随机顺序分散这些位,这样载荷就不会是图像顶部一整块整齐的区域。

LSB 只适用于无损格式,如 PNG 和 BMP,这些格式会精确存储每个像素值。把同一张图存成 JPEG,压缩器会对数值取整,打乱最后几位,信息也随之被打乱。

它也是隐写分析者最了解的方法。自然照片的低位有一种特有的噪声模式,用看起来完全随机的加密数据覆盖这些位,就会改变这个模式。Westfeld 和 Pfitzmann 在 2000 年发表了针对 LSB 嵌入的统计攻击,Fridrich、Goljan 和 Du 在 2001 年表明,在彩色图像中,嵌入会增加“非常接近”的颜色对的数量。Wikipedia 报告说,仅改动 LSB 的做法“在某些情况下能以合理的可靠度检测出低至 1% 的嵌入密度”(“in some cases detecting encoding densities as low as 1% with reasonable reliability”),不过那里这个数字没有引用来源。在 Hacker News 上,有位评论者说得更直白:朴素的 LSB 替换引入的噪声“VERY easy to detect”(非常容易检测)。

JPEG 隐写在压缩内部工作

手机和网络上的大多数照片都是 JPEG,所以工具必须找到办法挺过 JPEG 的取整。答案是在取整之后而不是之前藏数据。

JPEG 编码器把图像切成 8×8 像素的块,并用一种叫离散余弦变换(DCT)的变换,把每个块转成 64 个频率系数:一个数代表块的平均亮度,其他的代表粗略和精细的细节。然后它把这些系数除以一张质量表并取整,损失就发生在这里。JPEG 隐写直接编辑取整后的系数本身,所以编码器之后做的任何事,都不会把信息取整抹掉。

早期的工具做得很朴素。JSteg 和 JPHide 是 Provos 和 Honeyman 在 2001 年测试检测的两个程序,它们改动系数低位的方式会让整体直方图发生偏移,统计检验能抓到。OutGuess 由 Niels Provos 在 1999 年编写,它把位嵌入伪随机选择的系数,然后调整其他系数,让全局直方图与原图一致,专门用来对付卡方攻击。

这没能持续:Fridrich、Goljan 和 Hogea 在 2002 年的一次攻击,测量了 JPEG 块边缘的“块状度”(blockiness),攻破了 OutGuess,甚至估算出了隐藏信息的长度。steghide 是最著名的命令行工具,它对 JPEG 和音频走了另一条路:用密码短语选出数值对进行交换,使每种颜色或采样值的计数保持不变,其作者称这能抵抗“first-order statistical tests”(一阶统计检验)。

这场军备竞赛仍是这个领域的形态。Fridrich、Pevny 和 Kodovsky 在 2007 年把统计上不可检测的 JPEG 隐写称为一个满是“dead ends”(死胡同)的研究难题,到 2012 年,隐写分析已转向在数千个图像特征上训练的机器学习分类器。读者在网上会看到的两种说法,隐写术“很容易被检测”和“很难被检测”,都是对的。分界线在算法上,而不在这个想法本身。

大多数“图片里藏文件”,是追加的数据,不是真正的隐写术

在 YouTube 上搜“怎么把文件藏进图片”,最常见的演示根本不是 LSB。它是 Windows 上的一行命令:

  1. 把一张照片和一个 zip 压缩包放在同一个文件夹里。
  2. 在命令提示符里运行 copy /b photo.jpg + archive.zip combined.jpg。在 macOS 或 Linux 上,cat photo.jpg archive.zip > combined.jpg 效果相同。
  3. 打开 combined.jpg:它显示的是那张照片。把它改名为 .zip,很多压缩工具就能打开里面的文件。

这能行得通,是因为格式的结尾方式。JPEG 以一个图像结束标记收尾,PNG 以一个叫 IEND 的数据块收尾,PNG 规范把它定义为“the last chunk in a PNG datastream”(PNG 数据流中的最后一个数据块)。图像查看器读到那里就停止,忽略后面的任何东西。相反,zip 读取器会在文件末尾寻找压缩包的目录,并往回读。一个能被两个不同程序都当作有效文件读取的文件,叫作多态文件(polyglot),而“old RAR trick”(古老的 RAR 把戏),正如 r/Steganography 上一位 Reddit 用户所称,就是最常见的例子,也就是常说的“图种”。

在文件中隐藏数据的三种方式的示意图:改动图像数据内部像素的低位、在结束标记之后追加字节、在结束标记之前加入格式允许的额外记录。
隐藏数据可以放的三个位置。只有第一种改变了图片本身。

还有第三类介于两者之间。大多数格式都是带标签数据块的容器,并定义了添加自定义数据块的办法:PNG 有辅助数据块,JPEG 有应用段,MP4 有用户自定义的 box,PDF 允许注释。不认识某个数据块的查看器会跳过它,所以文件保持完全有效,在任何地方都能正常打开。容量很大,视频里可达数 GB,而图片本身没有任何变化。

后两种严格说来都不是隐写术,因为它们既没有碰像素,也没有设法让自己在统计上看起来正常。文件只是携带了超出画面所需的东西。在近期一批样本里,r/Steganography 上获赞最多的帖子,把数据藏进 BMP 扫描行的填充字节里,而它自己的评论者就看出了破绽:有人算了容量账,另一人说他们会“be immediately suspicious if someone sent me an image that's not png or jpg”(如果有人给我发的图片不是 png 或 jpg,我会立刻起疑)。追加和插入的数据能瞒过看图片的人,却瞒不过读取整个文件的程序,而且这个文件明显比同样尺寸的普通照片大。

分享照片常常会毁掉载荷

隐藏的数据只有在原样的字节都保留下来时才能存活,而人们分享照片的许多方式并不会保留它们。

聊天 App、社交网络或图片托管服务收到一张照片时,往往并不会保存你发出的那个文件。它会把图像解码成像素,经常还会缩小尺寸,并为了节省带宽写出一个全新的压缩文件。像素里的 LSB 位会被新的压缩打乱。系数级的 JPEG 载荷在质量改变时被取整抹去。追加的字节和额外的格式记录通常不会被复制,因为 App 是只根据像素写出新文件的。

流程图:带有隐藏数据的图片,通过 U 盘、云盘、电子邮件附件或“以文档发送”作为文件复制,会原样到达,数据得以保留。作为照片发送时,App 会解码、缩放并写出新文件,所以像素位被打乱,额外的字节和记录根本不会被写入。
只有逐字节复制文件时,隐藏的部分才会跟着走。

目前没有可靠的公开测试说明哪些 App 会这么做,而且行为会随版本以及“照片”与“文档”发送模式而变化,所以在被证明无损之前,请把任何分享都当作有损的。这些轶事都指向同一个方向。r/Steganography 上有位 Reddit 用户发现,某个工具的载荷只在从 Reddit 下载的 PNG 里存活,在 Reddit 提供的 WebP 版本里则没有。Hacker News 上有位评论者回忆起较早的一次尝试:“most image hosts were smart enough to detect the sub-optimal encoding and 'helpfully' fix it”(大多数图片托管服务足够聪明,能检测到次优的编码并‘好心地’修复它)。安全的习惯很简单:把文件当作文件来发送,并在删除原件之前,先检查收到的那份副本还能打开。

隐写分析,是找出隐藏数据的手艺

Wikipedia 把隐写分析定义为“the study of detecting messages hidden using steganography”(研究如何检测用隐写术隐藏的信息),与密码分析相对。它的目标分三步:发现可疑文件,判断它们是否带有载荷,并在可能时恢复它。前两步是统计和文件检查。第三步通常需要密码,这就是为什么即使隐藏已被看穿,加密仍然重要。

最著名的实地研究说明了第三步在实践中有多难。2001 年,Niels Provos 和 Peter Honeyman 扫描了 eBay 拍卖中的两百万张图片。他们的检测器标记了约 17,000 张,对这些做字典攻击,却一条隐藏信息也没找到。Wikipedia 的文章警告说,只凭隐写分析采取行动是“a very dicey proposition unless a payload has been completely recovered and decrypted”(除非载荷已被完全恢复并解密,否则是非常冒险的),因为否则分析者只有“a statistic indicating that a file may have been modified”(一个表明文件可能被修改过的统计数字)。检测是什么样子、它的局限,完整的故事见隐藏的照片能被发现吗。双方的工具,连同它们的年龄和弱点,在隐写术工具对比中做了比较。

HideFile 能帮上什么

大多数搜索隐写术的人并不需要隐蔽信道。他们只是想让几张私密照片或几个文件,在别人会拿起的手机或笔记本电脑上不再显眼。对这个目的,经典工具很别扭:steghide 在 JPEG 和 WAV 里只能放小载荷,LSB 工具需要 PNG,而且没有一个能给你一个可以浏览的图库。

HideFile 采用的是上面示意图里的第三种办法。在 hidefile.app,在任何现代浏览器里,你选一张普通的照片、视频或 PDF 作为封面,添加私密文件,并设置密码。HideFile 在浏览器里加密这些文件,并把它们存成文件格式所允许的记录,所以封面在其他任何地方仍然是一张正常的图片、视频或文档。到 hidefile.app 输入密码打开,文件就出现了。不会上传到 HideFile 的服务器,也不需要安装任何东西。

它是“隐蔽加加密”,并且坦率地说明是哪一部分在起作用。HideFile 不改动像素,也不试图骗过统计检验。专门的工具能看出封面里被加了数据;它做不到的是在没有密码时读出内容。这使它适合让东西避开浏览你设备的人,而对于训练有素的检查人员、边境检查或法律压力,它不合适。

密码无法找回,而且会重新编码照片的 App 可能会把隐藏的部分去掉,所以请把封面当作文件来保存和分享。想全面了解各种隐藏技巧能骗过谁,见怎么把文件藏在眼皮底下;照片保险箱 App 的对比,见照片保险箱 App 安全吗。

资料来源