最后更新:
隐写术 - 在文本中隐藏秘密信息的技术与字符数
这段文字中隐藏着秘密信息 - 如果有人这样告诉你,你会在哪里寻找?每句话的首字母?特定字符之间的间距?还是可能嵌入了不可见的字符?隐写术(steganography)是隐藏信息存在本身的技术。如果说加密是让信息"无法阅读"的技术,那么隐写术就是"让人根本察觉不到信息存在"的技术。而这项技术有时可以通过字符计数这一简单行为来检测。
从古代延续至今的"隐藏"技术
隐写术的历史可以追溯到公元前 5 世纪的希腊。历史学家希罗多德在《历史》中留下的两个故事被视为最早的记录。其一是希斯提亚埃乌斯把最信任的仆人的头剃光,在头皮上刻下信息,等头发重新长出后才派往部下阿里斯塔戈拉斯那里。信使本人读不到自己携带的内容,接收方只凭"到了就把他的头剃光看一看"这一句指示便取出了信息。其二是德马拉托斯为了传递进攻希腊的计划,把字直接写在涂蜡书写板的木质底板上,再重新涂上一层蜡。当时的书写板是把蜡熔化后反复使用的书写工具,只要蜡面是空白的,就没有人会怀疑底板。
这两个例子在隐藏的性质上正好相反。头皮上的刻字要等头发长出才能出发,代价是速度,好处是途中被检查也不会露出文字;涂蜡的书写板可以立刻送出,代价是只要有人刮掉蜡层就会一次性暴露。"不易被发觉"与"传递得快"构成取舍,这个格局在后文提到的现代数字手法中依然照原样重复。
此后,用隐形墨水(柠檬汁、牛奶、尿液等)进行秘密通信、把摩尔斯电码编织进携带者身上的毛线等物理隐藏的办法被长期使用。进入 20 世纪后,把文件拍照缩小到极小尺寸的微点技术登场。缩小成直径 1 毫米左右圆形的影像,与铅字的句号或小写 i 上的点无法分辨,可以就这样通过邮政寄送。这种方式最早在两次世界大战之间的德国投入使用,此后多个国家都把它当作穿过审查邮路的手段加以采用。
基于文本的隐写术手法
数字时代的文本隐写术有几种代表性手法。
离合诗 - 隐藏在首字母中的信息
离合诗(acrostic)是将每行或每句的首字母连接起来就会出现秘密信息的手法。这是最古典的文本隐写术,自古以来就用于诗歌和歌词中。
真正引发过问题的例子,是 2009 年 10 月加利福尼亚州州长阿诺德·施瓦辛格针对州议员汤姆·阿米亚诺提交的法案发出的否决咨文。把正文第 3 行到第 9 行的首字母竖着读下来,正好拼成一句粗话的短句。州长一方解释说这只是巧合,但有数学家反驳称,从统计上看很难认为是偶然。这件事说明,离合诗的棘手之处并不在于嵌入手法有多巧妙,而在于"除了本人以外谁都无法确定它是巧合还是有意"。
离合诗可以在不增加字符数的情况下嵌入信息,但能嵌入的信息量受行数束缚。10 行的文章只能藏进 10 个字符,而且每行的开头都必须用指定的字母起笔,文句容易变得不自然。再加上只要有心去挑首字母谁都能找出来,因此论隐蔽性,它比现代手法要低得多。
空白字符操作
通过操纵单词间空格数量来嵌入比特信息的手法。1 个空格表示"0",2 个空格表示"1",编码二进制数据。人眼难以察觉空格的微妙差异,但使用字符计数工具可以检测到"相对于可见单词数,空格过多"。
零宽字符隐写术 - 不可见字符的世界
现代文本隐写术中最强大的手法是利用零宽不可见字符。Unicode 定义了多个在屏幕上不显示但作为字符数据存在的"零宽字符"。
| Unicode 码位 | 名称 | 原始用途 | 隐写术中的角色 |
|---|---|---|---|
| U+200B | 零宽空格 | 指定可换行位置 | 表示比特"0" |
| U+200C | 零宽非连接符 | 抑制连字 | 表示比特"1" |
| U+200D | 零宽连接符 | 促进连字 | 附加比特值 |
| U+FEFF | 零宽不换行空格(BOM) | 字节序标记 | 分隔符 |
使用 U+200B 和 U+200C 两种零宽字符,可以用 2 值(0 和 1)表示 1 比特。8 个零宽字符构成 1 字节,即 1 个 ASCII 字符。隐藏 5 个字符的信息"Hello"需要 40 个零宽字符。
将这 40 个零宽字符分散嵌入普通文本的单词之间,外观完全不变。但用字符计数工具比较"可见字符数"和"实际码位数",就会检测到不自然的差异。零宽字符在 UTF-8 中每个占 3 字节,所以 40 个也会表现为增加 120 字节。理解Unicode 基础知识就能确定这种差异的原因是零宽字符。
零宽字符隐写术的实现示例
来看具体的嵌入流程。考虑将秘密信息"Hi"嵌入普通文本"Good morning"的情况。
"H"的 ASCII 码是 72,二进制为 01001000。"i"是 105,二进制为 01101001。将 0 转换为 U+200B(零宽空格),1 转换为 U+200C(零宽非连接符),生成 16 个零宽字符串。
将这 16 个零宽字符插入"Good"和"morning"之间。外观仍然是"Good morning",但实际数据包含 16 个不可见字符。文本编辑器计数为 12 个字符,但程序计算 Unicode 码位数为 28 个。差值 16 个字符就是隐藏信息的真面目。
更高级的实现使用 3 种以上零宽字符进行三值以上编码,用更少的零宽字符表示相同信息。使用 U+200B、U+200C、U+200D 三种,每个字符可以承载 log₂3 ≒ 1.58 比特,因此 8 比特的信息量在理论上只要 5.05 个字符就装得下。不过实现时必须把不足一个的零头向上取整。三种字符用 5 个能表示的组合只有 3 的 5 次方 = 243 种,覆盖不了 1 字节的 256 种,所以实际上要用 6 个字符(3 的 6 次方 = 729 种)。相比二值方式的 8 个字符,缩短了 25%。
同形字攻击 - 外观相同的不同字符
同形字(homoglyph)是指外观几乎相同但 Unicode 码位不同的字符。例如,拉丁字母"a"(U+0061)和西里尔字母"а"(U+0430)在许多字体中外观完全相同。
| 拉丁字母 | 码位 | 西里尔字母 | 码位 | 外观差异 |
|---|---|---|---|---|
| a | U+0061 | а | U+0430 | 几乎相同 |
| e | U+0065 | е | U+0435 | 几乎相同 |
| o | U+006F | о | U+043E | 几乎相同 |
| p | U+0070 | р | U+0440 | 几乎相同 |
| c | U+0063 | с | U+0441 | 几乎相同 |
同形字攻击利用了这一特性。在钓鱼网站 URL 中将"apple.com"的"a"替换为西里尔字母"а",外观相同但会导向完全不同的域名。在隐写术中,将文本中特定字符替换为同形字可以嵌入比特信息。
检测同形字需要检查每个字符的 Unicode 码位。正如密码长度与安全性中提到的,外观相同但字节序列不同的情况构成严重的安全风险。
作为对策,主流浏览器限制了 IDN(国际化域名)的显示。当域名混合多种文字(拉丁字母和西里尔字母等)时,浏览器以 Punycode(以 xn-- 开头的编码格式)显示域名,警告用户这是假冒网站。Firefox 从版本 22 起、Chrome 从版本 51 起采用了这种判定方式,Safari 则会把有问题的字符集合用 Punycode 描绘出来。
文本水印技术
作为隐写术的应用,存在文本数字水印技术。虽然图像和视频水印广为人知,但在文本中嵌入水印的技术也存在。
| 水印方法 | 原理 | 检测方法 | 抗性 |
|---|---|---|---|
| 零宽字符嵌入 | 用不可见字符存储比特信息 | 字符计数 | 复制粘贴时可能丢失 |
| 同义词替换 | "大的"→"巨大的"等同义词替换 | 与原文比较 | 对文本编辑有抗性 |
| 句法转换 | 主动语态→被动语态等句法转换 | 与原文比较 | 对文本编辑有抗性 |
| 空白操作 | 操纵空格和制表符数量 | 空白字符统计分析 | 格式变更时丢失 |
同义词替换水印在不改变文本含义的情况下嵌入比特信息。例如,将"大的"替换为"巨大的"表示 1 比特信息。这种方法可能改变字符数,但对文本编辑和复制粘贴有抗性。
加密与隐写术的区别
加密(encryption)和隐写术经常被混淆,但它们是根本不同的技术。
| 特性 | 加密 | 隐写术 |
|---|---|---|
| 目的 | 使信息内容不可读 | 隐藏信息的存在 |
| 可检测性 | 密文的存在是明显的 | 信息的存在本身未知 |
| 对字符数的影响 | 与原文相当 | 载体文本字符数可能增加 |
| 密钥需求 | 解密需要密钥 | 知道方法就可能提取 |
| 组合使用 | 可单独使用 | 与加密并用时被发觉后内容仍受保护 |
最安全的方法是先加密信息,再用隐写术隐藏。即使隐写术被破解、信息的存在被发现,如果已加密则内容仍然不可读。
通过字符计数检测隐写术
检测基于文本的隐写术最简单的方法是字符计数。以下不自然的差异可作为检测线索。
可见字符数与实际字符数(码位数)不一致。嵌入零宽字符时,文本编辑器中可见的字符数少于程序计算的字符数。例如,外观为 100 个字符的文本实际包含 180 个字符的数据,则可能嵌入了 80 个零宽字符。
字符编码大小的不自然也是线索。纯 ASCII 文本(仅字母数字)在 UTF-8 中应为 1 字符 = 1 字节。但如果混入西里尔同形字,一些看起来是 ASCII 的字符变成 2 字节。如果总字节数超过字符数,应怀疑同形字的存在。
Twitter(现 X)的字符计数与零宽字符
Twitter(现 X)的字符计数库"twitter-text"给每个字符分配权重,再看权重总和能否收在上限 280 之内来判断帖子能否发出。配置中默认的权重是 2,只有 U+0000~U+10FF 和 U+2000~U+200D 这样一部分区间被定义为权重 1。零宽空格(U+200B)、零宽非连接符(U+200C)、零宽连接符(U+200D)都落在这个权重 1 的区间里,因此即使在画面上看不见,也会一个一个地消耗额度。位于区间之外的 U+FEFF 则按默认的权重 2 计数。
结果是,用零宽字符藏了信息的帖子,必然比看上去要"长"。二值方式下每藏 1 个秘密字符就需要 8 个零宽字符,所以即使把 280 的额度全部投进去,能运送的也只有 35 个字符,扣掉载体文本的部分还会更少。也就是说,字符数上限本身就在充当零宽字符隐写术的容量上限。
隐写术检测工具和技术
存在几种专门用于检测基于文本的隐写术的工具和技术。
| 检测方法 | 目标 | 原理 | 局限性 |
|---|---|---|---|
| 字符数与字节数比较 | 零宽字符 | 可见字符数与实际字节数不一致 | 难以区分合法零宽字符 |
| Unicode 类别分析 | 同形字 | 验证文本中字符所属 Unicode 块的一致性 | 多语言文本中误报多 |
| 统计分析 | 空白操作 | 验证空格分布是否符合自然语言统计 | 短文本精度低 |
| 熵分析 | 所有手法 | 验证文本信息熵是否在自然语言范围内 | 难以应对高级手法 |
最简单有效的检测方法是将文本复制粘贴为纯文本,比较与原文的字节数。如果包含零宽字符或同形字,字节数会有差异。字符计数工具如果能同时显示"可见字符数"和"Unicode 码位数",这种差异只要把数字放在一起对比就能确认。
零宽字符带来的实务风险
零宽字符成为问题,往往不是先出现在有意的秘密通信里,而是以"无意间混进来"的形式。从网页上复制的文本里夹带了零宽字符,于是搜索词对不上、标识符校验通不过、又或者卡在输入框的字符数限制上。由于外观完全一致,只靠肉眼比对根本追不到原因。能不能注意到"看得见的字符数"与"码位数"出现了不一致,就是分水岭。
同样的性质也可以用于追踪。只要给每一份分发出去的文件都埋入不同的零宽字符排列,就能在完全不改变外观的前提下留下识别分发对象的印记,这就是文件指纹的思路。从泄露出去的文件里读出这串排列,便可以缩小范围,判断它出自哪一个分发对象。不过零宽字符可能在复制粘贴的路径上、或者在把文本规范化的处理中丢失,因此印记能保留到什么程度取决于经过的路径。
隐写术本身是一项不偏向善恶任何一方的技术。既有隐藏通信的存在以保护自身的用法,也有带走信息的一方用它抹掉痕迹的用法。无论站在哪一边,把"看得见的字符数"与"实际的码位数、字节数"放在一起核对这样简单的确认,都是最先触到被隐藏之物的线索。