藝術鬼才,Unicode 字元還能這麼玩?

上周的時候,朋友圈的直升飛機不知道為什麼就火了,很多朋友開著各種花式飛機帶著起飛。

圖片來自網路

還沒來得及了解咋回事來著,這個直升飛機就🔥到的微博熱搜。

圖片來自網路

後面越來越多人開來他們的直升飛機,盤旋在朋友圈上方。於是很多朋友開來他們的坦克,專打直升飛機,一轟一個準。

圖片來自網路

好了,說回正題!

程式設計師朋友應該都很熟悉 Unicode (萬國碼),它幾乎包含世界上所有符號,比如組成直升飛機這幾個特殊符號對應的 Unicode 碼分別為:

ps:推薦一個網站,可以根據符號搜對應的 Unicode 碼://unicode.yunser.com/unicode

除了這些正常字元以外,Unicode 還包含著各種各樣的奇葩字元。

奇葩字元

除了正常的我們熟知的文字以外,Unicode 中還有一些奇怪的文字,比如下面這些文字

這咋讀?某少?

世代?

恩?超出認知範圍

除了這些奇怪文字以外,Unicode 還有一些奇葩的的符號。

例如下面一整套麻將牌:

一整套的撲克牌:

一整套國際象棋:

image-20200725215319183

除了這些,通過組合符合,我們還可以造出各種各樣的顏文字(๑•̀ㅂ•́)و✧、

另外 Unicode 還收錄著我們常用的 Emoji

除了這些之外,Unicode 中還有一些特殊字元的,利用這些字元,我們還可以玩出很多有趣的騷操作。

組合字元

Unicode 有一類字元稱為組合字元,它可以附加在前一個非組合字元上,從而使整體看起來像是一個字元。

組合字元原來目的是為了解決一些地區語言、文字特殊的需要,比如說泰文聲調符號與母音符號。

正常使用的情況下,這些組合字元數量都會有一些限制。但是在 Unicode 組合字元設計上,並沒有加這種限制,這樣使我們可以無限加這類組合字元。

利用這個特性,可以達到一些惡搞效果,比如「擊穿天花板」與「鑿穿地板」的效果。

上面實現原理其是利用以下兩個組合字元:

上翻字元

下翻字元

只要複製這兩個字元相應的 HTML 程式碼,跟在正常的字元後面,就可以使這兩個字元附加在普通字元上,比如下面實現效果為

黑̮̑

Unicode 碼值通常使用 U+N(16 進位N 代表碼值),比如 A 的碼值為 U+0041。

在 HTML 中 Unicode 可以使用 &#N;(十進位,N 代表碼值)表示

在 JS 中 Unicode 中需要使用] \uN(16 進位N 代表碼值)表示

只要我們在普通字元多複製幾個這類附加字元,就可以形成上述「擊穿」效果。

還記得上面說的泰文嗎,曾經有一段時間貼吧,很流行一種噴射文,比如下面的效果。

向左噴

向右噴

左右互噴

這種噴射文實際原理就是利用泰文中聲調符號附加在其他正常符號上。

不過現在這個效果貌似已經沒辦法再復現了,現在我們只能看到這樣的效果:

在一些老版本的系統/瀏覽器可能還能看到這種效果,知道的小夥伴留言區可以告知一下。

零寬字元

Unicode 中還有一類格式字元,不可見,不可列印,主要作用於調整字元的顯示格式,所以我們將其稱為零寬字元。

零寬字元主要有以下幾類:

零寬度空格符 (zero-width space) U+200B : 用於較長單詞的換行分隔

零寬度非斷空格符 (zero width no-break space) U+FEFF : 用於阻止特定位置的換行分隔

零寬度連字元 (zero-width joiner) U+200D : 用於阿拉伯文與印度語系等文字中,使不會發生連字的字元間產生連字效果

零寬度斷字元 (zero-width non-joiner) U+200C : 用於阿拉伯文,德文,印度語系等文字中,阻止會發生連字的字元間的連字效果

左至右符 (left-to-right mark) U+200E : 用於在混合文字方向的多種語言文本中(例:混合左至右書寫的英語與右至左書寫的希伯來語),規定排版文字書寫方向為左至右

右至左符 (right-to-left mark) U+200F : 用於在混合文字方向的多種語言文本中,規定排版文字書寫方向為右至左

利用零寬字元不不可見的特性,我們也可以玩出一些騷效果。

空白微博

發布微博的時候,如果內容都是空格,將沒辦法發布。

但是如果我們將零寬字元,比如說「零寬度空格符 U+200B」複製到微博,這樣我們就可以發布空白微博。

我們可以利用 Chrome 瀏覽器的控制台複製零寬字元,操作方式如下:

發布效果如下:

真的沒有改 HTML 導致的.jpg

隱形水印

對於一些內部論壇或者說小說網站來說,可以通過零寬字元在帖子或小說內容嵌入隱形水印。

當這些內容被一些爬蟲複製到其他網站時,我們就可以通過隱形水印,輕鬆查找時那位用戶泄漏內容。

隱形水印主要原理就是將用戶資訊比如用戶名,通過一定演算法轉成零寬字元,這樣普通用戶瀏覽時完全看不到這個水印。

如果內容被複制到其他網站,隱形誰贏也被複制,只要找到這個水印,將這些零寬字元反轉成用戶名即可。

下面展示一種轉換方法,JS 程式碼主要參考以下 Github 項目:

//github.com/umpox/zero-width-detection

隱形水印生成方法

第一步我們需要將明文字元串每個字元都轉成二進位串。

    // 每個字元轉為二進位,用空格分隔
    const textToBinary = username => (
      username
      .split('')
      // charCodeAt 將字元轉成相應的 Unicode 碼值
      .map(char => char.charCodeAt(0).toString(2))
      .join(' ')
    );

示例如下:

第二步,將二進位串轉為零度字元串,轉換規則如下:

  • 1 轉換為 \u200b 零寬度字元(zero-width space)
  • 0 轉換為 \u200c 零寬度斷字元(zero-width non-joiner)
  • 其他(剩餘就是空格) 轉換為 \u200d 零寬度連字元 (zero-width joiner)
  • 最後使用 \ufeff 零寬度非斷空格符 (zero width no-break space) 作為分隔符
const binaryToZeroWidth = binary => (
  binary.split('').map((binaryNum) => {
    const num = parseInt(binaryNum, 10);
    if (num === 1) {
      return '\u200b'; // \u200b 零寬度字元(zero-width space)
    } else if(num===0) {
      return '\u200c'; // \u200c 零寬度斷字元(zero-width non-joiner)
    }
    return '\u200d'; // \u200d 零寬度連字元 (zero-width joiner)

  }).join('\ufeff') // \ufeff 零寬度非斷空格符 (zero width no-break space)
);

最終加密方法如下:

const encode = username => {
  const binaryUsername = textToBinary(username);
  const zeroWidthUsername = binaryToZeroWidth(binaryUsername);
  return zeroWidthUsername;
};

使用加密方法將明文字元串加密之後,加密字元串肉眼是看不到了,但是實際還是存在的。

實際上,如果我們將加密之後字元串複製到 BEJSON 網站,就可以看到字元。

image-20200722083507869

另外你還可以把加密字元串複製到 IDEA 中,可以看到相應的 Unicode 編碼值。

解密隱形水印

知道了加密的方式,解密其實就很簡單,我們只要按照相反步驟的來就可以了。

第一步,將隱形水印按照以下規則轉換為二進位串。轉換規則如下:

  • 使用 \ufeff 分隔字元串
  • \u200b 轉為 1
  • \u200c 轉為 0
  • 其他字元使用空格
const zeroWidthToBinary = string => (
  string.split('\ufeff').map((char) => { // \ufeff 零寬度非斷空格符 (zero width no-break space)
    if (char === '\u200b') { // \u200b 零寬度字元(zero-width space)
      return '1';
    } else if(char === '\u200c') { // \u200c 零寬度斷字元(zero-width non-joiner)
      return '0';
    }
    return ' ';
  }).join('')
);

調用該方法,隱形水印轉成二進位串。

第二步,將二進位再轉為相應的字元。

const binaryToText = string => (
  // fromCharCode 二進位轉化
  string.split(' ').map(num => String.fromCharCode(parseInt(num, 2))).join('')
);

最終解密方法如下:

const decode = zeroWidthUsername => {
  const binaryUsername = zeroWidthToBinary(zeroWidthUsername);
  const textUsername = binaryToText(binaryUsername);
  return textUsername;
};

解密示例如下:

短網址

我們常用的短網址,域名後面會跟上一串隨機串,從而實現短網址到長網址的映射。比如以下網址:

//sourl.cn/iLyn9S

然而我們可以利用零寬字元也可以實現短網址的效果,,比如下面這個網站,就可以生成這類短網址。

//zws.im/

可以看到這個短網址後面看不到任何字元,實際上這後面跟著一串零寬字元。當瀏覽器訪問該短網址時,後端程式只要反解密的後面零寬字元,拿到相應的網址,然後在做跳轉就可以到指定的網站。

反解密的原理可以參考上面隱形水印的程式碼

小心零寬字元

日常開發過程中,我們有時需要從一些文件中讀取文本內容,然後做相應的處理。

有時候我們可能會碰到一些詭異的現象,比如我們之前碰到的例子。

後台程式從 Excel 讀取文本內容,然後程式中判斷是讀取的文本內容是否與指定的字元串相等。

然後當我們讀取一份 Excel 內容後,返現這段比較邏輯怎麼也通過不了。本來以為是 Excel 內容存在空格什麼的,但是打開 Excel 仔細一看,跟指定字元串一模一樣,並沒有什麼其他字元。

第一次碰到這種例子,沒有什麼經驗,真的排查了很久,到最後都有點懷疑人生了。最後無意間將文本內容複製到了 IDEA 中,才發現整理混雜著零寬字元!

如果各位小夥伴也碰到這類問題,不妨將複製文本內容,然後到 IDEA 中查看是否存在某些看不見字元~

最後(點個讚唄!)

這兩個星期一直很忙,一直都在 9106 的節奏,真的是累,所以斷更了一周!

所幸最近項目提測,稍微輕鬆了一點,能有點划水時間來寫寫文章。不過再提起筆來寫文章,就有點斷節奏了!

這篇文章墨跡了很久才水出來,下周開始再次恢復周更的節奏,再忙再累,每周都來一篇。

歡迎各位小夥伴,每周來這裡蹲我,Gank 我!!!

好了,我是樓下小黑哥,下周見!!!

參考鏈接

  1. //juejin.im/post/5d3f01e7f265da03c23ead69
  2. //zero.rovelast.com/
  3. //zws.im/
  4. //imweb.io/topic/5a08a5c7ef79bc941c30d8dd

歡迎關注我的公眾號:程式通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的部落格:studyidea.cn

Tags: