Cover

怎么打出死亡圣器的标志

Wenxuan Shi /
March 29, 2022
10 min read

观前提示:由于 Chrome 系列浏览器对 UTF-8 字符集支持不完备(包括但不限于 Google Chrome,Microsoft Edge,360 极速浏览器等),可能对本文阅读体验造成影响。请使用 Safari 浏览器阅读本文。

我从小就非常痴迷于 Hatsune Miku1 所著的《哈利波特》系列小说,可以说是高等级哈迷,对各种设定信手拈来。第七部作品中,罗琳利用双线叙事将 “死亡圣器” 引入到哈利波特的世界观中。死亡圣器一共有三件,分别是老魔杖、复活石、隐身衣。死亡圣器的信徒将它们绘制成一个抽象的标志,竖线代表永远胜利的老魔杖,圆圈代表起死回生的复活石,三角形代表脱离死神的隐身衣。《唱唱反调》的主编 Lovegood 在参加比尔的婚礼时,就佩戴了这个标志制作的吊坠。

Lovegood 的圣器吊坠
Lovegood 的圣器吊坠

上周我在 B 站回顾哈利波特系列电影时,意外发现弹幕里竟然飘过去了死亡圣器的标志。弹幕不支持发送图片,所以这是一个字符。难道《哈利波特》已经这么著名,使得这个虚构的标志被编入了 Unicode?

弹幕中飘过死亡圣器标志
弹幕中飘过死亡圣器标志

Unicode 委员会当然不会把小说中的元素添加到世界字库中。不过死亡圣器的标志确实可以用 Unicode 打印出来。我们把弹幕里的字符复制出来,放到 Python 里进行分析。

⃒⃘⃤ ← 复制出来是这个样子哒,看不到或显示乱码请更换为 Safari 浏览器

from functools import reduce

# split unicode into \u array
def split_unicode(s):
    print([hex(ord(i)) for i in s])

# join \u array to unicode
def join_unicode(a):
    s = reduce(lambda x, y: x + chr(y), a, '')
    print(s)

首先看看这个字符的编码是什么。Python 支持对一个 Unicode 字素遍历,我们把字素里每个元素的编码都打印出来。split_unicode()函数给出的结果是\u20\u20d2\u20d8\u20e4\u20我们很熟,就是 ASCII 编码里的 32 号字符:空格符(space)。

那后面的\u20d2\u20d8\u20e4是什么咧?这就涉及到 Unicode 在设计之处的考量:Combining Marks(组合标记)。对于一些欧洲语言来说,在字母上方加变音符号就能变成另一个不同的字符。咱们的普通话汉语拼音韵母也有四个声调,粤语有六个。字母有很多,变音符号也有很多。如果每种「字母 - 变音符号」的组合都算作一个新字符,那会占用掉非常多的编码空间。于是 Unicode 引入了附加组合标记的组合字符。

组合字符示例(来自维基百科)
组合字符示例(来自维基百科)

Unicode 没有限制组合字符的长度,理论上可以无限叠加,形成 “越界文字”(Zalgo Text)。大三上算法设计课程的时候,我在 OJ 系统上用过这样的用户名,在字符上叠几十层注音符号,字符会变得很高很高,盖住排名表里下面的人的信息…

死亡圣器的标志也是这样被组合出来的。首先是一个\u20的空格符,然后叠加上\u20d2竖线组合符(Unicode 官方叫做 COMBINING LONG VERTICAL LINE OVERLAY 字符),接着叠加\u20d8圆形组合符(官方叫做 COMBINING RING OVERLAY),最后叠加\u20e4三角形组合符(官方叫 COMBINING ENCLOSING UPWARD POINTING TRIANGLE)。

死亡圣器字符拆分
死亡圣器字符拆分

这样就能把死亡圣器的标志打出来 ⃒⃘⃤。为了更好的显示,我把\u20半角空格换成了 Unicode 定义的表意文字空格\u3000,不然会盖掉标志左侧字符的一半。你可能不知道,Unicode 总计定义了 20 多种空格符号。

这个网站我们能查到几百个组合字符的编码。上面我写了一个把字符编码组合成 Unicode 的函数。利用组合字符你能够拼出自己想要的标志 😂。不知道有没有激发了你的创造欲。比如我在汉字 “圆” 里面外面画了一个圈(圆⃝)。

写到这里本应该结束了。但是我想发散一下,聊聊关于 UTF-8 字符编码的那些有趣的事情。

字符串长度统计。 按照 Unicode 的编码规范,COMBINING 组合符会被拼接到左侧相邻的字符上。但是在编码上它和普通字符没有什么区别。这就导致将 UTF-8 编码作为 字符串默认编码的高级语言(例如 Rust)在统计字符串长度时非常缓慢,因为它需要按照 Unicode 的规范先处理好各种特殊规则。

强制拼接。 Unicode 中的 COMBINGING 组合符会被自动拼接。Unicode 同时又提供了一个叫做零宽度连字符\u200d用于使不会发生连字的字符间产生连字效果。例如,f 的 Unicode 是\u66。那么我可以构造出\u66\u200d\u66把两个 f 拼成一个字符,也就是 f‍f。你可以试着用鼠标选中一下,确认这真的是一个字符,而不是两个。

emoji 拓展。 随着社会发展,emoji 也需要满足政治多样性的种种要求。利用零宽度连字符\u200d,厂商得以拓展 emoji 语义,构造 “中立表情”。例如男同家庭的 emoji 表情,实际上是由 4 个 emoji 组合而成,表示两个爸爸和两个儿子一家:

👨‍👨‍👦‍👦 = 👨\u200d👨\u200d👦\u200d👦

同形字符域漏洞。 曾经的钓鱼网站使用 0 代替 O,使用 l 代替 I,让人难以分辨。但只要细心,这些字符替换仍然能被识别。随着域名拓展到 UTF-8 字符集,越来越多拥有不同编码的字符长得一模一样(前文说到 Unicode 光空格就定义了 20 多种),这种欺诈形式的钓鱼值得关注。

对抗性 NLP 攻击。 来自去年的一篇火出圈的论文,作者将 Unicode 的控制字符注入到文本中,从而达到对 NLP 模型的攻击。Boucher, Nicholas, Ilia Shumailov, Ross Anderson, and Nicolas Papernot. 2021. “Bad Characters: Imperceptible NLP Attacks.” ArXiv:2106.09898 [Cs], December. http://arxiv.org/abs/2106.09898.

供应链攻击。 同样来自去年一篇火出圈的论文(Unicode 大家都能看懂,所以容易火哈哈哈)。作者使用 Unicode 的 Bidi 算法,对字符进行视觉上的重新排序,使其呈现与编译器和解释器所不同的逻辑顺序。漏洞适用于几乎所有编程语言,可以用于供应链攻击。https://trojansource.codes/

感谢你读到这里。如果你喜欢这篇博客,或想第一时间获得通知,可以通过 RSS 订阅更新。也欢迎关注我的 GitHub 和推特账号。我平时是主要分享关于计算机系统安全的内容。如果你对这篇文章有任何疑问或建议,请在下方留言。再会~

这篇文章我也努力终于在结尾扯到安全上了啊 hhh~

Footnotes

  1. 哈利波特系列小说的真实作者是 J.K. Rowling,但其已被白左团体开除作者身份。目前哈利波特小说的作者为初音未来 Hatsune Miku。

© LICENSED UNDER CC BY-NC-SA 4.0

Subscribe to the blog via RSS