mywang88
2019-07-21
簡介
筆者在數據採集時遇到了一個字體加密的網站。
思路
1 打開網頁
在瀏覽器中打開一個某八同城的網頁。
選中網頁中的有價值信息中的一個常見的漢字字符,複製粘貼到一個文本編輯器(例如 notepad++)中,會發現我們得到的內容與我們最初複製的內容並不相同。通常,得到的是一個無法正常顯示的字符,例如一個方框或空白。
那麼問題來了,爲什麼我們無法成功複製我們看到的內容呢?
這是由計算機中字符的顯示機製造成的。
2 字符顯示
用戶所看到的一個字符,實際上可以被理解成一幅圖片,一幅特殊的黑白圖片。
而計算機中的一個字符,實際上可以被理解成一個編碼。
字體庫,就是字符的編碼集合到字符的圖片集合之間的一個映射。
當計算機要給用戶顯示一個字符的時候,會根據字符的編碼,去特定的字體庫中查找到編碼對應的字體圖片,最終將圖片顯示在屏幕上(例如瀏覽器頁面中)。
而當用戶用鼠標選中並複製屏幕上的一個字符的時候,用戶獲取到的實際上是這個字符的編碼,而非圖片。
接下來,用戶將這個編碼粘貼(輸入)到了另一個文本編輯器中,而文本編輯器與瀏覽器一樣,顯示給用戶的同樣並不是字符的編碼。文本編輯器也需要根據字符的編碼,去特定的字體庫中查找到編碼對應的字體圖片,最終將圖片顯示在屏幕上(編輯器的用戶界面中)。
值得注意的是,對於同一個字符的編碼,不同的應用程序(例如瀏覽器和文本編輯器)去查找的往往並不是同一個字體庫。例如,瀏覽器去查找的字體庫是“微軟雅黑”,而文本編輯器去查找的字體庫是“宋體”。
對於同一個字符,即同一個字符編碼,雖然它對應的微軟雅黑字體圖片與宋體字體圖片之間存在差別,但通常情況下,兩幅字體圖片能讓用戶聯想到的將是同一個字。這也可以用來解釋通常情況下我們對文字進行的複製粘貼往往能夠看到期望的結果。
然而,一個字體庫,本質上只是一個字符的編碼到圖片之間的映射。在實際中,用戶或者開發者完全可以根據自己的喜好,創建一個自己的字體庫。在這樣一個字體庫中,一個通用的字符的編碼被映射爲一幅怎樣的圖片,將完全由開發者自己說了算。
幸運或不幸,這種計算機中的字符顯示的機制(編碼→字體庫→圖片)被應用到了數據加密中。
3 字符編碼
在開始討論字體加密問題之前,還需要對字符的編碼問題進行簡單的介紹。
長久以來,爲了信息存儲和交流的便利,人們逐步制定了統一的字符編碼標準。對於字符編碼標準的討論並不在本文的討論範圍內。
舉一個簡單的例子:
漢字字符 王
與 Unicode 編碼 \u738b
唯一對應。
那麼當計算機需要存儲或傳輸這個字符的信息的時候,就只需要存儲或傳輸它的 Unicode 編碼,這個編碼由 4 個 16 進制的數字組成,在內存中只佔用 16 個二進制位,即 2 Byte 的空間。相比字符的圖片(往往需要佔用數十倍於編碼的內存空間),採用字符的編碼極大地降低了成本,提高了效率。
因此,計算機在存儲和傳輸字符信息的時候,處理的都是字符的編碼,而非字符的圖片。
只有當計算機需要向用戶展示字符的時候,才通過查找字體庫的方式,將字符的編碼映射爲字符的圖片,最終顯示在屏幕上(或打印在紙上)。
一個通用的字體庫,必須能夠把一個字符編碼標準規定的字符編碼,映射爲一幅正確的圖片。例如 \u738b
在字符編碼標準中對應 王
這個漢字字符,那麼一個通用的字體庫就必須把它映射爲一幅能讓用戶聯想到這個漢字的圖片(不管這副圖片中的這個漢字用的是隸書、楷書、還是草書)。
通用的字體庫要實現的目標,就是把正確的字符編碼,映射爲正確的字體圖片。
那麼不通用的字體庫呢?
4 字體加密
不通用的字體庫可以用來實現字體加密。
通常來說,字體加密的主要目的之一,就是不讓用戶獲得符合統一的字符編碼標準的字符編碼。
這聽起來有點繞,讓我們來舉例說明。
案例1
小明在瀏覽器中訪問某個網頁,網站服務器想讓小明看到一幅能聯想到漢字 王
的字體圖片,它決定給小明的瀏覽器傳輸的內容,是這個漢字在 Unicode 字符編碼標準中約定好的編碼:\u738b
。
接下來,小明的瀏覽器爲了能讓小明看到人能看懂的內容,去瀏覽器默認的字體庫中查找這個字符編碼,並把映射到的圖片顯示在屏幕上。
這裏查找的字體庫是一個通用字體庫,映射可以近似理解爲這樣的:\u738b -> 王.jpg
。
最終,這個案例中,小明看到的圖片跟複製到的字符編碼都是正確的,未加密的。
案例2
小明在瀏覽器中訪問某個網頁,網站服務器想讓小明看到一幅能聯想到漢字 王
的字體圖片,然而,爲了保護網站服務器的數據不被小明覆制粘貼,網站服務器這次決定採用字體加密。
這一次,網站服務器給小明的瀏覽器傳輸的內容也是一個字符編碼:\u8d75
,以及一個字體庫:\u8d75 -> 王.jpg
,同時,它還告訴瀏覽器不要去查找默認的字體庫,改爲查找它提供的這個字體庫。
於是乎,瀏覽器通過查找網站服務器提供的字體庫,最終還是成功地給小明顯示了一幅能聯想到漢字 王
的字體圖片。如果小王只滿足於看到網站服務器想讓他看到的內容,那麼案例 1 與 案例 2 對他來說基本是沒有任何區別的,他甚至都什麼都感受不到。
然而,一旦小明想要通過複製粘貼的方式來“盜取”網頁提供的數據,他就會發現問題:自己看到的明明是一個長得像漢字 王
的字符,複製粘貼到文本編輯器後怎麼就變了呢?
在本例中,小明覆制粘貼過去的實際上是 \u8d75
這個字符編碼,在 Unicode 字符編碼標準中,它被約定對應的實際上是 趙
這個漢字。當然,這不重要,總之,這個字符編碼對應的並不是小明看到的 王
。
文本編輯器,以及系統中的任何其它地方,都無法將這個字符編碼與 王
這個漢字字符產生任何聯繫。只有加載了網站服務器給的特定字體庫的瀏覽器映射出來的字體圖片,才能反應它本身所代表的信息。
一旦網站服務器啓用了這種字體加密方案,不止常規用戶無法通過複製粘貼獲取有效信息,網絡爬蟲也將無法直接從 html 文檔中提取有效信息。
網站有效地提高了獲取它的有效數據所需付出的成本,作爲代價,網站服務器需要消耗額外的計算資源對數據進行加密處理,消耗額外的帶寬來傳輸字體庫,並在一定程度上降低用戶的使用體驗。
那麼字體加密可以破解嗎?
5 破解字體加密
破解字體加密,要做的就是把錯誤的字符編碼替換回正確的字符編碼。
不管網站服務器使用的字體加密算法的細節如何,它都必須爲用戶提供一個字體庫。如果沒有這個字體庫,網站可能也就沒有用戶了。
所以說,破解字體加密的關鍵,就是這個字體庫。
瀏覽器在顯示每個字體編碼的時候,都會先在這個字體庫中進行搜索,如果找到了該編碼,就將對應的字體圖片顯示出來,如果找不到該編碼,再到備用的通用字體庫中進行搜索。
爲了節省成本和提高效率,這個字體庫有時只需要包含數十個最常見的數字和漢字即可。
如果我們能夠通過某種辦法,得到這個字體庫中的每個字體圖片到標準字符編碼的映射,就可以完成對字體加密的破解工作。
筆者畫了一張示意圖:
6 破解步驟
本文寫作的目的是整理思路,不對具體的實施細節進行討論。
總結上文,破解字體加密的主要步驟如下:
- 獲取被加密的 html 文檔。
- 獲取被加密的 html 文檔對應的字體文件,有時它被以 base64 編碼寫在 html 文檔中。
- 解析網頁字體文件,獲得加密編碼到字體圖片的映射。
- 通過種種途徑,在本地準備一個標準字體文件,即,標準編碼到字體圖片的映射。
- 設計匹配算法,生成網頁字體文件的字體圖片到標準字體文件的字體圖片的映射。
- 最終,鏈接三個映射,獲得網頁加密編碼到標準編碼的映射,即,解密字典。
- 利用解密字典,將被加密的 html 文檔中的所有加密編碼,替換爲標準編碼。
其中第 4 5 兩步,尤其是第 5 步,牽扯的工作最爲複雜。
最後,給出一點有用的代碼:
from fontTools.ttLib import TTFont
from io import BytesIO
from base64 import b64decode
# 利用字符串方法從 html 文檔中提取 base64_str
# 解碼
bin_data = b64decode(base64_str)
# 存入本地字體文件
open('font.woff', 'wb').write(bin_data)
# 生成字體實例
font = TTFont(BytesIO(bin_data))
# 提取字體映射
glyf = font['glyf']
# 轉換爲 Python 字典
font_dict = dict(glyf)
Python 的 fontTools 庫可以有效處理字體文件,shelve 庫可以有效存儲字體和字典對象。
在 win10 系統下, FontCreator 軟件可以有效查看和處理字體文件。