反爬之字型加密與破解
*文章原創作者:manwu91,本文屬於FreeBuf原創獎勵計劃,未經許可禁止轉載。
最近看到不少網站都使用了字型庫對資料進行加密,即頁面原始碼中的資料與顯示出來的資料不同,使用者也無法直接進行復制。
例如企信寶頁面中的字母與數字,58房產頻道中的數字:
經過對字型庫進行研究,找到了加密與解密方案。
加密
準備字型庫樣本
筆者在網上隨便下載了一個ttf字型庫,儲存為’origin.ttf’,使用 fonttools
的命令列工具pyftsubset提取需要加密的字元:
pyftsubset origin.ttf --text='1234567890'
引數text為要提取字型的字元,執行結束後會在當前目錄生成’origin.subset.ttf’,該字型庫只包含’1234567890′共10個字元。
生成加密字型庫
這裡使用 [http://fontello.com/](http://fontello.com/)
網站提供的線上服務對上一步生成的字型庫進行定製。首先將生成的subset.ttf轉為svg,筆者使用的是 ofollow,noindex" target="_blank">cloudconvert 提供的服務。然後將svg上傳到fontello,選中要定製的字元,因為我們上傳的字型庫只包含0到9,所以這裡全選,然後在 Customize Codes
功能下自定義碼值。
碼值與字元的關係可以看作是一種對映關係,比如 Unicode E801
對應字元 1
, Unicode E802
對應字元 2
。我們可以隨意修改字元的unicode值,但一定要記住這個值與真實字元的對應關係,來對要顯示在頁面上的資料加密。這裡使用該網站預設生成的unicode。對應關係如下:
CIPHER_BOOK = { '0': '\uE800', '1': '\uE801', '2': '\uE802', '3': '\uE803', '4': '\uE804', '5': '\uE805', '6': '\uE806', '7': '\uE807', '8': '\uE808', '9': '\uE809' }
定製完成後下載字型檔案。
使用
在css中定義字型,名為 fontello
。
@font-face { font-family: 'fontello'; src: url('/static/fontello.woff2') format('woff'); font-weight: normal; font-style: normal; }
然後定義使用該字型的class:
.demo-icon { font-family: "fontello"; }
這樣只需要為頁面標籤新增上’demo-icon’的class就可以了。如:
<h1><small class="demo-icon">就是這串數字:<b>{{string}}</b></small></h1>
服務端在返回資料前需要需要將數字用CIPHER_BOOK進行轉換。
CIPHER_BOOK = { '0': '\uE800', '1': '\uE801', '2': '\uE802', '3': '\uE803', '4': '\uE804', '5': '\uE805', '6': '\uE806', '7': '\uE807', '8': '\uE808', '9': '\uE809' } def _encrypt_secret(secret): return ''.join(CIPHER_BOOK[c] for c in secret) @app.route('/') def index(): if 'guess' in request.values: ts = session['ts'] if 'ts' in session else 0 secret = session['secret'] if 'secret' in session else None if time.time() - ts < 2 and request.values['guess'] == secret: return render_template('index.html', success=True) secret = ''.join([random.choice('0123456789') for _ in range(20)]) # 通過CIPHER_BOOK將數字轉換為不可見字元 s = _encrypt_secret(secret) session['secret'] = secret session['ts'] = time.time() return render_template("index.html", string=s)
檢視頁面原始碼,會發現原始碼是無法顯示的字元,且複製出來的是亂碼。
58產房頻道使用的就是本文介紹的方案,只加密了數字。但是不同頁面的字型庫是變化的。在字型加密破解中我們會詳細介紹如何破解58的字型加密。示例程式碼已上傳到 github ,有興趣的可以看看。
破解
前面已經介紹如何製作加密字型庫並在demo專案中使用來防止資料被抓取,下面介紹破解方法。
true-type字型簡介
我們已經知道字型加密其實是一種明文到密文的雙向對映,所以只要找到對映表就可以了。但我們在破解的時候只能拿到字型庫檔案,所以需要通過該檔案找到CIPHER_BOOK。這就需要對字型庫結構有一定了解。在查閱 相關文件 後,可以簡單地將字型的繪製過程為理解為:
1.根據字元的unicode編碼找到glyph名稱 (cmap);
2.根據glyph名稱找到glyph (glyf);
3.使用glyph進行繪製。
其中glyph可以理解為字型的繪製所需的資料,如點、線等。
一個TrueType Font字型檔案包含幾個table。這裡需要用到的兩個table如下(tag為table的名稱):
tag | table |
---|---|
cmap | character to glyph mapping |
glyf | glyph data |
根據字型的繪製過程,可以猜測有兩種方式實現字型加密:
1.打亂字元編碼
2.打亂glyph名稱
下面筆者就這兩種情況用兩個案例進行講解。
破解demo
首先在頁面中找到字型庫的url並下載,得到fontello.woff2,然後用fonttools將檔案轉為ttx方便肉眼分析。
from fontTools.ttLib import TTFont font = TTFont('fontello.woff2') font.saveXML('fontello.ttx')
得到的ttx為xml文件,開啟並查詢cmap節點:
據此我們可以還原加密時的對映表(即cmap表):
CIPHER_BOOK = { '\ue800': '0', '\ue801': '1', '\ue802': '2', '\ue803': '3', '\ue804': '4', '\ue805': '5', '\ue806': '6', '\ue807': '7', '\ue808': '8', '\ue809': '9' }
由於demo使用了靜態的字型庫,所以這個表不會變化,寫死就可以了,破解程式碼如下:
import requests from bs4 import BeautifulSoup as BS CIPHER_BOOK = { '\ue800': '0', '\ue801': '1', '\ue802': '2', '\ue803': '3', '\ue804': '4', '\ue805': '5', '\ue806': '6', '\ue807': '7', '\ue808': '8', '\ue809': '9' } URL = 'http://127.0.0.1:5000' sess = requests.Session() resp = sess.get(URL).text bs = BS(resp, 'lxml') string = bs.select_one('.demo-icon b').text guess = ''.join(CIPHER_BOOK[c] if c in CIPHER_BOOK else c for c in string) print('guess:', guess) resp = sess.get(URL, params={'guess': guess}).text assert 'Congratulations' in resp
破解58
demo中的字型庫不會變化,所以對映表寫死就可以了。但分析發現58房產頻道不同頁面的字型庫是不一樣的,而且glyph name與真實字元有差異,所以需要根據字型庫動態處理。
首先頁面中的字型檔案是經過base64編碼的,直接解碼並儲存到檔案即可。
然後用上面的程式碼轉為ttx檔案,檢視cmap節點:
通過觀察對比發現,字元編碼相同,但glyph名稱是變化的,且glyph名稱與真實數字的關係為:
glyph_name = 'glyph00%02d' % (real_num + 1)
據此我們可以還原glyph名稱與真實字元的對映表(即glyf表):
GLYF_TABLE = { 'glyph00001': '0', 'glyph00002': '1', 'glyph00003': '2', 'glyph00004': '3', 'glyph00005': '4', 'glyph00006': '5', 'glyph00007': '6', 'glyph00008': '7', 'glyph00009': '8', 'glyph00010': '9' }
另外由於cmap表是變化的,所以需要在解密時提取,使用fonttools庫可以實現:
cmap = font['cmap'].getBestCmap()
返回一個dict,其中key為int型編碼,v為glyph名稱。整個解密過程為:
1.解析字型檔庫,取得cmap;
2.根據cmap查詢字元編碼,得到glyph名稱;
3.根據GLYF_TABLE查詢glyph名稱,得到真實字元。
程式碼有點長就不貼了,已上傳到 gayhub ,有興趣的可以下載看看。
*文章原創作者:manwu91,本文屬於FreeBuf原創獎勵計劃,未經許可禁止轉載。