反爬之字体加密与破解

摘要

最近看到不少网站都使用了字体库对数据进行加密,即页面源码中的数据与显示出来的数据不同,用户也无法直接进行复制。

例如企信宝页面中的字母与数字,58房产频道中的数字:

qxb_font1.png58_font.jpg

经过对字体库进行研究,找到了加密与解密方案。

加密

准备字体库样本

笔者在网上随便下载了一个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,笔者使用的是cloudconvert提供的服务。然后将svg上传到fontello,选中要定制的字符,因为我们上传的字体库只包含0到9,所以这里全选,然后在Customize Codes功能下自定义码值。

font_customize.png

码值与字符的关系可以看作是一种映射关系,比如Unicode E801对应字符1Unicode 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) 

查看页面源码,会发现源码是无法显示的字符,且复制出来的是乱码。

font_effect.png

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节点:

font_cmap_fontello.png

据此我们可以还原加密时的映射表(即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编码的,直接解码并保存到文件即可。

font_58.png

然后用上面的代码转为ttx文件,查看cmap节点:

font_cmap_58.png

通过观察对比发现,字符编码相同,但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,有兴趣的可以下载看看。

目前评论:0 条

发表评论