乱码菜单、乱码标语、乱码短信,生活中,这些乱码现象时常让我们一头雾水。它们背后隐藏着怎样的秘密?又是如何被破译的?一起来看看答主们如何把乱码变成明文,让信息重见天日。禄陋脟脠脪酶脨脨拢潞戮炉脤猫脮漏脝颅隆拢脮媒脭脷驴陋脡猫禄陋脟脠脪酶脨脨脮脣禄搂拢驴脛煤碌脛脪禄麓脦脨脭脙脺脗毛拢篓脟毛脦冒脫毛脠脦潞脦脠脣鹿虏脧铆拢漏脦陋 205871 隆拢麓脣脪禄麓脦脨脭脙脺脗毛陆芦脫脷脨脗录脫脝脗脢卤录盲 12:08 脢搂脨搂隆拢一类是「月」字旁的字,主要是生僻字;另一类是常用字,读音基本上是 lu、mao 之类的。像这样,一部分字按部首排列,一部分字按音序排列,符合中文 GBK 编码的特征。查一下 GBK 码表,发现这些字居然完全落在 C2A0 ~ C2BF、C380 ~ C3BF 这样的小范围内:于是更加确信第一步应该先按 GBK 编码把乱码汉字转成十六进制。结果如下:C2A0 ~ C2BF、C380 ~ C3BF 这些编码中,第二个字节以 8, 9, A, B 开头,符合 UTF-8 编码的特征。把这些双字节组合按 UTF-8 解码,可以得到 Unicode 中编码为 U+00A0 ~ U+00FF 的字符。但这些字符基本上都是带帽子的拉丁字母,正常的文本不可能是这样:于是考虑它还是乱码。而 A0 ~ FF 恰恰又是中文 GBK(以及日文 EUC、韩文 KS X 1001 等编码)所使用的字节区段。于是把这些 A0 ~ FF 字节再按 GBK 编码解码成汉字,就得到:总结一下,就是 GBK 编码 → UTF-8 解码 → Unicode 单字节编码 → GBK 解码。一开始讨论,有人在猜是不是打字的人没看屏幕,还有人猜是不是用谐音写的别的语言。但果然它还是更像某种软硬件问题导致打印时出现的乱码。因为 UTF-8 编码的长度,和其他常见汉字编码(也包括 UTF-16)都不一样。而这张图的排版却很均匀,并没有明显地歪掉,所以乱码和原文应该字数一致(或顶多相差一个字左右)。那么是不是 UTF-16(BE 或 LE)跟 GBK 间的问题呢?试验了一下发现不是这样,因为这三者之间,无论如何转换,都会含有不合法编码或 Unicode 未赋字符的码位或特殊字符。这段文字出现在近期本地制作的标语里,也不可能涉及 Big5、Shift-JIS 等其他地区的旧软件编码。除了这些以外,我能想到的(而且实际见过的),就只剩下「电脑字体的字图发生错位」这种情况了:常见的电脑字体,由于包含的字符通常只是 Unicode 的很小的字集且对应编码不连续,于是其包含的各个字的图形(字图,glyph)一般都会在内部先以某种编号排列,然后再提供一个 Unicode 码→字图编号的表(一般是 cmap 表),用于拣出正确的字图来。但如果这张图在打印时,对方程序(由于 bug 或者字体用了不常见的格式)搞混了不同的编号,或者是程序内部先缓存了文本的字图编号,但字体文件却由于某些原因被换成了别的,其字图的排列与原字体不一致,就会拣出不正确的字图来呈现。我之前实际遇到该问题,就是系统热更新了使用中的字体,而运行中的程序重新加载了更新后的字体,却用了更新前版本的字图编号,就会出现这样的现象。不过,如果真的是这样的话,能不能破译原文,就不再是个能说准的事了……因为字体中的字图排列,理论上讲是可以完全任意的,不管发生什么都是有可能的。如果想要破译的话,只能寄希望于两套字体有相同且有规律的字图排列顺序(比如是按 Unicode 升序),且收字范围基本一致了……这时,参与讨论的另一个人也提出了这种可能,并且指出可以尝试给文本每个字的 Unicode 码加上个偏移量,穷举可能的偏移量并筛选出里面大部分字都是常用字的(比如 GB 2312 范围内,或者再严些,它的一级字范围内),再人工挑出通顺的。于是很快写了段程序试验了下。结果却是——完全没有通顺的。看来恐怕并不是每个字都有相同的偏移量,这下就难办了,因为可能性一下子大了好多个量级。于是,只好利用一点脑洞,来硬猜一下了!注意到里面的「晃」和「皖」,按康熙字典部首分别是「日部」「白部」,而 Unicode 汉字基本区的字正是按康熙字典部首加笔画数排列的,同部且笔画数相近的字会排得较近。- 「是」是 U+662F,「晃」是 U+6643,乱码比原码往后偏了 20
- 「的」是 U+7684,「皖」是 U+7696,乱码比原码往后偏了 18
这两个差值都不大,且很接近!于是再次写了个程序来试验,以偏移量 +19 为中心,把邻近编码的字都列出来看看能不能从中挑出通顺的文本来:虽然眼花缭乱,但似乎可以从中挑出对应「田咏」的大概是「生命」,偏移量分别是 +17 和 +18。但其他的就看不太出了。而如果想要再扩大范围的话,眼睛就要更花了……这时想到,要不扩大范围的同时再筛选一下,只显示 GB 2312 范围内的字如何呢:再次观察,可以注意到对应「众丧」「宙兴」的,有可能是「企业」「安全」,偏移分别是 +22、+13、+16、+12。再大胆一点的话,还可以猜想下对应「聱己」的会不会是「职工」(偏移 +37 和 +12)。这样的话,目前初步的破译结果是「??是企业的生命 安全是职工的生命」,已经很常见的标语了~然而,在试图这样猜开头两个字「贰鍪」的原文时,还是发现了意外:「鍪」字(U+936A)往前数好几十码还没见着常用字,甚至往前数一百个码,只有一个 GB 2312 范围内的字「錾」(U+933E),偏移 +44。如果再要扩大范围的话,可能性恐怕还是会多得多。整理了下思路,突然想到,按 Unicode 码来偏移的话,偏移量固然浮动很大,但实际上这个偏移应当是发生在字体的「收字范围」内。所以如果在偏移编码时,跳过不在 GB 2312 的字,那么偏移量的浮动会不会小得多了呢?于是先稍微验证一下想法:在上面列出的仅含 GB 2313 字的表中,从乱码字上方开始往上数(不含自身),但只数列出的字,看看会数出几个:- 从「晃」上方数到「是」:晁显昼昶昵昴昱是,8 个字
- 从「皖」上方数到「的」:皓皑皎皋皈皇皆的,8 个字
- 从「聱」上方数到「职」:聪聩聚聘联聒聍职,8 个字
试了三个字结果都是数出 8 个字,样本里甚至没出现浮动!所以如果把乱码字全都这样往上数 8 个字,会是什么结果呢?于是重新写了程序,把偏移改为只计 GB 2312 范围内的字,统一往前偏移 8 个字。于是得到的结果:居然一次就得到了通顺的标语!这样的话,破译就可以宣告成功了~原文本是「质量是企业的生命 安全是职工的生命」,字体用的是一款收字范围是 GB 2312 的。可是在打印时,字体的字图编号被程序搞错了,全都往后偏移了 8。于是每个字都变成了「按 Unicode 码往后数第八个 GB 2312 范围内的字」,就变成了「贰鍪晃众丧皖田咏 宙兴晃聱己皖田咏」。看到一张满是乱码的菜单,我不禁跃跃欲试,想要破解这些乱码。首先观察菜单,六道菜里有四道菜是鸡肉,而它们的名称中都有「宵」字,于是可以推测「宵」代表「鸡」。而还有两道菜的名称「橙巩宵旗」「橙巩狱旗」只差一个字,于是可以推测「狱」代表「鸭」。此外,注意到「橙巩宵旗」「橙巩狱旗」两道菜的英文完全不同,可知中英文的对应并不精确。即便如此,右上角那道菜的中文名称应该是没有什么异议的,肯定是「宫爆鸡丁」或者「宫保鸡丁」。我们大胆假设「堙充宵哺」对应「宫爆鸡丁」或者「宫保鸡丁」,这样子线索就多了。我们知道,汉字编码方案中汉字的顺序,要么是按音序排列,要么是按部首排列。如果乱码和明文采用的是同一种排序方式,只是相互错开了若干个位置,那么它们要么常常具有相同的声母,要么常常具有相同的部首。然而「堙充宵哺」与「宫爆鸡丁」或者「宫保鸡丁」不满足任一种条件,那么就只能是下面两种情况之一了:注意到「丁」字在部首排列中十分靠前(事实上,它是 Unicode 汉字基本区的第二个字,仅次于「一」),而「哺」字在音序排列中并不十分靠前,故可以猜测,「乱码按部首排列,明文按音序排列」是更有可能的。现在列出「堙充宵哺」与「宫爆/保鸡丁」各字的 Unicode 编码与 GB2312 编码(我暂未考虑 Big5 码):
那么,就来计算一下对应的编码之差吧!(以下计算均为十六进制,明文的第二个字姑且选择「爆」)
GB2312(宫) - Unicode(堙) = B9AC - 5819 = 6193
GB2312(爆) - Unicode(充) = B1AC - 5145 = 6067
GB2312(鸡) - Unicode(宵) = BCA6 - 5BB5 = 60F1
GB2312(丁) - Unicode(哺) = B6A1 - 54FA = 61A7
可见两个编码之差并非定值,但波动范围不大。
前面答主的 @思無邪SyiMyuZya 的文章中遇到过相同的情况,其解决办法是,保持 Unicode 编码中字符的顺序,但仅保留 GB2312 字符集中的字。这一操作也可以表述成「把 GB2312 字符集中的字按 Unicode 顺序重排」。
现在我们来数一下,在把 GB2312 字符集中的字按 Unicode 顺序重排后,「堙充宵哺」分别是第几个字(从 0 开始)。同时,我们再数一下「宫爆鸡丁」在 GB2312 本身中是第几个字。结果如下:
乱码汉字 | 重排后的次序 | 明文汉字 | 在 GB2312 中的次序 | 差值 |
堙 | 1094 | 宫 | 857 | 237 |
充 | 342 | 爆 | 105 | 237 |
宵 | 1370 | 鸡 | 1133 | 237 |
哺 | 801 | 丁 | 564 | 237 |
Bingo,差值全都相同!我们这就得到了乱码与明文的对应规律!把这个规律用 Python 语言表达出来,是这样的:用上述代码来破译一下菜单。左下角那道菜的第一个字只能看到右半边是「它」,不妨猜测它是个「坨」:
最开始根据经验猜是某种编码下的偏移,但是试过了各种编码,都不太符合。不过「3129」「3131」都可以看出是「2018」「2020」,所以偏移这点还是能确定的。于是猜想,大概是按字体内的 Glyph 顺序偏移的。但并不能确定是哪款字体,所以就再观察了一下。也就是说,我观察到乱码字符几乎在 GB/T 2312 内,就想到了 GB/T 2312。但是按 GB/T 2312 序不对,索性按 UCS 序重排。不过,标点之类的还是不太符合,但同区块的比较来看也是 UCS 序。注意到「谞」超出了 GB/T 2312,这是因为「高」偏移后超出了范围。德阳,我们共同的家园。2018年至2020年,德阳正全力争创第六届全国文明城市。创建全国文明城市,建设幸福美好家园,离不开大家的共同努力和积极行动。在此,我们倡议:弘扬文明风尚:培育践行「富强、民主、文明、和谐,自由、平等、公正、法治,爱国、敬业、诚信、友善」的社会主义核心价值观,深入开展「共创全国文明城市·共享幸福美好生活」主题活动,大力弘扬「学习雷锋、奉献他人、提升自己」的志愿服务理念,积极参加各类社会公益活动,争当文明有礼德阳人。遵守社会公德:遵守市民文明守则、规范个人行为,做到交通文明、言语文明、行为文明、出游文明、友善待人、关爱未成年人,及时劝阻和制止不文明现象,共同维护城市文明形象。建设家庭美德:大力倡扬夫妻和睦、孝老爱亲、勤俭节约、邻里互助的家庭文明新风,崇尚文明健康的生活方式,以实际行动促进家庭和谐、邻里和善、社区和美。市民朋友们,建设环境优美、和谐有序、文明向上的德阳是我们共同的期盼;创建含金量最高、公信力最强的全国文明城市是我们共同的心愿。让我们立即行动起来,从自己做起,从现在做起,让我们的家园环境更美好、城市更靓丽、社会更和谐、生活更幸福!另外,字体应该是方正楷体简体,因为当中的「谞」来源于 748。也因为是 748 部分所以没作正确映射,直接打「谞」出不来——得打 U+E23F。稍微了解一点的人应该能明白这套西文是近年换上去的,所以应该是某个旧版。题图来源:答主@思無邪SyiMyuZya