一个在IT行业摸爬打滚的程序猿

0%

woff字体反爬实战,10分钟就能学会(ttf字体同理)

声明:本帖子仅是用于学习用途,请勿与用于恶意破坏别人网站,本人不承担法律责任。

来继续学爬虫呀!

前言
简单描述一下这种手段,html源码的数字跟页面展示的数字是不一致的!当时就一脸黑人问号,嗯???
1
2
3
经过分析,当前这种字体反爬机制是:通过获取指定链接的woff字体文件,然后根据html源码的数字
去woff字体文件里面查找真正的数字,讲到底就是一个映射关系/查找字典。如html源码是123,去woff文件里面
查找出来的是:623。好了,看到这里,你一定想说:废话讲那么多干嘛?赶紧上教程啊!!
那先来看一下大致流程呗:

分析目标网站页面(在这里我不打算贴出网站地址,请大家自己找网站练习),这里看到html源码和页面展示的数字是不一致的,如下图:
在这里插入图片描述

1
2
3
tips:
一开始不知道是怎么下手,只能谷歌搜索字体反爬,一搜果然很多说法,有说woff文件的、有说CSS的、还有说svg曲线啥的,
然后我就去查看Network里面的All,就发现关键字眼woff,就开始猜测可能是属于这种类型的反爬手段,接着开始干活。
混淆前字体:

在这里插入图片描述

混淆后的字体:

在这里插入图片描述

找了一会,发现.woff2文件和woff文件前后不一样,然后开始着手解决

如需下载woff文件,请点击这里, 提取码: ghnx

但是本地打不开woff字体文件,需要借助的软件是fontcreator,这个你自己去找一下,很多破解的

在这里插入图片描述
但是这好像看不出什么,然后我们接着需要从另外一方面下手,重点来了》==将woff文件转换为xml文件==
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import os
import requests
from fontTools.ttLib import TTFont

base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

url = "http://xxxxxx.xxx.woff"

woff_dir = os.path.join(base_dir, "statics/woffs/")
file_name = url.split("/")[-1]
xml_name = file_name.replace(file_name.split(".")[-1], "xml")
save_woff = os.path.join(woff_dir, file_name)
save_xml = os.path.join(woff_dir, xml_name)

resp = requests.get(url="xxx")
with open(save_woff, "wb") as f:
f.write(resp.content)
f.close()
font = TTFont(save_woff)
font.saveXML(save_xml) # 转换为xml文件

然后打开xml文件看,先来查看一下缩略的内容,红色圈圈的那两个是本次重点破解的分析的内容:
主要涉及内容破解版块

然后先查看cmap,发现线索,里面注释的地方有标注了。然后我们大胆猜测:NINE对应的name=cid00018,code=0x39,这翻译过来就是9对应的name=cid00018,其id标记为0x39:
在这里插入图片描述

接着来看一下code=0x39,其对应的name=cid00018,然后我们拿这个cid00018去搜索,发现在部分里面看到:
<GlyphID id="3" name="cid00018"/>,这表明什么呢?结合前后两个映射关系,然后连起来再大胆猜测一下,可能是9对应3?
在这里插入图片描述
为了验证这个猜想,继续再找一下其他例子,我使用已经转换为如下格式,方便你们对比,你们也可以从三张截图来对比,哪三张截图呢?分别是:①是前面包含“code=0x39,name=cid00018”的截图;②是包含“id=3,name=cid00018”的截图;③是文章的第二张截图。
你们可以①②截图来一个个列出映射关系,建议先列出①的映射关系,再列出②的映射关系,然后再将①、②的映射关系组合起来,得出一个新的映射关系,这个新的映射关系就是我们所需的,下面来给你们看一下我提取的①、②的映射关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
①的映射关系,在这里我定义为before_code_id
②的映射关系,在这里我定义为affter_code_id,结果如下:
before_code_id = {
"0": "cid00019",
"1": "cid00020",
"2": "cid00017",
"3": "cid00021",
"4": "cid00022",
"5": "cid00024",
"6": "cid00026",
"7": "cid00025",
"8": "cid00023",
"9": "cid00018"
}
affter_code_id = {
"cid00017": 2,
"cid00018": 3,
"cid00019": 4,
"cid00020": 5,
"cid00021": 6,
"cid00022": 7,
"cid00023": 8,
"cid00024": 9,
"cid00025": 10,
"cid00026": 11
}

然后从html源码到before_code_id, affter_code_id应用起来就是如下:
前端数字—中间人code—最终的数字,即:
"0"——"cid00019"——4
"1"——"cid00020"——5
"2"——"cid00017"——2
"3"——"cid00021"——6
"4"——"cid00022"——7
"5"——"cid00024"——9
"6"——"cid00026"——11
"7"——"cid00025"——10
"8"——"cid00023"——8
"9"——"cid00018——3

我们再简化一步,直接从html源码数字到最终的数字映射为如下(即直接省去中间的cidxxxxx这串):
"0"——4
"1"——5
"2"——2
"3"——6
"4"——7
"5"——9
"6"——11
"7"——10
"8"——8
"9"——3


但是你们发现这映射后的数字很奇怪吗,比如"6"、"7"映射之后分别为11和10,
但是在我们的正常逻辑之中不对呀,要不我们再列一下html源码跟前端的肉眼看到的数字的映射关系呗:
"0"——2
"1"——3
"2"——0
"3"——4
"4"——5
"5"——7
"6"——9
"7"——8
"8"——6
"9"——1
哇,这列出来之后不是很相似吗,跟前面的结果,要不我再放在一起给你们好对比一下呗:
xml提取的映射 html源码跟网页展示的,提取的映射
"0"——4 "0"——2
"1"——5 "1"——3
"2"——2 "2"——0
"3"——6 "3"——4
"4"——7 "4"——5
"5"——9 "5"——7
"6"——11 "6"——9
"7"——10 "7"——8
"8"——8 "8"——6
"9"——3 "9"——1

到此,我们发现从xml提取的映射跟html源码跟网页展示的提取的映射数值都是相差2,所以我们大胆猜测:网页上看到的数值是可以从xml提取的映射关系里面每个数字减去2所得的,即:

1
2
3
4
5
6
7
8
9
10
"0"——4-2=2
"1"——5--2=3
"2"——2-2=0
"3"——6-2=4
"4"——7-2=5
"5"——9-2=7
"6"——11-2=9
"7"——10-2=8
"8"——8-2=6
"9"——3-2=1

所以这就是破解了嘛,到此,这个教程总可以理解吧,写得辣么辛苦、改的辣么辛苦,赶快评论点赞收藏一套走起来

好了,别嗨了,实操才是王道,下面来看一下核心代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#!/usr/bin/python3
# -*- coding: utf-8 -*-
# @Time : 2019/8/19 13:08
# @Author : qizai
# @File : crawl_woff.py
# @Software: PyCharm

# 先安装:pip3 install fontTools
import os
import requests
from fake_useragent import UserAgent
from fontTools.ttLib import TTFont # 对字体文件进行格式转换

base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
ua = UserAgent()
header = {
"user-agent": ua.chrome,
}


def parse_woff(url=""):
"""这里是下载字体并且解析对应的值"""
global cookie
global header

woff_dir = os.path.join(base_dir, "statics/woffs/")
file_name = url.split("/")[-1]
xml_name = file_name.replace(file_name.split(".")[-1], "xml")
save_woff = os.path.join(woff_dir, file_name)
save_xml = os.path.join(woff_dir, xml_name)

if os.path.exists(save_woff): # 存在本地的话直接提取本地的文件去解析即可省去下载,避免浪费资源
font = TTFont(save_woff)
else:
resp = requests.get(url=url, cookies=cookie, headers=header)
with open(save_woff, "wb") as f:
f.write(resp.content)
f.close()
font = TTFont(save_woff)
font.saveXML(save_xml) # 转换为xml文件

cmap = font.getBestCmap() # 这个是xml源码里面的【数值-中间人code】映射,数值还不一定是html源码里面的数值,而是每位数经过加上一定的数值之后的
tmp = { # 这个是对应的才是我们需要的值,或者你也可以在每次获取的时候,将这个值对应减去48即可,就可以省去这这个映射
48: 0, # html源码里面的0对应xml源码里面的48
49: 1, # html源码里面的1对应xml源码里面的49
50: 2, # html源码里面的2对应xml源码里面的50
51: 3, # html源码里面的3对应xml源码里面的51
52: 4, # html源码里面的4对应xml源码里面的52
53: 5, # html源码里面的5对应xml源码里面的53
54: 6, # html源码里面的6对应xml源码里面的54
55: 7, # html源码里面的7对应xml源码里面的55
56: 8, # html源码里面的8对应xml源码里面的56
57: 9, # html源码里面的9对应xml源码里面的57
} # 注意:个人猜测以上这个tmp字典,xml源码的数字跟html源码数字的映射关系可能会定期改变的

before_code_id = {} # 转换之后before_code_id为:1:cid00019 key就是html源码数字,value就是用来查询的中间人code
for k, v in cmap.items():
if k not in set(range(48, 58)):
continue
before_code_id[tmp.get(k)] = v # 这一步其实是将49:cid00019的映射格式转换为好理解的1:cid00019映射关系

code_id_list = font.getGlyphOrder()[2:] # 这个返回的值有11个,但是我这里只是取了第三个到最后一个,是用来取计算前端看到的真正的数值
affter_code_id = {k:v for k,v in zip(code_id_list, range(2, 12))} # 将每一个按照顺序映射为cid00562:2这种

return before_code_id, affter_code_id


if __name__ == '__main__':
"""使用如下"""
before_code_id, affter_code_id = parse_woff(url="xxxx")

# html源码数字:假设为0
html_number = 0
tmp_code = before_code_id.get(html_number) # 先匹配中间人code
real_number = affter_code_id.get(tmp_code) - 2 # 再提取中间人code对应的真正的数字,记得要减去2,因为本来是每位数字已经多了2
print("当前html源码数字html_number:{} 真正的数字为real_number:{}".format(html_number, real_number))

当前的woff字体反爬已经破解了,如果有不妥的地方请指出,大家一起学习。

至此本文教程写完了,希望能够帮助到各位在爬虫路上的小伙伴们,觉得不错点个赞呗
感谢认真读完这篇教程的您

先别走呗,这里有可能有你需要的文章:

CSS字体反爬实战,10分钟就能学会
爬虫:js逆向目前遇到的知识点集合