前言
字體加密在源代碼中是亂碼的,但在瀏覽器顯示是正常的。
本博文僅供學習研究。
加密
字體加密的大概流程:
1、在后端返回數據到前端時,將一個unicode編碼與被加密字符映射並生成字體文件;
2、此時后端返回的數據是與被加密字符映射的unicode,此unicode與加密字符並無關系,根據編碼表轉換是轉換成其他字符;
3、前端接收到數據,根據字體文件的映射解析數據,顯示到界面。
此流程為本人猜想,並未實踐過。
解密
以 58 為例,
待解密字符:驋驋龤;
解密思路:按照瀏覽器解析行為
1、找到字體文件
2、根據待解密字符的unicode編碼找到此unicode編碼的真正映射字符
-
手動解密
1、找到待解密字符的字體文件,可以通過css樣式找到;
58字體文件為base64:
2、將base64 轉碼並存儲為 .ttf 文件:
在此(https://www.motobit.com/util/base64-decoder-encoder.asp)網站可解碼base64並存儲為二進制數據的 .bin 文件,將下載的二進制數據的 .bin 文件后綴更改為 .ttf,
3、通過 FontCreator 軟件打開字體文件,可看到字符與unicode的映射關系:
4、將待解密字符轉換為unicode編碼:驋驋龤:驋驋龤
可通過此網站(https://www.css-js.com/tools/unicode.html)在線轉換,得到結果:
5、根據待解密字符的unicode編碼(驋驋龤)在FontCreator 軟件中查找對應的字符,得知 驋驋龤 解密后為 220。
-
代碼解密
python為例:
1 from fontTools.ttLib import TTFont 2 import base64 3 4 5 6 # 模塊依賴: 7 # base64 :解碼base64 8 # TTFont :解析字體文件 9 10 11 # 存儲的ttf文件路徑及名稱 12 filePath = 'decode.ttf' 13 14 # 字體文件的base64 15 base64str = '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' 16 17 # 待解密字符 18 encodestr = u'驋驋龤' 19 20 21 # 存儲文件(以二進制方式) 22 # filePath :存儲的文件路徑及名稱 23 # data :需要存儲的數據 24 def writeTTFFile(filePath, data): 25 #print(encodestr.encode("unicode_escape").decode()) 26 #print(int(encodestr.encode("unicode_escape")[2:], 16)) 27 with open(filePath, 'wb') as f: 28 f.write(data) 29 30 # 解析字體並解密待解密字符 31 # filePath :字體文件的路徑及名稱 32 # str :待解密字符 33 # return :返回解密后的字符 34 def TTFDecode(filePath, encodestr): 35 font = TTFont(filePath) 36 strRes = font.getBestCmap() 37 print(strRes) 38 glypDict = font.getReverseGlyphMap() 39 print(glypDict) 40 ret = '' 41 for ch in encodestr: #遍歷待解密字符,挨個解密 42 strResKey = int(ch.encode("unicode_escape")[2:], 16) #將單個字符轉換為unicode並截取16進制部分,並轉換為10進制數字 43 glypDictKey = strRes[strResKey] #通過轉換出的10進制數字在 'strRes' 里查找對應的glyph資源 44 ret = ret + str(glypDict[glypDictKey] - 1) #在 'strRes' 查找出的值在 'glypDict' 里查找出對應的字符並拼接,得到解密后的字符, 這里減1 是58加密時的規則,其他網站的加密可能不是這樣 45 return ret 46 47 writeTTFFile(filePath, base64.b64decode(base64str)) 48 decodestr = TTFDecode(filePath, encodestr) 49 print(decodestr)
代碼重點在 34、35行,這兩行解析了字體文件的映射關系;
print(strRes) 輸出為:{38006: 'glyph00009', 38287: 'glyph00004', 39228: 'glyph00002', 39499: 'glyph00003', 40506: 'glyph00010', 40611: 'glyph00007', 40804: 'glyph00008', 40850: 'glyph00005', 40868: 'glyph00001', 40869: 'glyph00006'}
print(glypDict):{'glyph00000': 0, 'glyph00001': 1, 'glyph00002': 2, 'glyph00003': 3, 'glyph00004': 4, 'glyph00005': 5, 'glyph00006': 6, 'glyph00007': 7, 'glyph00008': 8, 'glyph00009': 9, 'glyph00010': 10}
例如:待解密字符:驋 的unicode編碼的10進制為 39499 ,那么在 strRes 中查找對應的值為 glyph00003 ,glyph00003 在 glypDict 中查找對應的值為 3,再減 1就是2,得到解密后的字符就是 2。
減 1 操作是58網站的加密規則,可通過對比觀察得到。
可以將 ttf 文件轉為 ttx 文件,ttx 文件直觀展示了映射關系,通過代碼:TTFont('encode.ttf').saveXML('decode.ttx'),轉換;
font.getBestCmap():這段代碼就是解析得到
font.getReverseGlyphMap():這段代碼就是解析得到:
-
其他演示
待解密字符: 𘢘𘢗𘢓𘢕;
字體文件:http://qidian.gtimg.com/qd_anti_spider/PUNzUOms.woff
1 def decode(): 2 encodestr = '𘢘𘢗𘢓𘢕' 3 glypDict = { 4 'zero': '0', 5 'one': '1', 6 'two': '2', 7 'three': '3', 8 'four': '4', 9 'five': '5', 10 'six': '6', 11 'seven': '7', 12 'eight': '8', 13 'nine': '9', 14 'period': '.' 15 16 } 17 strRes = TTFont(BytesIO(requests.get('http://qidian.gtimg.com/qd_anti_spider/PUNzUOms.woff').content)).getBestCmap() 18 print(strRes) 19 ret = '' 20 for ch in encodestr: 21 ret = ret + glypDict[strRes[int(ch.encode('unicode_escape')[2:], 16)]] 22 return ret 23 print(decode())
與58不同,字體文件是 .woff,但原理一樣,在解密時,應手動解密一遍,配合 FontCreator 軟件,或轉成 xml格式,仔細觀察。